npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

zcb

v0.2.6

Published

Build configs with type safety from zod schema.

Downloads

148

Readme

zcb

Build configs with type safety from zod schema.

Build and publish npm version License: MIT

Define a configuration schema with zod and use the output of that in zcb to create a config builder with method autocomplete and value type validation.

Use our cli module to build and/or watch a config builder file and transform it into a literally typed config object.

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.

These 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.

Installation

npm add zcb

Usage

Create schema

Create the schema for your configuration like in the example below.

// ./schema.ts
import { z } from 'zod';
import {
  countryCodes,
  countryNames,
  distanceUnits,
  languageCodes,
  timezones,
} from 'zcb';

export const baseSectionSchema = z.object({
  name: z.string(),
});

export type SectionType = z.infer<typeof baseSectionSchema> & {
  sections?: SectionType[];
};

export const sectionSchema: z.ZodType<SectionType> = baseSectionSchema.extend({
  sections: z.lazy(() => sectionSchema.array()).optional(),
});

export const pageSchema = z.object({
  name: z.string(),
  path: z.string().optional(),
  queryParams: z.array(z.string()).optional(),
  sections: z.array(sectionSchema).optional(),
});

export type PageType = z.infer<typeof pageSchema>;

const baseRouteSchema = z.object({
  aliases: z.array(z.string()).optional(),
  page: z.string(),
  path: z.string(),
});

export type RouteType = z.infer<typeof baseRouteSchema> & {
  routes?: RouteType[];
};

export const routeSchema: z.ZodType<RouteType> = baseRouteSchema.extend({
  routes: z.lazy(() => routeSchema.array()).optional(),
});

export const configSchema = z.object({
  countryCode: z.enum(countryCodes).optional(),
  countryName: z.enum(countryNames).optional(),
  distanceUnit: z.enum(distanceUnits).optional(),
  languageCodes: z.array(z.enum(languageCodes)).optional(),
  locales: z.array(z.string().regex(/[a-z]{2}_[A-Z]{2}/)).optional(),
  name: z.string().optional(),
  pages: z.record(pageSchema).optional(),
  routes: z.array(routeSchema).optional(),
  timeouts: z.record(z.number()).optional(),
  timezone: z.enum(timezones).optional(),
});

export type ConfigType = z.infer<typeof configSchema>;

Create config builder

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.

// ./configBuilder.ts
import { kebabCase } from 'lodash-es';
import { createConfigBuilder } from 'zcb';
import {
  type ConfigType,
  type PageType,
  type RouteType,
  type SectionType,
  configSchema,
  pageSchema,
  routeSchema,
  sectionSchema,
} from './schema.ts';

const configBuilder = createConfigBuilder<ConfigType>(configSchema);
const routeBuilder = createConfigBuilder<RouteType>(routeSchema, undefined, { path: ({ page }) => kebabCase(page) });
const pageBuilder = createConfigBuilder<PageType>(pageSchema);
const sectionBuilder = createConfigBuilder<SectionType>(sectionSchema);
const subsectionBuilder = sectionBuilder.$fork();

configBuilder
  .countryCode('GB')
  .countryName('United Kingdom')
  .distanceUnit('km')
  .languageCodes(['en'])
  .locales(({ countryCode, languageCodes }) =>
    languageCodes?.length && countryCode ? languageCodes.map(code => `${code}_${countryCode}`) : []
  )
  .name('alpha')
  .pages({
    contactDetails: pageBuilder
      .name('contactDetails')
      .sections([
        sectionBuilder.name('header').$flush(),
        sectionBuilder
          .name('body')
          .sections([subsectionBuilder.name('main').$flush(), subsectionBuilder.name('sidebar').$flush()])
          .$flush(),
        sectionBuilder.name('footer').$flush(),
      ])
      .$flush(),
    personalDetails: pageBuilder
      .name('personalDetails')
      .sections([
        sectionBuilder.name('header').$flush(),
        sectionBuilder
          .name('body')
          .sections([subsectionBuilder.name('main').$flush(), subsectionBuilder.name('sidebar').$flush()])
          .$flush(),
        sectionBuilder.name('footer').$flush(),
      ])
      .$flush(),
  })
  .routes([routeBuilder.page('personalDetails').$flush(), routeBuilder.page('contactDetails').$flush()])
  .timeouts({ apollo: 10_000 })
  .timezone('Europe/London');

