sho-gen
v0.2.0
Published
Declarative model-driven code generation pipeline for JSON/YAML models — JSONata model-to-model and EJS model-to-text rules in one config
Maintainers
Readme
sho-gen
Declarative, deterministic code & config generation from JSON/YAML models. One config describes your models and rules — sho-gen generates code, config files, docs, or transformed models. Non-interactive, schema-validated, byte-identical on every run.
# team.gen.yaml
models:
- name: team
json:
- { id: ada, name: Ada Lovelace, role: Pioneer }
- { id: grace, name: Grace Hopper, role: Admiral }
rules:
- input: team # array ⇒ one output PER item
template: "# <%= name %>\n\nRole: <%= role %>"
output: { file: "pages/<%= id %>.md" }$ shogen team.gen.yaml -o out
-> out/pages/ada.md
-> out/pages/grace.mdThe same mechanics scale from this to real pipelines — see api-scaffold: one data model → TypeScript types
- SQL schema + HTML forms.
Features
- JSON or YAML configuration defining
- model sources: inline JSON, inline YAML, or files (
.json,.yaml,.yml) - rules with
- an input: a model name or a JSONata expression over the model container
- a template: EJS (string, array of lines, or file), a JSONata expression (model-to-model transformation), or a structural
constructtree with$let/$if/$each/$firstOf/$litand the decision directives$decide(exhaustive tables),$pick(preference cascades) and$dispatch(exhaustive type dispatch) - an output: a file (path is EJS-interpolated per item) or a model path inside the container
- model sources: inline JSON, inline YAML, or files (
- array inputs fan out: one rule generates one output per array item (multi-file generation from a single template)
- rule outputs written back into the model container can feed later rules (transformation pipelines)
- JSON-Schema validation of every generator configuration (schema/gen.schema.json)
- regeneration merge on file outputs (designer-owned elements survive),
--dry-runwith unified diffs, and a per-run manifest with a deterministic hash - per-rule
bind:context and a small JSONata helper stdlib ($uppercaseFirst, $kebab, ..., $json) - output sandbox: generated files can never escape the configured output directory
- extensible handler registry (model / template / output handlers can be registered programmatically)
Why shogen?
shogen fills a gap between three tool families (see docs/competitive-landscape.md for the full analysis):
- Scaffolders (plop, hygen, Yeoman) are interactive and prompt-driven — they have no notion of a data model, no transformation pipelines, and their logic lives in code (plopfiles, generator classes). shogen is non-interactive and driven entirely by a declarative config.
- Data-templating / config languages (Jsonnet, ytt, CUE) transform data with their own functional languages, but have no rule model, no template-per-array-item fan-out (an open feature request in ytt), and no code-generation focus.
- Model-driven engineering tools (Telosys, Acceleo, Xtend) are Java/Eclipse-centric, use their own model DSLs, and keep generation logic in template code — none is a config-driven pipeline on plain JSON/YAML models in the npm ecosystem.
What no active tool offers in combination — and shogen does:
- one declarative JSON/YAML config (schema-validated) defining models and rules
- model-to-model (JSONata) and model-to-text (EJS) in the same rule model
- pipelines: rule outputs flow back into the model container and feed later rules
- declarative array fan-out: one rule → one file per array item, paths EJS-interpolated
The closest historical relative, mdgen (EJS on JSON models with fan-out), has been dead since 2016 and was bound to the StarUML model format.
Documentation
- Getting started — first generator in 2 minutes
- Guides — pipelines,
construct& decision directives, regeneration/breakout, extending - Language reference — every construct, directive and option
- Runnable examples — hello-world, api-scaffold (data model → types + SQL + forms), ci-workflows, regeneration
Getting started
Install
npm install sho-genRequires Node.js >= 22. (The npm package is named sho-gen; the CLI command it
installs is shogen.)
CLI
shogen example.gen.jsonOptions:
| Option | Description |
|---|---|
| [config] / --config, -c | path to a generator file, or a directory (all .json/.yaml/.yml files in it are run) |
| --output, -o | target directory for generated files (default: current directory) |
| --print-result, -p | print the resulting model container as JSON |
API
import { run } from "sho-gen";
const generator = {
models: [
{ name: "customers", file: "customers.json" },
{ name: "settings", json: { locale: "de" } },
],
rules: [
{
input: "customers", // array → one file per customer
template: "Hello <%= name %>", // EJS
output: { file: "gen/<%= id %>.txt" },
},
{
input: "customers",
template: { jsonata: "$.{ 'id': id }" }, // model-to-model
output: { model: "ids" }, // stored back into the container
},
],
};
const result = await run(generator, { workingDir: __dirname, outputDir: "out" });
console.log(result.ids);Advanced usage (custom handlers, explicit instance):
import { ShoGen, registerTemplateHandler, validateGeneratorConfig } from "sho-gen";
registerTemplateHandler({
name: "upper",
async handle(model, rule) {
/* return a result or null to pass to the next handler */
return null;
},
});
const shogen = new ShoGen({ workingDir: process.cwd(), outputDir: "gen" });
const config = validateGeneratorConfig(JSON.parse(raw)); // throws with readable errors
const models = await shogen.generate(config.models, config.rules);Generator configuration
A generator is a JSON (or YAML) document with two parts, validated against schema/gen.schema.json:
Models
Every model is a named JSON value in the model container. Sources:
{
"models": [
{ "name": "inline", "json": { "any": "value" } },
{ "name": "fromYaml", "yaml": "key: value" },
{ "name": "fromFile", "file": "model.json" }, // format by extension
{ "name": "explicit", "file": { "path": "data.txt", "parse": "yaml" } } // format explicit
]
}File paths are resolved relative to the generator file.
Rules
A rule reads its input from the container, optionally runs a template, and writes the result to an output:
{
"rules": [
{
"input": "customers", // model name or JSONata path ("customers[0]")
"template": ["# <%= name %>", ""], // EJS: string or array of lines …
"output": { "file": "docs/<%= id %>.md" } // … file target, EJS-interpolated per item
},
{
"input": "customers",
"template": { "jsonata": "$count($)" }, // JSONata expression
"output": { "model": "stats.count" } // store into the container
},
{
"input": "stats",
"template": { "jsonata": "$" },
"output": "gen/stats" // string output → JSON file (".json" appended)
}
]
}If input resolves to an array, the rule runs once per item — templates and output paths see the single item as their model.
Roadmap (not yet implemented)
These are planned but not part of the current release: Handlebars, prettier formatting of results, further model sources (CSV, JS/TS, MongoDB, markdown, PDF), partial injection into existing files, restricted regions, and an auto-loading plugin ecosystem (shogen-* packages).
Contributing & development
See CONTRIBUTING.md. Quick start:
npm install
npm run build # tsc → dist/
npm test # jest; examples are smoke-tested against frozen outputs
npm run lint # type-checkThe name: 諸元 (shogen) — "specifications, basic data": the model data the generators run on.
