Skip to content

Commit 9e578e1

Browse files
authored
Merge pull request #1144 from OpenFn/dotenv
Support .env files
2 parents e8dafa7 + 5d1b875 commit 9e578e1

11 files changed

Lines changed: 171 additions & 4 deletions

File tree

.changeset/sixty-books-tell.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@openfn/logger': minor
3+
---
4+
5+
Add colour to output

.changeset/twelve-boats-matter.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@openfn/cli': minor
3+
---
4+
5+
Support .env files in all CLI commands

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
.env
2+
13
# dev
24
*.code-workspace
35
examples/**/dist
@@ -35,3 +37,5 @@ tmp
3537

3638
*.drawio.bkp
3739
.fuse
40+
41+

packages/cli/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
"@openfn/project": "workspace:^",
5858
"@openfn/runtime": "workspace:*",
5959
"chalk": "^5.6.2",
60+
"dotenv": "^17.2.3",
6061
"figures": "^5.0.0",
6162
"rimraf": "^6.0.1",
6263
"treeify": "^1.1.0",

packages/cli/src/cli.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import yargs, { Arguments } from 'yargs';
22
import { hideBin } from 'yargs/helpers';
33

4+
import loadDotEnv from './env';
45
import apolloCommand from './apollo/command';
56
import collectionsCommand from './collections/command';
67
import compileCommand from './compile/command';
@@ -18,6 +19,13 @@ import checkoutCommand from './checkout/command';
1819
import mergeCommand from './merge/command';
1920
import workflowVersionCommand from './version/command';
2021

22+
const env = loadDotEnv();
23+
if (env) {
24+
// Write the env so that the inner process can use it
25+
// This is just a convenience for logging back to the user
26+
process.env.$DOT_ENV_OVERRIDES = Object.keys(env).join(',');
27+
}
28+
2129
const y = yargs(hideBin(process.argv));
2230

2331
export const cmd = y

packages/cli/src/commands.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import mapAdaptorsToMonorepo, {
2121
} from './util/map-adaptors-to-monorepo';
2222
import printVersions from './util/print-versions';
2323
import abort from './util/abort';
24+
import { report } from './env';
2425

2526
export type CommandList =
2627
| 'apollo'
@@ -80,6 +81,9 @@ const parse = async (options: Opts, log?: Logger) => {
8081
await printVersions(logger, options);
8182
}
8283

