Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Load .env files when launching from CLI #133

Merged
merged 9 commits into from
Sep 25, 2023
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/nasty-cheetahs-care.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@bluecadet/launchpad-utils": minor
"@bluecadet/launchpad-content": patch
---

Load dotenv files when launching from CLI
6 changes: 4 additions & 2 deletions .docs/generate.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import jsdoc2md from 'jsdoc-to-markdown';
import fs from 'fs-extra';
import path from 'path';
import chalk from 'chalk';
import { ConfigManager, LogManager } from '@bluecadet/launchpad-utils';
import { loadConfigFromFile, LogManager } from '@bluecadet/launchpad-utils';
import { findConfigFile } from 'typescript';

const config = ConfigManager.getInstance().getConfig();
const configFile = findConfigFile();
const config = configFile ? (await loadConfigFromFile(configFile)) : {};
const logger = LogManager.getInstance(config).getLogger('docs');

/**
Expand Down
85 changes: 43 additions & 42 deletions packages/content/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,54 +45,55 @@ Currently supported sources are:

Some content sources require credentials to access their APIs.

These can all be stored in a local `.credentials.json` file which maps content-source IDs to their credentials. For example:
These can all be stored in a `.env` or `.env.local` file which will be automatically loaded by launchpad.

### `.credentials.json`
### `.env.local`

```json
{
"airtable-cms": {
"apiKey": "<YOUR_AIRTABLE_API_KEY>"
},
"contentful-cms": {
"previewToken": "<YOUR_CONTENTFUL_PREVIEW_TOKEN>",
"deliveryToken": "<YOUR_CONTENTFUL_DELIVERY_TOKEN>",
"usePreviewApi": false
},
"sanity-cms": {
"apiToken": "<YOUR_API_TOKEN>"
},
"strapi-cms": {
"identifier": "<YOUR_API_USER>",
"password": "<YOUR_API_PASS>"
}
}
```sh
AIRTABLE_API_KEY=<YOUR_AIRTABLE_API_KEY>

CONTENTFUL_PREVIEW_TOKEN=<YOUR_CONTENTFUL_PREVIEW_TOKEN>
CONTENTFUL_DELIVERY_TOKEN=<YOUR_CONTENTFUL_DELIVERY_TOKEN>
CONTENTFUL_USE_PREVIEW_API=false

SANITY_API_TOKEN=<YOUR_API_TOKEN>

STRAPI_IDENTIFIER=<YOUR_API_USER>
STRAPI_PASSWORD=<YOUR_API_PASS>
```

### `launchpad.json`
### `launchpad.config.js`

```js
{
"content": {
"sources": [{
"id": "airtable-cms",
"type": "airtable",
//...
}, {
"id": "contentful-cms",
"type": "contentful",
//...
}, {
"id": "sanity-cms",
"type": "sanity",
//...
}, {
"id": "strapi-cms",
"type": "strapi",
//...
}]
}
}
export default defineConfig({
content: {
sources: [
{
id: "airtable-cms",
type: "airtable",
apiKey: process.env.AIRTABLE_API_KEY,
},
{
id: "contentful-cms",
type: "contentful",
previewToken: process.env.CONTENTFUL_PREVIEW_TOKEN,
deliveryToken: process.env.CONTENTFUL_DELIVERY_TOKEN,
usePreviewApi: false,
},
{
id: "sanity-cms",
type: "sanity",
apiToken: process.env.SANITY_API_TOKEN,
},
{
id: "strapi-cms",
type: "strapi",
identifier: process.env.STRAPI_IDENTIFIER,
},
],
},
});

```

## Post Processing
Expand Down
2 changes: 1 addition & 1 deletion packages/content/docs/contentful-source.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Also supports all fields of the Contentful SDK's config.
See: 'Configuration' under https://contentful.github.io/contentful.js/contentful/9.1.7/
| Property | Type | Default | Description |
| - | - | - | - |
| <a name="module_contentful-source.ContentfulOptions+space">`space`</a> | <code>string</code>| <code>''</code> | Your Contentful space ID. Note that credentials.json will require an accessToken in addition to this |
| <a name="module_contentful-source.ContentfulOptions+space">`space`</a> | <code>string</code>| <code>''</code> | Your Contentful space ID. Note that an accessToken will be required in addition to this |
| <a name="module_contentful-source.ContentfulOptions+locale">`locale`</a> | <code>string</code>| <code>'en-US'</code> | Optional. Used to pull localized images. |
| <a name="module_contentful-source.ContentfulOptions+filename">`filename`</a> | <code>string</code>| <code>'content.json'</code> | Optional. The filename you want to use for where all content (entries and assets metadata) will be stored. |
| <a name="module_contentful-source.ContentfulOptions+contentTypes">`contentTypes`</a> | <code>Array.&lt;string&gt;</code>| | Optionally limit queries to these content types.<br>This will also apply to linked assets.<br>Types that link to other types will include up to 10 levels of child content.<br>E.g. filtering by Story, might also include Chapters and Images.<br>Uses `searchParams['sys.contentType.sys.id[in]']` under the hood. |
Expand Down
4 changes: 2 additions & 2 deletions packages/content/docs/strapi-source.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@ Options for StrapiSource
| <a name="module_strapi-source.StrapiOptions+limit">`limit`</a> | <code>number</code>| <code>100</code> | Max number of entries per page. |
| <a name="module_strapi-source.StrapiOptions+maxNumPages">`maxNumPages`</a> | <code>number</code>| <code>-1</code> | Max number of pages. Use the default of `-1` for all pages |
| <a name="module_strapi-source.StrapiOptions+pageNumZeroPad">`pageNumZeroPad`</a> | <code>number</code>| <code>0</code> | How many zeros to pad each json filename index with. |
| <a name="module_strapi-source.StrapiOptions+identifier">`identifier`</a> | <code>string</code>| | Username or email. Should be configured via `./credentials.json` |
| <a name="module_strapi-source.StrapiOptions+password">`password`</a> | <code>string</code>| | Should be configured via `./credentials.json` |
| <a name="module_strapi-source.StrapiOptions+identifier">`identifier`</a> | <code>string</code>| | Username or email. Should be configured via `./.env.local` |
| <a name="module_strapi-source.StrapiOptions+password">`password`</a> | <code>string</code>| | Should be configured via `./.env.local` |
| <a name="module_strapi-source.StrapiOptions+token">`token`</a> | <code>string</code>| | Can be used instead of identifer/password if you previously generated one. Otherwise this will be automatically generated using the identifier or password. |
5 changes: 2 additions & 3 deletions packages/content/lib/content-options.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const TIMESTAMP_TOKEN = '%TIMESTAMP%';
* @property {Array<Object<string, number>>} [imageTransforms] A list of image transforms to apply to a copy of each downloaded image.
* @property {Object<string, string>} [contentTransforms] A list of content transforms to apply to all donwloaded content.
* @property {string} [downloadPath] The path at which to store all downloaded files. Defaults to '.downloads/'.
* @property {string} [credentialsPath] The path to the json containing credentials for all content sources. Defaults to '.credentials.json'.
* @property {string} [credentialsPath] The path to the json containing credentials for all content sources. Defaults to '.credentials.json'. Deprecated in favor of `.env`/`.env.local`.
* @property {string} [tempPath] Temp file directory path. Defaults to '%DOWNLOAD_PATH%/.tmp/'.
* @property {string} [backupPath] Temp directory path where all downloaded content will be backed up before removal. Defaults to '%TIMESTAMP%/.tmp-backup/'.
* @property {string} [keep] Which files to keep in `dest` if `clearOldFilesOnSuccess` or `clearOldFilesOnStart` are `true`. E.g. `'*.json|*.csv|*.xml|*.git*'`
Expand All @@ -41,14 +41,13 @@ export const TIMESTAMP_TOKEN = '%TIMESTAMP%';
*/

