Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Install and versions

Get a toolchain that works, then find out exactly which SDK you ended up with and what it can do. This page is the book’s normative home for two things: the runtime version pins your package.json must carry, and which SDK version each feature needs — see What each SDK version gives you before you decide whether to build a single widget or a suite, because that choice is frozen at your first publish.

Node

VersionWhy
Hard floor20.19.0 (or 22.12.0+)Vite 7 declares engines: ^20.19.0 || >=22.12.0, and the scaffold pins vite: ^7.3.1.
Feature floor20.12.0process.loadEnvFile landed in 20.12 (and 21.7). npm run dev:ssr uses it.
Recommended22.12+ LTSSatisfies both, and matches the Node this SDK is developed on.

The 20.12 floor is the one that bites, because it fails silently. The SSR preview harness loads .env.local and .env itself — Vite is not involved — and swallows any failure:

// packages/create-hr-plugin/template/scripts/dev-ssr.mts:26-32 (verbatim)
// — byte-identical at template-suite/scripts/dev-ssr.mts:41-47
for (const file of [".env.local", ".env"]) {
  try {
    process.loadEnvFile(file);
  } catch {
    // file absent — fine
  }
}

On Node 20.0–20.11 process.loadEnvFile is not a function, the TypeError lands in that catch, and the script carries on with no env loaded at allHR_SSR_HYDRATE, HR_RUNTIME_URL, PORT and your NEXT_PUBLIC_* keys are simply absent and every default silently applies. You get a working-looking server pointed at the wrong places (../local-dev/ssr-preview.md covers those keys). Nothing warns you either: the template declares no engines field, so npm install prints no EBADENGINE for the 20.12 requirement. Enforcement level: advisory (unvalidated) — see the legend in ../contracts/limits-and-errors.md.

Scaffold a project

# packages/create-hr-plugin/bin/create-hr-plugin.js:26-82 — the whole argv surface
npx create-hr-plugin acme-weather --author "Acme Corp"   # one widget (the default)
npx create-hr-plugin acme-suite --template suite         # several widgets + a layout
cd acme-weather

The CLI is entirely non-interactive. It asks nothing; everything comes from argv. --help, -h, or no arguments at all prints this and exits 0:

# packages/create-hr-plugin/bin/create-hr-plugin.js:29-47 — the help text, verbatim
  Usage: create-hr-plugin <plugin-name> [options]

  Creates a HomeRunner widget plugin project (vm.Script SSR + IIFE client).

  Arguments:
    plugin-name     Plugin slug — lowercase letters, digits and hyphens
                    (e.g. "acme-weather"). Becomes the plugin's manifest id.

  Options:
    --template <t>   Project shape: "single" (one widget, default) or
                     "suite" (several widgets + a layout, one publish).
                     --template=<t> works too.
    --author <name>  Author name (default: from git config)
    --no-install     Skip npm install
    --help, -h       Show this help

  Examples:
    npx create-hr-plugin acme-weather --author "Acme Corp"
    npx create-hr-plugin acme-suite --template suite
BehaviourDetail
Slugargv[0], always. Put flags after the slug — a first argument starting with - is refused rather than turned into a directory name.
Slug validation (0.8.1+)Checked before any file is written, against the publish audit’s own rule /^[a-z0-9][a-z0-9-]*$/ plus the reserved names dist, widgets, media. Earlier versions accepted anything and you found out at publish, with the project already built and committed. The regex belongs to manifest.id../contracts/manifest-root.md.
--template <single|suite> (0.8.0+)Project shape; default single. --template=suite works too (0.8.1+ — 0.8.0 parsed only the space-separated form and silently scaffolded single, which matters because the shape is frozen at first publish). Anything else, including --template with nothing usable after it, exits 1.
--author <name>Falls back to git config user.name. If that command fails (git absent, or user.name unset) the author becomes the literal string Plugin Author. A dangling --author with no value falls back the same way.
--no-installSkips npm install. Without it, a failed install is not fatal: the scaffold prints a warning and still exits 0.
Target existsHard error, exit 1.
Copy mechanismEvery template file is read as UTF-8 text, token-replaced and rewritten. Nothing in either template directory may be binary.
_gitignoreRenamed to .gitignore after the copy (npm strips .gitignore from published packages).

Every failure message the scaffolder can print is quoted verbatim in ../contracts/limits-and-errors.md.

--template single writes the single-widget project; --template suite writes a three-widget suite with a layout, a preset and a media/ folder. Both shapes run npm run dev, npm run dev:ssr, npm run build and npm run preview. See ../concepts/manifest-shapes.md for the choice you are making — it is frozen at your first publish — then 02-your-first-widget.md or 03-build-a-suite.md to run it.

The React-family pins

Pin these exactly, with no caret, in your plugin’s package.json:

PackagePin
react19.2.5
react-dom19.2.5
@tanstack/react-query5.95.2

Shape tag: Both. Pin only the ones you actually import — the real suite pdp-suite declares no React Query dependency at all. @types/react and @types/react-dom are caret-ranged (^19.1.3); only the runtime packages are exact.