84+
// Tell the user whether we're using env vars
85+
report(logger);
86+
8387
const { monorepoPath } = options;
8488
if (monorepoPath) {
8589
// TODO how does this occur?

packages/cli/src/env.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { Logger } from '@openfn/logger';
2+
import dotenv from 'dotenv';
3+
4+
let env: any;
5+
6+
export default (path?: string) => {
7+
env = dotenv.config({ path, override: true, debug: false, quiet: true });
8+
if (env.error) {
9+
return null;
10+
}
11+
return env.parsed;
12+
};
13+
14+
export const report = (logger?: Logger) => {
15+
// workaround for the CLI's inner process
16+
17+
let envs = [];
18+
if (process.env.$DOT_ENV_OVERRIDES) {
19+
envs = process.env.$DOT_ENV_OVERRIDES.split(',').map((s) => s.trim());
20+
} else {
21+
envs = Object.keys(env?.parsed ?? {});
22+
}
23+
24+
if (envs.length) {
25+
logger?.always(`Imported ${envs.length} env vars from .env file`);
26+
logger?.debug('Envs set from .env: ', envs.join(', '));
27+
} else if (env && env.error) {
28+
logger?.debug('.env not found');
29+
}
30+
};

packages/cli/test/commands.test.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -761,7 +761,7 @@ test.serial('docs should print documentation with full names', async (t) => {
761761
opts.repoDir = '/repo';
762762

763763
await commandParser(opts, logger);
764-
const docs = logger._parse(logger._history[2]).message as string;
764+
const docs = logger._find('print', /API Reference/).message;
765765

766766
// match the signature
767767
t.regex(docs, /\#\# fn\(\)/);
@@ -796,7 +796,7 @@ test.serial('docs adaptor should list operations', async (t) => {
796796

797797
await commandParser(opts, logger);
798798

799-
const docs = logger._parse(logger._history[3]).message as string;
799+
const docs = logger._find('print', /language-common/).message;
800800
t.notRegex(docs, /\[object Object\]/);
801801
t.notRegex(docs, /\#\#\# Usage Examples/);
802802
t.regex(docs, /fn.+\(a, b\)/);
@@ -823,7 +823,8 @@ test.serial(
823823
opts.repoDir = '/repo';
824824

825825
await commandParser(opts, logger);
826-
const docs = logger._parse(logger._history[2]).message as string;
826+
827+
const docs = logger._find('print', /API Reference/).message;
827828
// match the signature
828829
t.regex(docs, /\#\# fn\(\)/);
829830
// Match usage examples

packages/cli/test/env.test.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import test from 'ava';
2+
import mock from 'mock-fs';
3+
import { createMockLogger } from '@openfn/logger';
4+
5+
import load, { report } from '../src/env';
6+
7+
const logger = createMockLogger(undefined, { level: 'debug' });
8+
9+
test.afterEach(() => {
10+
logger._reset();
11+
mock.restore();
12+
});
13+
14+
test('should load and return values from .env', (t) => {
15+
mock({
16+
'/.env': `
17+
FOO: BAR
18+
`,
19+
});
20+
21+
const env = load('/.env');
22+
t.deepEqual(env, {
23+
FOO: 'BAR',
24+
});
25+
});
26+
27+
test('should report values loaded from .env', (t) => {
28+
mock({
29+
'/.env': `
30+
FOO: BAR
31+
`,
32+
});
33+
34+
load('/.env');
35+
36+
report(logger);
37+
38+
const a = logger._find('always', /imported 1 env vars/i);
39+
t.truthy(a);
40+
41+
const b = logger._find('debug', /set from .env:/i);
42+
t.truthy(b);
43+
44+
const c = logger._find('debug', /FOO/);
45+
t.truthy(c);
46+
});
47+
48+
test('should return null if no env is found', (t) => {
49+
mock({});
50+
51+
const env = load('/.env');
52+
t.is(env, null);
53+
});
54+
55+
test('should log if no env is found', (t) => {
56+
mock({});
57+
58+
load('/.env');
59+
60+
report(logger);
61+
62+
const a = logger._find('debug', /.env not found/i);
63+
t.truthy(a);
64+
});
65+
66+
test('should use $DOT_ENV_OVERRIDES found', (t) => {
67+
mock({});
68+
69+
// don't load .env
70+
// but set this magic env var instead
71+
process.env.$DOT_ENV_OVERRIDES = 'FOO,BAR';
72+
73+
report(logger);
74+
75+
const a = logger._find('always', /imported 1 env vars/i);
76+
t.truthy(a);
77+
78+
const b = logger._find('debug', /set from .env:/i);
79+
t.truthy(b);
80+
81+
const c = logger._find('debug', /FOO, BAR/);
82+
t.truthy(c);
83+
});

packages/logger/src/logger.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,21 @@ export type JSONLog = {
5353

5454
export type StringLog = [LogFns | 'confirm' | 'print', ...any];
5555

56+
export const colourize = (level: string, str: string) => {
57+
if (typeof str === 'string') {
58+
if (level === DEBUG) {
59+
return c.grey(str);
60+
}
61+
if (level === ERROR) {
62+
return c.red(str);
63+
}
64+
if (level === WARN) {
65+
return c.yellow(str);
66+
}
67+
}
68+
return str;
69+
};
70+
5671
// Design for a logger
5772
// some inputs:
5873
// This will be passed into a job, so must look like the console object
@@ -224,7 +239,9 @@ export default function (name?: string, options: LogOptions = {}): Logger {
224239
output.push(styleLevel(level));
225240
}
226241

227-
emitter[level](...output.concat(cleanedArgs));
242+
emitter[level](
243+
...output.concat(cleanedArgs).map((s) => colourize(level, s))
244+
);
228245
}
229246
}
230247
};

0 commit comments

Comments
 (0)