/**
* @type {Required<ContentOptions>}
* @satisfies {ContentOptions}
*/
export const CONTENT_OPTION_DEFAULTS = {
sources: [],
imageTransforms: [],
contentTransforms: {},
downloadPath: '.downloads/',
credentialsPath: '.credentials.json',
tempPath: '%DOWNLOAD_PATH%/.tmp/',
backupPath: '%TIMESTAMP%/.tmp-backup/',
keep: '',
Expand Down
2 changes: 1 addition & 1 deletion packages/content/lib/content-sources/contentful-source.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { Logger } from '@bluecadet/launchpad-utils';

/**
* @typedef BaseContentfulOptions
* @property {string} space Your Contentful space ID. Note that credentials.json will require an accessToken in addition to this
* @property {string} space Your Contentful space ID. Note that an accessToken is required in addition to this
* @property {string} [locale] Optional. Used to pull localized images.
* @property {string} [filename] Optional. The filename you want to use for where all content (entries and assets metadata) will be stored. Defaults to 'content.json'
* @property {string} [protocol] Optional. Defaults to 'https'
Expand Down
4 changes: 2 additions & 2 deletions packages/content/lib/content-sources/strapi-source.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ import { Logger } from '@bluecadet/launchpad-utils';

/**
* @typedef StrapiLoginCredentials
* @property {string} identifier Username or email. Should be configured via `./credentials.json`
* @property {string} password Should be configured via `./credentials.json`
* @property {string} identifier Username or email. Should be configured via `./.env.local`
* @property {string} password Should be configured via `./.env.local`
*
* @typedef StrapiTokenCredentials
* @property {string} token Can be used instead of identifer/password if you previously generated one. Otherwise this will be automatically generated using the identifier or password.
Expand Down
20 changes: 15 additions & 5 deletions packages/content/lib/credentials.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,34 @@ import { Logger } from '@bluecadet/launchpad-utils';
*/
let creds = {};

/**
* @deprecated Use .env for managing sensitive data instead
*/
class Credentials {
/** @type {Logger | Console} */
static logger = console;

/**
* @param {string} credentialsPath
* @param {string} [credentialsPath]
* @param {Logger | Console} logger
* @deprecated Use .env for managing sensitive data instead
*/
static init(credentialsPath, logger = console) {
this.logger = logger;

if (!credentialsPath) {
return;
}

this.logger.warn(`${chalk.white('credentialsPath')} option is deprecated. Please use ${chalk.white('.env')}/${chalk.white('.env.local')} instead.`);

try {
if (credentialsPath && fsx.existsSync(credentialsPath)) {
if (fsx.existsSync(credentialsPath)) {
this.logger.info(chalk.gray(`Loading credentials from '${chalk.white(credentialsPath)}'`));
const rawdata = fsx.readFileSync(credentialsPath);
creds = JSON.parse(rawdata.toString());
} else if (credentialsPath) {
this.logger.warn(`No credentials file found at '${credentialsPath}'`);
} else {
this.logger.warn(chalk.yellow('No credentials path specified'));
this.logger.warn(`No credentials file found at '${credentialsPath}'`);
}
} catch (err) {
if (err instanceof Error) {
Expand All @@ -36,6 +45,7 @@ class Credentials {

/**
* @param {string} id
* @deprecated Use .env for managing sensitive data instead
*/
static getCredentials(id) {
if (id in creds) {
Expand Down
2 changes: 1 addition & 1 deletion packages/content/lib/launchpad-content.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export class LaunchpadContent {
}
}

/** @type {Required<import('./content-options.js').ResolvedContentOptions>} */
/** @type {import('./content-options.js').ResolvedContentOptions} */
_config;

/** @type {Logger} */
Expand Down
4 changes: 2 additions & 2 deletions packages/content/test/test.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { launchFromCli, ConfigManager } from '@bluecadet/launchpad-utils';
import { launchFromCli, importJsConfig } from '@bluecadet/launchpad-utils';
import LaunchpadContent from '../lib/launchpad-content.js';

const getConfig = async (paths = ['user-config.js', 'config.js']) => {
return ConfigManager.importJsConfig(paths, import.meta);
return importJsConfig(paths, import.meta);
};

launchFromCli(import.meta, {
Expand Down
91 changes: 58 additions & 33 deletions packages/launchpad/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ Launchpad is a highly configurable suite of tools to manage media installations.
%%{ init: { 'flowchart': { 'curve': 'bumpX' } } }%%
graph LR
Launchpad:::package

Launchpad --> Scaffold:::package -.-> PCs([PCs])
Launchpad --> Content:::package -.-> APIs([APIs])
Launchpad --> Monitor:::package -.-> Apps([Apps])

APIs -.-> Cache[(Cache)]
Apps -.-> Cache

Expand All @@ -32,14 +32,14 @@ graph LR

1. Install launchpad: `npm i @bluecadet/launchpad`
2. Create a `launchpad.config.js` config (see [configuration](#configuration))
3. *Optional: [Bootstrap](/packages/scaffold) your PC with `npx launchpad scaffold`*
3. _Optional: [Bootstrap](/packages/scaffold) your PC with `npx launchpad scaffold`_
4. Run `npx launchpad`

![Screen Recording of Launchpad on Windows 11](https://user-images.githubusercontent.com/295789/197365153-d62d9218-2ffa-4611-ac61-fa5bf786766a.gif)

Run `npx launchpad --help` to see all available commands.

*Note: Launchpad is typically triggered run by a startup task (e.g. Windows Task Scheduler) using `npx launchpad`. When installed globally (`npm i -g @bluecadet/launchpad`), you can use the `launchpad` command instead. See [config loading](#config-loading) for more info.*
_Note: Launchpad is typically triggered run by a startup task (e.g. Windows Task Scheduler) using `npx launchpad`. When installed globally (`npm i -g @bluecadet/launchpad`), you can use the `launchpad` command instead. See [config loading](#config-loading) for more info._

## Configuration

Expand All @@ -49,32 +49,34 @@ Each [launchpad package](#packages) is configured via its own section in `launch
import { defineConfig } from "@bluecadet/launchpad";

export default defineConfig({
"content": {
"sources": [
{
"id": "flickr-images",
"files": {
"spaceships.json": "https://api.flickr.com/services/feeds/photos_public.gne?format=json&nojsoncallback=1&tags=spaceship",
"rockets.json": "https://api.flickr.com/services/feeds/photos_public.gne?format=json&nojsoncallback=1&tags=rocket"
}
}
]
},
"monitor": {
"apps": [
{
"pm2": {
"name": "my-app",
"script": "my-app.exe",
"cwd": "./builds/"
}
}
]
}
content: {
sources: [
{
id: "flickr-images",
files: {
"spaceships.json":
"https://api.flickr.com/services/feeds/photos_public.gne?format=json&nojsoncallback=1&tags=spaceship",
"rockets.json":
"https://api.flickr.com/services/feeds/photos_public.gne?format=json&nojsoncallback=1&tags=rocket",
},
},
],
},
monitor: {
apps: [
{
pm2: {
name: "my-app",
script: "my-app.exe",
cwd: "./builds/",
},
},
],
},
});
```

*Note: [Scaffold](/packages/scaffold) is configured separately in a PowerShell file. This is a guided process when you run `npx launchpad scaffold`.*
_Note: [Scaffold](/packages/scaffold) is configured separately in a PowerShell file. This is a guided process when you run `npx launchpad scaffold`._

### Documentation

Expand All @@ -94,19 +96,42 @@ All available config settings across packages can be found in the links below:
### Config Loading

- By default, Launchpad looks for `launchpad.config.js`, `launchpad.config.mjs`, `launchpad.json` or `config.json` at the cwd (where you ran `npx launchpad`/`launchpad` from)
- You can change the default path with `--config=<YOUR_FILE_PATH>` (e.g. `npx launchpad --config=../settings/my-config.json`)
- You can change the default path with `--config=<YOUR_FILE_PATH>` or `-c=<YOUR_FILE_PATH>` (e.g. `npx launchpad --config=../settings/my-config.json`)
- If no config is found, Launchpad will traverse up directories (up to 64) to find one
- All config values can be overridden via `--foo=bar` (e.g. `--logging.level=debug`)

### `.env` Files

Launchpad uses [dotenv](https://github.com/motdotla/dotenv) to load in environment variables from `.env` and `.env.local` files located in the same directory as your config file.

Environment variables are loaded before the config file is parsed, so you can use them in your config file. For example, you can use `process.env.MY_ENV_VAR` in your config file to access the value of `MY_ENV_VAR` in your `.env` file.

> [!WARNING]
> We recommend using `.env.local` for sensitive credentials that should not be committed to source control. You should add `*.local` to your `.gitignore` to avoid them being checked into git.

All Launchpad CLI commands also accept `--env <ENV_FILE_PATH(S)>` (alias `-e`) options to manually specify one or more `.env` files to load. These paths are relative to the CWD (where you ran `npx launchpad`/`launchpad` from).

```sh
# Load ../.env then ../.env.develop
npx launchpad -e ../.env -e ../.env.develop
```

Additionally, the `--cascade-env=<ENV_NAME>` (alias `-E`) option which will load the following files located alongside your config file:

- `.env`
- `.env.local`
- `.env.<ENV_NAME>`
- `.env.<ENV_NAME>.local`

## Packages

This repo is a monorepo that includes the following packages:

* [`@bluecadet/launchpad`](/packages/launchpad)
* [`@bluecadet/launchpad-content`](/packages/content)
* [`@bluecadet/launchpad-monitor`](/packages/monitor)
* [`@bluecadet/launchpad-scaffold`](/packages/scaffold)
* [`@bluecadet/launchpad-utils`](/packages/utils)
- [`@bluecadet/launchpad`](/packages/launchpad)
- [`@bluecadet/launchpad-content`](/packages/content)
- [`@bluecadet/launchpad-monitor`](/packages/monitor)
- [`@bluecadet/launchpad-scaffold`](/packages/scaffold)
- [`@bluecadet/launchpad-utils`](/packages/utils)

Each of these packages can be launched and configured independently (except for utils), so if you only need app-monitoring or content updates, you can install only `@bluecadet/launchpad-monitor` or `@bluecadet/launchpad-content`.

Expand Down
4 changes: 2 additions & 2 deletions packages/launchpad/test/test.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import LaunchpadCore from '@bluecadet/launchpad';
import { ConfigManager, launchFromCli, onExit } from '@bluecadet/launchpad-utils';
import { importJsConfig, launchFromCli, onExit } from '@bluecadet/launchpad-utils';

const getConfig = async (paths = ['user-config.js', 'config.js']) => {
return ConfigManager.importJsConfig(paths, import.meta);
return importJsConfig(paths, import.meta);
};

const wait = async (seconds) => {
Expand Down
Loading