Skip to content

Commit

Permalink
allow .env style output from resolve and add associated docs
Browse files Browse the repository at this point in the history
  • Loading branch information
philmillman committed Nov 12, 2024
1 parent 03eb697 commit aab732c
Show file tree
Hide file tree
Showing 4 changed files with 64 additions and 6 deletions.
18 changes: 12 additions & 6 deletions packages/core/src/cli/commands/resolve.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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);
Expand Down Expand Up @@ -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()));
Expand Down
21 changes: 21 additions & 0 deletions packages/core/src/cli/lib/env-file-helpers.test.ts
Original file line number Diff line number Diff line change
@@ -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"');
});
});
11 changes: 11 additions & 0 deletions packages/core/src/cli/lib/env-file-helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export function stringifyObjectAsEnvFile(obj: Record<string, string>) {
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');
}
20 changes: 20 additions & 0 deletions packages/docs-site/src/content/docs/docs/guides/env-files.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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) */}

0 comments on commit aab732c

Please sign in to comment.