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.
Files changed (62) hide show
  1. package/README.md +3 -5
  2. package/bin/zcb.js +2 -2
  3. package/dist/cjs/cli.cjs +1 -1
  4. package/dist/cjs/cli.cjs.map +1 -1
  5. package/dist/cjs/index.cjs +1 -1
  6. package/dist/cjs/index.cjs.map +1 -1
  7. package/dist/cjs/templates/config.ts.hbs +1 -1
  8. package/dist/esm/cli.mjs +1 -1
  9. package/dist/esm/cli.mjs.map +1 -1
  10. package/dist/esm/index.mjs +1 -1
  11. package/dist/esm/index.mjs.map +1 -1
  12. package/dist/esm/templates/config.ts.hbs +1 -1
  13. package/dist/production.analysis.txt +32 -43
  14. package/dist/types/cjs/cli.d.cts +1 -4
  15. package/dist/types/cjs/cli.d.cts.map +1 -1
  16. package/dist/types/cjs/createConfigBuilder.d.cts.map +1 -1
  17. package/dist/types/cjs/createConfigReader.d.cts.map +1 -1
  18. package/dist/types/cjs/transformers/cloneNonEnumerableValues.d.cts.map +1 -1
  19. package/dist/types/cjs/transformers/removeDisabledSlices.d.cts.map +1 -1
  20. package/dist/types/cjs/transformers/runExperiments.d.cts.map +1 -1
  21. package/dist/types/cjs/transformers/setupExperiments.d.cts.map +1 -1
  22. package/dist/types/cjs/types.d.cts +16 -18
  23. package/dist/types/cjs/types.d.cts.map +1 -1
  24. package/dist/types/cjs/utils/jsonStringifyReplacer.d.cts +92 -0
  25. package/dist/types/cjs/utils/jsonStringifyReplacer.d.cts.map +1 -1
  26. package/dist/types/cjs/utils/transformConfig.d.cts.map +1 -1
  27. package/dist/types/cjs/utils/typedGet.d.cts +3 -0
  28. package/dist/types/cjs/utils/typedGet.d.cts.map +1 -0
  29. package/dist/types/esm/cli.d.ts +1 -4
  30. package/dist/types/esm/cli.d.ts.map +1 -1
  31. package/dist/types/esm/createConfigBuilder.d.ts.map +1 -1
  32. package/dist/types/esm/createConfigReader.d.ts.map +1 -1
  33. package/dist/types/esm/transformers/cloneNonEnumerableValues.d.ts.map +1 -1
  34. package/dist/types/esm/transformers/removeDisabledSlices.d.ts.map +1 -1
  35. package/dist/types/esm/transformers/runExperiments.d.ts.map +1 -1
  36. package/dist/types/esm/transformers/setupExperiments.d.ts.map +1 -1
  37. package/dist/types/esm/types.d.ts +16 -18
  38. package/dist/types/esm/types.d.ts.map +1 -1
  39. package/dist/types/esm/utils/jsonStringifyReplacer.d.ts +92 -0
  40. package/dist/types/esm/utils/jsonStringifyReplacer.d.ts.map +1 -1
  41. package/dist/types/esm/utils/transformConfig.d.ts.map +1 -1
  42. package/dist/types/esm/utils/typedGet.d.ts +3 -0
  43. package/dist/types/esm/utils/typedGet.d.ts.map +1 -0
  44. package/dist/types/tsconfig.build.tsbuildinfo +1 -1
  45. package/package.json +35 -27
  46. package/src/__testUtils__/builtConfig.ts +4 -3
  47. package/src/__testUtils__/configBuilder.ts +4 -2
  48. package/src/__testUtils__/schema.ts +2 -1
  49. package/src/cli.ts +3 -6
  50. package/src/createConfigBuilder.ts +6 -7
  51. package/src/createConfigReader.test.ts +92 -17
  52. package/src/createConfigReader.ts +24 -11
  53. package/src/templates/config.ts.hbs +1 -1
  54. package/src/transformers/cloneNonEnumerableValues.ts +2 -2
  55. package/src/transformers/removeDisabledSlices.ts +3 -8
  56. package/src/transformers/runExperiments.ts +2 -8
  57. package/src/transformers/setupExperiments.ts +2 -7
  58. package/src/types.ts +37 -26
  59. package/src/utils/transformConfig.test.ts +150 -2
  60. package/src/utils/transformConfig.ts +5 -10
  61. package/src/utils/typedGet.ts +8 -0
  62. 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.1",
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.16",
42
+ "@types/shelljs": "^0.8.17",
35
43
  "@types/uuid": "^10.0.0",
