Skip to content

Commit 3d53a24

Browse files
authored
Merge pull request #98 from launchcodedev/environment-meta-settings
Provide meta file options for `environmentAliases` and `environmentTypeNames`
2 parents a829ade + 17d1894 commit 3d53a24

File tree

7 files changed

+235
-22
lines changed

7 files changed

+235
-22
lines changed

app-config/src/config-source.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,14 +81,16 @@ export class FlexibleFileSource extends ConfigSource {
8181
private readonly environmentOverride?: string,
8282
private readonly environmentAliases: EnvironmentAliases = defaultAliases,
8383
private readonly fileExtensions: string[] = ['yml', 'yaml', 'toml', 'json', 'json5'],
84+
private readonly environmentSourceNames?: string[] | string,
8485
) {
8586
super();
8687
}
8788

8889
// share 'resolveSource' so that read() returns a ParsedValue pointed to the FileSource, not FlexibleFileSource
8990
private async resolveSource(): Promise<FileSource> {
9091
const aliases = this.environmentAliases;
91-
const environment = this.environmentOverride ?? currentEnvironment(aliases);
92+
const environment =
93+
this.environmentOverride ?? currentEnvironment(aliases, this.environmentSourceNames);
9294
const environmentAlias = Object.entries(aliases).find(([, v]) => v === environment)?.[0];
9395

9496
const filesToTry = [];

app-config/src/config.test.ts

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -573,3 +573,165 @@ describe('Dynamic Parsing Extension Loading', () => {
573573
);
574574
});
575575
});
576+
577+
describe('Environment Aliases', () => {
578+
it('uses environmentAliases as an override', async () => {
579+
await withTempFiles(
580+
{
581+
'.app-config.yml': `
582+
foo: bar-default
583+
`,
584+
'.app-config.production.yml': `
585+
foo: bar-production
586+
`,
587+
'.app-config.meta.yml': `
588+
environmentAliases:
589+
Release: production
590+
`,
591+
},
592+
async (inDir) => {
593+
process.env.APP_CONFIG_ENV = 'Release';
594+
const { fullConfig } = await loadConfig({ directory: inDir('.') });
595+
596+
expect(fullConfig).toEqual({ foo: 'bar-production' });
597+
},
598+
);
599+
});
600+
601+
it('uses environmentAliases in $env', async () => {
602+
await withTempFiles(
603+
{
604+
'.app-config.yml': `
605+
foo:
606+
$env:
607+
default: bar-default
608+
production: bar-production
609+
`,
610+
'.app-config.meta.yml': `
611+
environmentAliases:
612+
Release: production
613+
`,
614+
},
615+
async (inDir) => {
616+
process.env.APP_CONFIG_ENV = 'Release';
617+
const { fullConfig } = await loadConfig({ directory: inDir('.') });
618+
619+
expect(fullConfig).toEqual({ foo: 'bar-production' });
620+
},
621+
);
622+
});
623+
624+
it('uses environmentAliases for $APP_CONFIG_ENV substitution', async () => {
625+
await withTempFiles(
626+
{
627+
'.app-config.yml': `
628+
foo:
629+
$subs: bar-$APP_CONFIG_ENV
630+
`,
631+
'.app-config.meta.yml': `
632+
environmentAliases:
633+
Release: production
634+
environmentSourceNames:
635+
- CONFIGURATION
636+
`,
637+
},
638+
async (inDir) => {
639+
process.env.CONFIGURATION = 'Release';
640+
const { fullConfig } = await loadConfig({ directory: inDir('.') });
641+
642+
expect(fullConfig).toEqual({ foo: 'bar-production' });
643+
},
644+
);
645+
});
646+
});
647+
648+
describe('Environment Variable Name', () => {
649+
it('uses environmentSourceNames', async () => {
650+
await withTempFiles(
651+
{
652+
'.app-config.yml': `
653+
foo: bar-default
654+
`,
655+
'.app-config.production.yml': `
656+
foo: bar-production
657+
`,
658+
'.app-config.meta.yml': `
659+
environmentSourceNames: CONFIGURATION
660+
`,
661+
},
662+
async (inDir) => {
663+
process.env.CONFIGURATION = 'production';
664+
const { fullConfig } = await loadConfig({ directory: inDir('.') });
665+
666+
expect(fullConfig).toEqual({ foo: 'bar-production' });
667+
},
668+
);
669+
});
670+
671+
it('uses environmentSourceNames for $env', async () => {
672+
await withTempFiles(
673+
{
674+
'.app-config.yml': `
675+
foo:
676+
$env:
677+
default: bar-default
678+
production: bar-production
679+
`,
680+
'.app-config.meta.yml': `
681+
environmentSourceNames: CONFIGURATION
682+
`,
683+
},
684+
async (inDir) => {
685+
process.env.CONFIGURATION = 'production';
686+
const { fullConfig } = await loadConfig({ directory: inDir('.') });
687+
688+
expect(fullConfig).toEqual({ foo: 'bar-production' });
689+
},
690+
);
691+
});
692+
693+
it('uses array for environmentSourceNames', async () => {
694+
await withTempFiles(
695+
{
696+
'.app-config.yml': `
697+
foo:
698+
$env:
699+
default: bar-default
700+
production: bar-production
701+
`,
702+
'.app-config.meta.yml': `
703+
environmentSourceNames:
704+
- CONFIGURATION
705+
- OTHER_ENV
706+
`,
707+
},
708+
async (inDir) => {
709+
process.env.OTHER_ENV = 'production';
710+
const { fullConfig } = await loadConfig({ directory: inDir('.') });
711+
712+
expect(fullConfig).toEqual({ foo: 'bar-production' });
713+
},
714+
);
715+
});
716+
717+
it('uses environmentSourceNames for $APP_CONFIG_ENV substitution', async () => {
718+
await withTempFiles(
719+
{
720+
'.app-config.yml': `
721+
foo:
722+
$subs: $APP_CONFIG_ENV
723+
`,
724+
'.app-config.meta.yml': `
725+
environmentSourceNames:
726+
- CONFIGURATION
727+
`,
728+
},
729+
async (inDir) => {
730+
process.env.CONFIGURATION = 'production';
731+
const { fullConfig } = await loadConfig({ directory: inDir('.') });
732+
733+
expect(fullConfig).toEqual({ foo: 'production' });
734+
},
735+
);
736+
});
737+
});

