Skip to content

Commit 8005c36

Browse files
committed
refactor: splits up app-config/extensions package
1 parent 7e62083 commit 8005c36

20 files changed

+2205
-2130
lines changed
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import { LiteralSource } from '@app-config/core';
2+
import { forKey } from '@app-config/extension-utils';
3+
import { envDirective } from './env-directive';
4+
5+
describe('$env directive', () => {
6+
it('fails when not in an environment', async () => {
7+
const source = new LiteralSource({ $env: {} });
8+
await expect(source.read([envDirective()])).rejects.toThrow();
9+
});
10+
11+
it('fails when no options match current environment', async () => {
12+
process.env.NODE_ENV = 'test';
13+
const source = new LiteralSource({ $env: { dev: true } });
14+
await expect(source.read([envDirective()])).rejects.toThrow();
15+
});
16+
17+
it('fails when options is not an object', async () => {
18+
const source = new LiteralSource({
19+
foo: {
20+
$env: 'invalid',
21+
},
22+
});
23+
24+
await expect(source.read([envDirective()])).rejects.toThrow();
25+
});
26+
27+
it('resolves to default environment', async () => {
28+
const source = new LiteralSource({ $env: { default: 42 } });
29+
const parsed = await source.read([envDirective()]);
30+
31+
expect(parsed.toJSON()).toEqual(42);
32+
});
33+
34+
it('fails to resolve with no current environment', async () => {
35+
process.env.NODE_ENV = undefined;
36+
37+
const source = new LiteralSource({ $env: { test: 42 } });
38+
await expect(source.read([envDirective()])).rejects.toThrow();
39+
});
40+
41+
it('resolves to default with no current environment', async () => {
42+
process.env.NODE_ENV = undefined;
43+
44+
const source = new LiteralSource({ $env: { default: 42 } });
45+
46+
expect(await source.readToJSON([envDirective()])).toBe(42);
47+
});
48+
49+
it('resolves to test environment', async () => {
50+
process.env.NODE_ENV = 'test';
51+
const source = new LiteralSource({ $env: { test: 84, default: 42 } });
52+
const parsed = await source.read([envDirective()]);
53+
54+
expect(parsed.toJSON()).toEqual(84);
55+
});
56+
57+
it('resolves to environment alias', async () => {
58+
process.env.NODE_ENV = 'development';
59+
const source = new LiteralSource({ $env: { dev: 84, default: 42 } });
60+
const parsed = await source.read([envDirective()]);
61+
62+
expect(parsed.toJSON()).toEqual(84);
63+
});
64+
65+
it('uses environment alias', async () => {
66+
process.env.NODE_ENV = 'dev';
67+
const source = new LiteralSource({ $env: { development: 84, default: 42 } });
68+
const parsed = await source.read([envDirective()]);
69+
70+
expect(parsed.toJSON()).toEqual(84);
71+
});
72+
73+
it('resolves to object', async () => {
74+
process.env.NODE_ENV = 'test';
75+
const source = new LiteralSource({
76+
$env: { test: { testing: true }, default: { testing: false } },
77+
});
78+
79+
const parsed = await source.read([envDirective()]);
80+
expect(parsed.toJSON()).toEqual({ testing: true });
81+
});
82+
83+
it('resolves to null', async () => {
84+
process.env.NODE_ENV = 'test';
85+
const source = new LiteralSource({
86+
$env: { test: null },
87+
});
88+
89+
const parsed = await source.read([envDirective()]);
90+
expect(parsed.toJSON()).toEqual(null);
91+
});
92+
93+
it('uses the none option', async () => {
94+
delete process.env.NODE_ENV;
95+
const source = new LiteralSource({
96+
$env: { default: 1, none: 2 },
97+
});
98+
99+
const parsed = await source.read([envDirective()]);
100+
expect(parsed.toJSON()).toEqual(2);
101+
});
102+
103+
it('uses the default over the none option when env is defined', async () => {
104+
process.env.NODE_ENV = 'test';
105+
const source = new LiteralSource({
106+
$env: { default: 1, none: 2 },
107+
});
108+
109+
const parsed = await source.read([envDirective()]);
110+
expect(parsed.toJSON()).toEqual(1);
111+
});
112+
113+
it('doesnt evaluate non-current environment', async () => {
114+
const failDirective = forKey('$fail', () => () => {
115+
throw new Error();
116+
});
117+
118+
process.env.NODE_ENV = 'test';
119+
const source = new LiteralSource({
120+
$env: { test: null, dev: { $fail: true } },
121+
});
122+
123+
const parsed = await source.read([envDirective(), failDirective]);
124+
expect(parsed.toJSON()).toEqual(null);
125+
});
126+
127+
it('merges selection with sibling keys', async () => {
128+
const source = new LiteralSource({
129+
sibling: true,
130+
testing: false,
131+
$env: {
132+
test: { testing: true },
133+
default: { testing: false },
134+
},
135+
});
136+
137+
process.env.NODE_ENV = 'test';
138+
const parsed = await source.read([envDirective()]);
139+
expect(parsed.toJSON()).toEqual({ sibling: true, testing: true });
140+
141+
process.env.NODE_ENV = 'development';
142+
const parsed2 = await source.read([envDirective()]);
143+
expect(parsed2.toJSON()).toEqual({ sibling: true, testing: false });
144+
});
145+
});
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import type { ParsingExtension } from '@app-config/core';
2+
import { AppConfigError } from '@app-config/core';
3+
import { named, forKey, keysToPath, validateOptions } from '@app-config/extension-utils';
4+
import { currentEnvironment, defaultAliases, EnvironmentAliases } from '@app-config/node';
5+
6+
/** Looks up an environment-specific value ($env) */
7+
export function envDirective(
8+
aliases: EnvironmentAliases = defaultAliases,
9+
environmentOverride?: string,
10+
environmentSourceNames?: string[] | string,
11+
): ParsingExtension {
12+
const environment = environmentOverride ?? currentEnvironment(aliases, environmentSourceNames);
13+
const metadata = { shouldOverride: true };
14+
15+
return named(
16+
'$env',
17+
forKey(
18+
'$env',
19+
validateOptions(
20+
(SchemaBuilder) => SchemaBuilder.emptySchema().addAdditionalProperties(),
21+
(value, _, ctx) => (parse) => {
22+
if (!environment) {
23+
if ('none' in value) {
24+
return parse(value.none, metadata);
25+
}
26+
27+
if ('default' in value) {
28+
return parse(value.default, metadata);
29+
}
30+
31+
throw new AppConfigError(
32+
`An $env directive was used (in ${keysToPath(
33+
ctx,
34+
)}), but current environment (eg. NODE_ENV) is undefined`,
35+
);
36+
}
37+
38+
for (const [envName, envValue] of Object.entries(value)) {
39+
if (envName === environment || aliases[envName] === environment) {
40+
return parse(envValue, metadata);
41+
}
42+
}
43+
44+
if ('default' in value) {
45+
return parse(value.default, metadata);
46+
}
47+
48+
const found = Object.keys(value).join(', ');
49+
50+
throw new AppConfigError(
51+
`An $env directive was used (in ${keysToPath(
52+
ctx,
53+
)}), but none matched the current environment (wanted ${environment}, saw [${found}])`,
54+
);
55+
},
56+
// $env is lazy so that non-applicable envs don't get evaluated
57+
{ lazy: true },
58+
),
59+
),
60+
);
61+
}

0 commit comments

Comments
 (0)