Creating extensions
CairnCMS ships a small toolchain for creating extensions:
create-cairncms-extension— scaffolds a new extension package with the right files and dependencies. Also reachable ascce.@cairncms/extensions-sdk— the SDK itself:define*helpers, types, and thecairncms-extensionCLI used to build, watch, and link extensions during development.
Together they cover the lifecycle: scaffold → develop → build → install → publish.
Scaffolding a new extension
Section titled “Scaffolding a new extension”The fastest way to start a new extension is the scaffolder:
npm init cairncms-extensionThis walks you through interactive prompts: extension type, extension name, and (for everything except bundles) language (JavaScript or TypeScript). Bundles skip the language prompt because bundle code is structural rather than implementation. The result is an npm package configured for your chosen extension type, with @cairncms/extensions-sdk pre-installed.
The scaffolder is also available as a longer-named bin (create-cairncms-extension) and as the shorter cce:
npx create-cairncms-extension# ornpx cceThe scaffolder always prompts interactively; CLI arguments are not read.
If you want to combine several extensions into one distributable package, scaffold a Bundle instead and add entries to it.
Project structure
Section titled “Project structure”The scaffolder creates an npm package that looks like this (for a non-bundle extension):
my-extension/├── package.json├── src/│ └── index.{js,ts}└── ...The package.json contains a cairncms:extension block with the extension’s metadata:
{ "cairncms:extension": { "type": "interface", "path": "dist/index.js", "source": "src/index.js", "host": "^1.0.0" }}type— one of the nine extension types (interface, display, layout, module, panel, hook, endpoint, operation, bundle).path— the built output the loader will read.source— the source entrypoint passed to the build.host— a semver range describing which CairnCMS versions this extension is compatible with.
The build CLI uses these fields by default. The type, source, and path values can be overridden at the command line; host cannot.
Building
Section titled “Building”Build with:
npm run buildThe generated package.json calls the SDK’s CLI:
{ "scripts": { "build": "cairncms-extension build" }}Internally, the CLI uses Rollup to bundle the extension into a single entrypoint.
The build command supports several flags:
-w,--watch— rebuild on file change. Use this during active development.--sourcemap— include source maps in the output.--no-minify— skip minification.-t,--type <type>— override the type frompackage.json.-i,--input <file>— override the source path.-o,--output <file>— override the output path.
Most projects only ever need the bare build command and --watch.
Output format
Section titled “Output format”The build CLI emits ESM by default for newly scaffolded extensions. The scaffolder writes "type": "module" into the generated package.json, and the build reads that field to choose the output format.
Existing extensions without a "type" field continue to emit CommonJS, so a rebuild does not change their loader behavior. To migrate an existing extension to ESM, add "type": "module" to its package.json and rebuild.
The output file extension overrides the manifest. A path ending in .mjs always emits ESM. A path ending in .cjs always emits CommonJS. App-side bundles always emit ESM regardless of type.
The same rules apply when building with explicit -t -i -o flags. The CLI consults the current directory’s package.json to determine the type, with the same file-extension override.
Custom Rollup configuration
Section titled “Custom Rollup configuration”To extend the Rollup config — for example, to add a plugin — create one of these files at the root of the extension package:
extension.config.jsextension.config.mjsextension.config.cjs
export default { plugins: [ /* additional Rollup plugins */ ],};The supported option is plugins, which is an array of Rollup plugins added on top of the SDK’s built-in plugins.
Live reloading during development
Section titled “Live reloading during development”CairnCMS can reload extensions automatically when their files change on disk. Set the operator-side environment variable:
EXTENSIONS_AUTO_RELOAD=trueThere is one important caveat: the extension watcher is intentionally disabled when NODE_ENV=development, on the assumption that development environments use a process-level reloader (nodemon, the dev script, and so on) that would conflict with the in-process watcher. To exercise the auto-reload path, leave NODE_ENV unset or set to production.
The combination most developers want is cairncms-extension build --watch running in one terminal (rebuilding the extension on file change) and CairnCMS running with EXTENSIONS_AUTO_RELOAD=true (picking up the rebuilt output without a manual restart).
Symlinking a local extension
Section titled “Symlinking a local extension”If you want CairnCMS to pick up an extension you are developing in a separate directory, symlink it into a CairnCMS extensions folder:
cairncms-extension link <path-to-extensions-folder>The path argument is mandatory and is resolved as-is — the command does not read CairnCMS project configuration. Pass the absolute or relative path to the target instance’s extensions folder, and the current package is symlinked into it. Changes to the source files are visible without copying or installing.
Working on a bundle
Section titled “Working on a bundle”Inside a bundle package, you can add new entries (sub-extensions) without editing the manifest by hand:
cairncms-extension addThis opens an interactive prompt for the entry’s type, name, and language, then updates the bundle’s cairncms:extension.entries array and creates the source files.
Installing an extension
Section titled “Installing an extension”CairnCMS discovers extensions from three sources at startup. Pick whichever fits how you ship the extension.
Package extensions
Section titled “Package extensions”Install from npm into the project’s node_modules. The loader auto-discovers packages whose names match any of:
cairncms-extension-<name>@<scope>/cairncms-extension-<name>@cairncms/extension-<name>
cd <cairncms-project-folder>npm install <package-name>This is the right path for shipping an extension to other operators or for installing one published by someone else.
Local package extensions
Section titled “Local package extensions”Place a full package directory (with its own package.json) inside EXTENSIONS_PATH. The loader treats these the same as installed packages but resolved from a local path. Bundles are typically installed this way.
Local file extensions
Section titled “Local file extensions”For non-bundle extensions, place pre-built output into the type-folder layout:
<EXTENSIONS_PATH>/├── interfaces/│ └── my-interface/│ └── index.js├── displays/│ └── ...└── hooks/ └── ...For Operation extensions (which have both an app and an api side), use app.js and api.js instead of index.js:
<EXTENSIONS_PATH>/operations/my-operation/├── app.js└── api.jsThis path is convenient for one-off extensions that do not need to live in their own package.
Publishing to npm
Section titled “Publishing to npm”To make an extension available to other CairnCMS operators, publish the npm package the SDK created:
- Make sure the package name matches one of the auto-discovery patterns above.
- Run
npm publish.
Operators install with npm install <name> and CairnCMS auto-discovers it.
The CairnCMS extension naming convention exists so the loader can find packages without configuration. A package named cairncms-extension-my-fancy-thing is auto-discovered; a package named my-fancy-thing is not.