Skip to content

Commit 365a59a

Browse files
committed
Merge branch 'master' into v3
2 parents c0995d8 + 5e0cf0d commit 365a59a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+3498
-2194
lines changed

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
1+
## v2.5.1
2+
3+
Adds `env` option on `$extends` and `$override` directives.
4+
5+
## v2.5.0
6+
7+
Adds some early deprecation warnings in preparation for version 3. See [here](https://github.com/launchcodedev/app-config/issues/130).
8+
9+
Allows default extensions to be used twice without conflicts.
10+
11+
Adds `$parseBool`, `$parseFloat` and `$parseInt` directives.
12+
13+
Adds `allowMissing` option on `$envVar` that's a shorthand for `allowNull` + `fallback: null`.
14+
15+
Creates new calling interface for `currentEnvironment` and `FlexibleFileSource`.
16+
117
## v2.4.5
218

319
Adds `$hidden` directive mainly for shared properties.

app-config-cli/src/validation.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,19 @@ import { FileSource, EnvironmentAliases, defaultAliases } from '@app-config/node
44
import { loadValidatedConfig } from '@app-config/config';
55

66
export interface Options {
7+
/** Where app-config files are */
78
directory?: string;
9+
/** Override for aliasing of environments */
810
environmentAliases?: EnvironmentAliases;
11+
/** If app-config should be validating in a "no current environment" state */
912
includeNoEnvironment?: boolean;
1013
}
1114

15+
/**
16+
* Loads and validations app-config values in every environment detectable.
17+
*
18+
* Uses a hueristic to find which environments are available, because these are arbitrary.
19+
*/
1220
export async function validateAllConfigVariants({
1321
directory = '.',
1422
environmentAliases = defaultAliases,
@@ -24,9 +32,9 @@ export async function validateAllConfigVariants({
2432
const appConfigEnvironments = new Set<string>();
2533

2634
for (const filename of appConfigFiles) {
27-
const environment = /^\.app-config\.(?:secrets\.)?(.*)\.(?:yml|yaml|json|json5|toml)$/.exec(
28-
filename,
29-
)?.[1];
35+
// extract the environment out, which is the first capture group
36+
const regex = /^\.app-config\.(?:secrets\.)?(.*)\.(?:yml|yaml|json|json5|toml)$/;
37+
const environment = regex.exec(filename)?.[1];
3038

3139
if (environment && environment !== 'meta' && environment !== 'schema') {
3240
appConfigEnvironments.add(environmentAliases[environment] ?? environment);

app-config-config/src/index.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,35 @@ describe('Configuration Loading', () => {
219219
},
220220
);
221221
});
222+
223+
it('extends from another file with different environment', async () => {
224+
await withTempFiles(
225+
{
226+
'.app-config.yml': `
227+
$extends:
228+
path: ./other-file.yml
229+
env: production
230+
bar:
231+
$envVar: APP_CONFIG_ENV
232+
`,
233+
'other-file.yml': `
234+
foo:
235+
$env:
236+
default: default
237+
prod: production
238+
`,
239+
},
240+
async (inDir) => {
241+
expect(
242+
(await loadUnvalidatedConfig({ directory: inDir('.'), environmentOverride: 'test' }))
243+
.fullConfig,
244+
).toEqual({
245+
foo: 'production',
246+
bar: 'test',
247+
});
248+
},
249+
);
250+
});
222251
});
223252

224253
describe('Configuration Loading Options', () => {

app-config-config/src/index.ts

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
defaultAliases,
1616
EnvironmentAliases,
1717
EnvironmentSource,
18+
asEnvOptions,
1819
} from '@app-config/node';
1920
import { markAllValuesAsSecret } from '@app-config/extensions';
2021
import { defaultExtensions, defaultEnvExtensions } from '@app-config/default-extensions';
@@ -90,6 +91,12 @@ export async function loadUnvalidatedConfig({
9091
const environmentAliases =
9192
environmentAliasesArg ?? meta.value.environmentAliases ?? defaultAliases;
9293

94+
const environmentOptions = asEnvOptions(
95+
environmentOverride,
96+
environmentAliases,
97+
environmentSourceNames,
98+
);
99+
93100
const parsingExtensions = parsingExtensionsArg ?? defaultExtensions();
94101

95102
const secretsFileExtensions =
@@ -106,19 +113,11 @@ export async function loadUnvalidatedConfig({
106113
logger.verbose(`Trying to read files for configuration`);
107114

108115
const [mainConfig, secrets] = await Promise.all([
109-
new FlexibleFileSource(
110-
join(directory, fileNameBase),
111-
environmentOverride,
112-
environmentAliases,
113-
environmentSourceNames,
114-
).read(parsingExtensions),
115-
116-
new FlexibleFileSource(
117-
join(directory, secretsFileNameBase),
118-
environmentOverride,
119-
environmentAliases,
120-
environmentSourceNames,
121-
)
116+
new FlexibleFileSource(join(directory, fileNameBase), undefined, environmentOptions).read(
117+
parsingExtensions,
118+
),
119+
120+
new FlexibleFileSource(join(directory, secretsFileNameBase), undefined, environmentOptions)
122121
.read(secretsFileExtensions)
123122
.catch((error) => {
124123
// NOTE: secrets are optional, so not finding them is normal

app-config-core/src/common.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { logger } from '@app-config/logging';
33

44
export type KeyFormatter = (key: string, separator: string) => string;
55

6+
/** Strategy used in 'app-config vars' for variable names */
67
export function camelToScreamingCase(key: string, separator: string = '_'): string {
78
return key
89
.replace(/([^A-Z]+)([A-Z][a-z])/g, `$1${separator}$2`)
@@ -11,6 +12,7 @@ export function camelToScreamingCase(key: string, separator: string = '_'): stri
1112
.toUpperCase();
1213
}
1314

15+
/** Strategy used in 'app-config vars' to extract variable names from hierachy */
1416
export function flattenObjectTree(
1517
obj: JsonObject,
1618
prefix: string = '',
@@ -40,6 +42,7 @@ export function flattenObjectTree(
4042
}, {});
4143
}
4244

45+
/** Strategy for renaming keys, used for 'app-config vars' */
4346
export function renameInFlattenedTree(
4447
flattened: { [key: string]: string },
4548
renames: string[] = [],

app-config-core/src/config-source.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class FailingSource extends ConfigSource {
1515
}
1616
}
1717

18-
const flattenExtension: ParsingExtension = (value, [[_, key]]) => {
18+
const flattenExtension: ParsingExtension = (value, [_, key]) => {
1919
if (key === '$flatten') {
2020
return (parse) => parse(value, { shouldFlatten: true });
2121
}

app-config-core/src/config-source.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import { logger } from '@app-config/logging';
77
import { ParsedValue, ParsingContext, ParsingExtension } from './parsed-value';
88
import { AppConfigError, NotFoundError, ParsingError, BadFileType } from './errors';
99

10+
/**
11+
* File formats that app-config supports.
12+
*/
1013
export enum FileType {
1114
YAML = 'YAML',
1215
TOML = 'TOML',
@@ -84,7 +87,7 @@ export class CombinedSource extends ConfigSource {
8487
// override so that ParsedValue is directly from the originating ConfigSource
8588
async read(extensions?: ParsingExtension[], context?: ParsingContext): Promise<ParsedValue> {
8689
const values = await Promise.all(
87-
this.sources.map((source) => source.read(extensions, context)),
90+
this.sources.map((source) => source.read(extensions, { ...context })),
8891
);
8992

9093
const merged = values.reduce<ParsedValue | undefined>((acc, parsed) => {
@@ -160,6 +163,9 @@ export class FallbackSource extends ConfigSource {
160163
}
161164
}
162165

166+
/**
167+
* Converts a JSON object to a string, using specified file type.
168+
*/
163169
export function stringify(config: Json, fileType: FileType, minimal: boolean = false): string {
164170
switch (fileType) {
165171
case FileType.JSON:
@@ -182,6 +188,9 @@ export function stringify(config: Json, fileType: FileType, minimal: boolean = f
182188
}
183189
}
184190

191+
/**
192+
* Returns which file type to use, based on the file extension.
193+
*/
185194
export function filePathAssumedType(filePath: string): FileType {
186195
switch (extname(filePath).toLowerCase().slice(1)) {
187196
case 'yml':
@@ -200,6 +209,9 @@ export function filePathAssumedType(filePath: string): FileType {
200209
}
201210
}
202211

212+
/**
213+
* Parses string based on a file format.
214+
*/
203215
export async function parseRawString(contents: string, fileType: FileType): Promise<Json> {
204216
switch (fileType) {
205217
case FileType.JSON:
@@ -215,6 +227,9 @@ export async function parseRawString(contents: string, fileType: FileType): Prom
215227
}
216228
}
217229

230+
/**
231+
* Try to parse string as different file formats, returning the first that worked.
232+
*/
218233
export async function guessFileType(contents: string): Promise<FileType> {
219234
for (const tryType of [FileType.JSON, FileType.TOML, FileType.JSON5, FileType.YAML]) {
220235
try {

app-config-core/src/parsed-value.test.ts

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,23 @@ describe('parseValue', () => {
6060
return false;
6161
};
6262

63-
const secretExtension: ParsingExtension = (_, [[keyType, key]]) => {
63+
const appendExtension = (suffix: string): ParsingExtension => (value) => {
64+
if (typeof value === 'string') {
65+
return (parse) => parse(value + suffix);
66+
}
67+
68+
return false;
69+
};
70+
71+
const namedAppendExtension = (suffix: string): ParsingExtension => {
72+
const extension = appendExtension(suffix);
73+
74+
return Object.assign(extension, {
75+
extensionName: 'namedAppendExtension',
76+
});
77+
};
78+
79+
const secretExtension: ParsingExtension = (_, [keyType, key]) => {
6480
if (keyType === InObject && key === '$secret') {
6581
return (parse) => parse('revealed!', { shouldFlatten: true });
6682
}
@@ -175,11 +191,21 @@ describe('parseValue', () => {
175191
it('allows the same extension to be applied twice', async () => {
176192
const source = new LiteralSource('string');
177193
const parsed = await parseValue(await source.readValue(), source, [
178-
uppercaseExtension,
179-
uppercaseExtension,
194+
appendExtension('-appended'),
195+
appendExtension('-appended2'),
196+
]);
197+
198+
expect(parsed.toJSON()).toEqual('string-appended-appended2');
199+
});
200+
201+
it('respects extensionName', async () => {
202+
const source = new LiteralSource('string');
203+
const parsed = await parseValue(await source.readValue(), source, [
204+
namedAppendExtension('-appended'),
205+
namedAppendExtension('-appended2'),
180206
]);
181207

182-
expect(parsed.toJSON()).toEqual('STRING');
208+
expect(parsed.toJSON()).toEqual('string-appended');
183209
});
184210

185211
it('allows the same extension apply at different levels of a tree', async () => {

0 commit comments

Comments
 (0)