zcb 0.0.7 → 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/README.md CHANGED
@@ -23,6 +23,7 @@ npm install zcb
23
23
  * [Create config builder](#create-config-builder)
24
24
  * [Transform config builder](#transform-config-builder)
25
25
  * [Create config reader](#create-config-reader)
26
+ * [Use config reader](#use-config-reader)
26
27
 
27
28
  ### Create schema
28
29
 
@@ -31,11 +32,13 @@ Create the schema for your configuration like in the example below.
31
32
  ```ts
32
33
  // ./schema.ts
33
34
  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';
35
+ import {
36
+ countryCodes,
37
+ countryNames,
38
+ distanceUnits,
39
+ languageCodes,
40
+ timezones,
41
+ } from 'zcb';
39
42
 
40
43
  export const baseSectionSchema = z.object({
41
44
  name: z.string(),
@@ -90,7 +93,7 @@ export type ConfigType = z.infer<typeof configSchema>;
90
93
 
91
94
  ### Create config builder
92
95
 
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.
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.
94
97
 
95
98
  ```ts
96
99
  // ./configBuilder.ts
@@ -280,17 +283,63 @@ export default {
280
283
  } as const;
281
284
  ```
282
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
+
283
322
  ### Create config reader
284
323
 
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.
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.
286
337
 
287
338
  ```ts
288
- import builtConfig from '.builtConfig.ts';
289
- import { createConfigReader } from 'zcb';
339
+ import configReader from './configReader.ts';
290
340
 
291
- const reader = createConfigReader(builtConfig);
292
341
  // scope config path autocompletion and validation
293
- const scopedReader = reader.scope('pages.contactDetails')
342
+ const scopedReader = configReader.scope('pages.contactDetails')
294
343
  .scope('sections.1.sections')
295
344
  .scope('0');
296
345
  // reader config path autocompletion and validation
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.7",
4
+ "version": "0.0.8",
5
5
  "author": "Dylan Aubrey",
6
6
  "license": "MIT",
7
7
  "homepage": "https://github.com/badbatch/zod-config-builder",
@@ -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,7 +2,7 @@ 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 { importValidateWriteConfig } from './utils/importValidateWriteConfig.ts';
5
+ import { importValidateTransformWriteConfig } from './utils/importValidateTransformWriteConfig.ts';
6
6
 
7
7
  export enum Commands {
8
8
  BUILD = 'build',
@@ -38,7 +38,7 @@ export const cli = () => {
38
38
  watchFile(resolve(process.cwd(), argv['input-file']), () => {
39
39
  shelljs.echo('zcd watch => file change detected');
40
40
 
41
- importValidateWriteConfig(
41
+ importValidateTransformWriteConfig(
42
42
  argv['input-file'],
43
43
  argv['output-file'],
44
44
  Commands.WATCH,
@@ -54,7 +54,7 @@ export const cli = () => {
54
54
  argv => {
55
55
  shelljs.echo(`zcd build => building file: ${argv['input-file']}`);
56
56
 
57
- importValidateWriteConfig(
57
+ importValidateTransformWriteConfig(
58
58
  argv['input-file'],
59
59
  argv['output-file'],
60
60
  Commands.BUILD,
@@ -2,6 +2,7 @@ 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';
@@ -53,7 +54,7 @@ export const createConfigBuilder = <ZodTypes>(
53
54
 
54
55
  let config = initialValues as Config;
55
56
 
56
- Object.defineProperty(config, '__zcb', {
57
+ Object.defineProperty(config, NonEmumeralProperties.ZCB, {
57
58
  configurable: false,
58
59
  enumerable: false,
59
60
  value: true,
@@ -63,7 +64,7 @@ export const createConfigBuilder = <ZodTypes>(
63
64
 
64
65
  const configBuilder = {
65
66
  disable: () => {
66
- Object.defineProperty(config, '__disabled', {
67
+ Object.defineProperty(config, NonEmumeralProperties.DISABLED, {
67
68
  configurable: false,
68
69
  enumerable: false,
69
70
  value: true,
@@ -80,7 +81,7 @@ export const createConfigBuilder = <ZodTypes>(
80
81
  }
81
82
  },
82
83
  experiment: (key: string) => {
83
- Object.defineProperty(config, '__experiment', {
84
+ Object.defineProperty(config, NonEmumeralProperties.EXPERIMENT, {
84
85
  configurable: false,
85
86
  enumerable: false,
86
87
  value: key,
@@ -97,7 +98,7 @@ export const createConfigBuilder = <ZodTypes>(
97
98
  const values = configBuilder.values();
98
99
  config = {} as Config;
99
100
 
100
- Object.defineProperty(config, '__zcb', {
101
+ Object.defineProperty(config, NonEmumeralProperties.ZCB, {
101
102
  configurable: false,
102
103
  enumerable: false,
103
104
  value: true,
@@ -129,7 +130,7 @@ export const createConfigBuilder = <ZodTypes>(
129
130
  },
130
131
  } as unknown as ConfigBuilder;
131
132
 
132
- Object.defineProperty(configBuilder, '__callbacks', {
133
+ Object.defineProperty(configBuilder, NonEmumeralProperties.CALLBACKS, {
133
134
  configurable: false,
134
135
  enumerable: false,
135
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 { config } from './__testUtils__/config.ts';
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', async () => {
6
- const { createConfigReader } = await import('./createConfigReader.ts');
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', async () => {
15
- const { createConfigReader } = await import('./createConfigReader.ts');
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', async () => {
25
- const { createConfigReader } = await import('./createConfigReader.ts');
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', async () => {
37
- const { createConfigReader } = await import('./createConfigReader.ts');
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, Join } from 'type-fest';
3
- import type { Leaves, Paths } from './types.ts';
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 = <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
-
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,4 +1,5 @@
1
1
  export * from './createConfigBuilder.ts';
2
+ export * from './createConfigParser.ts';
2
3
  export * from './createConfigReader.ts';
3
4
  export * from './data/countryCodes.ts';
4
5
  export * from './data/countryNames.ts';
@@ -1,6 +1,6 @@
1
- import type { AnyRecord, TransformConfigHandlerSync } from '../types.ts';
1
+ import { type AnyRecord, NonEmumeralProperties, type TransformConfigHandlerSync } from '../types.ts';
2
2
 
3
- const NON_ENUMERABLE_KEYS = new Set(['__disabled', '__experiment', '__zcb']);
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 { type AnyRecord, TransformConfigHandlerAction, type TransformConfigHandlerSync } from '../types.ts';
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 ('__disabled' in config) {
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 type { AnyRecord, ExperimentsCallback, TransformConfigHandler } from '../types.ts';
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: ExperimentsCallback): TransformConfigHandler =>
9
+ (callback: SetupExperimentsCallback): TransformConfigHandler =>
5
10
  async <Config extends AnyRecord>(clone: Config, config: Config) => {
6
- if ('__experiment' in config && typeof config.__experiment === 'string') {
7
- const { action, value = {} } = await callback(config.__experiment, clone, config);
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
- action,
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 interface WriteConfigOptions {
8
- experimentsCallback?: ExperimentsCallback;
9
- outputFile: string;
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 ExperimentsCallback = <Config extends AnyRecord>(
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<TransformConfigHandlerReturnType<Config>>;
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
  : {
@@ -1,10 +1,10 @@
1
1
  import { resolve } from 'node:path';
2
2
  import shelljs from 'shelljs';
3
3
  import type { Commands } from '../cli.ts';
4
- import type { ExperimentsCallback } from '../types.ts';
5
- import { writeConfig } from './writeConfig.ts';
4
+ import type { SetupExperimentsCallback } from '../types.ts';
5
+ import { transformWriteConfig } from './transformWriteConfig.ts';
6
6
 
7
- export const importValidateWriteConfig = (
7
+ export const importValidateTransformWriteConfig = (
8
8
  inputFile: string,
9
9
  outputFile: string,
10
10
  command: Commands,
@@ -19,6 +19,7 @@ export const importValidateWriteConfig = (
19
19
  }) => {
20
20
  if (!configBuilder.validate()) {
21
21
  shelljs.echo(`zcd ${command} => invalid config`);
22
+ shelljs.echo(`zcd ${command} => config values:\n${configBuilder.toJson()}\n`);
22
23
  shelljs.echo(`zcd ${command} => errors:\n${JSON.stringify(configBuilder.errors(), undefined, 2)}\n`);
23
24
  shelljs.exit(1);
24
25
  }
@@ -28,8 +29,8 @@ export const importValidateWriteConfig = (
28
29
 
29
30
  if (experimentCallbackFile) {
30
31
  import(resolve(process.cwd(), experimentCallbackFile))
31
- .then(({ default: experimentsCallback }: { default: ExperimentsCallback }) => {
32
- void writeConfig(configBuilder.values(), { experimentsCallback, outputFile });
32
+ .then(({ default: experimentsCallback }: { default: SetupExperimentsCallback }) => {
33
+ void transformWriteConfig(configBuilder.values(), { experimentsCallback, outputFile });
33
34
  })
34
35
  .catch((error: unknown) => {
35
36
  if (error instanceof Error) {
@@ -44,7 +45,7 @@ export const importValidateWriteConfig = (
44
45
  return;
45
46
  }
46
47
 
47
- void writeConfig(configBuilder.values(), { outputFile });
48
+ void transformWriteConfig(configBuilder.values(), { outputFile });
48
49
  }
49
50
  )
50
51
  .catch((error: unknown) => {
@@ -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 writeConfig = async <Config extends object>(
12
+ export const transformWriteConfig = async <Config extends object>(
13
13
  config: Config,
14
14
  { experimentsCallback, outputFile }: WriteConfigOptions
15
15
  ) => {