app-config/src/config.ts

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { FlexibleFileSource, FileSource, EnvironmentSource, FallbackSource } fro
66
import { defaultExtensions, defaultEnvExtensions, markAllValuesAsSecret } from './extensions';
77
import { loadSchema, JSONSchema, Options as SchemaOptions } from './schema';
88
import { NotFoundError, WasNotObject, ReservedKeyError } from './errors';
9-
import { loadExtraParsingExtensions } from './meta';
9+
import { loadMetaConfig, loadExtraParsingExtensions } from './meta';
1010
import { logger } from './logging';
1111

1212
export interface Options {
@@ -17,6 +17,7 @@ export interface Options {
1717
extensionEnvironmentVariableNames?: string[];
1818
environmentOverride?: string;
1919
environmentAliases?: EnvironmentAliases;
20+
environmentSourceNames?: string[] | string;
2021
parsingExtensions?: ParsingExtension[];
2122
secretsFileExtensions?: ParsingExtension[];
2223
environmentExtensions?: ParsingExtension[];
@@ -45,9 +46,10 @@ export async function loadConfig({
4546
environmentVariableName = 'APP_CONFIG',
4647
extensionEnvironmentVariableNames = ['APP_CONFIG_EXTEND', 'APP_CONFIG_CI'],
4748
environmentOverride,
48-
environmentAliases = defaultAliases,
49-
parsingExtensions = defaultExtensions(environmentAliases, environmentOverride),
50-
secretsFileExtensions = parsingExtensions.concat(markAllValuesAsSecret()),
49+
environmentAliases: environmentAliasesArg,
50+
environmentSourceNames: environmentSourceNamesArg,
51+
parsingExtensions: parsingExtensionsArg,
52+
secretsFileExtensions: secretsFileExtensionsArg,
5153
environmentExtensions = defaultEnvExtensions(),
5254
defaultValues,
5355
}: Options = {}): Promise<Configuration> {
@@ -70,26 +72,44 @@ export async function loadConfig({
7072
if (!(error instanceof NotFoundError)) throw error;
7173
}
7274

73-
logger.verbose(`Trying to read files for configuration`);
75+
const meta = await loadMetaConfig({ directory });
76+
77+
const environmentSourceNames = environmentSourceNamesArg ?? meta.value.environmentSourceNames;
78+
const environmentAliases =
79+
environmentAliasesArg ?? meta.value.environmentAliases ?? defaultAliases;
80+
81+
const parsingExtensions =
82+
parsingExtensionsArg ??
83+
defaultExtensions(environmentAliases, environmentOverride, undefined, environmentSourceNames);
7484

75-
const extraParsingExtensions = await loadExtraParsingExtensions({ directory });
85+
const secretsFileExtensions =
86+
secretsFileExtensionsArg ?? parsingExtensions.concat(markAllValuesAsSecret());
87+
88+
logger.verbose(`Loading extra parsing extensions`);
89+
const extraParsingExtensions = await loadExtraParsingExtensions(meta);
7690

7791
logger.verbose(`${extraParsingExtensions.length} user-defined parsing extensions found`);
7892

7993
parsingExtensions.splice(0, 0, ...extraParsingExtensions);
8094
secretsFileExtensions.splice(0, 0, ...extraParsingExtensions);
8195

96+
logger.verbose(`Trying to read files for configuration`);
97+
8298
const [mainConfig, secrets] = await Promise.all([
8399
new FlexibleFileSource(
84100
join(directory, fileNameBase),
85101
environmentOverride,
86102
environmentAliases,
103+
undefined,
104+
environmentSourceNames,
87105
).read(parsingExtensions),
88106

89107
new FlexibleFileSource(
90108
join(directory, secretsFileNameBase),
91109
environmentOverride,
92110
environmentAliases,
111+
undefined,
112+
environmentSourceNames,
93113
)
94114
.read(secretsFileExtensions)
95115
.catch((error) => {

app-config/src/environment.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,25 @@ export const defaultAliases: EnvironmentAliases = {
77
dev: 'development',
88
};
99

10-
export function currentEnvironment(aliases: EnvironmentAliases = defaultAliases) {
11-
const value = process.env.APP_CONFIG_ENV ?? process.env.NODE_ENV ?? process.env.ENV;
10+
export function currentEnvironment(
11+
environmentAliases: EnvironmentAliases = defaultAliases,
12+
environmentSourceNames: string[] | string = ['APP_CONFIG_ENV', 'NODE_ENV', 'ENV'],
13+
) {
14+
let value: string | undefined;
15+
16+
for (const name of Array.isArray(environmentSourceNames)
17+
? environmentSourceNames
18+
: [environmentSourceNames]) {
19+
if (process.env[name]?.length) {
20+
value = process.env[name];
21+
break;
22+
}
23+
}
1224

1325
if (!value) return undefined;
1426

15-
if (aliases[value]) {
16-
return aliases[value];
27+
if (environmentAliases[value]) {
28+
return environmentAliases[value];
1729
}
1830

1931
return value;

app-config/src/extensions.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,18 @@ export function defaultExtensions(
1414
aliases: EnvironmentAliases = defaultAliases,
1515
environmentOverride?: string,
1616
symmetricKey?: DecryptedSymmetricKey,
17+
environmentSourceNames?: string[] | string,
1718
): ParsingExtension[] {
1819
return [
1920
v1Compat(),
20-
envDirective(aliases, environmentOverride),
21+
envDirective(aliases, environmentOverride, environmentSourceNames),
2122
extendsDirective(),
2223
extendsSelfDirective(),
2324
overrideDirective(),
2425
encryptedDirective(symmetricKey),
2526
timestampDirective(),
2627
unescape$Directives(),
27-
environmentVariableSubstitution(aliases, environmentOverride),
28+
environmentVariableSubstitution(aliases, environmentOverride, environmentSourceNames),
2829
gitRefDirectives(),
2930
];
3031
}
@@ -81,8 +82,9 @@ export function extendsSelfDirective(): ParsingExtension {
8182
export function envDirective(
8283
aliases: EnvironmentAliases = defaultAliases,
8384
environmentOverride?: string,
85+
environmentSourceNames?: string[] | string,
8486
): ParsingExtension {
85-
const environment = environmentOverride ?? currentEnvironment(aliases);
87+
const environment = environmentOverride ?? currentEnvironment(aliases, environmentSourceNames);
8688
const metadata = { shouldOverride: true };
8789

8890
return (value, [_, key]) => {
@@ -184,6 +186,7 @@ export function unescape$Directives(): ParsingExtension {
184186
export function environmentVariableSubstitution(
185187
aliases: EnvironmentAliases = defaultAliases,
186188
environmentOverride?: string,
189+
environmentSourceNames?: string[] | string,
187190
): ParsingExtension {
188191
const performAllSubstitutions = (text: string): string => {
189192
let output = text;
@@ -216,7 +219,8 @@ export function environmentVariableSubstitution(
216219
// we'll recurse again, so that ${FOO:-${FALLBACK}} -> ${FALLBACK} -> value
217220
output = performAllSubstitutions(output.replace(fullMatch, fallback));
218221
} else if (varName === 'APP_CONFIG_ENV') {
219-
const envType = environmentOverride ?? currentEnvironment(aliases);
222+
const envType =
223+
environmentOverride ?? currentEnvironment(aliases, environmentSourceNames);
220224

221225
if (!envType) {
222226
throw new AppConfigError(`Could not find environment variable ${varName}`);

app-config/src/meta.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ export interface MetaProperties {
3131
encryptionKeys?: EncryptedSymmetricKey[];
3232
generate?: GenerateFile[];
3333
parsingExtensions?: (ParsingExtensionWithOptions | string)[];
34+
environmentAliases?: Record<string, string>;
35+
environmentSourceNames?: string[] | string;
3436
}
3537

3638
export interface MetaConfiguration {
@@ -109,14 +111,14 @@ export async function loadMetaConfigLazy(options?: Options): Promise<MetaConfigu
109111
return metaConfig;
110112
}
111113

112-
export async function loadExtraParsingExtensions(options?: Options): Promise<ParsingExtension[]> {
113-
return loadMetaConfig(options).then(({ value }) => {
114-
if (value.parsingExtensions) {
115-
return Promise.all(value.parsingExtensions.map(loadExtraParsingExtension));
116-
}
114+
export async function loadExtraParsingExtensions({
115+
value,
116+
}: MetaConfiguration): Promise<ParsingExtension[]> {
117+
if (value.parsingExtensions) {
118+
return Promise.all(value.parsingExtensions.map(loadExtraParsingExtension));
119+
}
117120

118-
return Promise.resolve([]);
119-
});
121+
return Promise.resolve([]);
120122
}
121123

122124
export async function loadExtraParsingExtension(

docs/guide/intro/settings.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Currently, the important values stored in meta files are:
1111

1212
1. Encryption keys and team members
1313
2. Code generation steps
14+
3. Overrides for parsing and environment variable names
1415

1516
One thing is special about meta files - if one is _not_ found in the current working directory,
1617
App Config will try to look at parent directories until it finds a repository root.
@@ -38,6 +39,16 @@ encryptionKeys:
3839
key: "-----BEGIN PGP MESSAGE-----\r\nVersion: OpenPGP.js v4.10.8\r\nComment: https://openpgpjs.org\r\n\r\n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\r\nbg==\r\n=xBAf\r\n-----END PGP MESSAGE-----\r\n"
3940
```
4041
42+
## Parse and Loading Configuration
43+
44+
The meta file can contain:
45+
46+
- `parsingExtensions`: an array of NPM modules to load as parsing extensions ([more](./extensions.md#loading-custom-extensions))
47+
- `environmentAliases`: an object that maps 'aliases' to real environment names
48+
- eg. `{ Release: 'production' }`
49+
- `environmentSourceNames`: a string or array of strings that are used an environment variable names to read for the current environment
50+
- eg. `['ENV', 'NODE_ENV', 'MY_ENV']`
51+
4152
## App Config Settings
4253

4354
There are a small set of user settings that App Config stores.

0 commit comments

Comments
 (0)