Implement Generate
The generate lifecycle method fetches the upstream spec, parses it, and produces TypeScript types, a runtime index, and a registry. Core provides generatePipeline to orchestrate this; you supply provider-specific callbacks.
Generation Pipeline
Section titled “Generation Pipeline”import { generatePipeline, type GeneratePipelineConfig } from "@intentius/chant/codegen/generate";
const result = await generatePipeline({ fetchSchemas: async (opts) => fetchMySchemas(opts.force), parseSchema: (name, data) => parseMySchema(data), createNaming: (results) => new NamingStrategy(results, myConfig), generateRegistry: (results, naming) => buildLexiconJSON(results, naming), generateTypes: (results, naming) => buildTypesDTS(results, naming), generateRuntimeIndex: (results, naming) => buildRuntimeIndex(results, naming), augmentSchemas: async (schemas, opts, log) => { /* patches, overlays */ }, augmentResults: (results, opts, log) => { /* fallbacks, synthetic resources */ }, generateExtraArtifacts: (results, naming) => ({ "extra.json": buildExtra(results) }),});See
lexicons/aws/src/codegen/generate.tsfor the complete AWS generation pipeline.
Pipeline data flow
Section titled “Pipeline data flow”The pipeline executes these steps in order:
fetchSchemas(opts)->Map<typeName, Buffer>— raw schema files keyed by type nameaugmentSchemas(schemas, opts, log)(optional) — patch or overlay schemas before parsing. Skipped whenopts.schemaSourceis provided (e.g. during testing). Returns{ schemas, extraResults?, warnings? }parseSchema(typeName, data)->T | T[] | null— parse each schema buffer. Returnnullto skip a file, or an array when one file yields several results (the k8s OpenAPI spec and the GitLab CI schema both do)augmentResults(results, opts, log)(optional) — add synthetic resources or fallbacks after parsing. Returns{ results, warnings? }createNaming(results)->NamingStrategy— build the naming strategy from all parsed resultsgenerateRegistry(results, naming)->string— JSON content for the lexicon registrygenerateTypes(results, naming)->string— TypeScript declaration content (.d.ts)generateRuntimeIndex(results, naming)->string— runtime index with factory exports (index.ts)generateExtraArtifacts(results, naming)(optional) ->Record<filename, string>— further derived tables from the same pass, returned onGenerateResult.extraArtifactsso they cannot skew against the registry and the types
ParsedResult contract
Section titled “ParsedResult contract”Your parser’s return type must extend ParsedResult:
interface ParsedResult { propertyTypes: Array<{ name: string }>; // property type definitions enums: Array<unknown>; // enum definitions}The pipeline uses .propertyTypes.length and .enums.length for stats only — the arrays are passed through untouched to your generate callbacks. Extend ParsedResult with any additional fields your callbacks need:
interface MyParsedResult extends ParsedResult { typeName: string; description: string; properties: Map<string, PropertyDef>; attributes: string[];}See
lexicons/aws/src/spec/parse.tsfor the AWS parser that produces aParsedResult.
Naming Strategy
Section titled “Naming Strategy”The NamingStrategy class implements a 5-phase collision-free naming algorithm for TypeScript class names. Supply your provider’s data tables via NamingConfig:
import { NamingStrategy, type NamingConfig, type NamingInput } from "@intentius/chant/codegen/naming";
const config: NamingConfig = { priorityNames: { "Provider::S3::Bucket": "Bucket" }, priorityAliases: {}, priorityPropertyAliases: {}, serviceAbbreviations: {}, shortName: (t) => t.split("::").pop()!, serviceName: (t) => t.split("::")[1], // Once the lexicon has published a surface, feed it the names it already // shipped so an unrelated upstream change cannot rename them (chant #1459): // reservedNames: reservedNamesFromSnapshot(snapshot),};
const naming = new NamingStrategy(inputs, config);See
lexicons/aws/src/codegen/naming.tsfor the AWS naming configuration with real data tables.
Fetch Utilities
Section titled “Fetch Utilities”fetchWithCache and extractFromZip handle HTTP download + caching + zip extraction:
import { fetchWithCache, extractFromZip } from "@intentius/chant/codegen/fetch";
const zipData = await fetchWithCache({ url: SCHEMA_URL, cacheFile: CACHE_PATH });const schemas = await extractFromZip(zipData, (name) => name.endsWith(".json"));See
lexicons/aws/src/spec/fetch.tsfor the AWS schema fetcher.
Reach for fetchWithCache from src/spec/fetch.ts and nowhere else. Generation is the one phase allowed to fetch: a serializer, a plugin hook or a lint rule that reached upstream would put a network call on chant build, which Network Egress guarantees is offline and a guard test fails on. fetchWithCache also caches to disk and falls back to the committed snapshot when the endpoint is unreachable, so generation itself survives a network that is not there.
When there is no upstream JSON schema
Section titled “When there is no upstream JSON schema”generatePipeline only requires fetchSchemas to hand back a
Map<string, Buffer>. It never inspects the bytes, so fetching can just as
well be a local file read or a transform over something that was never a
schema. Three shipped lexicons take three different routes:
| Lexicon | What its fetchSchemas returns | Source |
|---|---|---|
| k3s | Five pinned Go source files from pkg/cli/cmds/, packed into one JSON envelope keyed K3s::Config. k3s publishes no schema; the config keys are the CLI flag names, and the flags are urfave/cli struct literals in that source (chant #1599) | lexicons/k3s/src/spec/fetch.ts |
| k3d | The v1alpha5/schema.json from a tagged source tree rather than a moving branch, so the pin is the git tag | lexicons/k3d/src/spec/fetch.ts |
| cedar | A file read with a three-step resolution order: the project’s cedar.schema config key, then schema.cedarschema in the project root, then a real schema bundled in the package. No network at all | lexicons/cedar/src/spec/fetch.ts |
Cedar’s third step is the one to copy if your spec is local. Without a bundled
fallback, generate() has nothing to read in a fresh clone. Nothing gates it,
and CI never exercises the pipeline. fountain does the same thing on
the other axis: it fetches its pinned OpenAPI release and falls back to a
committed fountain-openapi.snapshot.json when the network is absent
(lexicons/fountain/src/spec/fetch.ts).
A hand-authored spec with no upstream at all belongs in src/spec/, read by
fetchSchemas. Skip upstreamPin in that case. The rest of the pipeline is
unchanged, and parseSchema still owns the shape.
Runtime Factories
Section titled “Runtime Factories”Use createResource and createProperty to generate Declarable-marked constructors:
import { createResource, createProperty } from "@intentius/chant/runtime";
const MyResource = createResource("Provider::Service::Type", "my-lexicon", { arn: "Arn" });const MyProperty = createProperty("Provider::Service::Type.PropType", "my-lexicon");Writing Generated Artifacts
Section titled “Writing Generated Artifacts”After generatePipeline returns a GenerateResult, write the files using writeGeneratedArtifacts:
import { writeGeneratedArtifacts } from "@intentius/chant/codegen/generate";
writeGeneratedArtifacts({ baseDir: pkgDir, // root of your lexicon package generatedSubdir: "src/generated", // default; can be customized files: { "lexicon.json": result.lexiconJSON, "index.d.ts": result.typesDTS, "index.ts": result.indexTS, },});See
lexicons/aws/src/codegen/generate.tsfor the complete generate + write flow.
Helper Utilities
Section titled “Helper Utilities”Runtime Index Generator
Section titled “Runtime Index Generator”import { generateRuntimeIndex, type RuntimeIndexConfig } from "@intentius/chant/codegen/generate-runtime-index";
const indexTS = generateRuntimeIndex(resources, properties, { lexiconName: "my-lexicon", intrinsicReExports: [], pseudoReExports: [],});Registry Builder
Section titled “Registry Builder”import { buildRegistry, serializeRegistry } from "@intentius/chant/codegen/generate-registry";
const registry = buildRegistry(results, naming, { shortName: (t) => naming.shortName(t), buildEntry: (r, shortName) => ({ resourceType: r.typeName, kind: "resource" }), buildPropertyEntry: (r, shortName) => ({ resourceType: r.typeName, kind: "property" }),});const lexiconJSON = serializeRegistry(registry);JSON Schema Utilities
Section titled “JSON Schema Utilities”For lexicons with JSON Schema-based specs:
import { resolvePropertyType, extractConstraints, isEnumDefinition } from "@intentius/chant/codegen/json-schema";resolvePropertyType(prop, schema, resolveDefName)— resolve a schema property to a TypeScript type string. Handles$ref,oneOf/anyOf, arrays, objects, and primitives. Inlineenumarrays produce sorted string-literal union types (e.g."Allow" | "Deny"). When a$refpoints to an enum definition, it callsresolveDefNameto produce a named enum type (e.g.Bucket_Status); passnullto fall back to"string"extractConstraints(prop)— extract validation constraints (min/max, pattern, allowed values, enum arrays)isEnumDefinition(def)— detect if a schema definition is a pure string enum (hasenumarray, noproperties)
PseudoParameter
Section titled “PseudoParameter”import { PseudoParameter, createPseudoParameters } from "@intentius/chant/pseudo-parameter";
const pseudos = createPseudoParameters({ "My::Region": "The deployment region", "My::AccountId": "The account identifier",});Import Utilities
Section titled “Import Utilities”For template import (converting existing templates to chant TypeScript):
import { BaseValueParser } from "@intentius/chant/import/base-parser";import { hasIntrinsicInValue, irUsesIntrinsic, collectDependencies } from "@intentius/chant/import/ir-utils";BaseValueParser— abstract base class for parsing template values into IR nodeshasIntrinsicInValue(value, name)— check whether a raw value contains a call to the named intrinsicirUsesIntrinsic(ir, name)— check whether an IR template uses a specific intrinsiccollectDependencies(value, isDependency)— collect resource dependencies from a value. The predicate is yours: given an object, return its logical id ornull
Intrinsic Interpolation
Section titled “Intrinsic Interpolation”For lexicons with string interpolation intrinsics (like CloudFormation’s Fn::Sub):
import { buildInterpolatedString, defaultInterpolationSerializer } from "@intentius/chant/intrinsic-interpolation";
// parts/values come straight from a tagged template literal's arguments.const result = buildInterpolatedString(parts, values, defaultInterpolationSerializer);buildInterpolatedString walks the template parts and the interpolated values
in lockstep, rendering each value through the serializer you pass.
Pinning the upstream schema — upstreamPin
Section titled “Pinning the upstream schema — upstreamPin”A lexicon generated from an upstream schema should pin the version it generated
from, so a regeneration is reproducible and a version bump is a reviewable diff
rather than whatever upstream published that morning. aws is the cautionary
case: its spec URL is unpinned, so any prepack folds in whatever CloudFormation
shipped today, and its resource count moves without a commit saying so.
upstreamPin tells the self-upgrade tooling where the pin lives and where to
look for a newer one:
upstreamPin: { file: "src/spec/fetch.ts", pattern: /export const K8S_SCHEMA_VERSION\s*=\s*"([^"]+)"/, replace: (v, line) => line.replace(/= "[^"]+"/, `= "${v}"`), upstream: { owner: "kubernetes", repo: "kubernetes", kind: "releases" },},| Field | What it is |
|---|---|
file | Source file holding the pinned version constant, relative to the package root |
pattern | Regex whose first capture group is the current version |
replace(v, line) | Rebuild a matched line with the new version |
upstream.kind | releases for published releases, tags for every git tag |
upstream.tagSuffix | Only consider tags with this suffix — gitlab uses -ee |
k8s, k3s, gcp, gitlab, docker and cedar declare one. checkPinnedUpgrade
(packages/core/src/codegen/pinned-upgrade.ts) uses it to report an available
upgrade, apply it, regenerate, and revert cleanly if the regeneration fails —
which is the reason replace exists rather than the tooling doing a blind
string substitution.
When there is no version to pin to
Section titled “When there is no version to pin to”upstreamPin resolves a version constant against GitHub releases or tags. Some
upstreams offer neither. The CloudFormation Registry schema is a single “latest”
artifact on an S3-backed host, republished constantly, with no version anywhere
in the path — so aws generated against whatever AWS shipped that morning, and a
docs-only branch twice picked up a resource-count move with nothing in the
commit explaining it (chant #1390).
Where there is no version, pin the content: lexicons/aws/src/spec/pin.ts
records a sha256 over the extracted schemas — sorted typeName ->
sha256(schema) — plus the type names it covered, and generation refuses when
upstream no longer matches:
The upstream CloudFormation schema has moved since the pinned one.
pinned sha256:a2d99e08… (1650 resources, accepted 2026-08-03) upstream sha256:1357a446… (1651 resource types, +1 against the pin) added AWS::Fake::NewThingTwo details worth copying if you need the same thing:
- Digest the extracted content, not the archive. A repackaged zip has a new
ETagand new bytes while the schemas inside are identical; a pin that fires on that is noise nobody will keep. - Commit the type names beside the digest. Generated artifacts are not committed, so without the list an acceptance is one opaque hash replacing another. With it, the PR that moves the pin shows exactly what AWS added.
Unlike the emulator image pins, which only report (chant #808), this one refuses. An emulator that drifts fails a test you can see; a spec that drifts rewrites committed artifacts in a branch about something else.
Next Steps
Section titled “Next Steps”With generation working, the next step is to create a serializer that converts evaluated resources to your target format.