zcb 0.3.1 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -5
- package/bin/zcb.js +2 -2
- package/dist/cjs/cli.cjs +1 -1
- package/dist/cjs/cli.cjs.map +1 -1
- package/dist/cjs/index.cjs +1 -1
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/templates/config.ts.hbs +1 -1
- package/dist/esm/cli.mjs +1 -1
- package/dist/esm/cli.mjs.map +1 -1
- package/dist/esm/index.mjs +1 -1
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/templates/config.ts.hbs +1 -1
- package/dist/production.analysis.txt +32 -43
- package/dist/types/cjs/cli.d.cts +1 -4
- package/dist/types/cjs/cli.d.cts.map +1 -1
- package/dist/types/cjs/createConfigBuilder.d.cts.map +1 -1
- package/dist/types/cjs/createConfigReader.d.cts.map +1 -1
- package/dist/types/cjs/transformers/cloneNonEnumerableValues.d.cts.map +1 -1
- package/dist/types/cjs/transformers/removeDisabledSlices.d.cts.map +1 -1
- package/dist/types/cjs/transformers/runExperiments.d.cts.map +1 -1
- package/dist/types/cjs/transformers/setupExperiments.d.cts.map +1 -1
- package/dist/types/cjs/types.d.cts +16 -18
- package/dist/types/cjs/types.d.cts.map +1 -1
- package/dist/types/cjs/utils/jsonStringifyReplacer.d.cts +92 -0
- package/dist/types/cjs/utils/jsonStringifyReplacer.d.cts.map +1 -1
- package/dist/types/cjs/utils/transformConfig.d.cts.map +1 -1
- package/dist/types/cjs/utils/typedGet.d.cts +3 -0
- package/dist/types/cjs/utils/typedGet.d.cts.map +1 -0
- package/dist/types/esm/cli.d.ts +1 -4
- package/dist/types/esm/cli.d.ts.map +1 -1
- package/dist/types/esm/createConfigBuilder.d.ts.map +1 -1
- package/dist/types/esm/createConfigReader.d.ts.map +1 -1
- package/dist/types/esm/transformers/cloneNonEnumerableValues.d.ts.map +1 -1
- package/dist/types/esm/transformers/removeDisabledSlices.d.ts.map +1 -1
- package/dist/types/esm/transformers/runExperiments.d.ts.map +1 -1
- package/dist/types/esm/transformers/setupExperiments.d.ts.map +1 -1
- package/dist/types/esm/types.d.ts +16 -18
- package/dist/types/esm/types.d.ts.map +1 -1
- package/dist/types/esm/utils/jsonStringifyReplacer.d.ts +92 -0
- package/dist/types/esm/utils/jsonStringifyReplacer.d.ts.map +1 -1
- package/dist/types/esm/utils/transformConfig.d.ts.map +1 -1
- package/dist/types/esm/utils/typedGet.d.ts +3 -0
- package/dist/types/esm/utils/typedGet.d.ts.map +1 -0
- package/dist/types/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +35 -27
- package/src/__testUtils__/builtConfig.ts +4 -3
- package/src/__testUtils__/configBuilder.ts +4 -2
- package/src/__testUtils__/schema.ts +2 -1
- package/src/cli.ts +3 -6
- package/src/createConfigBuilder.ts +6 -7
- package/src/createConfigReader.test.ts +92 -17
- package/src/createConfigReader.ts +24 -11
- package/src/templates/config.ts.hbs +1 -1
- package/src/transformers/cloneNonEnumerableValues.ts +2 -2
- package/src/transformers/removeDisabledSlices.ts +3 -8
- package/src/transformers/runExperiments.ts +2 -8
- package/src/transformers/setupExperiments.ts +2 -7
- package/src/types.ts +37 -26
- package/src/utils/transformConfig.test.ts +150 -2
- package/src/utils/transformConfig.ts +5 -10
- package/src/utils/typedGet.ts +8 -0
- package/src/utils/__snapshots__/transformConfig.test.ts.snap +0 -151
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zcb",
|
|
3
3
|
"description": "Build configs with type safety from zod schema.",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.3",
|
|
5
5
|
"author": "miami-man",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"homepage": "https://github.com/badbatch/zod-config-builder",
|
|
@@ -22,6 +22,14 @@
|
|
|
22
22
|
},
|
|
23
23
|
"import": "./dist/esm/index.mjs",
|
|
24
24
|
"require": "./dist/cjs/index.cjs"
|
|
25
|
+
},
|
|
26
|
+
"./cli": {
|
|
27
|
+
"types": {
|
|
28
|
+
"import": "./dist/types/esm/cli.d.ts",
|
|
29
|
+
"require": "./dist/types/cjs/cli.d.cts"
|
|
30
|
+
},
|
|
31
|
+
"import": "./dist/esm/cli.mjs",
|
|
32
|
+
"require": "./dist/cjs/cli.cjs"
|
|
25
33
|
}
|
|
26
34
|
},
|
|
27
35
|
"publishConfig": {
|
|
@@ -31,11 +39,11 @@
|
|
|
31
39
|
"@types/fs-extra": "^11.0.4",
|
|
32
40
|
"@types/json-schema": "^7.0.15",
|
|
33
41
|
"@types/lodash-es": "^4.17.12",
|
|
34
|
-
"@types/shelljs": "^0.8.
|
|
42
|
+
"@types/shelljs": "^0.8.17",
|
|
35
43
|
"@types/uuid": "^10.0.0",
|
|
36
44
|
"@types/yargs": "^17.0.33",
|
|
37
|
-
"core-js": "^3.
|
|
38
|
-
"fs-extra": "^11.3.
|
|
45
|
+
"core-js": "^3.45.0",
|
|
46
|
+
"fs-extra": "^11.3.1",
|
|
39
47
|
"handlebars": "^4.7.8",
|
|
40
48
|
"json-schema": "^0.4.0",
|
|
41
49
|
"lodash-es": "^4.17.21",
|
|
@@ -45,42 +53,42 @@
|
|
|
45
53
|
"type-fest": "^4.41.0",
|
|
46
54
|
"uuid": "^11.1.0",
|
|
47
55
|
"yargs": "^18.0.0",
|
|
48
|
-
"zod": "^3.25.
|
|
49
|
-
"zod-to-json-schema": "^3.24.
|
|
56
|
+
"zod": "^3.25.76",
|
|
57
|
+
"zod-to-json-schema": "^3.24.6"
|
|
50
58
|
},
|
|
51
59
|
"devDependencies": {
|
|
52
60
|
"@commitlint/cli": "^19.8.1",
|
|
53
61
|
"@jest/globals": "^29.7.0",
|
|
54
|
-
"@repodog/cli": "^1.6.
|
|
55
|
-
"@repodog/commitlint-config": "^19.0.
|
|
56
|
-
"@repodog/eslint-config": "^9.0.
|
|
57
|
-
"@repodog/eslint-config-jest": "^9.0.
|
|
58
|
-
"@repodog/jest-config": "^29.0.
|
|
59
|
-
"@repodog/markdownlint-config": "^1.2.
|
|
60
|
-
"@repodog/prettier-config": "^3.0.
|
|
61
|
-
"@repodog/rollup-config": "^4.0.
|
|
62
|
-
"@repodog/swc-config": "^1.0.
|
|
63
|
-
"@repodog/syncpack-config": "^13.0.
|
|
64
|
-
"@repodog/ts-config": "^5.0.
|
|
62
|
+
"@repodog/cli": "^1.6.45",
|
|
63
|
+
"@repodog/commitlint-config": "^19.0.5",
|
|
64
|
+
"@repodog/eslint-config": "^9.0.18",
|
|
65
|
+
"@repodog/eslint-config-jest": "^9.0.6",
|
|
66
|
+
"@repodog/jest-config": "^29.0.5",
|
|
67
|
+
"@repodog/markdownlint-config": "^1.2.5",
|
|
68
|
+
"@repodog/prettier-config": "^3.0.3",
|
|
69
|
+
"@repodog/rollup-config": "^4.0.11",
|
|
70
|
+
"@repodog/swc-config": "^1.0.12",
|
|
71
|
+
"@repodog/syncpack-config": "^13.0.4",
|
|
72
|
+
"@repodog/ts-config": "^5.0.5",
|
|
65
73
|
"@rollup/plugin-swc": "^0.3.0",
|
|
66
|
-
"@swc/core": "^1.
|
|
67
|
-
"@swc/jest": "^0.2.
|
|
74
|
+
"@swc/core": "^1.13.3",
|
|
75
|
+
"@swc/jest": "^0.2.39",
|
|
68
76
|
"@types/jest": "^29.5.13",
|
|
69
|
-
"@types/node": "^24.
|
|
70
|
-
"cts-types": "^0.0.
|
|
77
|
+
"@types/node": "^24.2.1",
|
|
78
|
+
"cts-types": "^0.0.10",
|
|
71
79
|
"del-cli": "^6.0.0",
|
|
72
|
-
"eslint": "^9.
|
|
80
|
+
"eslint": "^9.33.0",
|
|
73
81
|
"generate-changelog": "^1.8.0",
|
|
74
82
|
"husky": "^9.1.7",
|
|
75
83
|
"jest": "^29.7.0",
|
|
76
84
|
"jest-environment-jsdom": "^29.7.0",
|
|
77
85
|
"jest-environment-node": "^29.7.0",
|
|
78
86
|
"markdownlint-cli2": "^0.18.1",
|
|
79
|
-
"prettier": "^3.
|
|
80
|
-
"rollup": "^4.
|
|
87
|
+
"prettier": "^3.6.2",
|
|
88
|
+
"rollup": "^4.46.2",
|
|
81
89
|
"suppress-experimental-warnings": "^2.0.0",
|
|
82
90
|
"syncpack": "^13.0.4",
|
|
83
|
-
"typescript": "^5.
|
|
91
|
+
"typescript": "^5.9.2"
|
|
84
92
|
},
|
|
85
93
|
"keywords": [
|
|
86
94
|
"config",
|
|
@@ -106,7 +114,7 @@
|
|
|
106
114
|
"test": "COMPILER=swc node --require=suppress-experimental-warnings --experimental-vm-modules node_modules/jest/bin/jest.js",
|
|
107
115
|
"type-check": "tsc --noEmit",
|
|
108
116
|
"validate": "pnpm run syncpack && pnpm run build && pnpm run lint && pnpm run type-check && pnpm run test",
|
|
109
|
-
"zcb:build": "NODE_OPTIONS=\"--
|
|
110
|
-
"zcb:watch": "NODE_OPTIONS=\"--
|
|
117
|
+
"zcb:build": "NODE_OPTIONS=\"--experimental-strip-types\" node ./bin/zcb.js build ./src/__testUtils__/configBuilder.ts ./src/__testUtils__/builtConfig.ts",
|
|
118
|
+
"zcb:watch": "NODE_OPTIONS=\"--experimental-strip-types\" node ./bin/zcb.js watch ./src/__testUtils__/configBuilder.ts ./src/__testUtils__/builtConfig.ts"
|
|
111
119
|
}
|
|
112
120
|
}
|
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
/* eslint-disable prettier/prettier, import-x/no-default-export, unicorn/numeric-separators-style */
|
|
1
|
+
/* eslint-disable prettier/prettier, import-x/no-default-export, unicorn/numeric-separators-style, unicorn/no-null */
|
|
2
2
|
/* This file is autogenerated, do not edit directly, your changes will not perist. */
|
|
3
3
|
|
|
4
4
|
export default {
|
|
5
5
|
countryCode: "GB",
|
|
6
|
-
countryName:
|
|
6
|
+
countryName: null,
|
|
7
7
|
distanceUnit: "km",
|
|
8
|
+
enabled: true,
|
|
8
9
|
languageCodes: [
|
|
9
10
|
"en"
|
|
10
11
|
],
|
|
11
12
|
locales: [
|
|
12
13
|
"en_GB"
|
|
13
14
|
],
|
|
14
|
-
name:
|
|
15
|
+
name: undefined,
|
|
15
16
|
pages: {
|
|
16
17
|
contactDetails: {
|
|
17
18
|
name: "contactDetails",
|
|
@@ -19,13 +19,15 @@ const subsectionBuilder = sectionBuilder.$fork();
|
|
|
19
19
|
|
|
20
20
|
configBuilder
|
|
21
21
|
.countryCode('GB')
|
|
22
|
-
|
|
22
|
+
// Want to test using null value
|
|
23
|
+
// eslint-disable-next-line unicorn/no-null
|
|
24
|
+
.countryName(null)
|
|
23
25
|
.distanceUnit('km')
|
|
26
|
+
.enabled(true)
|
|
24
27
|
.languageCodes(['en'])
|
|
25
28
|
.locales(({ countryCode, languageCodes }) =>
|
|
26
29
|
languageCodes?.length && countryCode ? languageCodes.map(code => `${code}_${countryCode}`) : [],
|
|
27
30
|
)
|
|
28
|
-
.name('alpha')
|
|
29
31
|
.pages({
|
|
30
32
|
contactDetails: pageBuilder
|
|
31
33
|
.name('contactDetails')
|
|
@@ -42,8 +42,9 @@ export const routeSchema: z.ZodType<RouteType> = baseRouteSchema.extend({
|
|
|
42
42
|
|
|
43
43
|
export const configSchema = z.object({
|
|
44
44
|
countryCode: z.enum(countryCodes).optional(),
|
|
45
|
-
countryName: z.enum(countryNames).optional(),
|
|
45
|
+
countryName: z.enum(countryNames).optional().nullable(),
|
|
46
46
|
distanceUnit: z.enum(distanceUnits).optional(),
|
|
47
|
+
enabled: z.boolean().optional(),
|
|
47
48
|
languageCodes: z.array(z.enum(languageCodes)).optional(),
|
|
48
49
|
locales: z.array(z.string().regex(/[a-z]{2}_[A-Z]{2}/)).optional(),
|
|
49
50
|
name: z.string().optional(),
|
package/src/cli.ts
CHANGED
|
@@ -5,10 +5,7 @@ import yargs, { type Argv } from 'yargs';
|
|
|
5
5
|
import { hideBin } from 'yargs/helpers';
|
|
6
6
|
import { importValidateTransformWriteConfig } from './utils/importValidateTransformWriteConfig.ts';
|
|
7
7
|
|
|
8
|
-
export
|
|
9
|
-
BUILD = 'build',
|
|
10
|
-
WATCH = 'watch',
|
|
11
|
-
}
|
|
8
|
+
export type Commands = 'build' | 'watch';
|
|
12
9
|
|
|
13
10
|
const generateArguments = (cmdYargs: Argv) =>
|
|
14
11
|
cmdYargs
|
|
@@ -50,7 +47,7 @@ export const cli = () => {
|
|
|
50
47
|
importValidateTransformWriteConfig(
|
|
51
48
|
cmdYargs['input-file'],
|
|
52
49
|
cmdYargs['output-file'],
|
|
53
|
-
|
|
50
|
+
'watch',
|
|
54
51
|
cmdYargs['experiments-callback-file'],
|
|
55
52
|
);
|
|
56
53
|
});
|
|
@@ -66,7 +63,7 @@ export const cli = () => {
|
|
|
66
63
|
importValidateTransformWriteConfig(
|
|
67
64
|
cmdYargs['input-file'],
|
|
68
65
|
cmdYargs['output-file'],
|
|
69
|
-
|
|
66
|
+
'build',
|
|
70
67
|
cmdYargs['experiments-callback-file'],
|
|
71
68
|
);
|
|
72
69
|
},
|
|
@@ -4,7 +4,6 @@ import { v4 as uuidV4 } from 'uuid';
|
|
|
4
4
|
import { type ZodError, type z } from 'zod';
|
|
5
5
|
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
6
6
|
import { cloneNonEnumerableValues } from './transformers/cloneNonEnumerableValues.ts';
|
|
7
|
-
import { NonEmumeralProperties } from './types.ts';
|
|
8
7
|
import { collateDefaultValues } from './utils/collateDefaultValues.ts';
|
|
9
8
|
import { isDerivedValueCallback } from './utils/isDerivedValueCallback.ts';
|
|
10
9
|
import { isInvalidPropertyOverride } from './utils/isInvalidPropertyOverride.ts';
|
|
@@ -70,20 +69,20 @@ export const createConfigBuilder = <ZodTypes>(
|
|
|
70
69
|
const uuid = options.overrides?.uuid ?? uuidV4;
|
|
71
70
|
|
|
72
71
|
const addNonEnumeralBaseProperties = (conf: Config) => {
|
|
73
|
-
Object.defineProperty(conf,
|
|
72
|
+
Object.defineProperty(conf, '__zcb', {
|
|
74
73
|
configurable: false,
|
|
75
74
|
enumerable: false,
|
|
76
75
|
value: true,
|
|
77
76
|
});
|
|
78
77
|
|
|
79
|
-
Object.defineProperty(conf,
|
|
78
|
+
Object.defineProperty(conf, '__id', {
|
|
80
79
|
configurable: false,
|
|
81
80
|
enumerable: false,
|
|
82
81
|
value: uuid(),
|
|
83
82
|
});
|
|
84
83
|
|
|
85
84
|
if (options.type) {
|
|
86
|
-
Object.defineProperty(conf,
|
|
85
|
+
Object.defineProperty(conf, '__type', {
|
|
87
86
|
configurable: false,
|
|
88
87
|
enumerable: false,
|
|
89
88
|
value: options.type,
|
|
@@ -107,7 +106,7 @@ export const createConfigBuilder = <ZodTypes>(
|
|
|
107
106
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
108
107
|
const configBuilder = {
|
|
109
108
|
$disable: () => {
|
|
110
|
-
Object.defineProperty(config,
|
|
109
|
+
Object.defineProperty(config, '__disabled', {
|
|
111
110
|
configurable: false,
|
|
112
111
|
enumerable: false,
|
|
113
112
|
value: true,
|
|
@@ -126,7 +125,7 @@ export const createConfigBuilder = <ZodTypes>(
|
|
|
126
125
|
}
|
|
127
126
|
},
|
|
128
127
|
$experiment: (key: string) => {
|
|
129
|
-
Object.defineProperty(config,
|
|
128
|
+
Object.defineProperty(config, '__experiment', {
|
|
130
129
|
configurable: false,
|
|
131
130
|
enumerable: false,
|
|
132
131
|
value: key,
|
|
@@ -172,7 +171,7 @@ export const createConfigBuilder = <ZodTypes>(
|
|
|
172
171
|
},
|
|
173
172
|
} as unknown as ConfigBuilder<ZodTypes>;
|
|
174
173
|
|
|
175
|
-
Object.defineProperty(configBuilder,
|
|
174
|
+
Object.defineProperty(configBuilder, '__callbacks', {
|
|
176
175
|
configurable: false,
|
|
177
176
|
enumerable: false,
|
|
178
177
|
value: callbacks,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createReader } from './__testUtils__/configReader.ts';
|
|
2
2
|
|
|
3
3
|
describe('createConfigReader', () => {
|
|
4
|
-
describe('when
|
|
4
|
+
describe('when that value is a string', () => {
|
|
5
5
|
it('should return the correct value', () => {
|
|
6
6
|
const reader = createReader();
|
|
7
7
|
const value = reader.read('countryCode');
|
|
@@ -9,11 +9,69 @@ describe('createConfigReader', () => {
|
|
|
9
9
|
});
|
|
10
10
|
});
|
|
11
11
|
|
|
12
|
-
describe('when
|
|
12
|
+
describe('when that value is a number', () => {
|
|
13
13
|
it('should return the correct value', () => {
|
|
14
14
|
const reader = createReader();
|
|
15
|
-
const value = reader.read('
|
|
16
|
-
expect(value).toBe(
|
|
15
|
+
const value = reader.read('timeouts.apollo');
|
|
16
|
+
expect(value).toBe(10_000);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('when that value is a boolean', () => {
|
|
21
|
+
it('should return the correct value', () => {
|
|
22
|
+
const reader = createReader();
|
|
23
|
+
const value = reader.read('enabled');
|
|
24
|
+
expect(value).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('when that value is null', () => {
|
|
29
|
+
it('should return the correct value', () => {
|
|
30
|
+
const reader = createReader();
|
|
31
|
+
const value = reader.read('countryName');
|
|
32
|
+
expect(value).toBeNull();
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('when that value is undefined', () => {
|
|
37
|
+
it('should return the correct value', () => {
|
|
38
|
+
const reader = createReader();
|
|
39
|
+
// Required for testing
|
|
40
|
+
// eslint-disable-next-line @typescript-eslint/no-confusing-void-expression
|
|
41
|
+
const value = reader.read('name');
|
|
42
|
+
expect(value).toBeUndefined();
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('when that value is an array', () => {
|
|
47
|
+
describe('when the array is of primitives', () => {
|
|
48
|
+
it('should return the correct value', () => {
|
|
49
|
+
const reader = createReader();
|
|
50
|
+
const value = reader.read('languageCodes');
|
|
51
|
+
expect(value).toEqual(['en']);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('when the array is of objects', () => {
|
|
56
|
+
it('should throw the expected error', () => {
|
|
57
|
+
const reader = createReader();
|
|
58
|
+
|
|
59
|
+
// @ts-expect-error Required to test error
|
|
60
|
+
expect(() => reader.read('pages.contactDetails.sections')).toThrow(
|
|
61
|
+
'Path resolved to an object or an array of objects, but `read` can only resolve to a primitive value or an array of primitives. Use the `scope` method instead',
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('when that value is an object', () => {
|
|
68
|
+
it('should throw the expected error', () => {
|
|
69
|
+
const reader = createReader();
|
|
70
|
+
|
|
71
|
+
// @ts-expect-error Required to test error
|
|
72
|
+
expect(() => reader.read('timeouts')).toThrow(
|
|
73
|
+
'Path resolved to an object or an array of objects, but `read` can only resolve to a primitive value or an array of primitives. Use the `scope` method instead.',
|
|
74
|
+
);
|
|
17
75
|
});
|
|
18
76
|
});
|
|
19
77
|
|
|
@@ -24,14 +82,11 @@ describe('createConfigReader', () => {
|
|
|
24
82
|
};
|
|
25
83
|
|
|
26
84
|
describe('when a user accesses a property that is not a string', () => {
|
|
27
|
-
it('should throw the
|
|
85
|
+
it('should throw the expected error', () => {
|
|
28
86
|
const reader = createReader();
|
|
29
87
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
new Error(
|
|
33
|
-
'config reader received variables to use in string template, but the path did not resolve to a string.',
|
|
34
|
-
),
|
|
88
|
+
expect(() => reader.read('timeouts.apollo', vars)).toThrow(
|
|
89
|
+
'Config reader received variables to use in string template, but the path did not resolve to a string.',
|
|
35
90
|
);
|
|
36
91
|
});
|
|
37
92
|
});
|
|
@@ -46,7 +101,7 @@ describe('createConfigReader', () => {
|
|
|
46
101
|
});
|
|
47
102
|
|
|
48
103
|
describe('when the reader is scoped', () => {
|
|
49
|
-
describe('when
|
|
104
|
+
describe('when the value is an object', () => {
|
|
50
105
|
it('should return the correct value', () => {
|
|
51
106
|
const reader = createReader();
|
|
52
107
|
const scopedReader = reader.scope('pages.contactDetails');
|
|
@@ -54,15 +109,35 @@ describe('createConfigReader', () => {
|
|
|
54
109
|
expect(value).toBe('contactDetails');
|
|
55
110
|
});
|
|
56
111
|
});
|
|
57
|
-
});
|
|
58
112
|
|
|
59
|
-
|
|
60
|
-
describe('when a user accesses a known property', () => {
|
|
113
|
+
describe('when the value is an array of objects', () => {
|
|
61
114
|
it('should return the correct value', () => {
|
|
62
115
|
const reader = createReader();
|
|
63
|
-
const scopedReader = reader.scope('pages.contactDetails
|
|
64
|
-
const
|
|
65
|
-
|
|
116
|
+
const scopedReader = reader.scope('pages.contactDetails.sections');
|
|
117
|
+
const subScopedReader = scopedReader.scope('0');
|
|
118
|
+
const value = subScopedReader.read('name');
|
|
119
|
+
expect(value).toBe('header');
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe('when the value is an array of primitives', () => {
|
|
124
|
+
it('should throw the expected error', () => {
|
|
125
|
+
const reader = createReader();
|
|
126
|
+
|
|
127
|
+
expect(() => reader.scope('languageCodes')).toThrow(
|
|
128
|
+
'Path resolved to a primitive or an array of primitive, but `scope` can only resolve to an object or an array of object. Use the `read` method instead.',
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe('when the value is a primitive', () => {
|
|
134
|
+
it('should throw the expected error', () => {
|
|
135
|
+
const reader = createReader();
|
|
136
|
+
|
|
137
|
+
// @ts-expect-error Required to test error
|
|
138
|
+
expect(() => reader.scope('timeouts.apollo')).toThrow(
|
|
139
|
+
'Path resolved to a primitive or an array of primitive, but `scope` can only resolve to an object or an array of object. Use the `read` method instead.',
|
|
140
|
+
);
|
|
66
141
|
});
|
|
67
142
|
});
|
|
68
143
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isPlainObject, isString } from 'lodash-es';
|
|
2
2
|
import { type Get } from 'type-fest';
|
|
3
3
|
import { type Path, type Scope } from './types.ts';
|
|
4
|
+
import { get } from './utils/typedGet.ts';
|
|
4
5
|
|
|
5
6
|
export interface ConfigReader<Config extends object> {
|
|
6
7
|
read: <P extends Path<Config>>(path: P, variables?: Record<string, string | number>) => Get<Config, P>;
|
|
@@ -10,17 +11,23 @@ export interface ConfigReader<Config extends object> {
|
|
|
10
11
|
export const createConfigReader = <Config extends object>(config: Config): ConfigReader<Config> => {
|
|
11
12
|
// Aimed at reducing the amount of work for typescript to resolve type.
|
|
12
13
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
13
|
-
|
|
14
|
+
return {
|
|
14
15
|
read: <P extends Path<Config>>(path: P, variables?: Record<string, string | number>) => {
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
const output = get(config, path);
|
|
17
|
+
|
|
18
|
+
if (isPlainObject(output) || (Array.isArray(output) && output.some(entry => isPlainObject(entry)))) {
|
|
19
|
+
throw new Error(
|
|
20
|
+
'Path resolved to an object or an array of objects, but `read` can only resolve to a primitive value or an array of primitives. Use the `scope` method instead.',
|
|
21
|
+
);
|
|
17
22
|
}
|
|
18
23
|
|
|
19
|
-
|
|
24
|
+
if (!variables) {
|
|
25
|
+
return output;
|
|
26
|
+
}
|
|
20
27
|
|
|
21
28
|
if (!isString(output)) {
|
|
22
29
|
throw new Error(
|
|
23
|
-
'
|
|
30
|
+
'Config reader received variables to use in string template, but the path did not resolve to a string.',
|
|
24
31
|
);
|
|
25
32
|
}
|
|
26
33
|
|
|
@@ -28,10 +35,16 @@ export const createConfigReader = <Config extends object>(config: Config): Confi
|
|
|
28
35
|
return acc.replace(new RegExp(`{{${key}}}`), String(variables[key]));
|
|
29
36
|
}, output);
|
|
30
37
|
},
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
scope: <S extends Scope<Config>>(scope: S) => createConfigReader(get(config, scope) as Get<Config, S>),
|
|
34
|
-
} as ConfigReader<Config>;
|
|
38
|
+
scope: <S extends Scope<Config>>(scope: S) => {
|
|
39
|
+
const output = get(config, scope);
|
|
35
40
|
|
|
36
|
-
|
|
41
|
+
if (!isPlainObject(output) && !(Array.isArray(output) && output.some(entry => isPlainObject(entry)))) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
'Path resolved to a primitive or an array of primitive, but `scope` can only resolve to an object or an array of object. Use the `read` method instead.',
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return createConfigReader(output);
|
|
48
|
+
},
|
|
49
|
+
} as ConfigReader<Config>;
|
|
37
50
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* eslint-disable prettier/prettier, import-x/no-default-export, unicorn/numeric-separators-style */
|
|
1
|
+
/* eslint-disable prettier/prettier, import-x/no-default-export, unicorn/numeric-separators-style, unicorn/no-null */
|
|
2
2
|
/* This file is autogenerated, do not edit directly, your changes will not perist. */
|
|
3
3
|
|
|
4
4
|
export default {{{ config }}} as const;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { type AnyRecord,
|
|
1
|
+
import { type AnyRecord, type TransformConfigHandlerSync } from '../types.ts';
|
|
2
2
|
|
|
3
|
-
const NON_ENUMERABLE_KEYS =
|
|
3
|
+
const NON_ENUMERABLE_KEYS = ['__callbacks', '__disabled', '__experiment', '__id', '__type', '__zcb'];
|
|
4
4
|
|
|
5
5
|
export const cloneNonEnumerableValues: TransformConfigHandlerSync = <
|
|
6
6
|
Config1 extends AnyRecord,
|
|
@@ -1,17 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type AnyRecord,
|
|
3
|
-
NonEmumeralProperties,
|
|
4
|
-
TransformConfigHandlerAction,
|
|
5
|
-
type TransformConfigHandlerSync,
|
|
6
|
-
} from '../types.ts';
|
|
1
|
+
import { type AnyRecord, type TransformConfigHandlerSync } from '../types.ts';
|
|
7
2
|
|
|
8
3
|
export const removeDisabledSlices: TransformConfigHandlerSync = <Config extends AnyRecord>(
|
|
9
4
|
clone: Config,
|
|
10
5
|
config: Config,
|
|
11
6
|
) => {
|
|
12
|
-
if (
|
|
7
|
+
if ('__disabled' in config) {
|
|
13
8
|
return {
|
|
14
|
-
action:
|
|
9
|
+
action: 'DELETE_NODE',
|
|
15
10
|
};
|
|
16
11
|
}
|
|
17
12
|
|
|
@@ -1,15 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type AnyRecord,
|
|
3
|
-
type Buckets,
|
|
4
|
-
NonEmumeralProperties,
|
|
5
|
-
type RunExperimentsCallback,
|
|
6
|
-
type TransformConfigHandler,
|
|
7
|
-
} from '../types.ts';
|
|
1
|
+
import { type AnyRecord, type Buckets, type RunExperimentsCallback, type TransformConfigHandler } from '../types.ts';
|
|
8
2
|
|
|
9
3
|
export const runExperiments =
|
|
10
4
|
(callback: RunExperimentsCallback): TransformConfigHandler =>
|
|
11
5
|
async <Config extends AnyRecord>(clone: Config, config: Config) => {
|
|
12
|
-
if (
|
|
6
|
+
if ('__experiment' in config && typeof config.__experiment === 'object') {
|
|
13
7
|
// As __experiment is private property, Typescript doesn't know what type is.
|
|
14
8
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
15
9
|
const { buckets, id } = config.__experiment as { buckets: Buckets<Config>; id: string };
|
|
@@ -1,14 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type AnyRecord,
|
|
3
|
-
NonEmumeralProperties,
|
|
4
|
-
type SetupExperimentsCallback,
|
|
5
|
-
type TransformConfigHandler,
|
|
6
|
-
} from '../types.ts';
|
|
1
|
+
import { type AnyRecord, type SetupExperimentsCallback, type TransformConfigHandler } from '../types.ts';
|
|
7
2
|
|
|
8
3
|
export const setupExperiments =
|
|
9
4
|
(callback: SetupExperimentsCallback): TransformConfigHandler =>
|
|
10
5
|
async <Config extends AnyRecord>(clone: Config, config: Config) => {
|
|
11
|
-
if (
|
|
6
|
+
if ('__experiment' in config && typeof config.__experiment === 'string') {
|
|
12
7
|
const buckets = await callback(config.__experiment, clone, config);
|
|
13
8
|
|
|
14
9
|
// @ts-expect-error private property
|
package/src/types.ts
CHANGED
|
@@ -15,14 +15,7 @@ export interface Experiment<Config extends AnyRecord> {
|
|
|
15
15
|
value?: Config;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
export
|
|
19
|
-
CALLBACKS = '__callbacks',
|
|
20
|
-
DISABLED = '__disabled',
|
|
21
|
-
EXPERIMENT = '__experiment',
|
|
22
|
-
ID = '__id',
|
|
23
|
-
TYPE = '__type',
|
|
24
|
-
ZCB = '__zcb',
|
|
25
|
-
}
|
|
18
|
+
export type NonEmumeralProperties = '__callbacks' | '__disabled' | '__experiment' | '__id' | '__type' | '__zcb';
|
|
26
19
|
|
|
27
20
|
export type Path<Config extends object> = Join<Leaves<Config>, '.'>;
|
|
28
21
|
|
|
@@ -46,10 +39,7 @@ export type TransformConfigHandlerSync = <Config extends AnyRecord>(
|
|
|
46
39
|
config: Config,
|
|
47
40
|
) => TransformConfigHandlerReturnType<Config>;
|
|
48
41
|
|
|
49
|
-
export
|
|
50
|
-
DELETE_NODE = 'deleteNode',
|
|
51
|
-
SKIP_NODE = 'skipNode',
|
|
52
|
-
}
|
|
42
|
+
export type TransformConfigHandlerAction = 'DELETE_NODE' | 'SKIP_NODE';
|
|
53
43
|
|
|
54
44
|
export interface TransformConfigHandlerReturnType<Config extends AnyRecord> {
|
|
55
45
|
action?: TransformConfigHandlerAction;
|
|
@@ -61,22 +51,43 @@ export interface WriteConfigOptions {
|
|
|
61
51
|
outputFile: string;
|
|
62
52
|
}
|
|
63
53
|
|
|
64
|
-
export type
|
|
65
|
-
? LeafPath
|
|
66
|
-
: Depth extends 15
|
|
67
|
-
? LeafPath
|
|
68
|
-
: {
|
|
69
|
-
[K in keyof T & string]: Leaves<T[K], [...LeafPath, K], LeafPath['length']>;
|
|
70
|
-
}[keyof T & string];
|
|
54
|
+
export type Primitive = string | number | boolean | bigint | symbol | null | undefined;
|
|
71
55
|
|
|
72
|
-
export type
|
|
73
|
-
? LeafPath
|
|
74
|
-
: Depth extends 15
|
|
75
|
-
? LeafPath
|
|
76
|
-
: {
|
|
77
|
-
[K in keyof T & string]: T[K] extends object ? Paths<T[K], [...LeafPath, K], LeafPath['length']> : LeafPath;
|
|
78
|
-
}[keyof T & string];
|
|
56
|
+
export type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...0[]];
|
|
79
57
|
|
|
58
|
+
export type IsArrayOfPrimitives<T> = T extends readonly (infer U)[]
|
|
59
|
+
? Exclude<U, Primitive> extends never
|
|
60
|
+
? true
|
|
61
|
+
: false
|
|
62
|
+
: false;
|
|
63
|
+
|
|
64
|
+
export type OwnKeys<T> = Exclude<
|
|
65
|
+
{
|
|
66
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
67
|
+
[K in Extract<keyof T, string>]: T[K] extends (...args: any[]) => any ? never : K;
|
|
68
|
+
}[Extract<keyof T, string>],
|
|
69
|
+
`${string}[Symbol.${string}`
|
|
70
|
+
>;
|
|
71
|
+
|
|
72
|
+
export type Leaves<T, LeafPath extends string[] = [], Depth extends number = 10> = Depth extends never
|
|
73
|
+
? never
|
|
74
|
+
: T extends Primitive
|
|
75
|
+
? LeafPath
|
|
76
|
+
: IsArrayOfPrimitives<T> extends true
|
|
77
|
+
? LeafPath
|
|
78
|
+
: {
|
|
79
|
+
[K in OwnKeys<T>]: Leaves<T[K], [...LeafPath, K], Prev[Depth]>;
|
|
80
|
+
}[OwnKeys<T>];
|
|
81
|
+
|
|
82
|
+
export type Paths<T, ScopePath extends string[] = [], Depth extends number = 10> = Depth extends never
|
|
83
|
+
? never
|
|
84
|
+
: T extends Primitive
|
|
85
|
+
? never
|
|
86
|
+
: ScopePath extends []
|
|
87
|
+
? { [K in OwnKeys<T>]: Paths<T[K], [K], Prev[Depth]> }[OwnKeys<T>]
|
|
88
|
+
: ScopePath | { [K in OwnKeys<T>]: Paths<T[K], [...ScopePath, K], Prev[Depth]> }[OwnKeys<T>];
|
|
89
|
+
|
|
90
|
+
// This type is not used, but is left here for reference
|
|
80
91
|
export type LeavesScopeDiffs<T1 extends readonly string[][], T2 extends string[]> = {
|
|
81
92
|
[K1 in keyof T1]: List.Length<List.Intersect<T1[K1], T2, '<-contains'>> extends 1
|
|
82
93
|
? List.Length<T1[K1]> extends 1
|