diff --git a/packages/core/src/cli/commands/resolve.command.ts b/packages/core/src/cli/commands/resolve.command.ts index 8786c465..a405f8d1 100644 --- a/packages/core/src/cli/commands/resolve.command.ts +++ b/packages/core/src/cli/commands/resolve.command.ts @@ -14,6 +14,7 @@ import { addCacheFlags } from '../lib/cache-helpers'; import { addWatchMode } from '../lib/watch-mode-helpers'; import { CliExitError } from '../lib/cli-error'; import { checkForConfigErrors, checkForSchemaErrors } from '../lib/check-errors-helpers'; +import { stringifyObjectAsEnvFile } from '../lib/env-file-helpers'; const program = new DmnoCommand('resolve') .summary('Loads config schema and resolves config values') @@ -23,7 +24,9 @@ const program = new DmnoCommand('resolve') .option('--show-all', 'shows all items, even when config is failing') .example('dmno resolve', 'Loads the resolved config for the root service') .example('dmno resolve --service service1', 'Loads the resolved config for service1') - .example('dmno resolve --service service1 --format json', 'Loads the resolved config for service1 in JSON format'); + .example('dmno resolve --service service1 --format json', 'Loads the resolved config for service1 in JSON format') + .example('dmno resolve --service service1 --format env', 'Loads the resolved config for service1 and outputs it in .env file format') + .example('dmno resolve --service service1 --format env >> .env.local', 'Loads the resolved config for service1 and outputs it in .env file format and writes to .env.local'); addWatchMode(program); // must be first addCacheFlags(program); @@ -55,20 +58,23 @@ program.action(async (opts: { await workspace.resolveConfig(); checkForConfigErrors(service, { showAll: opts?.showAll }); - // console.log(service.config); - if (opts.format === 'json') { + const getExposedConfigValues = () => { let exposedConfig = service.config; if (opts.public) { exposedConfig = _.pickBy(exposedConfig, (c) => !c.type.getMetadata('sensitive')); } - const valuesOnly = _.mapValues(exposedConfig, (val) => val.resolvedValue); + return _.mapValues(exposedConfig, (val) => val.resolvedValue); + }; - console.log(JSON.stringify(valuesOnly)); + // console.log(service.config); + if (opts.format === 'json') { + console.log(JSON.stringify(getExposedConfigValues())); } else if (opts.format === 'json-full') { - // TODO: this includes sensitive info when using --public option console.dir(service.toJSON(), { depth: null }); } else if (opts.format === 'json-injected') { console.log(JSON.stringify(service.configraphEntity.getInjectedEnvJSON())); + } else if (opts.format === 'env') { + console.log(stringifyObjectAsEnvFile(getExposedConfigValues())); } else { _.each(service.config, (item) => { console.log(getItemSummary(item.toJSON())); diff --git a/packages/core/src/cli/lib/env-file-helpers.test.ts b/packages/core/src/cli/lib/env-file-helpers.test.ts new file mode 100644 index 00000000..4e0af09d --- /dev/null +++ b/packages/core/src/cli/lib/env-file-helpers.test.ts @@ -0,0 +1,21 @@ +import { expect, test, describe } from 'vitest'; +import { stringifyObjectAsEnvFile } from './env-file-helpers'; + +describe('stringifyObjectAsEnvFile', () => { + test('basic', () => { + const result = stringifyObjectAsEnvFile({ foo: 'bar', baz: 'qux' }); + expect(result).toEqual('foo="bar"\nbaz="qux"'); + }); + test('escapes backslashes', () => { + const result = stringifyObjectAsEnvFile({ foo: 'bar\\baz' }); + expect(result).toEqual('foo="bar\\\\baz"'); + }); + test('escapes newlines', () => { + const result = stringifyObjectAsEnvFile({ foo: 'bar\nbaz' }); + expect(result).toEqual('foo="bar\\nbaz"'); + }); + test('escapes double quotes', () => { + const result = stringifyObjectAsEnvFile({ foo: 'bar"baz' }); + expect(result).toEqual('foo="bar\\"baz"'); + }); +}); diff --git a/packages/core/src/cli/lib/env-file-helpers.ts b/packages/core/src/cli/lib/env-file-helpers.ts new file mode 100644 index 00000000..d6610023 --- /dev/null +++ b/packages/core/src/cli/lib/env-file-helpers.ts @@ -0,0 +1,11 @@ +export function stringifyObjectAsEnvFile(obj: Record) { + return Object.entries(obj).map(([key, value]) => { + // Handle newlines and quotes by wrapping in double quotes and escaping + const formattedValue = String(value) + .replace(/\\/g, '\\\\') // escape backslashes first + .replace(/\n/g, '\\n') // escape newlines + .replace(/"/g, '\\"'); // escape double quotes + + return `${key}="${formattedValue}"`; + }).join('\n'); +} diff --git a/packages/docs-site/src/content/docs/docs/guides/env-files.mdx b/packages/docs-site/src/content/docs/docs/guides/env-files.mdx index 221fd584..a60fec55 100644 --- a/packages/docs-site/src/content/docs/docs/guides/env-files.mdx +++ b/packages/docs-site/src/content/docs/docs/guides/env-files.mdx @@ -50,4 +50,24 @@ If you're using [plugins](/docs/plugins/overview/) to handle your sensitive conf When running `dmno init`, we prompt you to move any gitignored `.env` files we find into your `.dmno` folder. This means that other tools that may be looking for will not find them - which is on purpose. Instead, you should pass resolved config to those external tools via `dmno run`, whether `.env` files are being used or not. ::: +## Resolving config and outputting to .env format + +You can use the [`dmno resolve` command](/docs/reference/cli/resolve/) to load the resolved config for a service and output it in `.env` file format. This is useful for quickly exporting all your config values to a file for use in other systems, especially in some serverless environments where you may need to set a lot of environment variables at once and you don't have as much control over the running process as you do locally. + +Consider the following example where we want to load an `.env` file for use in Supabase Edge Functions. + +You can run the following command to load the resolved config for your `api` service and output it to a file. + +```bash +pnpm exec dmno resolve --service api --format env >> .env.production +supabase secrets set --env-file .env.production +``` + +If you want to do this with a single command, you can combine them like this: + +```bash +supabase secrets set --env-file <(pnpm exec dmno resolve --service api --format env) +``` +This has the added benefit of writing no file, so you don't need to worry about deleting it later or accidentally checking it into source control. + {/* Will need to add a note about nested config and special `__` separator (ie PARENT__CHILD) */}