Stone.jsDocs
Paradigm

Extending

Participate in the build

Some packages need to do work at build time, not just at runtime: generate a module, pre-scan a folder, inject configuration the app will read once bundled. A Stone.js CLI plugin is how a package takes part in the build and bundle. The contract is fully agnostic: the CLI never knows what a plugin does, only when to call it.

#The contract

A plugin is a plain object (usually the return of a factory). It exposes a name and up to three optional hooks, one per moment of the lifecycle.

FieldTypeDescription
namestringA unique, human-readable name. Shown when the plugin is loaded.
descriptionstring?A short summary of what the plugin does.
blueprintMiddlewaremiddleware[]?Config phase: run in the CLI blueprint pipeline to read or augment stone.builder.* before any command.
onPrepare(ctx) => void?Codegen phase (build and dev): write files into .stone/ and contribute modules or blueprint statements to the built app.
onBundle(ctx) => void?Bundle phase: run just before Rollup or Vite, for advanced bundler-level participation.

#Three moments, earliest to latest

HookTypeDescription
blueprintMiddlewareconfigAugment builder config. Runs for every command, before the builders.
onPreparecodegenThe workhorse. Runs once per build and per dev run, before the entry point is generated.
onBundlebundleRuns after every onPrepare, right before the bundler. Reach for it only for bundler options.

#The plugin context

Build hooks receive a stable facade. Everything a plugin needs is here, and nothing else: plugin authors depend on these helpers, never on CLI internals, so the CLI can evolve without breaking published plugins.

MemberTypeDescription
blueprintIBlueprintRead stone.builder.*, or set config the built app will read.
commandstringWhich command is driving the lifecycle: build, dev, preview, ...
eventIncomingEventThe console event, carrying the CLI flags and arguments.
reporterStoneReporterBranded output that matches the CLI look.
writeFile(path, content) => stringWrite a file into the .stone/tmp build directory.
addModule(specifier) => voidAdd a module to the built app: its exports join the app modules, exactly like app/** files.
addBlueprint(statement) => voidInject a statement into the entry configure step, where a local blueprint is in scope (server and console entries).

#A minimal plugin

The common shape: in onPrepare, generate a module into .stone/, then add it to the app with addModule. Its exports are collected like any other app module, so a generated defineConfig(...) reaches the built app.

src/cli.tsts
import { defineConfig } from '@stone-js/core'

export function acmeCliPlugin (options = {}) {
  return {
    name: '@acme/stone-acme',
    description: 'Generates the Acme config at build time.',
    onPrepare (ctx) {
      // Do any build-time work here (scan a folder, read files, compute config).
      const settings = { greeting: options.greeting ?? 'hello' }

      // Generate a module the app will bundle...
      ctx.writeFile('plugins/acme.mjs', [
        "import { defineConfig } from '@stone-js/core'",
        'export const acme = defineConfig({ stone: { acme: ' + JSON.stringify(settings) + ' } })'
      ].join('\n'))

      // ...and hand it to the built app.
      ctx.addModule('./plugins/acme.mjs')
    }
  }
}

// A ready-to-use default export, so the package can be auto-discovered.
export default acmeCliPlugin()

#Two ways to load a plugin

Both paths end in the same place. Which one you use is a trust decision, not a capability one.

#Explicit, in stone.config (any package)

The primary path, open to every package. List the plugins your app uses. This is always available and always safe: the developer sees exactly what runs at build time.

stone.config.mjsts
import { defineBuilderConfig } from '@stone-js/cli'
import { acmeCliPlugin } from '@acme/stone-acme/cli'

export default defineBuilderConfig({
  plugins: [acmeCliPlugin({ greeting: 'bonjour' })]
})

#Auto-discovered, first-party only (@stone-js/*)

A package advertises its plugin through a contract in its own package.json. The CLI loads it automatically, but only for first-party @stone-js/* packages, and only from your project direct dependencies. This keeps first-party modules truly zero-config while never running unvetted build-time code from a third party.

node_modules/@stone-js/i18n/package.jsonts
{
  "name": "@stone-js/i18n",
  "stone": { "cliPlugin": "./dist/cli.js" }
}

#How contributions reach the app

The built app assembles its blueprint at runtime from the modules the bundler collected. A plugin does not mutate the CLI blueprint and hope it carries over: it contributes real modules and config that the entry point bundles. addModule works everywhere, including the browser build. addBlueprint targets the server and console entries, where a live blueprint is in scope; for the browser, prefer a generated module that exports defineConfig(...).

#Register a whole build target

A plugin can go further than contributing to a build: it can be one. A target is a declaration on the Blueprint, so a package that knows how to turn its own sources into an application says so, and the CLI resolves it like any other.

src/cli/index.tsts
import { StoneBuilderDefinition, StoneCliPlugin } from '@stone-js/cli'

export const myBuilderDefinition: StoneBuilderDefinition = {
  target: 'my-target',
  priority: 10,
  devMode: 'self-hosted',                     // or 'supervised', if the CLI runs the process
  devEntry: () => buildPath('server.mjs'),
  previewEntry: () => buildPath('preview.mjs'),
  match: (blueprint) => hasMySources(blueprint),
  resolver: (context) => new MyBuilder(context)
}

export function myCliPlugin (): StoneCliPlugin {
  return {
    name: '@acme/my-renderer',
    description: 'Adds the my-target build.',
    blueprintMiddleware: [{ module: SetMyBuilderMiddleware, priority: 4 }]
  }
}

Three fields carry the whole contract. match answers whether this target is the one for this project, so detection stays with the package that can detect it. devMode says whether the CLI supervises a process it started or follows one that hosts itself. resolver returns the builder, whose middleware pipelines are ordinary pipelines: a plugin can insert into them by priority.


Stone.js

Your app exists in every runtime. Until you run it.

An open-source project by Stone Foundation
Created by Mr. Stone (Evens Pierre)