zcb 0.0.6 → 0.0.7
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 +310 -3
- package/bin/zcb.mjs +1 -1
- package/package.json +19 -13
- package/src/__testUtils__/builtConfig.ts +1 -0
- package/src/cli.ts +50 -70
- package/src/createConfigBuilder.test.ts +2 -1
- package/src/createConfigBuilder.ts +1 -13
- package/src/index.ts +2 -0
- package/src/templates/config.ts.hbs +1 -0
- package/src/utils/importValidateWriteConfig.ts +59 -0
- package/src/utils/isPropertyReservedWord.ts +11 -1
- package/src/utils/writeConfig.ts +1 -1
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,8 +1,315 @@
|
|
|
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
|
+
|
|
27
|
+
### Create schema
|
|
28
|
+
|
|
29
|
+
Create the schema for your configuration like in the example below.
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
// ./schema.ts
|
|
33
|
+
import { z } from 'zod';
|
|
34
|
+
import { countryCodes } from '../data/countryCodes.ts';
|
|
35
|
+
import { countryNames } from '../data/countryNames.ts';
|
|
36
|
+
import { distanceUnits } from '../data/distanceUnits.ts';
|
|
37
|
+
import { languageCodes } from '../data/languageCodes.ts';
|
|
38
|
+
import { timezones } from '../data/timezones.ts';
|
|
39
|
+
|
|
40
|
+
export const baseSectionSchema = z.object({
|
|
41
|
+
name: z.string(),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
export type SectionType = z.infer<typeof baseSectionSchema> & {
|
|
45
|
+
sections?: SectionType[];
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const sectionSchema: z.ZodType<SectionType> = baseSectionSchema.extend({
|
|
49
|
+
sections: z.lazy(() => sectionSchema.array()).optional(),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
export const pageSchema = z.object({
|
|
53
|
+
name: z.string(),
|
|
54
|
+
path: z.string().optional(),
|
|
55
|
+
queryParams: z.array(z.string()).optional(),
|
|
56
|
+
sections: z.array(sectionSchema).optional(),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
export type PageType = z.infer<typeof pageSchema>;
|
|
60
|
+
|
|
61
|
+
const baseRouteSchema = z.object({
|
|
62
|
+
aliases: z.array(z.string()).optional(),
|
|
63
|
+
page: z.string(),
|
|
64
|
+
path: z.string(),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
export type RouteType = z.infer<typeof baseRouteSchema> & {
|
|
68
|
+
routes?: RouteType[];
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const routeSchema: z.ZodType<RouteType> = baseRouteSchema.extend({
|
|
72
|
+
routes: z.lazy(() => routeSchema.array()).optional(),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
export const configSchema = z.object({
|
|
76
|
+
countryCode: z.enum(countryCodes).optional(),
|
|
77
|
+
countryName: z.enum(countryNames).optional(),
|
|
78
|
+
distanceUnit: z.enum(distanceUnits).optional(),
|
|
79
|
+
languageCodes: z.array(z.enum(languageCodes)).optional(),
|
|
80
|
+
locales: z.array(z.string().regex(/[a-z]{2}_[A-Z]{2}/)).optional(),
|
|
81
|
+
name: z.string().optional(),
|
|
82
|
+
pages: z.record(pageSchema).optional(),
|
|
83
|
+
routes: z.array(routeSchema).optional(),
|
|
84
|
+
timeouts: z.record(z.number()).optional(),
|
|
85
|
+
timezone: z.enum(timezones).optional(),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
export type ConfigType = z.infer<typeof configSchema>;
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Create config builder
|
|
92
|
+
|
|
93
|
+
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.
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
// ./configBuilder.ts
|
|
97
|
+
import kebabCase from 'lodash/kebabCase.js';
|
|
98
|
+
import { createConfigBuilder } from 'zcb';
|
|
99
|
+
import {
|
|
100
|
+
type ConfigType,
|
|
101
|
+
type PageType,
|
|
102
|
+
type RouteType,
|
|
103
|
+
type SectionType,
|
|
104
|
+
configSchema,
|
|
105
|
+
pageSchema,
|
|
106
|
+
routeSchema,
|
|
107
|
+
sectionSchema,
|
|
108
|
+
} from './schema.ts';
|
|
109
|
+
|
|
110
|
+
const configBuilder = createConfigBuilder<ConfigType>(configSchema);
|
|
111
|
+
const routeBuilder = createConfigBuilder<RouteType>(routeSchema, { path: ({ page }) => kebabCase(page) });
|
|
112
|
+
const pageBuilder = createConfigBuilder<PageType>(pageSchema);
|
|
113
|
+
const sectionBuilder = createConfigBuilder<SectionType>(sectionSchema);
|
|
114
|
+
const subsectionBuilder = sectionBuilder.fork();
|
|
115
|
+
|
|
116
|
+
configBuilder
|
|
117
|
+
.countryCode('GB')
|
|
118
|
+
.countryName('United Kingdom')
|
|
119
|
+
.distanceUnit('km')
|
|
120
|
+
.languageCodes(['en'])
|
|
121
|
+
.locales(({ countryCode, languageCodes }) =>
|
|
122
|
+
languageCodes?.length && countryCode ? languageCodes.map(code => `${code}_${countryCode}`) : []
|
|
123
|
+
)
|
|
124
|
+
.name('alpha')
|
|
125
|
+
.pages({
|
|
126
|
+
contactDetails: pageBuilder
|
|
127
|
+
.name('contactDetails')
|
|
128
|
+
.sections([
|
|
129
|
+
sectionBuilder.name('header').flush(),
|
|
130
|
+
sectionBuilder
|
|
131
|
+
.name('body')
|
|
132
|
+
.sections([subsectionBuilder.name('main').flush(), subsectionBuilder.name('sidebar').flush()])
|
|
133
|
+
.flush(),
|
|
134
|
+
sectionBuilder.name('footer').flush(),
|
|
135
|
+
])
|
|
136
|
+
.flush(),
|
|
137
|
+
personalDetails: pageBuilder
|
|
138
|
+
.name('personalDetails')
|
|
139
|
+
.sections([
|
|
140
|
+
sectionBuilder.name('header').flush(),
|
|
141
|
+
sectionBuilder
|
|
142
|
+
.name('body')
|
|
143
|
+
.sections([subsectionBuilder.name('main').flush(), subsectionBuilder.name('sidebar').flush()])
|
|
144
|
+
.flush(),
|
|
145
|
+
sectionBuilder.name('footer').flush(),
|
|
146
|
+
])
|
|
147
|
+
.flush(),
|
|
148
|
+
})
|
|
149
|
+
.routes([routeBuilder.page('personalDetails').flush(), routeBuilder.page('contactDetails').flush()])
|
|
150
|
+
.timeouts({ apollo: 10_000 })
|
|
151
|
+
.timezone('Europe/London');
|
|
152
|
+
|
|
153
|
+
export default configBuilder;
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
#### builder API
|
|
157
|
+
|
|
158
|
+
**disable: `() => ConfigBuilder`**
|
|
159
|
+
|
|
160
|
+
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.
|
|
161
|
+
|
|
162
|
+
**errors: `() => ZodIssue[]`**
|
|
163
|
+
|
|
164
|
+
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.
|
|
165
|
+
|
|
166
|
+
**experiment: `() => ConfigBuilder`**
|
|
167
|
+
|
|
168
|
+
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.
|
|
169
|
+
|
|
170
|
+
**extend: `(value: ConfigBuilder) => void`**
|
|
171
|
+
|
|
172
|
+
Use to extend an existing config builder.
|
|
173
|
+
|
|
174
|
+
**flush: `() => JsonObject`**
|
|
175
|
+
|
|
176
|
+
Use to flush the values from a config builder so that it can be immediately reused.
|
|
177
|
+
|
|
178
|
+
**fork: `() => ConfigBuilder`**
|
|
179
|
+
|
|
180
|
+
Create a clone of a config builder. Useful if you need to use the same config builder within itself.
|
|
181
|
+
|
|
182
|
+
**toJson: `() => string`**
|
|
183
|
+
|
|
184
|
+
Returns the config values as a pretty-printed JSON string.
|
|
185
|
+
|
|
186
|
+
**validate: `() => boolean`**
|
|
187
|
+
|
|
188
|
+
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.
|
|
189
|
+
|
|
190
|
+
**values: `() => JsonObject`**
|
|
191
|
+
|
|
192
|
+
Use to return the config values as an object.
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
### Transform config builder
|
|
197
|
+
|
|
198
|
+
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.
|
|
199
|
+
|
|
200
|
+
```sh
|
|
201
|
+
npx zcb watch ./configBuilder.ts ./builtConfig.ts
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
// ./builtConfig.ts
|
|
206
|
+
/* eslint-disable prettier/prettier, import/no-default-export, unicorn/numeric-separators-style */
|
|
207
|
+
/* This file is autogenerated, do not edit directly, your changes will not perist. */
|
|
208
|
+
|
|
209
|
+
export default {
|
|
210
|
+
countryCode: "GB",
|
|
211
|
+
countryName: "United Kingdom",
|
|
212
|
+
distanceUnit: "km",
|
|
213
|
+
languageCodes: [
|
|
214
|
+
"en"
|
|
215
|
+
],
|
|
216
|
+
locales: [
|
|
217
|
+
"en_GB"
|
|
218
|
+
],
|
|
219
|
+
name: "alpha",
|
|
220
|
+
pages: {
|
|
221
|
+
contactDetails: {
|
|
222
|
+
name: "contactDetails",
|
|
223
|
+
sections: [
|
|
224
|
+
{
|
|
225
|
+
name: "header"
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
name: "body",
|
|
229
|
+
sections: [
|
|
230
|
+
{
|
|
231
|
+
name: "main"
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
name: "sidebar"
|
|
235
|
+
}
|
|
236
|
+
]
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
name: "footer"
|
|
240
|
+
}
|
|
241
|
+
]
|
|
242
|
+
},
|
|
243
|
+
personalDetails: {
|
|
244
|
+
name: "personalDetails",
|
|
245
|
+
sections: [
|
|
246
|
+
{
|
|
247
|
+
name: "header"
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
name: "body",
|
|
251
|
+
sections: [
|
|
252
|
+
{
|
|
253
|
+
name: "main"
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
name: "sidebar"
|
|
257
|
+
}
|
|
258
|
+
]
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
name: "footer"
|
|
262
|
+
}
|
|
263
|
+
]
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
routes: [
|
|
267
|
+
{
|
|
268
|
+
page: "personalDetails",
|
|
269
|
+
path: "personal-details"
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
page: "contactDetails",
|
|
273
|
+
path: "contact-details"
|
|
274
|
+
}
|
|
275
|
+
],
|
|
276
|
+
timeouts: {
|
|
277
|
+
apollo: 10000
|
|
278
|
+
},
|
|
279
|
+
timezone: "Europe/London"
|
|
280
|
+
} as const;
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Create config reader
|
|
284
|
+
|
|
285
|
+
Then use the autogenerated config to create a config reader that you can access config values with. The config reader comes with config path autocomplete and return value preview. The autogenerated config will always be a default import.
|
|
286
|
+
|
|
287
|
+
```ts
|
|
288
|
+
import builtConfig from '.builtConfig.ts';
|
|
289
|
+
import { createConfigReader } from 'zcb';
|
|
290
|
+
|
|
291
|
+
const reader = createConfigReader(builtConfig);
|
|
292
|
+
// scope config path autocompletion and validation
|
|
293
|
+
const scopedReader = reader.scope('pages.contactDetails')
|
|
294
|
+
.scope('sections.1.sections')
|
|
295
|
+
.scope('0');
|
|
296
|
+
// reader config path autocompletion and validation
|
|
297
|
+
// value and type preview
|
|
298
|
+
const value = scopedReader('name');
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
#### reader API
|
|
302
|
+
|
|
303
|
+
**scope: `(value: string) => Get<Config, string>`**
|
|
304
|
+
|
|
305
|
+
Use to scope a reader to a slice of config, rather than having to pass in the full config path every time.
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
## Changelog
|
|
310
|
+
|
|
311
|
+
Check out the [features, fixes and more](CHANGELOG.md) that go into each major, minor and patch version.
|
|
312
|
+
|
|
313
|
+
## License
|
|
314
|
+
|
|
315
|
+
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.7",
|
|
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
|
}
|
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 { importValidateWriteConfig } from './utils/importValidateWriteConfig.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
|
+
importValidateWriteConfig(
|
|
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
|
+
importValidateWriteConfig(
|
|
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(),
|
|
@@ -5,7 +5,7 @@ import { cloneNonEnumerableValues } from './transformers/cloneNonEnumerableValue
|
|
|
5
5
|
import { arrayHasInvalidDefaults } from './utils/arrayHasInvalidDefaults.ts';
|
|
6
6
|
import { isDerivedValueCallback } from './utils/isDerivedValueCallback.ts';
|
|
7
7
|
import { isInvalidPropertyOverride } from './utils/isInvalidPropertyOverride.ts';
|
|
8
|
-
import { isPropertyReservedWord } from './utils/isPropertyReservedWord.ts';
|
|
8
|
+
import { RESERVED_KEYWORDS, isPropertyReservedWord } from './utils/isPropertyReservedWord.ts';
|
|
9
9
|
import { isSchemaValid } from './utils/isSchemaValid.ts';
|
|
10
10
|
import { isValidPropertyDefinition } from './utils/isValidPropertyDefinition.ts';
|
|
11
11
|
import { isValidValue } from './utils/isValidValue.ts';
|
|
@@ -13,18 +13,6 @@ import { objectHasInvalidDefaults } from './utils/objectHasInvalidDefaults.ts';
|
|
|
13
13
|
import { recordHasInvalidDefaults } from './utils/recordHasInvalidDefaults.ts';
|
|
14
14
|
import { transformConfigSync } from './utils/transformConfig.ts';
|
|
15
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
16
|
export const createConfigBuilder = <ZodTypes>(
|
|
29
17
|
zodSchema: z.ZodSchema,
|
|
30
18
|
derivedValueCallbacks: Partial<
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export * from './createConfigBuilder.ts';
|
|
2
|
+
export * from './createConfigReader.ts';
|
|
2
3
|
export * from './data/countryCodes.ts';
|
|
3
4
|
export * from './data/countryNames.ts';
|
|
4
5
|
export * from './data/distanceUnits.ts';
|
|
5
6
|
export * from './data/languageCodes.ts';
|
|
6
7
|
export * from './data/timezones.ts';
|
|
8
|
+
export * from './types.ts';
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import shelljs from 'shelljs';
|
|
3
|
+
import type { Commands } from '../cli.ts';
|
|
4
|
+
import type { ExperimentsCallback } from '../types.ts';
|
|
5
|
+
import { writeConfig } from './writeConfig.ts';
|
|
6
|
+
|
|
7
|
+
export const importValidateWriteConfig = (
|
|
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} => errors:\n${JSON.stringify(configBuilder.errors(), undefined, 2)}\n`);
|
|
23
|
+
shelljs.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
shelljs.echo('zcd ${command} => valid config');
|
|
27
|
+
shelljs.echo(`zcd ${command} => config values:\n${configBuilder.toJson()}\n`);
|
|
28
|
+
|
|
29
|
+
if (experimentCallbackFile) {
|
|
30
|
+
import(resolve(process.cwd(), experimentCallbackFile))
|
|
31
|
+
.then(({ default: experimentsCallback }: { default: ExperimentsCallback }) => {
|
|
32
|
+
void writeConfig(configBuilder.values(), { experimentsCallback, outputFile });
|
|
33
|
+
})
|
|
34
|
+
.catch((error: unknown) => {
|
|
35
|
+
if (error instanceof Error) {
|
|
36
|
+
shelljs.echo(`zcd ${command} => error message: ${error.message}`);
|
|
37
|
+
|
|
38
|
+
if (error.stack) {
|
|
39
|
+
shelljs.echo(`zcd ${command} => error stack:\n${error.stack}\n`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
void writeConfig(configBuilder.values(), { outputFile });
|
|
48
|
+
}
|
|
49
|
+
)
|
|
50
|
+
.catch((error: unknown) => {
|
|
51
|
+
if (error instanceof Error) {
|
|
52
|
+
shelljs.echo(`zcd ${command} => error message: ${error.message}`);
|
|
53
|
+
|
|
54
|
+
if (error.stack) {
|
|
55
|
+
shelljs.echo(`zcd ${command} => error stack:\n${error.stack}\n`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
};
|
|
@@ -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);
|
package/src/utils/writeConfig.ts
CHANGED
|
@@ -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
|
};
|