export default configBuilder;

builder API

$disable: () => ConfigBuilder

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.

$errors: () => ZodIssue[]

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.

$experiment: () => ConfigBuilder

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.

$extend: (value: ConfigBuilder) => void

Use to extend an existing config builder.

$flush: () => JsonObject

Use to flush the values from a config builder so that it can be immediately reused.

$fork: () => ConfigBuilder

Create a clone of a config builder. Useful if you need to use the same config builder within itself.

$toJson: () => string

Returns the config values as a pretty-printed JSON string.

$validate: () => boolean

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.

$values: () => JsonObject

Use to return the config values as an object.


Transform config builder

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.

If you require native ESM support, use NODE_OPTIONS="--loader ts-node/esm".

NODE_OPTIONS="--loader ts-node/register" npx zcb watch ./configBuilder.ts ./builtConfig.ts
// ./builtConfig.ts
/* eslint-disable */
/* This file is autogenerated, do not edit directly, your changes will not perist. */

export default {
  countryCode: "GB",
  countryName: "United Kingdom",
  distanceUnit: "km",
  languageCodes: [
    "en"
  ],
  locales: [
    "en_GB"
  ],
  name: "alpha",
  pages: {
    contactDetails: {
      name: "contactDetails",
      sections: [
        {
          name: "header"
        },
        {
          name: "body",
          sections: [
            {
              name: "main"
            },
            {
              name: "sidebar"
            }
          ]
        },
        {
          name: "footer"
        }
      ]
    },
    personalDetails: {
      name: "personalDetails",
      sections: [
        {
          name: "header"
        },
        {
          name: "body",
          sections: [
            {
              name: "main"
            },
            {
              name: "sidebar"
            }
          ]
        },
        {
          name: "footer"
        }
      ]
    }
  },
  routes: [
    {
      page: "personalDetails",
      path: "personal-details"
    },
    {
      page: "contactDetails",
      path: "contact-details"
    }
  ],
  timeouts: {
    apollo: 10000
  },
  timezone: "Europe/London"
} as const;

cli API

  • zcb build <input-file> <output-file>
Write config from a config builder

Positionals:
  input-file   The relative path to the config builder root file
                                                             [string] [required]
  output-file  The relative path to the output config file   [string] [required]

Options:
  --version                    Show version number                     [boolean]
  --help                       Show help                               [boolean]
  --experiments-callback-file  The relative path to the experiment callback file
                                                                        [string]
  • zcb watch <input-file> <output-file>
Watch a config builder and write config

Positionals:
  input-file   The relative path to the config builder root file
                                                             [string] [required]
  output-file  The relative path to the output config file   [string] [required]

Options:
  --version                    Show version number                     [boolean]
  --help                       Show help                               [boolean]
  --experiments-callback-file  The relative path to the experiment callback file
                                                                        [string]

Create config reader

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.

// ./configReader.ts
import { createConfigParser, createConfigReader } from 'zcb';
import builtConfig from './builtConfig.ts';

export default createConfigReader(builtConfig);

Use config reader

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.

import configReader from './configReader.ts';

// scope config path autocompletion and validation
const scopedReader = configReader.scope('pages.contactDetails')
                       .scope('sections.1.sections')
                       .scope('0');
// reader config path autocompletion and validation
// value and type preview
const value = scopedReader.read('name');

reader API

read: (value: string, options?: { vars?: Record<string, string | number> }) => Get<Config, string>

Use 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 a property of the second options argument. The value of each matching key in the vars object will be replaced in the string template.

const vars = {
  name: 'Simon',
  profession: 'pieman',
};

const stringTemplate = 'Simple {{name}} met a {{profession}} going to the fair';
const reader = createConfigReader({ stringTemplate })
const value = reader.read('stringTemplate', { vars });
console.log(value); // 'Simple Simon met a pieman going to the fair'

scope: (value: string) => Get<Config, string>

Use to scope a reader to a slice of config, rather than having to pass in the full config path every time.


Changelog

Check out the features, fixes and more that go into each major, minor and patch version.

License

zcb is MIT Licensed.