create-spellcraft-module
v0.2.0
Published
Scaffolds a new plugin for @c6fc/spellcraft.
Readme
create-spellcraft-module
Scaffolds a new plugin for SpellCraft.
npm init spellcraft-module my-pluginThe generated plugin installs clean, and its own npm test passes immediately.
Start by deleting the two example functions.
Usage
npm init spellcraft-module [directory] [options]
npx create-spellcraft-module [directory] [options]The directory is created if it doesn't exist and defaults to the current one.
Anything not passed as an option is prompted for, with a default drawn from your
npm config (init-author-name, init-author-email, init-scope), your git
config, and the directory name.
| option | default |
|---|---|
| --name <name> | directory name, scoped by init-scope |
| --description <text> | — |
| --author <author> | npm config, then git config |
| --license <id> | MIT |
| --repo <url> | git remote get-url origin, when the target is the repo root |
| -y, --yes | take every default, never prompt |
npm init can consume flags before they reach the generator, so pass them after
a -- separator, or call npx create-spellcraft-module directly:
npm init spellcraft-module my-plugin -- --yes --license Apache-2.0Without a TTY the generator never prompts: it takes every default rather than
hanging a CI job on a question, and fails outright only when it cannot settle on
a name — pass --name if the directory basename isn't a valid npm package name.
It also refuses to scaffold into a directory that holds other projects, which
is the shape of ~/repos and almost never the shape of a plugin directory.
What gets generated
├── module.js JavaScript half: native functions and metadata
├── module.libsonnet Jsonnet half: what users import
├── test.jsonnet exercises everything the module exposes
├── utils/
│ ├── test.js renders test.jsonnet through a real SpellFrame
│ └── cli-test.js runs the CLI with this plugin loaded
├── package.json
├── .gitignore
├── LICENSE MIT only; another --license emits none
└── README.md with doc markers `npm run doc` fills innpm test # render test.jsonnet and print the result
npm run cli # exercise CLI extensions
npm run doc # regenerate README.md's API reference from doc commentsThe plugin contract
A SpellCraft plugin is an ordinary npm package with "spellcraft": true in its
package.json. Core walks your dependencies, finds packages carrying that flag,
and loads each one's main. There is nothing to register.
Native functions
Every export other than _spellcraft_metadata becomes a Jsonnet native
function, registered as <package-name>:<export>:
exports.resourceName = [function (name) {
return `${this.environment}-${name}`;
}, 'name'];std.native("@you/your-plugin:resourceName")("artifacts")The namespacing is why two plugins can both export client without colliding.
It also means module.libsonnet must use the fully qualified name — this is
the single most common thing to get wrong, and the generated template
interpolates your package name so it starts out correct.
Export either a bare function or [fn, ...parameterNames]. Prefer the explicit
form: Jsonnet calls native functions by parameter name, so with a bare export
those names have to be recovered from the function's own source. That works for
source you wrote by hand, but not for minified code or a destructured
parameter — and core raises at load time naming the export rather than
registering something that fails later at a call site.
Native function arguments must be primitives. Jsonnet refuses to pass an object or an array into one — it raises "native extensions can only take primitives". Serialise on the way in with
std.manifestJsonExand parse inside the function. Return values carry no such restriction.
Results are memoised per (name, arguments) for the life of a render, so a
native called twice with the same arguments runs once. Side effects therefore
fire once too.
_spellcraft_metadata
Every field is optional.
exports._spellcraft_metadata = {
requires: ['@c6fc/spellcraft-aws-auth'],
functionContext: { environment: 'dev' },
fileTypeHandlers: { '.*?\\.env$': (content) => renderDotenv(content) },
init: async (spellframe) => { /* runs once, before the first render */ },
cliExtensions: (yargs, spellframe) => { /* add commands */ }
};requires— package names of plugins whose natives you call. Core refuses to start if one is missing, so a missing dependency is a clear message rather than a Jsonnet runtime error pointing at the caller.functionContext— merged into the sharedthisfor every native function in every loaded plugin. Use ordinaryfunction () {}declarations to reach it; an arrow function capturesthisfrom module scope instead.fileTypeHandlers— regex → serializer, for output filenames your plugin introduces. Core already claims.json,.yaml/.yml, and.md/.txt(written verbatim);@c6fc/spellcraft-terraformclaims.tf. The keys are JS strings compiled toRegExp, so a literal dot needs\\.. Registering a pattern also opts that extension into the render directory's cleaning pass.init— async, awaited once before the first render. Credentials, network calls and subprocesses belong here rather than at module scope.cliExtensions— receives theyargsinstance and theSpellFrame.
Sharing state between plugins
functionContext is the seam. A plugin reuses another's already initialised
state by reaching for its metadata directly, rather than authenticating twice:
const { aws } = require('@c6fc/spellcraft-aws-auth')._spellcraft_metadata.functionContext;For sequencing rather than state, the SpellFrame is an EventEmitter. A
plugin can announce a phase and let others hook it — spellcraft-terraform
emits @c6fc/spellcraft-terraform:pre-apply before terraform apply, which is
how spellcraft-gcp-terraform gets GCP services enabled first.
Versioning
The generator does not hardcode a core version. It runs npm install and reads
back whatever npm resolved, then derives the peer range from it. While core is
0.x that range is widened to the whole 0.x line, because semver reads every
0.x minor as breaking and a caret range would force a major bump of your
plugin on every core release. Once core is 1.x you get an ordinary ^1.0.0.
Development
npm testThe tests generate real plugins into temp directories and assert that they install cleanly, pass their own tests, and trip the safety guards. The templates encode the contract above by hand, so drift is only visible by running them.