36
44
  "@types/yargs": "^17.0.33",
37
- "core-js": "^3.43.0",
38
- "fs-extra": "^11.3.0",
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.63",
49
- "zod-to-json-schema": "^3.24.5"
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.43",
55
- "@repodog/commitlint-config": "^19.0.3",
56
- "@repodog/eslint-config": "^9.0.15",
57
- "@repodog/eslint-config-jest": "^9.0.4",
58
- "@repodog/jest-config": "^29.0.4",
59
- "@repodog/markdownlint-config": "^1.2.4",
60
- "@repodog/prettier-config": "^3.0.2",
61
- "@repodog/rollup-config": "^4.0.9",
62
- "@repodog/swc-config": "^1.0.10",
63
- "@repodog/syncpack-config": "^13.0.3",
64
- "@repodog/ts-config": "^5.0.4",
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.12.0",
67
- "@swc/jest": "^0.2.38",
74
+ "@swc/core": "^1.13.3",
75
+ "@swc/jest": "^0.2.39",
68
76
  "@types/jest": "^29.5.13",
69
- "@types/node": "^24.0.1",
70
- "cts-types": "^0.0.9",
77
+ "@types/node": "^24.2.1",
78
+ "cts-types": "^0.0.10",
71
79
  "del-cli": "^6.0.0",
72
- "eslint": "^9.28.0",
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.5.3",
80
- "rollup": "^4.43.0",
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.8.3"
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=\"--loader ts-node/esm\" node ./bin/zcb.mjs build ./src/__testUtils__/configBuilder.ts ./src/__testUtils__/builtConfig.ts",
110
- "zcb:watch": "NODE_OPTIONS=\"--loader ts-node/esm\" node ./bin/zcb.mjs watch ./src/__testUtils__/configBuilder.ts ./src/__testUtils__/builtConfig.ts"
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: "United Kingdom",
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: "alpha",
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
- .countryName('United Kingdom')
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 enum Commands {
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
- Commands.WATCH,
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
- Commands.BUILD,
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, NonEmumeralProperties.ZCB, {
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, NonEmumeralProperties.ID, {
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, NonEmumeralProperties.TYPE, {
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, NonEmumeralProperties.DISABLED, {
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, NonEmumeralProperties.EXPERIMENT, {
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, NonEmumeralProperties.CALLBACKS, {
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 a user accesses a known property', () => {
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 a user accesses a known nested property', () => {
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('pages.contactDetails.name');
16
- expect(value).toBe('contactDetails');
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 correct error', () => {
85
+ it('should throw the expected error', () => {
28
86
  const reader = createReader();
29
87
 
30
- // @ts-expect-error path does not resovle to a string
31
- expect(() => reader.read('pages.contactDetails', vars)).toThrow(
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 a user accesses a known property', () => {
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
- describe('when the reader is scoped multiple times', () => {
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').scope('sections.1.sections').scope('0');
64
- const value = scopedReader.read('name');
65
- expect(value).toBe('main');
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 { get, isString } from 'lodash-es';
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
- const configReader = {
14
+ return {
14
15
  read: <P extends Path<Config>>(path: P, variables?: Record<string, string | number>) => {
15
- if (!variables) {
16
- return get(config, path);
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
- const output = get(config, path);
24
+ if (!variables) {
25
+ return output;
26
+ }
20
27
 
21
28
  if (!isString(output)) {
22
29
  throw new Error(
23
- 'config reader received variables to use in string template, but the path did not resolve to a string.',
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
- // Aimed at reducing the amount of work for typescript to resolve type.
32
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
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
- return configReader;
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, NonEmumeralProperties, type TransformConfigHandlerSync } from '../types.ts';
1
+ import { type AnyRecord, type TransformConfigHandlerSync } from '../types.ts';
2
2
 
3
- const NON_ENUMERABLE_KEYS = new Set(Object.values(NonEmumeralProperties));
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 (NonEmumeralProperties.DISABLED in config) {
7
+ if ('__disabled' in config) {
13
8
  return {
14
- action: TransformConfigHandlerAction.DELETE_NODE,
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 (NonEmumeralProperties.EXPERIMENT in config && typeof config.__experiment === 'object') {
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 (NonEmumeralProperties.EXPERIMENT in config && typeof config.__experiment === 'string') {
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 enum NonEmumeralProperties {
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 enum TransformConfigHandlerAction {
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 Leaves<T, LeafPath extends string[] = [], Depth extends number = 0> = T extends string
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 Paths<T, LeafPath extends string[] = [], Depth extends number = 0> = T extends string
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