zcb 0.0.6 → 0.0.8
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/LICENSE +1 -1
- package/README.md +359 -3
- package/bin/zcb.mjs +1 -1
- package/package.json +19 -13
- package/src/__testUtils__/builtConfig.ts +1 -0
- package/src/__testUtils__/configReader.ts +7 -0
- package/src/cli.ts +50 -70
- package/src/createConfigBuilder.test.ts +2 -1
- package/src/createConfigBuilder.ts +7 -18
- package/src/createConfigParser.ts +16 -0
- package/src/createConfigReader.test.ts +9 -13
- package/src/createConfigReader.ts +4 -7
- package/src/index.ts +3 -0
- package/src/templates/config.ts.hbs +1 -0
- package/src/transformers/cloneNonEnumerableValues.ts +2 -2
- package/src/transformers/removeDisabledSlices.ts +7 -2
- package/src/transformers/runExperiments.ts +24 -0
- package/src/transformers/setupExperiments.ts +10 -6
- package/src/types.ts +30 -6
- package/src/utils/importValidateTransformWriteConfig.ts +60 -0
- package/src/utils/isPropertyReservedWord.ts +11 -1
- package/src/utils/{writeConfig.ts → transformWriteConfig.ts} +2 -2
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,8 +1,364 @@
|
|
|
1
|
-
#
|
|
1
|
+
# zcb
|
|
2
2
|
|
|
3
3
|
Build configs with type safety from zod schema.
|
|
4
4
|
|
|
5
|
-
[](https://badge.fury.io/js/zcb)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Define a configuration schema with [`zod`](https://github.com/colinhacks/zod) and use the output of that in `zcb` to create a config builder with autocomplete and setter value autocomplete.
|
|
9
|
+
|
|
10
|
+
Use our cli module to build and/or watch a config builder file and transform it into a literally typed config object.
|
|
11
|
+
|
|
12
|
+
Import that literally typed config in your components along with `zcb` and create a config reader with scoping abilities, config path autocomplete and return value preview.
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
npm install zcb
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
* [Create schema](#create-schema)
|
|
23
|
+
* [Create config builder](#create-config-builder)
|
|
24
|
+
* [Transform config builder](#transform-config-builder)
|
|
25
|
+
* [Create config reader](#create-config-reader)
|
|
26
|
+
* [Use config reader](#use-config-reader)
|
|
27
|
+
|
|
28
|
+
### Create schema
|
|
29
|
+
|
|
30
|
+
Create the schema for your configuration like in the example below.
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
// ./schema.ts
|
|
34
|
+
import { z } from 'zod';
|
|
35
|
+
import {
|
|
36
|
+
countryCodes,
|
|
37
|
+
countryNames,
|
|
38
|
+
distanceUnits,
|
|
39
|
+
languageCodes,
|
|
40
|
+
timezones,
|
|
41
|
+
} from 'zcb';
|
|
42
|
+
|
|
43
|
+
export const baseSectionSchema = z.object({
|
|
44
|
+
name: z.string(),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
export type SectionType = z.infer<typeof baseSectionSchema> & {
|
|
48
|
+
sections?: SectionType[];
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const sectionSchema: z.ZodType<SectionType> = baseSectionSchema.extend({
|
|
52
|
+
sections: z.lazy(() => sectionSchema.array()).optional(),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
export const pageSchema = z.object({
|
|
56
|
+
name: z.string(),
|
|
57
|
+
path: z.string().optional(),
|
|
58
|
+
queryParams: z.array(z.string()).optional(),
|
|
59
|
+
sections: z.array(sectionSchema).optional(),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
export type PageType = z.infer<typeof pageSchema>;
|
|
63
|
+
|
|
64
|
+
const baseRouteSchema = z.object({
|
|
65
|
+
aliases: z.array(z.string()).optional(),
|
|
66
|
+
page: z.string(),
|
|
67
|
+
path: z.string(),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
export type RouteType = z.infer<typeof baseRouteSchema> & {
|
|
71
|
+
routes?: RouteType[];
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const routeSchema: z.ZodType<RouteType> = baseRouteSchema.extend({
|
|
75
|
+
routes: z.lazy(() => routeSchema.array()).optional(),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
export const configSchema = z.object({
|
|
79
|
+
countryCode: z.enum(countryCodes).optional(),
|
|
80
|
+
countryName: z.enum(countryNames).optional(),
|
|
81
|
+
distanceUnit: z.enum(distanceUnits).optional(),
|
|
82
|
+
languageCodes: z.array(z.enum(languageCodes)).optional(),
|
|
83
|
+
locales: z.array(z.string().regex(/[a-z]{2}_[A-Z]{2}/)).optional(),
|
|
84
|
+
name: z.string().optional(),
|
|
85
|
+
pages: z.record(pageSchema).optional(),
|
|
86
|
+
routes: z.array(routeSchema).optional(),
|
|
87
|
+
timeouts: z.record(z.number()).optional(),
|
|
88
|
+
timezone: z.enum(timezones).optional(),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
export type ConfigType = z.infer<typeof configSchema>;
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Create config builder
|
|
95
|
+
|
|
96
|
+
Then use the schema and its types to create a config builder and build out your configuration like in the example below. The config builder comes with method autocompletion and value type validation. It is important to default export the config builder as this is what the cli build/watch scripts are expecting when they import the config builder.
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
// ./configBuilder.ts
|
|
100
|
+
import kebabCase from 'lodash/kebabCase.js';
|
|
101
|
+
import { createConfigBuilder } from 'zcb';
|
|
102
|
+
import {
|
|
103
|
+
type ConfigType,
|
|
104
|
+
type PageType,
|
|
105
|
+
type RouteType,
|
|
106
|
+
type SectionType,
|
|
107
|
+
configSchema,
|
|
108
|
+
pageSchema,
|
|
109
|
+
routeSchema,
|
|
110
|
+
sectionSchema,
|
|
111
|
+
} from './schema.ts';
|
|
112
|
+
|
|
113
|
+
const configBuilder = createConfigBuilder<ConfigType>(configSchema);
|
|
114
|
+
const routeBuilder = createConfigBuilder<RouteType>(routeSchema, { path: ({ page }) => kebabCase(page) });
|
|
115
|
+
const pageBuilder = createConfigBuilder<PageType>(pageSchema);
|
|
116
|
+
const sectionBuilder = createConfigBuilder<SectionType>(sectionSchema);
|
|
117
|
+
const subsectionBuilder = sectionBuilder.fork();
|
|
118
|
+
|
|
119
|
+
configBuilder
|
|
120
|
+
.countryCode('GB')
|
|
121
|
+
.countryName('United Kingdom')
|
|
122
|
+
.distanceUnit('km')
|
|
123
|
+
.languageCodes(['en'])
|
|
124
|
+
.locales(({ countryCode, languageCodes }) =>
|
|
125
|
+
languageCodes?.length && countryCode ? languageCodes.map(code => `${code}_${countryCode}`) : []
|
|
126
|
+
)
|
|
127
|
+
.name('alpha')
|
|
128
|
+
.pages({
|
|
129
|
+
contactDetails: pageBuilder
|
|
130
|
+
.name('contactDetails')
|
|
131
|
+
.sections([
|
|
132
|
+
sectionBuilder.name('header').flush(),
|
|
133
|
+
sectionBuilder
|
|
134
|
+
.name('body')
|
|
135
|
+
.sections([subsectionBuilder.name('main').flush(), subsectionBuilder.name('sidebar').flush()])
|
|
136
|
+
.flush(),
|
|
137
|
+
sectionBuilder.name('footer').flush(),
|
|
138
|
+
])
|
|
139
|
+
.flush(),
|
|
140
|
+
personalDetails: pageBuilder
|
|
141
|
+
.name('personalDetails')
|
|
142
|
+
.sections([
|
|
143
|
+
sectionBuilder.name('header').flush(),
|
|
144
|
+
sectionBuilder
|
|
145
|
+
.name('body')
|
|
146
|
+
.sections([subsectionBuilder.name('main').flush(), subsectionBuilder.name('sidebar').flush()])
|
|
147
|
+
.flush(),
|
|
148
|
+
sectionBuilder.name('footer').flush(),
|
|
149
|
+
])
|
|
150
|
+
.flush(),
|
|
151
|
+
})
|
|
152
|
+
.routes([routeBuilder.page('personalDetails').flush(), routeBuilder.page('contactDetails').flush()])
|
|
153
|
+
.timeouts({ apollo: 10_000 })
|
|
154
|
+
.timezone('Europe/London');
|
|
155
|
+
|
|
156
|
+
export default configBuilder;
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
#### builder API
|
|
160
|
+
|
|
161
|
+
**disable: `() => ConfigBuilder`**
|
|
162
|
+
|
|
163
|
+
Use to disable a slice of config. Disabled slices are removed from the config when the config builder is transformed into a literally typed object in the cli build/watch step.
|
|
164
|
+
|
|
165
|
+
**errors: `() => ZodIssue[]`**
|
|
166
|
+
|
|
167
|
+
Use to validate the config against the schema and return any errors. The primary use for this is internal within the cli build/watch step.
|
|
168
|
+
|
|
169
|
+
**experiment: `() => ConfigBuilder`**
|
|
170
|
+
|
|
171
|
+
Use to assign an experiment ID to a slice of config. If an experiment callback file is provided to the cli build/watch step, this ID is used as a marker for where to inject experiment configuration.
|
|
172
|
+
|
|
173
|
+
**extend: `(value: ConfigBuilder) => void`**
|
|
174
|
+
|
|
175
|
+
Use to extend an existing config builder.
|
|
176
|
+
|
|
177
|
+
**flush: `() => JsonObject`**
|
|
178
|
+
|
|
179
|
+
Use to flush the values from a config builder so that it can be immediately reused.
|
|
180
|
+
|
|
181
|
+
**fork: `() => ConfigBuilder`**
|
|
182
|
+
|
|
183
|
+
Create a clone of a config builder. Useful if you need to use the same config builder within itself.
|
|
184
|
+
|
|
185
|
+
**toJson: `() => string`**
|
|
186
|
+
|
|
187
|
+
Returns the config values as a pretty-printed JSON string.
|
|
188
|
+
|
|
189
|
+
**validate: `() => boolean`**
|
|
190
|
+
|
|
191
|
+
Use to validate the config against the schema and return true/false. The primary use for this is internal within the cli build/watch step.
|
|
192
|
+
|
|
193
|
+
**values: `() => JsonObject`**
|
|
194
|
+
|
|
195
|
+
Use to return the config values as an object.
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
### Transform config builder
|
|
200
|
+
|
|
201
|
+
Use the script below or its `build` equivalent to transform a config builder file into a file that default exports a literally typed object like the one in the following example.
|
|
202
|
+
|
|
203
|
+
```sh
|
|
204
|
+
npx zcb watch ./configBuilder.ts ./builtConfig.ts
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
// ./builtConfig.ts
|
|
209
|
+
/* eslint-disable prettier/prettier, import/no-default-export, unicorn/numeric-separators-style */
|
|
210
|
+
/* This file is autogenerated, do not edit directly, your changes will not perist. */
|
|
211
|
+
|
|
212
|
+
export default {
|
|
213
|
+
countryCode: "GB",
|
|
214
|
+
countryName: "United Kingdom",
|
|
215
|
+
distanceUnit: "km",
|
|
216
|
+
languageCodes: [
|
|
217
|
+
"en"
|
|
218
|
+
],
|
|
219
|
+
locales: [
|
|
220
|
+
"en_GB"
|
|
221
|
+
],
|
|
222
|
+
name: "alpha",
|
|
223
|
+
pages: {
|
|
224
|
+
contactDetails: {
|
|
225
|
+
name: "contactDetails",
|
|
226
|
+
sections: [
|
|
227
|
+
{
|
|
228
|
+
name: "header"
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
name: "body",
|
|
232
|
+
sections: [
|
|
233
|
+
{
|
|
234
|
+
name: "main"
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
name: "sidebar"
|
|
238
|
+
}
|
|
239
|
+
]
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
name: "footer"
|
|
243
|
+
}
|
|
244
|
+
]
|
|
245
|
+
},
|
|
246
|
+
personalDetails: {
|
|
247
|
+
name: "personalDetails",
|
|
248
|
+
sections: [
|
|
249
|
+
{
|
|
250
|
+
name: "header"
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
name: "body",
|
|
254
|
+
sections: [
|
|
255
|
+
{
|
|
256
|
+
name: "main"
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
name: "sidebar"
|
|
260
|
+
}
|
|
261
|
+
]
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
name: "footer"
|
|
265
|
+
}
|
|
266
|
+
]
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
routes: [
|
|
270
|
+
{
|
|
271
|
+
page: "personalDetails",
|
|
272
|
+
path: "personal-details"
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
page: "contactDetails",
|
|
276
|
+
path: "contact-details"
|
|
277
|
+
}
|
|
278
|
+
],
|
|
279
|
+
timeouts: {
|
|
280
|
+
apollo: 10000
|
|
281
|
+
},
|
|
282
|
+
timezone: "Europe/London"
|
|
283
|
+
} as const;
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
#### cli API
|
|
287
|
+
|
|
288
|
+
* `zcb build <input-file> <output-file>`
|
|
289
|
+
|
|
290
|
+
```sh
|
|
291
|
+
Write config from a config builder
|
|
292
|
+
|
|
293
|
+
Positionals:
|
|
294
|
+
input-file The relative path to the config builder root file
|
|
295
|
+
[string] [required]
|
|
296
|
+
output-file The relative path to the output config file [string] [required]
|
|
297
|
+
|
|
298
|
+
Options:
|
|
299
|
+
--version Show version number [boolean]
|
|
300
|
+
--help Show help [boolean]
|
|
301
|
+
--experiments-callback-file The relative path to the experiment callback file
|
|
302
|
+
[string]
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
* `zcb watch <input-file> <output-file>`
|
|
306
|
+
|
|
307
|
+
```sh
|
|
308
|
+
Watch a config builder and write config
|
|
309
|
+
|
|
310
|
+
Positionals:
|
|
311
|
+
input-file The relative path to the config builder root file
|
|
312
|
+
[string] [required]
|
|
313
|
+
output-file The relative path to the output config file [string] [required]
|
|
314
|
+
|
|
315
|
+
Options:
|
|
316
|
+
--version Show version number [boolean]
|
|
317
|
+
--help Show help [boolean]
|
|
318
|
+
--experiments-callback-file The relative path to the experiment callback file
|
|
319
|
+
[string]
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### Create config reader
|
|
323
|
+
|
|
324
|
+
Then use the autogenerated config to create a config reader that you can access config values with. The autogenerated config will always be a default import.
|
|
325
|
+
|
|
326
|
+
```ts
|
|
327
|
+
// ./configReader.ts
|
|
328
|
+
import { createConfigParser, createConfigReader } from 'zcb';
|
|
329
|
+
import builtConfig from './builtConfig.ts';
|
|
330
|
+
|
|
331
|
+
export default () => createConfigReader(builtConfig);
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### Use config reader
|
|
335
|
+
|
|
336
|
+
Then import the config reader into the file in which you want access to config values. The config reader comes with config path autocomplete and return value preview.
|
|
337
|
+
|
|
338
|
+
```ts
|
|
339
|
+
import configReader from './configReader.ts';
|
|
340
|
+
|
|
341
|
+
// scope config path autocompletion and validation
|
|
342
|
+
const scopedReader = configReader.scope('pages.contactDetails')
|
|
343
|
+
.scope('sections.1.sections')
|
|
344
|
+
.scope('0');
|
|
345
|
+
// reader config path autocompletion and validation
|
|
346
|
+
// value and type preview
|
|
347
|
+
const value = scopedReader('name');
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
#### reader API
|
|
351
|
+
|
|
352
|
+
**scope: `(value: string) => Get<Config, string>`**
|
|
353
|
+
|
|
354
|
+
Use to scope a reader to a slice of config, rather than having to pass in the full config path every time.
|
|
355
|
+
|
|
356
|
+
---
|
|
357
|
+
|
|
358
|
+
## Changelog
|
|
359
|
+
|
|
360
|
+
Check out the [features, fixes and more](CHANGELOG.md) that go into each major, minor and patch version.
|
|
361
|
+
|
|
362
|
+
## License
|
|
363
|
+
|
|
364
|
+
zcb is [MIT Licensed](LICENSE).
|
package/bin/zcb.mjs
CHANGED
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.0.
|
|
4
|
+
"version": "0.0.8",
|
|
5
5
|
"author": "Dylan Aubrey",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"homepage": "https://github.com/badbatch/zod-config-builder",
|
|
@@ -48,23 +48,23 @@
|
|
|
48
48
|
"@commitlint/config-conventional": "^17.6.3",
|
|
49
49
|
"@commitlint/prompt-cli": "^17.6.3",
|
|
50
50
|
"@jest/globals": "^29.5.0",
|
|
51
|
-
"@repodog/babel-config": "^1.
|
|
52
|
-
"@repodog/cli": "^1.1.
|
|
53
|
-
"@repodog/commitlint-config": "^1.
|
|
54
|
-
"@repodog/eslint-config": "^1.
|
|
55
|
-
"@repodog/jest-config": "^1.
|
|
56
|
-
"@repodog/markdownlint-config": "^1.
|
|
57
|
-
"@repodog/prettier-config": "^1.
|
|
58
|
-
"@repodog/rollup-config": "^1.
|
|
59
|
-
"@repodog/syncpack-config": "1.
|
|
60
|
-
"@repodog/ts-config": "^1.
|
|
51
|
+
"@repodog/babel-config": "^1.1.3",
|
|
52
|
+
"@repodog/cli": "^1.1.4",
|
|
53
|
+
"@repodog/commitlint-config": "^1.1.3",
|
|
54
|
+
"@repodog/eslint-config": "^1.1.3",
|
|
55
|
+
"@repodog/jest-config": "^1.1.3",
|
|
56
|
+
"@repodog/markdownlint-config": "^1.1.3",
|
|
57
|
+
"@repodog/prettier-config": "^1.1.3",
|
|
58
|
+
"@repodog/rollup-config": "^1.1.3",
|
|
59
|
+
"@repodog/syncpack-config": "^1.1.3",
|
|
60
|
+
"@repodog/ts-config": "^1.1.3",
|
|
61
61
|
"@rollup/plugin-babel": "^6.0.3",
|
|
62
62
|
"@rollup/plugin-image": "^3.0.2",
|
|
63
63
|
"@rollup/plugin-json": "^6.0.0",
|
|
64
64
|
"@rollup/plugin-node-resolve": "^15.0.2",
|
|
65
65
|
"@rollup/plugin-terser": "^0.4.1",
|
|
66
66
|
"@types/fs-extra": "^11.0.1",
|
|
67
|
-
"@types/jest": "^
|
|
67
|
+
"@types/jest": "^25.1.3",
|
|
68
68
|
"@types/json-schema": "^7.0.11",
|
|
69
69
|
"@types/lodash": "^4.14.191",
|
|
70
70
|
"@types/node": "^18.11.18",
|
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
"eslint-plugin-sort-destructure-keys": "^1.5.0",
|
|
91
91
|
"eslint-plugin-sort-keys-fix": "^1.1.2",
|
|
92
92
|
"eslint-plugin-typescript-sort-keys": "^2.3.0",
|
|
93
|
-
"eslint-plugin-unicorn": "^
|
|
93
|
+
"eslint-plugin-unicorn": "^46.0.1",
|
|
94
94
|
"generate-changelog": "^1.8.0",
|
|
95
95
|
"husky": "^8.0.3",
|
|
96
96
|
"identity-obj-proxy": "^3.0.0",
|
|
@@ -108,6 +108,11 @@
|
|
|
108
108
|
"type-fest": "^3.10.0",
|
|
109
109
|
"typescript": "^5.0.3"
|
|
110
110
|
},
|
|
111
|
+
"keywords": [
|
|
112
|
+
"config",
|
|
113
|
+
"configuration",
|
|
114
|
+
"zod"
|
|
115
|
+
],
|
|
111
116
|
"scripts": {
|
|
112
117
|
"build": "pnpm run clean:dist && pnpm run compile",
|
|
113
118
|
"clean:deps": "del-cli ./node_modules",
|
|
@@ -124,6 +129,7 @@
|
|
|
124
129
|
"test": "node --require=suppress-experimental-warnings --experimental-vm-modules node_modules/jest/bin/jest.js",
|
|
125
130
|
"type-check": "tsc --noEmit",
|
|
126
131
|
"validate": "syncpack format && syncpack lint-semver-ranges && pnpm run build && pnpm run lint && pnpm run type-check && pnpm run test",
|
|
132
|
+
"zcb:build": "NODE_OPTIONS=\"--loader ts-node/esm\" node ./bin/zcb.mjs build ./src/__testUtils__/configBuilder.ts ./src/__testUtils__/builtConfig.ts",
|
|
127
133
|
"zcb:watch": "NODE_OPTIONS=\"--loader ts-node/esm\" node ./bin/zcb.mjs watch ./src/__testUtils__/configBuilder.ts ./src/__testUtils__/builtConfig.ts"
|
|
128
134
|
}
|
|
129
135
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { createConfigParser } from '../createConfigParser.ts';
|
|
2
|
+
import { createConfigReader } from '../createConfigReader.ts';
|
|
3
|
+
import buildConfig from './builtConfig.ts';
|
|
4
|
+
|
|
5
|
+
const config = await createConfigParser(buildConfig);
|
|
6
|
+
|
|
7
|
+
export const createReader = () => createConfigReader(config);
|
package/src/cli.ts
CHANGED
|
@@ -2,85 +2,65 @@ import { watchFile } from 'node:fs';
|
|
|
2
2
|
import { resolve } from 'node:path';
|
|
3
3
|
import shelljs from 'shelljs';
|
|
4
4
|
import yargs from 'yargs';
|
|
5
|
-
import
|
|
6
|
-
|
|
5
|
+
import { importValidateTransformWriteConfig } from './utils/importValidateTransformWriteConfig.ts';
|
|
6
|
+
|
|
7
|
+
export enum Commands {
|
|
8
|
+
BUILD = 'build',
|
|
9
|
+
WATCH = 'watch',
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const generateArguments = (cmdYargs: yargs.Argv) =>
|
|
13
|
+
cmdYargs
|
|
14
|
+
.positional('input-file', {
|
|
15
|
+
demandOption: true,
|
|
16
|
+
desc: 'The relative path to the config builder root file',
|
|
17
|
+
type: 'string',
|
|
18
|
+
})
|
|
19
|
+
.positional('output-file', {
|
|
20
|
+
demandOption: true,
|
|
21
|
+
desc: 'The relative path to the output config file',
|
|
22
|
+
type: 'string',
|
|
23
|
+
})
|
|
24
|
+
.option('experiments-callback-file', {
|
|
25
|
+
desc: 'The relative path to the experiment callback file',
|
|
26
|
+
type: 'string',
|
|
27
|
+
});
|
|
7
28
|
|
|
8
29
|
export const cli = () => {
|
|
9
30
|
yargs
|
|
10
31
|
.command(
|
|
11
|
-
'watch <
|
|
12
|
-
'
|
|
13
|
-
cmdYargs =>
|
|
14
|
-
cmdYargs
|
|
15
|
-
.positional('inputFile', {
|
|
16
|
-
demandOption: true,
|
|
17
|
-
desc: 'The relative path to the config builder root file',
|
|
18
|
-
type: 'string',
|
|
19
|
-
})
|
|
20
|
-
.positional('outputFile', {
|
|
21
|
-
demandOption: true,
|
|
22
|
-
desc: 'The relative path to the output config file',
|
|
23
|
-
type: 'string',
|
|
24
|
-
})
|
|
25
|
-
.option('experiments-callback', {
|
|
26
|
-
desc: 'The relative path to the experiment callback file',
|
|
27
|
-
type: 'string',
|
|
28
|
-
}),
|
|
32
|
+
'watch <input-file> <output-file>',
|
|
33
|
+
'Watch a config builder and write config',
|
|
34
|
+
cmdYargs => generateArguments(cmdYargs),
|
|
29
35
|
argv => {
|
|
30
|
-
|
|
31
|
-
shelljs.echo(`zcd watch => watching file: ${argv.inputFile}`);
|
|
32
|
-
|
|
33
|
-
watchFile(fullWatchFilePath, () => {
|
|
34
|
-
import(fullWatchFilePath)
|
|
35
|
-
.then(
|
|
36
|
-
({
|
|
37
|
-
default: configBuilder,
|
|
38
|
-
}: {
|
|
39
|
-
default: ReturnType<typeof import('./createConfigBuilder.ts')['createConfigBuilder']>;
|
|
40
|
-
}) => {
|
|
41
|
-
shelljs.echo('zcd watch => file change detected');
|
|
42
|
-
|
|
43
|
-
if (!configBuilder.validate()) {
|
|
44
|
-
shelljs.echo('zcd watch => invalid config');
|
|
45
|
-
shelljs.echo(`zcd watch => errors:\n${JSON.stringify(configBuilder.errors(), undefined, 2)}\n`);
|
|
46
|
-
shelljs.exit(1);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
shelljs.echo('zcd watch => valid config');
|
|
50
|
-
shelljs.echo(`zcd watch => config values:\n${configBuilder.toJson()}\n`);
|
|
51
|
-
|
|
52
|
-
if (argv['experiments-callback']) {
|
|
53
|
-
import(resolve(process.cwd(), argv['experiments-callback']))
|
|
54
|
-
.then(({ default: experimentsCallback }: { default: ExperimentsCallback }) => {
|
|
55
|
-
void writeConfig(configBuilder.values(), { experimentsCallback, outputFile: argv.outputFile });
|
|
56
|
-
})
|
|
57
|
-
.catch((error: unknown) => {
|
|
58
|
-
if (error instanceof Error) {
|
|
59
|
-
shelljs.echo(`zcd watch => error message: ${error.message}`);
|
|
60
|
-
|
|
61
|
-
if (error.stack) {
|
|
62
|
-
shelljs.echo(`zcd watch => error stack:\n${error.stack}\n`);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
});
|
|
36
|
+
shelljs.echo(`zcd watch => watching file: ${argv['input-file']}`);
|
|
66
37
|
|
|
67
|
-
|
|
68
|
-
|
|
38
|
+
watchFile(resolve(process.cwd(), argv['input-file']), () => {
|
|
39
|
+
shelljs.echo('zcd watch => file change detected');
|
|
69
40
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
.
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if (error.stack) {
|
|
78
|
-
shelljs.echo(`zcd watch => error stack:\n${error.stack}\n`);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
});
|
|
41
|
+
importValidateTransformWriteConfig(
|
|
42
|
+
argv['input-file'],
|
|
43
|
+
argv['output-file'],
|
|
44
|
+
Commands.WATCH,
|
|
45
|
+
argv['experiments-callback-file']
|
|
46
|
+
);
|
|
82
47
|
});
|
|
83
48
|
}
|
|
84
49
|
)
|
|
50
|
+
.command(
|
|
51
|
+
'build <input-file> <output-file>',
|
|
52
|
+
'Write config from a config builder',
|
|
53
|
+
cmdYargs => generateArguments(cmdYargs),
|
|
54
|
+
argv => {
|
|
55
|
+
shelljs.echo(`zcd build => building file: ${argv['input-file']}`);
|
|
56
|
+
|
|
57
|
+
importValidateTransformWriteConfig(
|
|
58
|
+
argv['input-file'],
|
|
59
|
+
argv['output-file'],
|
|
60
|
+
Commands.BUILD,
|
|
61
|
+
argv['experiments-callback-file']
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
)
|
|
85
65
|
.help().argv;
|
|
86
66
|
};
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
pageSchema,
|
|
9
9
|
routeSchema,
|
|
10
10
|
} from './__testUtils__/schema.ts';
|
|
11
|
+
import { RESERVED_KEYWORDS } from './utils/isPropertyReservedWord.ts';
|
|
11
12
|
|
|
12
13
|
describe('createConfigBuilder', () => {
|
|
13
14
|
describe('when a user passes in a schema with a root type other than "object"', () => {
|
|
@@ -23,7 +24,7 @@ describe('createConfigBuilder', () => {
|
|
|
23
24
|
|
|
24
25
|
describe('when a user uses a key in the schema that is a reserved keyword', () => {
|
|
25
26
|
it('should throw an error', async () => {
|
|
26
|
-
const {
|
|
27
|
+
const { createConfigBuilder } = await import('./createConfigBuilder.ts');
|
|
27
28
|
|
|
28
29
|
const invalidSchema = z.object({
|
|
29
30
|
values: z.string(),
|
|
@@ -2,10 +2,11 @@ import { type JSONSchema7 } from 'json-schema';
|
|
|
2
2
|
import { type ZodError, type z } from 'zod';
|
|
3
3
|
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
4
4
|
import { cloneNonEnumerableValues } from './transformers/cloneNonEnumerableValues.ts';
|
|
5
|
+
import { NonEmumeralProperties } from './types.ts';
|
|
5
6
|
import { arrayHasInvalidDefaults } from './utils/arrayHasInvalidDefaults.ts';
|
|
6
7
|
import { isDerivedValueCallback } from './utils/isDerivedValueCallback.ts';
|
|
7
8
|
import { isInvalidPropertyOverride } from './utils/isInvalidPropertyOverride.ts';
|
|
8
|
-
import { isPropertyReservedWord } from './utils/isPropertyReservedWord.ts';
|
|
9
|
+
import { RESERVED_KEYWORDS, isPropertyReservedWord } from './utils/isPropertyReservedWord.ts';
|
|
9
10
|
import { isSchemaValid } from './utils/isSchemaValid.ts';
|
|
10
11
|
import { isValidPropertyDefinition } from './utils/isValidPropertyDefinition.ts';
|
|
11
12
|
import { isValidValue } from './utils/isValidValue.ts';
|
|
@@ -13,18 +14,6 @@ import { objectHasInvalidDefaults } from './utils/objectHasInvalidDefaults.ts';
|
|
|
13
14
|
import { recordHasInvalidDefaults } from './utils/recordHasInvalidDefaults.ts';
|
|
14
15
|
import { transformConfigSync } from './utils/transformConfig.ts';
|
|
15
16
|
|
|
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
17
|
export const createConfigBuilder = <ZodTypes>(
|
|
29
18
|
zodSchema: z.ZodSchema,
|
|
30
19
|
derivedValueCallbacks: Partial<
|
|
@@ -65,7 +54,7 @@ export const createConfigBuilder = <ZodTypes>(
|
|
|
65
54
|
|
|
66
55
|
let config = initialValues as Config;
|
|
67
56
|
|
|
68
|
-
Object.defineProperty(config,
|
|
57
|
+
Object.defineProperty(config, NonEmumeralProperties.ZCB, {
|
|
69
58
|
configurable: false,
|
|
70
59
|
enumerable: false,
|
|
71
60
|
value: true,
|
|
@@ -75,7 +64,7 @@ export const createConfigBuilder = <ZodTypes>(
|
|
|
75
64
|
|
|
76
65
|
const configBuilder = {
|
|
77
66
|
disable: () => {
|
|
78
|
-
Object.defineProperty(config,
|
|
67
|
+
Object.defineProperty(config, NonEmumeralProperties.DISABLED, {
|
|
79
68
|
configurable: false,
|
|
80
69
|
enumerable: false,
|
|
81
70
|
value: true,
|
|
@@ -92,7 +81,7 @@ export const createConfigBuilder = <ZodTypes>(
|
|
|
92
81
|
}
|
|
93
82
|
},
|
|
94
83
|
experiment: (key: string) => {
|
|
95
|
-
Object.defineProperty(config,
|
|
84
|
+
Object.defineProperty(config, NonEmumeralProperties.EXPERIMENT, {
|
|
96
85
|
configurable: false,
|
|
97
86
|
enumerable: false,
|
|
98
87
|
value: key,
|
|
@@ -109,7 +98,7 @@ export const createConfigBuilder = <ZodTypes>(
|
|
|
109
98
|
const values = configBuilder.values();
|
|
110
99
|
config = {} as Config;
|
|
111
100
|
|
|
112
|
-
Object.defineProperty(config,
|
|
101
|
+
Object.defineProperty(config, NonEmumeralProperties.ZCB, {
|
|
113
102
|
configurable: false,
|
|
114
103
|
enumerable: false,
|
|
115
104
|
value: true,
|
|
@@ -141,7 +130,7 @@ export const createConfigBuilder = <ZodTypes>(
|
|
|
141
130
|
},
|
|
142
131
|
} as unknown as ConfigBuilder;
|
|
143
132
|
|
|
144
|
-
Object.defineProperty(configBuilder,
|
|
133
|
+
Object.defineProperty(configBuilder, NonEmumeralProperties.CALLBACKS, {
|
|
145
134
|
configurable: false,
|
|
146
135
|
enumerable: false,
|
|
147
136
|
value: callbacks,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { runExperiments } from './transformers/runExperiments.ts';
|
|
2
|
+
import type { ConfigParserOptions, TransformConfigHandler } from './types.ts';
|
|
3
|
+
import { transformConfig } from './utils/transformConfig.ts';
|
|
4
|
+
|
|
5
|
+
export const createConfigParser = async <Config extends object>(
|
|
6
|
+
config: Config,
|
|
7
|
+
{ experimentsCallback }: ConfigParserOptions = {}
|
|
8
|
+
) => {
|
|
9
|
+
const handlers: TransformConfigHandler[] = [];
|
|
10
|
+
|
|
11
|
+
if (experimentsCallback) {
|
|
12
|
+
handlers.push(runExperiments(experimentsCallback));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return handlers.length > 0 ? await transformConfig(config, handlers) : config;
|
|
16
|
+
};
|
|
@@ -1,19 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createReader } from './__testUtils__/configReader.ts';
|
|
2
2
|
|
|
3
3
|
describe('createConfigReader', () => {
|
|
4
4
|
describe('when a user accesses a known property', () => {
|
|
5
|
-
it('should return the correct value',
|
|
6
|
-
const
|
|
7
|
-
const reader = createConfigReader(config);
|
|
5
|
+
it('should return the correct value', () => {
|
|
6
|
+
const reader = createReader();
|
|
8
7
|
const value = reader('countryCode');
|
|
9
8
|
expect(value).toBe('GB');
|
|
10
9
|
});
|
|
11
10
|
});
|
|
12
11
|
|
|
13
12
|
describe('when a user accesses a known nested property', () => {
|
|
14
|
-
it('should return the correct value',
|
|
15
|
-
const
|
|
16
|
-
const reader = createConfigReader(config);
|
|
13
|
+
it('should return the correct value', () => {
|
|
14
|
+
const reader = createReader();
|
|
17
15
|
const value = reader('pages.contactDetails.name');
|
|
18
16
|
expect(value).toBe('contactDetails');
|
|
19
17
|
});
|
|
@@ -21,9 +19,8 @@ describe('createConfigReader', () => {
|
|
|
21
19
|
|
|
22
20
|
describe('when the reader is scoped', () => {
|
|
23
21
|
describe('when a user accesses a known property', () => {
|
|
24
|
-
it('should return the correct value',
|
|
25
|
-
const
|
|
26
|
-
const reader = createConfigReader(config);
|
|
22
|
+
it('should return the correct value', () => {
|
|
23
|
+
const reader = createReader();
|
|
27
24
|
const scopedReader = reader.scope('pages.contactDetails');
|
|
28
25
|
const value = scopedReader('name');
|
|
29
26
|
expect(value).toBe('contactDetails');
|
|
@@ -33,9 +30,8 @@ describe('createConfigReader', () => {
|
|
|
33
30
|
|
|
34
31
|
describe('when the reader is scoped multiple times', () => {
|
|
35
32
|
describe('when a user accesses a known property', () => {
|
|
36
|
-
it('should return the correct value',
|
|
37
|
-
const
|
|
38
|
-
const reader = createConfigReader(config);
|
|
33
|
+
it('should return the correct value', () => {
|
|
34
|
+
const reader = createReader();
|
|
39
35
|
const scopedReader = reader.scope('pages.contactDetails').scope('sections.1.sections').scope('0');
|
|
40
36
|
const value = scopedReader('name');
|
|
41
37
|
expect(value).toBe('main');
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
import get from 'lodash/get.js';
|
|
2
|
-
import type { Get
|
|
3
|
-
import type {
|
|
2
|
+
import type { Get } from 'type-fest';
|
|
3
|
+
import type { Path, Scope } from './types.ts';
|
|
4
4
|
|
|
5
5
|
export const createConfigReader = <Config extends object>(config: Config) => {
|
|
6
|
-
const configReader = <
|
|
7
|
-
|
|
8
|
-
configReader.scope = <Scope extends Join<Paths<Config>, '.'>>(scope: Scope) =>
|
|
9
|
-
createConfigReader(get(config, scope) as Get<Config, Scope>);
|
|
10
|
-
|
|
6
|
+
const configReader = <P extends Path<Config>>(path: P) => get(config, path);
|
|
7
|
+
configReader.scope = <S extends Scope<Config>>(scope: S) => createConfigReader(get(config, scope) as Get<Config, S>);
|
|
11
8
|
return configReader;
|
|
12
9
|
};
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
export * from './createConfigBuilder.ts';
|
|
2
|
+
export * from './createConfigParser.ts';
|
|
3
|
+
export * from './createConfigReader.ts';
|
|
2
4
|
export * from './data/countryCodes.ts';
|
|
3
5
|
export * from './data/countryNames.ts';
|
|
4
6
|
export * from './data/distanceUnits.ts';
|
|
5
7
|
export * from './data/languageCodes.ts';
|
|
6
8
|
export * from './data/timezones.ts';
|
|
9
|
+
export * from './types.ts';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type AnyRecord, NonEmumeralProperties, type TransformConfigHandlerSync } from '../types.ts';
|
|
2
2
|
|
|
3
|
-
const NON_ENUMERABLE_KEYS = new Set(
|
|
3
|
+
const NON_ENUMERABLE_KEYS = new Set(Object.values(NonEmumeralProperties));
|
|
4
4
|
|
|
5
5
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
6
6
|
export const cloneNonEnumerableValues: TransformConfigHandlerSync = <Config extends AnyRecord>(
|
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type AnyRecord,
|
|
3
|
+
NonEmumeralProperties,
|
|
4
|
+
TransformConfigHandlerAction,
|
|
5
|
+
type TransformConfigHandlerSync,
|
|
6
|
+
} from '../types.ts';
|
|
2
7
|
|
|
3
8
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
4
9
|
export const removeDisabledSlices: TransformConfigHandlerSync = <Config extends AnyRecord>(
|
|
5
10
|
clone: Config,
|
|
6
11
|
config: Config
|
|
7
12
|
) => {
|
|
8
|
-
if (
|
|
13
|
+
if (NonEmumeralProperties.DISABLED in config) {
|
|
9
14
|
return {
|
|
10
15
|
action: TransformConfigHandlerAction.DELETE_NODE,
|
|
11
16
|
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnyRecord,
|
|
3
|
+
type Buckets,
|
|
4
|
+
NonEmumeralProperties,
|
|
5
|
+
type RunExperimentsCallback,
|
|
6
|
+
type TransformConfigHandler,
|
|
7
|
+
} from '../types.ts';
|
|
8
|
+
|
|
9
|
+
export const runExperiments =
|
|
10
|
+
(callback: RunExperimentsCallback): TransformConfigHandler =>
|
|
11
|
+
async <Config extends AnyRecord>(clone: Config, config: Config) => {
|
|
12
|
+
if (NonEmumeralProperties.EXPERIMENT in config && typeof config.__experiment === 'object') {
|
|
13
|
+
const { buckets, id } = config.__experiment as { buckets: Buckets<Config>; id: string };
|
|
14
|
+
const bucket = await callback(id);
|
|
15
|
+
|
|
16
|
+
if (bucket && bucket in buckets && buckets[bucket]) {
|
|
17
|
+
return buckets[bucket]!;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
value: clone,
|
|
23
|
+
};
|
|
24
|
+
};
|
|
@@ -1,16 +1,20 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
type AnyRecord,
|
|
3
|
+
NonEmumeralProperties,
|
|
4
|
+
type SetupExperimentsCallback,
|
|
5
|
+
type TransformConfigHandler,
|
|
6
|
+
} from '../types.ts';
|
|
2
7
|
|
|
3
8
|
export const setupExperiments =
|
|
4
|
-
(callback:
|
|
9
|
+
(callback: SetupExperimentsCallback): TransformConfigHandler =>
|
|
5
10
|
async <Config extends AnyRecord>(clone: Config, config: Config) => {
|
|
6
|
-
if (
|
|
7
|
-
const
|
|
11
|
+
if (NonEmumeralProperties.EXPERIMENT in config && typeof config.__experiment === 'string') {
|
|
12
|
+
const buckets = await callback(config.__experiment, clone, config);
|
|
8
13
|
|
|
9
14
|
// @ts-expect-error private property
|
|
10
15
|
clone.__experiment = {
|
|
11
|
-
|
|
16
|
+
buckets,
|
|
12
17
|
id: config.__experiment,
|
|
13
|
-
value,
|
|
14
18
|
};
|
|
15
19
|
|
|
16
20
|
return {
|
package/src/types.ts
CHANGED
|
@@ -1,19 +1,38 @@
|
|
|
1
1
|
import type { List } from 'ts-toolbelt';
|
|
2
|
-
import type { Includes } from 'type-fest';
|
|
2
|
+
import type { Includes, Join } from 'type-fest';
|
|
3
3
|
|
|
4
4
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5
5
|
export type AnyRecord = Record<string, any>;
|
|
6
6
|
|
|
7
|
-
export
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
export type Buckets<Config extends AnyRecord> = Record<string, Experiment<Config>>;
|
|
8
|
+
|
|
9
|
+
export interface ConfigParserOptions {
|
|
10
|
+
experimentsCallback?: RunExperimentsCallback;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface Experiment<Config extends AnyRecord> {
|
|
14
|
+
action?: TransformConfigHandlerAction;
|
|
15
|
+
value?: Config;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export enum NonEmumeralProperties {
|
|
19
|
+
CALLBACKS = '__callbacks',
|
|
20
|
+
DISABLED = '__disabled',
|
|
21
|
+
EXPERIMENT = '__experiment',
|
|
22
|
+
ZCB = '__zcb',
|
|
10
23
|
}
|
|
11
24
|
|
|
12
|
-
export type
|
|
25
|
+
export type Path<Config extends object> = Join<Leaves<Config>, '.'>;
|
|
26
|
+
|
|
27
|
+
export type RunExperimentsCallback = (id: string) => Promise<string>;
|
|
28
|
+
|
|
29
|
+
export type Scope<Config extends object> = Join<Paths<Config>, '.'>;
|
|
30
|
+
|
|
31
|
+
export type SetupExperimentsCallback = <Config extends AnyRecord>(
|
|
13
32
|
id: string,
|
|
14
33
|
clone: Config,
|
|
15
34
|
config: Config
|
|
16
|
-
) => Promise<
|
|
35
|
+
) => Promise<Buckets<Config>>;
|
|
17
36
|
|
|
18
37
|
export type TransformConfigHandler = <Config extends AnyRecord>(
|
|
19
38
|
clone: Config,
|
|
@@ -35,6 +54,11 @@ export interface TransformConfigHandlerReturnType<Config extends AnyRecord> {
|
|
|
35
54
|
value?: Config;
|
|
36
55
|
}
|
|
37
56
|
|
|
57
|
+
export interface WriteConfigOptions {
|
|
58
|
+
experimentsCallback?: SetupExperimentsCallback;
|
|
59
|
+
outputFile: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
38
62
|
export type Leaves<T, Path extends string[] = []> = T extends string
|
|
39
63
|
? Path
|
|
40
64
|
: {
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import shelljs from 'shelljs';
|
|
3
|
+
import type { Commands } from '../cli.ts';
|
|
4
|
+
import type { SetupExperimentsCallback } from '../types.ts';
|
|
5
|
+
import { transformWriteConfig } from './transformWriteConfig.ts';
|
|
6
|
+
|
|
7
|
+
export const importValidateTransformWriteConfig = (
|
|
8
|
+
inputFile: string,
|
|
9
|
+
outputFile: string,
|
|
10
|
+
command: Commands,
|
|
11
|
+
experimentCallbackFile?: string
|
|
12
|
+
) => {
|
|
13
|
+
import(resolve(process.cwd(), inputFile))
|
|
14
|
+
.then(
|
|
15
|
+
({
|
|
16
|
+
default: configBuilder,
|
|
17
|
+
}: {
|
|
18
|
+
default: ReturnType<typeof import('../createConfigBuilder.ts')['createConfigBuilder']>;
|
|
19
|
+
}) => {
|
|
20
|
+
if (!configBuilder.validate()) {
|
|
21
|
+
shelljs.echo(`zcd ${command} => invalid config`);
|
|
22
|
+
shelljs.echo(`zcd ${command} => config values:\n${configBuilder.toJson()}\n`);
|
|
23
|
+
shelljs.echo(`zcd ${command} => errors:\n${JSON.stringify(configBuilder.errors(), undefined, 2)}\n`);
|
|
24
|
+
shelljs.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
shelljs.echo('zcd ${command} => valid config');
|
|
28
|
+
shelljs.echo(`zcd ${command} => config values:\n${configBuilder.toJson()}\n`);
|
|
29
|
+
|
|
30
|
+
if (experimentCallbackFile) {
|
|
31
|
+
import(resolve(process.cwd(), experimentCallbackFile))
|
|
32
|
+
.then(({ default: experimentsCallback }: { default: SetupExperimentsCallback }) => {
|
|
33
|
+
void transformWriteConfig(configBuilder.values(), { experimentsCallback, outputFile });
|
|
34
|
+
})
|
|
35
|
+
.catch((error: unknown) => {
|
|
36
|
+
if (error instanceof Error) {
|
|
37
|
+
shelljs.echo(`zcd ${command} => error message: ${error.message}`);
|
|
38
|
+
|
|
39
|
+
if (error.stack) {
|
|
40
|
+
shelljs.echo(`zcd ${command} => error stack:\n${error.stack}\n`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
void transformWriteConfig(configBuilder.values(), { outputFile });
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
.catch((error: unknown) => {
|
|
52
|
+
if (error instanceof Error) {
|
|
53
|
+
shelljs.echo(`zcd ${command} => error message: ${error.message}`);
|
|
54
|
+
|
|
55
|
+
if (error.stack) {
|
|
56
|
+
shelljs.echo(`zcd ${command} => error stack:\n${error.stack}\n`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
};
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
export const RESERVED_KEYWORDS = new Set([
|
|
2
|
+
'disable',
|
|
3
|
+
'errors',
|
|
4
|
+
'experiment',
|
|
5
|
+
'extend',
|
|
6
|
+
'flush',
|
|
7
|
+
'fork',
|
|
8
|
+
'toJson',
|
|
9
|
+
'validate',
|
|
10
|
+
'values',
|
|
11
|
+
]);
|
|
2
12
|
|
|
3
13
|
export const isPropertyReservedWord = (propertyName: string) => RESERVED_KEYWORDS.has(propertyName);
|
|
@@ -9,7 +9,7 @@ import { setupExperiments } from '../transformers/setupExperiments.ts';
|
|
|
9
9
|
import type { TransformConfigHandler, WriteConfigOptions } from '../types.ts';
|
|
10
10
|
import { transformConfig } from './transformConfig.ts';
|
|
11
11
|
|
|
12
|
-
export const
|
|
12
|
+
export const transformWriteConfig = async <Config extends object>(
|
|
13
13
|
config: Config,
|
|
14
14
|
{ experimentsCallback, outputFile }: WriteConfigOptions
|
|
15
15
|
) => {
|
|
@@ -30,5 +30,5 @@ export const writeConfig = async <Config extends object>(
|
|
|
30
30
|
|
|
31
31
|
shelljs.echo(`zcd watch => writing to file: ${outputFile}`);
|
|
32
32
|
shelljs.echo(`zcd watch => content to write:\n${output}\n`);
|
|
33
|
-
outputFileSync(outputFile, output);
|
|
33
|
+
outputFileSync(resolve(process.cwd(), outputFile), output);
|
|
34
34
|
};
|