Generators as packages
SKMTC generators are JSR packages with a canonical internal layout. Understanding the package structure is the foundation for both using generators (install from JSR) and authoring or cloning them…
A generator is not a config block in a YAML file or a plugin loaded through a registry. It's a self-contained TypeScript package published to JSR. SKMTC's CLI knows how to install, clone, and run these packages, but the packages themselves are ordinary Deno code.
The one-paragraph essence
A SKMTC generator is a JSR package (or a local directory mirroring
the same layout). It has a deno.json declaring its identity and
peer dependencies, a mod.ts that re-exports the entry default, and
a src/ directory with the actual generator code: an entry function
in src/mod.ts, identifier/path conventions in src/base.ts,
enrichment schema in src/enrichments.ts, the main Projection class,
and any supporting Snippet classes. The CLI's install adds the
JSR specifier; clone copies the source locally; both write to the
project's deno.json#imports and the project's worker.ts bundles
all listed generators into a single bundle.js.
Per-generator package layout
The canonical layout for gen-x:
@skmtc/gen-x/ ← package root (JSR or local)
├── deno.json ← package manifest
├── mod.ts ← top-level re-export
└── src/
├── mod.ts ← entry function (toOasOperationEntry, etc.)
├── base.ts ← toIdentifierName, toExportPath (the customization seams)
├── enrichments.ts ← Valibot schema for user options
├── <MainProjection>.ts ← the Projection class
└── <Snippet>.ts ← supporting Snippet classes (optional, multiple)Each file's role:
deno.json
The package's identity and dependencies:
{
"name": "@skmtc/gen-x",
"version": "0.0.55",
"exports": "./mod.ts",
"imports": {
"@skmtc/core": "jsr:@skmtc/core@^0.3.7",
"@skmtc/lang-typescript": "jsr:@skmtc/lang-typescript@^0.4.0",
"@skmtc/worker": "jsr:@skmtc/worker@^0.2.0",
"@std/path": "jsr:@std/path@^1.0.0",
"valibot": "jsr:valibot@^0.40.0"
}
}The name matches the JSR scope/package. The version follows
semver (informally — many generators are pre-1.0 and use 0.0.x
patches). The imports list declares peer dependencies that the
consuming project must also pin compatibly.
mod.ts (top-level)
A thin re-export of the actual entry:
// gen-x/mod.ts
export { default } from './src/mod.ts'
export * from './src/mod.ts'The package's default export is the generator's entry function. The
re-export shape is so that consumers can import gen from '@skmtc/gen-x' and get the entry function directly.
src/mod.ts (entry)
The function the engine calls to register the generator. Pick the factory matching what the generator operates on:
toOasOperationEntry— one file per OAS operationtoGqlOperationEntry— one file per GraphQL operationtoModelEntry— one file per schema component (refName)
The three share a config skeleton; differences are documented in entry-factories reference.
OAS operation entry:
// gen-x/src/mod.ts
import { toOasOperationEntry } from '@skmtc/core'
import { MyProjection } from './MyProjection.ts'
import { toEnrichmentSchema, type EnrichmentSchema } from './enrichments.ts'
import denoJson from '../deno.json' with { type: 'json' }
const MyGenEntry = toOasOperationEntry<EnrichmentSchema>({
id: denoJson.name,
toEnrichmentSchema,
isSupported({ operation }) {
return /* boolean: handle this operation? */
},
transform({ context, operation }) {
context.insertOperation({ projection: MyProjection, operation })
}
})
export default MyGenEntryisSupported is the capability gate; transform is the per-item
hook. Output happens through side effects on context —
register, insertOperation, insertModel,
insertNormalizedModel. transform returns void; any return value
is ignored. See
how-generators-produce-output.
Model entry — same skeleton with two differences (no
isSupported, takes refName instead of operation):
const ZodEntry = toModelEntry<EnrichmentSchema>({
id: denoJson.name,
toEnrichmentSchema,
transform({ context, refName }) {
context.insertModel(ZodProjection, refName)
}
})Model entries are called once for every refName in the document —
there's no capability gate at the Entry level. Filter inside
transform if needed.
GraphQL operation entry — same skeleton, keyed on rootKind /
fieldName instead of path / method:
const MyGqlEntry = toGqlOperationEntry<EnrichmentSchema>({
id: denoJson.name,
toEnrichmentSchema,
isSupported({ operation }) {
return operation.rootKind === 'mutation'
},
transform({ context, operation }) {
if (operation.rootKind !== 'mutation') return
context.insertOperation({ projection: MyGen, operation })
}
})A side-by-side comparison of the three factories — config fields and enrichment routing paths — is in the entry-factories reference.
src/base.ts (projection base)
The toIdentifierName / toExportPath factory — these are the
customization seams when the generator is cloned:
// gen-x/src/base.ts
import { capitalize, camelCase } from '@skmtc/core'
import { toTsOasOperationProjectionBase } from '@skmtc/lang-typescript'
import { join } from '@std/path'
import { toEnrichmentSchema, type EnrichmentSchema } from './enrichments.ts'
import denoJson from '../deno.json' with { type: 'json' }
export const MyGenBase = toTsOasOperationProjectionBase<EnrichmentSchema>({
id: denoJson.name,
toEnrichmentSchema,
toIdentifierName({ operation }): string {
// The generator's identifier-naming convention.
// Hardcoded here on purpose — this is the seam users edit
// (in a clone) to change names.
return `${capitalize(operation.method)}${camelCase(operation.path, { upperFirst: true })}`
},
toIdentifierType: () => ({ type: 'variable' }),
toExportPath({ operation, enrichments, variant }): string {
// Where output files land. Must align with consumer's @ alias.
const name = this.toIdentifierName({ operation, enrichments, variant })
return join('@', 'my-gen', `${name}.generated.ts`)
}
})The hardcoded values in toIdentifierName and toExportPath are
deliberately literal. They're the primary clone seams.
src/enrichments.ts
The Valibot schema for user-supplied options. See enrichments.
src/<MainProjection>.ts
The Projection class — the actual code that renders output. Extends
the base from src/base.ts. See
projections and snippets.
src/<Snippet>.ts files
Optional. Anonymous Snippet classes used by the Projection. One file
per Snippet is the convention; some generators (like
gen-shadcn-form) have a src/fields/ subdirectory with one
Snippet per field type.
Peer dependencies
A generator depends on @skmtc/core (the engine), @skmtc/worker
(the runtime wrapper), and any external runtime libraries it
references in its output (valibot, react, etc.).
The peer dependencies must be version-compatible with the
consuming project's pins. If gen-x is built against
@skmtc/core@^0.3.0 but the project pins @skmtc/core@^0.2.0,
bundling will fail with cryptic No matching export … errors deep
in deno bundle output.
The peer-pin check
skmtc clone performs a pre-flight check that the cloned
generator's @skmtc/core peer version matches the project's. On
mismatch, it refuses with exit code 2 and a recipe error pointing at
the canonical remediation. See
clone vs install.
skmtc doctor's project-core-pin/<project> check surfaces the
same mismatch for already-installed setups.
Updating peer pins
When a generator updates its @skmtc/core pin (e.g., from ^0.2.x
to ^0.3.x), every project using the generator needs to:
- Update its own
@skmtc/corepin indeno.json - Run
skmtc bundle(if the project has clones) or justskmtc generate(otherwise)
The CLI doesn't auto-update peer pins on skmtc install or
skmtc clone — that would risk breaking the project's other
generators. The user resolves peer-pin drift explicitly.
JSR publishing
Generators are published to JSR. The publish flow is standard Deno:
cd gen-x/
deno publishJSR resolves the package's exports, runs basic validation, and
makes the package available at jsr:@skmtc/gen-x@<version>.
After publish, anyone can install:
skmtc install @skmtc/gen-x my-projectThe CLI fetches the latest matching version from JSR and adds it to
the project's deno.json#imports.
Versioning
Generators follow informal semver. Pre-1.0 generators use 0.0.x for patches, 0.x.0 for minor releases. Once a generator stabilizes, it typically jumps to 1.0.0 to signal API stability.
Breaking changes in the generator's output shape are not distinguishable from breaking changes in its peer pins via semver alone. Both bump the version; the user reads the changelog to know what changed.
Local cloning
skmtc clone <project> -g @skmtc/gen-x:
- Fetches the generator's source from JSR (the same content
installwould resolve) - Writes the source to
.skmtc/<project>/gen-x/ - Changes
deno.json#importsfrom a JSR specifier to a local path:{ "@skmtc/gen-x": "./gen-x/mod.ts" } - Triggers a post-clone rebundle (the project's
bundle.jsis updated)
After cloning, the source is the user's code. JSR is not consulted
at generate time; the local bundle.js is loaded by the Worker.
Edits to the cloned source take effect at the next skmtc bundle
(or skmtc dev, which auto-rebundles on file changes).
Common questions
Can a generator depend on another generator?
Yes — many stock generators do. gen-shadcn-form depends on
gen-tanstack-query-supabase-zod for the mutation hook. The
dependency is declared in gen-shadcn-form/deno.json#imports and
imported in gen-shadcn-form/src/ShadcnForm.ts.
When you install gen-shadcn-form, you need to install the
dependent generator too. The CLI doesn't auto-install transitive
deps — you list them explicitly.
How does the package version get embedded in the bundle?
deno.json is imported at runtime via import denoJson from '../deno.json' with { type: 'json' }. The version is accessible
through denoJson.version. Generators use this to stamp output with
their version, or for id (which is typically the package name).
Can I publish a fork of a stock generator?
Yes — just give it a different name in deno.json. Publish it to
your own JSR scope (@yourorg/gen-x-customized). Then other
projects can skmtc install @yourorg/gen-x-customized.
This is the path to sharing a customized generator across multiple projects — see clone vs install.
What's the difference between a generator's mod.ts and src/mod.ts?
mod.ts(at package root) is the public API — it's what JSR treats asexports, and what consumers import viaimport x from '@skmtc/gen-x'.src/mod.ts(insidesrc/) is the actual implementation — the entry function call (toOasOperationEntry({ ... })).
The split keeps the public API thin (it's just a re-export) and the implementation focused.
Why JSR and not npm?
JSR is Deno-native, supports TypeScript directly (no compilation step), and has a simpler version model. SKMTC runs on Deno (the CLI and Worker are both Deno), so JSR is the natural distribution channel. There's no inherent reason a generator couldn't be published to npm with the right tooling, but stock generators don't do this.
Are generators sandboxed at the package level?
No. Sandboxing happens at the Worker level (Deno permissions). All generators in a project's bundle run with the same permissions inside the same Worker. Per-generator isolation isn't a feature.
If you need to constrain a specific generator's capabilities, clone-and-audit is the answer.
Can a generator import third-party npm packages?
Yes, via Deno's npm compatibility layer. The generator's deno.json
adds an npm: specifier:
{ "imports": { "lodash": "npm:lodash@^4" } }The generator can then import { ... } from 'lodash'. The npm
package becomes part of the bundle.
This works but adds bundle size. Generators tend to prefer JSR dependencies or no dependencies where possible.
Further reading
- Clone vs install — the customization gradient
- Projects and workspaces — where installed and cloned generators live
- The Worker runtime — how generators get loaded
- Enrichments — how per-generator user options work
- Projections and Snippets — the DSL the generator implements
skmtc installreferenceskmtc clonereferenceskmtc-generatorskill — operational authoring guidance