Your client bundle does not ship React. viteHomerunnerWidget externalises react, react/jsx-runtime, react-dom and react-dom/client against the host page’s runtime IIFE, and the SSR sandbox hands your server bundle the renderer’s own copies. So the React you compile against and the React you execute against are different installs, and they must agree to the patch.

# pnpm-workspace.yaml:13-16 — the platform's own rationale, verbatim
# Why exact pins for react/react-dom: plugin client umds externalize react/react-dom/* against
# `window.HRWidgetRuntime` (= the runtime IIFE). Even patch drift between the renderer's
# server-side React and a plugin's bundled react-dom internals will trip the
# `Incompatible React versions` runtime check.

A caret is enough to break this. "react": "^19.2.5" resolves to whatever 19.x is newest the day you install, and react-dom asserts the two match exactly:

# node_modules/react-dom/cjs/react-dom-client.development.js:27934-27938
Incompatible React versions: The "react" and "react-dom" packages must have the exact same
version.

Two traps in that message. It is a development-build assertion, so a mismatch can pass a production build on your machine and misbehave on a customer page instead of throwing. And enforcement is advisory (unvalidated) — nothing at submit, review or publish reads your package.json, so a drifted pin ships.

What each SDK version gives you

runtime.widgetCore in your built manifest.json records the version you actually built with. The build writes it; never hand-write it — see ../contracts/manifest-root.md for the field and ../contracts/packaging-and-publishing.md for the gate it feeds.

widget-coreWhat it addedOn npm today?
0.8.0The /mock dev-mocking subpath and the /testing SSR harness (renderPluginSSR, createNodeMockServer, createSSRDevServer)yes (superseded)
0.9.0spacing replaces width on the base schema; parseWidgetConfigno (never published)
0.10.0Evergreen runtime — /runtime externalised, runtime.widgetCore stamped. The publish floor.yes
0.10.1The shadowDOM manifest flag; the raw-options contractyes
0.11.0Suites (widgets[]), registerPluginWidget, the hr-widget-build bin, layout widgets, presets, mediaSafeAuto, enforced externalFetchyes
0.12.0assets.fonts, bundled assets, layout pages, marketplace icon / coveryes
0.12.1Slot prefillyes
0.12.2createSSRDevServer takes a widget option (--widget {slug} / HR_SSR_WIDGET), so npm run dev:ssr previews one named widget of a suite; resolvePluginSSRTarget and widgetFromInvocation exported; the harness’s sandbox moved closer to the renderer’syes
0.12.3The SSR preview falls back to the widget’s manifest assets.css when the bundle exports no getStaticAssets — the shape --template suite scaffolds, which previewed unstyled before thisyes — latest

create-hr-plugin tracks it:

create-hr-pluginWhat it addedOn npm today?
0.6.0The template that pins widget-core ^0.10.0 and builds in three stepsyes — latest
0.7.0npm run build delegates to hr-widget-build; template pins ^0.11.0no
0.8.0--template suite; npm run preview renders the built widget; public/mockServiceWorker.js no longer ships in dist/; both templates pin ^0.12.1no
0.8.1--template=suite (equals form); slug validation before any file is written; npm run dev:ssr in the suite template (which moves its pin to ^0.12.3)no

Version-gate notes appear inline throughout the book as (0.11.0+). The full export inventory for 0.12.2 is ../contracts/sdk-exports.md.

Versions on npm

Both packages are published and current (checked 2026-09-08):

npm latest
@homerunner-next/widget-core0.12.3
create-hr-plugin0.8.1

So npx create-hr-plugin <slug> and npx create-hr-plugin <slug> --template suite both work straight from the registry, and every feature in the table above is installable. Nothing in this book needs a tarball or a private build.

Two things are still worth checking rather than assuming:

  • Which version you actually ran. npx caches. If a scaffold comes out single-widget after you asked for a suite, you ran a pre-0.8.0 CLI, which had no --template flag and silently ignored the argument — and the manifest shape is frozen at your first publish. Confirm with npx create-hr-plugin --help (it lists --template from 0.8.0) or npx create-hr-plugin@latest. Older CLIs also had no slug validation, so an invalid manifest.id went unreported until the publish audit.
  • What an existing project pins. A project scaffolded before 0.8.0 pins an older widget-core and still uses the three-step build:manifest && build:iife && build:ssr build. Suites need 0.11.0+, and a suite’s npm run dev:ssr needs 0.12.3 — on 0.12.2 it previews unstyled, because the scaffolded widgets export no getStaticAssets. Bump the pin and switch build to build:manifest && hr-widget-build before following 03-build-a-suite.md.

Do not start a plugin as single-widget “for now” if you intend to ship a suite. The manifest shape is frozen at your first publish and changing it needs a brand-new plugin slug (../concepts/manifest-shapes.md). Converting is free before you publish and impossible after.

Check what you actually have

# run these in your plugin directory before you trust any version-gated instruction
node -v                                          # want 20.19+ or 22.12+
npm ls @homerunner-next/widget-core react react-dom
ls node_modules/.bin/hr-widget-build             # absent => widget-core < 0.11.0
npm view @homerunner-next/widget-core dist-tags  # what the registry serves right now

After a build, the authoritative answer is in your own manifest: runtime.widgetCore is the exact version the build ran with. Symptoms that trace back to a version mismatch are indexed in ../troubleshooting.md.