{"id":14156454,"url":"https://github.com/badbatch/zod-config-builder","last_synced_at":"2025-07-18T04:34:36.860Z","repository":{"id":165741103,"uuid":"640223460","full_name":"badbatch/zod-config-builder","owner":"badbatch","description":"Build configs with type safety from zod schema.","archived":false,"fork":false,"pushed_at":"2025-06-21T15:59:30.000Z","size":1072,"stargazers_count":1,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-07-04T02:43:57.032Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/badbatch.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2023-05-13T11:40:16.000Z","updated_at":"2025-06-21T15:59:34.000Z","dependencies_parsed_at":"2023-11-22T12:26:38.051Z","dependency_job_id":"cf28097d-be31-43ed-99c4-f81fc2fcefa2","html_url":"https://github.com/badbatch/zod-config-builder","commit_stats":null,"previous_names":[],"tags_count":43,"template":false,"template_full_name":null,"purl":"pkg:github/badbatch/zod-config-builder","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/badbatch%2Fzod-config-builder","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/badbatch%2Fzod-config-builder/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/badbatch%2Fzod-config-builder/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/badbatch%2Fzod-config-builder/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/badbatch","download_url":"https://codeload.github.com/badbatch/zod-config-builder/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/badbatch%2Fzod-config-builder/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":265701044,"owners_count":23813746,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-17T08:05:29.407Z","updated_at":"2025-07-18T04:34:36.801Z","avatar_url":"https://github.com/badbatch.png","language":"TypeScript","readme":"# zcb\n\nBuild configs with type safety from zod schema.\n\n[![Build and publish](https://github.com/badbatch/zod-config-builder/actions/workflows/build-and-publish.yml/badge.svg)](https://github.com/badbatch/zod-config-builder/actions/workflows/build-and-publish.yml)\n[![npm version](https://badge.fury.io/js/zcb.svg)](https://badge.fury.io/js/zcb)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)\n\nDefine a configuration schema with [`zod`](https://github.com/colinhacks/zod) and use the output of that in `zcb` to create a config builder with method autocomplete and value type validation.\n\nUse our cli module to build and/or watch a config builder file and transform it into a literally typed config object.\n\nImport 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.\n\nThese three features can be used together in the above workflow or separately. For example, you can just use the config builder to build a type-safe config for use in your application, or you could handcraft a locales file and use the config reader to read its values.\n\n## Installation\n\n```sh\nnpm add zcb\n```\n\n## Usage\n\n* [Create schema](#create-schema)\n* [Create config builder](#create-config-builder)\n* [Transform config builder](#transform-config-builder)\n* [Create config reader](#create-config-reader)\n* [Use config reader](#use-config-reader)\n\n### Create schema\n\nCreate the schema for your configuration like in the example below.\n\n```ts\n// ./schema.ts\nimport { z } from 'zod';\nimport {\n  countryCodes,\n  countryNames,\n  distanceUnits,\n  languageCodes,\n  timezones,\n} from 'zcb';\n\nexport const baseSectionSchema = z.object({\n  name: z.string(),\n});\n\nexport type SectionType = z.infer\u003ctypeof baseSectionSchema\u003e \u0026 {\n  sections?: SectionType[];\n};\n\nexport const sectionSchema: z.ZodType\u003cSectionType\u003e = baseSectionSchema.extend({\n  sections: z.lazy(() =\u003e sectionSchema.array()).optional(),\n});\n\nexport const pageSchema = z.object({\n  name: z.string(),\n  path: z.string().optional(),\n  queryParams: z.array(z.string()).optional(),\n  sections: z.array(sectionSchema).optional(),\n});\n\nexport type PageType = z.infer\u003ctypeof pageSchema\u003e;\n\nconst baseRouteSchema = z.object({\n  aliases: z.array(z.string()).optional(),\n  page: z.string(),\n  path: z.string(),\n});\n\nexport type RouteType = z.infer\u003ctypeof baseRouteSchema\u003e \u0026 {\n  routes?: RouteType[];\n};\n\nexport const routeSchema: z.ZodType\u003cRouteType\u003e = baseRouteSchema.extend({\n  routes: z.lazy(() =\u003e routeSchema.array()).optional(),\n});\n\nexport const configSchema = z.object({\n  countryCode: z.enum(countryCodes).optional(),\n  countryName: z.enum(countryNames).optional(),\n  distanceUnit: z.enum(distanceUnits).optional(),\n  languageCodes: z.array(z.enum(languageCodes)).optional(),\n  locales: z.array(z.string().regex(/[a-z]{2}_[A-Z]{2}/)).optional(),\n  name: z.string().optional(),\n  pages: z.record(pageSchema).optional(),\n  routes: z.array(routeSchema).optional(),\n  timeouts: z.record(z.number()).optional(),\n  timezone: z.enum(timezones).optional(),\n});\n\nexport type ConfigType = z.infer\u003ctypeof configSchema\u003e;\n```\n\n### Create config builder\n\nThen 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.\n\n```ts\n// ./configBuilder.ts\nimport { kebabCase } from 'lodash-es';\nimport { createConfigBuilder } from 'zcb';\nimport {\n  type ConfigType,\n  type PageType,\n  type RouteType,\n  type SectionType,\n  configSchema,\n  pageSchema,\n  routeSchema,\n  sectionSchema,\n} from './schema.ts';\n\nconst configBuilder = createConfigBuilder\u003cConfigType\u003e(configSchema);\nconst routeBuilder = createConfigBuilder\u003cRouteType\u003e(routeSchema, undefined, { path: ({ page }) =\u003e kebabCase(page) });\nconst pageBuilder = createConfigBuilder\u003cPageType\u003e(pageSchema);\nconst sectionBuilder = createConfigBuilder\u003cSectionType\u003e(sectionSchema);\nconst subsectionBuilder = sectionBuilder.$fork();\n\nconfigBuilder\n  .countryCode('GB')\n  .countryName('United Kingdom')\n  .distanceUnit('km')\n  .languageCodes(['en'])\n  .locales(({ countryCode, languageCodes }) =\u003e\n    languageCodes?.length \u0026\u0026 countryCode ? languageCodes.map(code =\u003e `${code}_${countryCode}`) : []\n  )\n  .name('alpha')\n  .pages({\n    contactDetails: pageBuilder\n      .name('contactDetails')\n      .sections([\n        sectionBuilder.name('header').$flush(),\n        sectionBuilder\n          .name('body')\n          .sections([subsectionBuilder.name('main').$flush(), subsectionBuilder.name('sidebar').$flush()])\n          .$flush(),\n        sectionBuilder.name('footer').$flush(),\n      ])\n      .$flush(),\n    personalDetails: pageBuilder\n      .name('personalDetails')\n      .sections([\n        sectionBuilder.name('header').$flush(),\n        sectionBuilder\n          .name('body')\n          .sections([subsectionBuilder.name('main').$flush(), subsectionBuilder.name('sidebar').$flush()])\n          .$flush(),\n        sectionBuilder.name('footer').$flush(),\n      ])\n      .$flush(),\n  })\n  .routes([routeBuilder.page('personalDetails').$flush(), routeBuilder.page('contactDetails').$flush()])\n  .timeouts({ apollo: 10_000 })\n  .timezone('Europe/London');\n\nexport default configBuilder;\n```\n\n#### builder API\n\n**$disable: `() =\u003e ConfigBuilder`**\n\nUse 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.\n\n**$errors: `() =\u003e ZodIssue[]`**\n\nUse to validate the config against the schema and return any errors. The primary use for this is internal within the cli build/watch step.\n\n**$experiment: `() =\u003e ConfigBuilder`**\n\nUse 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.\n\n**$extend: `(value: ConfigBuilder) =\u003e void`**\n\nUse to extend an existing config builder.\n\n**$flush: `() =\u003e JsonObject`**\n\nUse to flush the values from a config builder so that it can be immediately reused.\n\n**$fork: `() =\u003e ConfigBuilder`**\n\nCreate a clone of a config builder. Useful if you need to use the same config builder within itself.\n\n**$toJson: `() =\u003e string`**\n\nReturns the config values as a pretty-printed JSON string.\n\n**$validate: `() =\u003e boolean`**\n\nUse to validate the config against the schema and return true/false. The primary use for this is internal within the cli build/watch step.\n\n**$values: `() =\u003e JsonObject`**\n\nUse to return the config values as an object.\n\n---\n\n### Transform config builder\n\nUse 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.\n\nIf you require native ESM support, use `NODE_OPTIONS=\"--loader ts-node/esm\"`.\n\n```sh\nNODE_OPTIONS=\"--loader ts-node/register\" npx zcb watch ./configBuilder.ts ./builtConfig.ts\n```\n\n```ts\n// ./builtConfig.ts\n/* eslint-disable */\n/* This file is autogenerated, do not edit directly, your changes will not perist. */\n\nexport default {\n  countryCode: \"GB\",\n  countryName: \"United Kingdom\",\n  distanceUnit: \"km\",\n  languageCodes: [\n    \"en\"\n  ],\n  locales: [\n    \"en_GB\"\n  ],\n  name: \"alpha\",\n  pages: {\n    contactDetails: {\n      name: \"contactDetails\",\n      sections: [\n        {\n          name: \"header\"\n        },\n        {\n          name: \"body\",\n          sections: [\n            {\n              name: \"main\"\n            },\n            {\n              name: \"sidebar\"\n            }\n          ]\n        },\n        {\n          name: \"footer\"\n        }\n      ]\n    },\n    personalDetails: {\n      name: \"personalDetails\",\n      sections: [\n        {\n          name: \"header\"\n        },\n        {\n          name: \"body\",\n          sections: [\n            {\n              name: \"main\"\n            },\n            {\n              name: \"sidebar\"\n            }\n          ]\n        },\n        {\n          name: \"footer\"\n        }\n      ]\n    }\n  },\n  routes: [\n    {\n      page: \"personalDetails\",\n      path: \"personal-details\"\n    },\n    {\n      page: \"contactDetails\",\n      path: \"contact-details\"\n    }\n  ],\n  timeouts: {\n    apollo: 10000\n  },\n  timezone: \"Europe/London\"\n} as const;\n```\n\n#### cli API\n\n* `zcb build \u003cinput-file\u003e \u003coutput-file\u003e`\n\n```sh\nWrite config from a config builder\n\nPositionals:\n  input-file   The relative path to the config builder root file\n                                                             [string] [required]\n  output-file  The relative path to the output config file   [string] [required]\n\nOptions:\n  --version                    Show version number                     [boolean]\n  --help                       Show help                               [boolean]\n  --experiments-callback-file  The relative path to the experiment callback file\n                                                                        [string]\n```\n\n* `zcb watch \u003cinput-file\u003e \u003coutput-file\u003e`\n\n```sh\nWatch a config builder and write config\n\nPositionals:\n  input-file   The relative path to the config builder root file\n                                                             [string] [required]\n  output-file  The relative path to the output config file   [string] [required]\n\nOptions:\n  --version                    Show version number                     [boolean]\n  --help                       Show help                               [boolean]\n  --experiments-callback-file  The relative path to the experiment callback file\n                                                                        [string]\n```\n\n### Create config reader\n\nThen 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.\n\n```ts\n// ./configReader.ts\nimport { createConfigParser, createConfigReader } from 'zcb';\nimport builtConfig from './builtConfig.ts';\n\nexport default createConfigReader(builtConfig);\n```\n\n### Use config reader\n\nThen 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.\n\n```ts\nimport configReader from './configReader.ts';\n\n// scope config path autocompletion and validation\nconst scopedReader = configReader.scope('pages.contactDetails')\n                       .scope('sections.1.sections')\n                       .scope('0');\n// reader config path autocompletion and validation\n// value and type preview\nconst value = scopedReader.read('name');\n```\n\n#### reader API\n\n**read: `(value: string, variables?: Record\u003cstring, string | number\u003e) =\u003e Get\u003cConfig, string\u003e`**\n\nUse to read a value out of config. If the value resolves to a string, the reader also supports the string being a template that uses double bracket notation (`{{key}}`) and passing a `vars` object of key/value pairs as the second argument. The value of each matching key in the `vars` object will be replaced in the string template.\n\n```ts\nconst vars = {\n  name: 'Simon',\n  profession: 'pieman',\n};\n\nconst stringTemplate = 'Simple {{name}} met a {{profession}} going to the fair';\nconst reader = createConfigReader({ stringTemplate })\nconst value = reader.read('stringTemplate', vars);\nconsole.log(value); // 'Simple Simon met a pieman going to the fair'\n```\n\n**scope: `(value: string) =\u003e Get\u003cConfig, string\u003e`**\n\nUse to scope a reader to a slice of config, rather than having to pass in the full config path every time.\n\n---\n\n## Changelog\n\nCheck out the [features, fixes and more](CHANGELOG.md) that go into each major, minor and patch version.\n\n## License\n\nzcb is [MIT Licensed](LICENSE).\n","funding_links":[],"categories":["others"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbadbatch%2Fzod-config-builder","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbadbatch%2Fzod-config-builder","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbadbatch%2Fzod-config-builder/lists"}