zcb 0.0.6

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 (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +8 -0
  3. package/bin/zcb.mjs +3 -0
  4. package/package.json +129 -0
  5. package/src/__testUtils__/builtConfig.ts +74 -0
  6. package/src/__testUtils__/config.ts +32 -0
  7. package/src/__testUtils__/configBuilder.ts +58 -0
  8. package/src/__testUtils__/schema.ts +56 -0
  9. package/src/cli.ts +86 -0
  10. package/src/createConfigBuilder.test.ts +523 -0
  11. package/src/createConfigBuilder.ts +234 -0
  12. package/src/createConfigReader.test.ts +45 -0
  13. package/src/createConfigReader.ts +12 -0
  14. package/src/data/countryCodes.ts +247 -0
  15. package/src/data/countryNames.ts +207 -0
  16. package/src/data/distanceUnits.ts +1 -0
  17. package/src/data/languageCodes.ts +185 -0
  18. package/src/data/timezones.ts +419 -0
  19. package/src/index.ts +6 -0
  20. package/src/templates/config.ts.hbs +3 -0
  21. package/src/transformers/cloneNonEnumerableValues.ts +24 -0
  22. package/src/transformers/removeDisabledSlices.ts +17 -0
  23. package/src/transformers/setupExperiments.ts +24 -0
  24. package/src/types.ts +58 -0
  25. package/src/utils/__snapshots__/transformConfig.test.ts.snap +151 -0
  26. package/src/utils/arrayHasInvalidDefaults.ts +6 -0
  27. package/src/utils/isDerivedValueCallback.ts +3 -0
  28. package/src/utils/isInvalidPropertyOverride.ts +3 -0
  29. package/src/utils/isPropertyReservedWord.ts +3 -0
  30. package/src/utils/isSchemaValid.ts +3 -0
  31. package/src/utils/isValidPropertyDefinition.ts +5 -0
  32. package/src/utils/isValidValue.ts +27 -0
  33. package/src/utils/objectHasInvalidDefaults.ts +5 -0
  34. package/src/utils/recordHasInvalidDefaults.ts +6 -0
  35. package/src/utils/transformConfig.test.ts +19 -0
  36. package/src/utils/transformConfig.ts +119 -0
  37. package/src/utils/writeConfig.ts +34 -0
  38. package/tsconfig.build.json +13 -0
  39. package/tsconfig.json +9 -0
@@ -0,0 +1,234 @@
1
+ import { type JSONSchema7 } from 'json-schema';
2
+ import { type ZodError, type z } from 'zod';
3
+ import { zodToJsonSchema } from 'zod-to-json-schema';
4
+ import { cloneNonEnumerableValues } from './transformers/cloneNonEnumerableValues.ts';
5
+ import { arrayHasInvalidDefaults } from './utils/arrayHasInvalidDefaults.ts';
6
+ import { isDerivedValueCallback } from './utils/isDerivedValueCallback.ts';
7
+ import { isInvalidPropertyOverride } from './utils/isInvalidPropertyOverride.ts';
8
+ import { isPropertyReservedWord } from './utils/isPropertyReservedWord.ts';
9
+ import { isSchemaValid } from './utils/isSchemaValid.ts';
10
+ import { isValidPropertyDefinition } from './utils/isValidPropertyDefinition.ts';
11
+ import { isValidValue } from './utils/isValidValue.ts';
12
+ import { objectHasInvalidDefaults } from './utils/objectHasInvalidDefaults.ts';
13
+ import { recordHasInvalidDefaults } from './utils/recordHasInvalidDefaults.ts';
14
+ import { transformConfigSync } from './utils/transformConfig.ts';
15
+
16
+ export const RESERVED_KEYWORDS = new Set([
17
+ 'disable',
18
+ 'errors',
19
+ 'experiment',
20
+ 'extend',
21
+ 'flush',
22
+ 'fork',
23
+ 'toJson',
24
+ 'validate',
25
+ 'values',
26
+ ]);
27
+
28
+ export const createConfigBuilder = <ZodTypes>(
29
+ zodSchema: z.ZodSchema,
30
+ derivedValueCallbacks: Partial<
31
+ Record<
32
+ keyof ZodTypes,
33
+ (c: {
34
+ [Key in keyof ZodTypes]: ZodTypes[Key];
35
+ }) => ZodTypes[keyof ZodTypes]
36
+ >
37
+ > = {},
38
+ initialValues: Partial<{
39
+ [Key in keyof ZodTypes]: ZodTypes[Key];
40
+ }> = {}
41
+ ) => {
42
+ type Config = {
43
+ [Key in keyof ZodTypes]: ZodTypes[Key];
44
+ };
45
+
46
+ type RequiredConfig = Required<Config>;
47
+ type DerivedValueCallback<K extends keyof Config = keyof Config> = (c: Config) => Config[K];
48
+
49
+ type ConfigBuilder = {
50
+ [Key in keyof RequiredConfig]: (
51
+ value: Config[Key] | DerivedValueCallback<Key>,
52
+ override?: boolean
53
+ ) => ConfigBuilder;
54
+ } & {
55
+ disable: () => ConfigBuilder;
56
+ errors: () => ZodError['errors'];
57
+ experiment: (key: string) => ConfigBuilder;
58
+ extend: (configBuilder: ConfigBuilder) => ConfigBuilder;
59
+ flush: () => Config;
60
+ fork: () => ConfigBuilder;
61
+ toJson: () => string;
62
+ validate: () => boolean;
63
+ values: () => Config;
64
+ };
65
+
66
+ let config = initialValues as Config;
67
+
68
+ Object.defineProperty(config, '__zcb', {
69
+ configurable: false,
70
+ enumerable: false,
71
+ value: true,
72
+ });
73
+
74
+ let callbacks: Partial<Record<keyof Config, DerivedValueCallback>> = { ...derivedValueCallbacks };
75
+
76
+ const configBuilder = {
77
+ disable: () => {
78
+ Object.defineProperty(config, '__disabled', {
79
+ configurable: false,
80
+ enumerable: false,
81
+ value: true,
82
+ });
83
+
84
+ return configBuilder;
85
+ },
86
+ errors: () => {
87
+ try {
88
+ zodSchema.parse(configBuilder.values());
89
+ return [];
90
+ } catch (error: unknown) {
91
+ return (error as ZodError).errors;
92
+ }
93
+ },
94
+ experiment: (key: string) => {
95
+ Object.defineProperty(config, '__experiment', {
96
+ configurable: false,
97
+ enumerable: false,
98
+ value: key,
99
+ });
100
+
101
+ return configBuilder;
102
+ },
103
+ extend: (configBuilder: ConfigBuilder) => {
104
+ config = transformConfigSync<Config>(configBuilder.values(), [cloneNonEnumerableValues]);
105
+ // @ts-expect-error private property
106
+ callbacks = { ...configBuilder.__callbacks } as Partial<Record<keyof Config, DerivedValueCallback>>;
107
+ },
108
+ flush: () => {
109
+ const values = configBuilder.values();
110
+ config = {} as Config;
111
+
112
+ Object.defineProperty(config, '__zcb', {
113
+ configurable: false,
114
+ enumerable: false,
115
+ value: true,
116
+ });
117
+
118
+ return values;
119
+ },
120
+ fork: () => createConfigBuilder<Config>(zodSchema, derivedValueCallbacks),
121
+ toJson: () => JSON.stringify(configBuilder.values(), undefined, 2),
122
+ validate: () => {
123
+ try {
124
+ zodSchema.parse(configBuilder.values());
125
+ return true;
126
+ } catch (error: unknown) {
127
+ console.error(error);
128
+ return false;
129
+ }
130
+ },
131
+ values: () => {
132
+ for (const property in callbacks) {
133
+ const callback = callbacks[property];
134
+
135
+ if (callback) {
136
+ config[property as keyof Config] = callback(config);
137
+ }
138
+ }
139
+
140
+ return config;
141
+ },
142
+ } as unknown as ConfigBuilder;
143
+
144
+ Object.defineProperty(configBuilder, '__callbacks', {
145
+ configurable: false,
146
+ enumerable: false,
147
+ value: callbacks,
148
+ });
149
+
150
+ const jsonSchema = zodToJsonSchema(zodSchema) as JSONSchema7;
151
+
152
+ if (isSchemaValid(jsonSchema)) {
153
+ throw new Error(`The root type of a config schema must be "object", but received "${String(jsonSchema.type)}"`);
154
+ }
155
+
156
+ for (const propertyName in jsonSchema.properties) {
157
+ if (isPropertyReservedWord(propertyName)) {
158
+ throw new Error(
159
+ `"${propertyName}" is a reserved keyword within the config builder. Please use a different property name. The full list of reserved keywords is: ${[
160
+ ...RESERVED_KEYWORDS,
161
+ ].join(', ')}`
162
+ );
163
+ }
164
+
165
+ const castProperty = propertyName as keyof Config;
166
+ const propertyDefinition = jsonSchema.properties[propertyName];
167
+
168
+ if (isValidPropertyDefinition(propertyDefinition)) {
169
+ if (propertyDefinition.default) {
170
+ config[castProperty] = propertyDefinition.default as Config[keyof Config];
171
+ }
172
+
173
+ if (propertyDefinition.type === 'array' && arrayHasInvalidDefaults(propertyDefinition)) {
174
+ throw new Error(
175
+ `When setting schema array defaults for the array assigned to "${String(
176
+ castProperty
177
+ )}", set them on the array and not the item.`
178
+ );
179
+ }
180
+
181
+ if (propertyDefinition.type === 'object') {
182
+ if (objectHasInvalidDefaults(propertyDefinition)) {
183
+ throw new Error(
184
+ `When setting schema property defaults for the object assigned to "${String(
185
+ castProperty
186
+ )}", set them on the object and not the property.`
187
+ );
188
+ }
189
+
190
+ if (recordHasInvalidDefaults(propertyDefinition)) {
191
+ throw new Error(
192
+ `When setting schema property defaults for the object assigned to "${String(
193
+ castProperty
194
+ )}", set them on the object and not the property.`
195
+ );
196
+ }
197
+ }
198
+ }
199
+
200
+ configBuilder[castProperty] = ((value: Config[keyof Config] | DerivedValueCallback, override?: boolean) => {
201
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
202
+ if (isInvalidPropertyOverride(config[castProperty], override)) {
203
+ throw new Error(
204
+ `A value already exists for "${String(
205
+ castProperty
206
+ )}". You may be trying to add a new values before flushing the old one. If you intended to override the existing value, pass in true as the second argument.`
207
+ );
208
+ }
209
+
210
+ let propertyValue: Config[keyof Config];
211
+ const MAX_DEPTH = 1;
212
+
213
+ if (!isValidValue(value)) {
214
+ throw new Error(
215
+ `"${String(
216
+ castProperty
217
+ )}" value has a depth greater than ${MAX_DEPTH}. To pass in objects with a depth greater than ${MAX_DEPTH}, create a builder for that config slice.`
218
+ );
219
+ }
220
+
221
+ if (isDerivedValueCallback<DerivedValueCallback>(value)) {
222
+ callbacks[castProperty] = value;
223
+ propertyValue = value(config);
224
+ } else {
225
+ propertyValue = value;
226
+ }
227
+
228
+ config[castProperty] = propertyValue;
229
+ return configBuilder;
230
+ }) as ConfigBuilder[keyof Config];
231
+ }
232
+
233
+ return configBuilder;
234
+ };
@@ -0,0 +1,45 @@
1
+ import { config } from './__testUtils__/config.ts';
2
+
3
+ describe('createConfigReader', () => {
4
+ describe('when a user accesses a known property', () => {
5
+ it('should return the correct value', async () => {
6
+ const { createConfigReader } = await import('./createConfigReader.ts');
7
+ const reader = createConfigReader(config);
8
+ const value = reader('countryCode');
9
+ expect(value).toBe('GB');
10
+ });
11
+ });
12
+
13
+ describe('when a user accesses a known nested property', () => {
14
+ it('should return the correct value', async () => {
15
+ const { createConfigReader } = await import('./createConfigReader.ts');
16
+ const reader = createConfigReader(config);
17
+ const value = reader('pages.contactDetails.name');
18
+ expect(value).toBe('contactDetails');
19
+ });
20
+ });
21
+
22
+ describe('when the reader is scoped', () => {
23
+ describe('when a user accesses a known property', () => {
24
+ it('should return the correct value', async () => {
25
+ const { createConfigReader } = await import('./createConfigReader.ts');
26
+ const reader = createConfigReader(config);
27
+ const scopedReader = reader.scope('pages.contactDetails');
28
+ const value = scopedReader('name');
29
+ expect(value).toBe('contactDetails');
30
+ });
31
+ });
32
+ });
33
+
34
+ describe('when the reader is scoped multiple times', () => {
35
+ describe('when a user accesses a known property', () => {
36
+ it('should return the correct value', async () => {
37
+ const { createConfigReader } = await import('./createConfigReader.ts');
38
+ const reader = createConfigReader(config);
39
+ const scopedReader = reader.scope('pages.contactDetails').scope('sections.1.sections').scope('0');
40
+ const value = scopedReader('name');
41
+ expect(value).toBe('main');
42
+ });
43
+ });
44
+ });
45
+ });
@@ -0,0 +1,12 @@
1
+ import get from 'lodash/get.js';
2
+ import type { Get, Join } from 'type-fest';
3
+ import type { Leaves, Paths } from './types.ts';
4
+
5
+ export const createConfigReader = <Config extends object>(config: Config) => {
6
+ const configReader = <Path extends Join<Leaves<Config>, '.'>>(path: Path) => get(config, path);
7
+
8
+ configReader.scope = <Scope extends Join<Paths<Config>, '.'>>(scope: Scope) =>
9
+ createConfigReader(get(config, scope) as Get<Config, Scope>);
10
+
11
+ return configReader;
12
+ };
@@ -0,0 +1,247 @@
1
+ export const countryCodes = [
2
+ 'AF',
3
+ 'AX',
4
+ 'AL',
5
+ 'DZ',
6
+ 'AS',
7
+ 'AD',
8
+ 'AO',
9
+ 'AI',
10
+ 'AQ',
11
+ 'AG',
12
+ 'AR',
13
+ 'AM',
14
+ 'AW',
15
+ 'AU',
16
+ 'AT',
17
+ 'AZ',
18
+ 'BS',
19
+ 'BH',
20
+ 'BD',
21
+ 'BB',
22
+ 'BY',
23
+ 'BE',
24
+ 'BZ',
25
+ 'BJ',
26
+ 'BM',
27
+ 'BT',
28
+ 'BO',
29
+ 'BA',
30
+ 'BW',
31
+ 'BV',
32
+ 'BR',
33
+ 'IO',
34
+ 'BN',
35
+ 'BG',
36
+ 'BF',
37
+ 'BI',
38
+ 'KH',
39
+ 'CM',
40
+ 'CA',
41
+ 'CV',
42
+ 'KY',
43
+ 'CF',
44
+ 'TD',
45
+ 'CL',
46
+ 'CN',
47
+ 'CX',
48
+ 'CC',
49
+ 'CO',
50
+ 'KM',
51
+ 'CG',
52
+ 'CD',
53
+ 'CK',
54
+ 'CR',
55
+ 'CI',
56
+ 'HR',
57
+ 'CU',
58
+ 'CY',
59
+ 'CZ',
60
+ 'DK',
61
+ 'DJ',
62
+ 'DM',
63
+ 'DO',
64
+ 'EC',
65
+ 'EG',
66
+ 'SV',
67
+ 'GQ',
68
+ 'ER',
69
+ 'EE',
70
+ 'ET',
71
+ 'FK',
72
+ 'FO',
73
+ 'FJ',
74
+ 'FI',
75
+ 'FR',
76
+ 'GF',
77
+ 'PF',
78
+ 'TF',
79
+ 'GA',
80
+ 'GM',
81
+ 'GE',
82
+ 'DE',
83
+ 'GH',
84
+ 'GI',
85
+ 'GR',
86
+ 'GL',
87
+ 'GD',
88
+ 'GP',
89
+ 'GU',
90
+ 'GT',
91
+ 'GG',
92
+ 'GN',
93
+ 'GW',
94
+ 'GY',
95
+ 'HT',
96
+ 'HM',
97
+ 'VA',
98
+ 'HN',
99
+ 'HK',
100
+ 'HU',
101
+ 'IS',
102
+ 'IN',
103
+ 'ID',
104
+ 'IR',
105
+ 'IQ',
106
+ 'IE',
107
+ 'IM',
108
+ 'IL',
109
+ 'IT',
110
+ 'JM',
111
+ 'JP',
112
+ 'JE',
113
+ 'JO',
114
+ 'KZ',
115
+ 'KE',
116
+ 'KI',
117
+ 'KR',
118
+ 'KW',
119
+ 'KG',
120
+ 'LA',
121
+ 'LV',
122
+ 'LB',
123
+ 'LS',
124
+ 'LR',
125
+ 'LY',
126
+ 'LI',
127
+ 'LT',
128
+ 'LU',
129
+ 'MO',
130
+ 'MK',
131
+ 'MG',
132
+ 'MW',
133
+ 'MY',
134
+ 'MV',
135
+ 'ML',
136
+ 'MT',
137
+ 'MH',
138
+ 'MQ',
139
+ 'MR',
140
+ 'MU',
141
+ 'YT',
142
+ 'MX',
143
+ 'FM',
144
+ 'MD',
145
+ 'MC',
146
+ 'MN',
147
+ 'ME',
148
+ 'MS',
149
+ 'MA',
150
+ 'MZ',
151
+ 'MM',
152
+ 'NA',
153
+ 'NR',
154
+ 'NP',
155
+ 'NL',
156
+ 'AN',
157
+ 'NC',
158
+ 'NZ',
159
+ 'NI',
160
+ 'NE',
161
+ 'NG',
162
+ 'NU',
163
+ 'NF',
164
+ 'MP',
165
+ 'NO',
166
+ 'OM',
167
+ 'PK',
168
+ 'PW',
169
+ 'PS',
170
+ 'PA',
171
+ 'PG',
172
+ 'PY',
173
+ 'PE',
174
+ 'PH',
175
+ 'PN',
176
+ 'PL',
177
+ 'PT',
178
+ 'PR',
179
+ 'QA',
180
+ 'RE',
181
+ 'RO',
182
+ 'RU',
183
+ 'RW',
184
+ 'BL',
185
+ 'SH',
186
+ 'KN',
187
+ 'LC',
188
+ 'MF',
189
+ 'PM',
190
+ 'VC',
191
+ 'WS',
192
+ 'SM',
193
+ 'ST',
194
+ 'SA',
195
+ 'SN',
196
+ 'RS',
197
+ 'SC',
198
+ 'SL',
199
+ 'SG',
200
+ 'SK',
201
+ 'SI',
202
+ 'SB',
203
+ 'SO',
204
+ 'ZA',
205
+ 'GS',
206
+ 'ES',
207
+ 'LK',
208
+ 'SD',
209
+ 'SR',
210
+ 'SJ',
211
+ 'SZ',
212
+ 'SE',
213
+ 'CH',
214
+ 'SY',
215
+ 'TW',
216
+ 'TJ',
217
+ 'TZ',
218
+ 'TH',
219
+ 'TL',
220
+ 'TG',
221
+ 'TK',
222
+ 'TO',
223
+ 'TT',
224
+ 'TN',
225
+ 'TR',
226
+ 'TM',
227
+ 'TC',
228
+ 'TV',
229
+ 'UG',
230
+ 'UA',
231
+ 'AE',
232
+ 'GB',
233
+ 'US',
234
+ 'UM',
235
+ 'UY',
236
+ 'UZ',
237
+ 'VU',
238
+ 'VE',
239
+ 'VN',
240
+ 'VG',
241
+ 'VI',
242
+ 'WF',
243
+ 'EH',
244
+ 'YE',
245
+ 'ZM',
246
+ 'ZW',
247
+ ] as const;