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
| Version | Why | |
|---|---|---|
| Hard floor | 20.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 floor | 20.12.0 | process.loadEnvFile landed in 20.12 (and 21.7). npm run dev:ssr uses it. |
| Recommended | 22.12+ LTS | Satisfies 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 all — HR_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
| Behaviour | Detail |
|---|---|
| Slug | argv[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-install | Skips npm install. Without it, a failed install is not fatal: the scaffold prints a warning and still exits 0. |
| Target exists | Hard error, exit 1. |
| Copy mechanism | Every template file is read as UTF-8 text, token-replaced and rewritten. Nothing in either template directory may be binary. |
_gitignore | Renamed 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:
| Package | Pin |
|---|---|
react | 19.2.5 |
react-dom | 19.2.5 |
@tanstack/react-query | 5.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-core | What it added | On npm today? |
|---|---|---|
| 0.8.0 | The /mock dev-mocking subpath and the /testing SSR harness (renderPluginSSR, createNodeMockServer, createSSRDevServer) | yes (superseded) |
| 0.9.0 | spacing replaces width on the base schema; parseWidgetConfig | no (never published) |
| 0.10.0 | Evergreen runtime — /runtime externalised, runtime.widgetCore stamped. The publish floor. | yes |
| 0.10.1 | The shadowDOM manifest flag; the raw-options contract | yes |
| 0.11.0 | Suites (widgets[]), registerPluginWidget, the hr-widget-build bin, layout widgets, presets, mediaSafeAuto, enforced externalFetch | yes |
| 0.12.0 | assets.fonts, bundled assets, layout pages, marketplace icon / cover | yes |
| 0.12.1 | Slot prefill | yes |
| 0.12.2 | createSSRDevServer 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’s | yes |
| 0.12.3 | The 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 this | yes — latest |
create-hr-plugin tracks it:
| create-hr-plugin | What it added | On npm today? |
|---|---|---|
| 0.6.0 | The template that pins widget-core ^0.10.0 and builds in three steps | yes — latest |
| 0.7.0 | npm run build delegates to hr-widget-build; template pins ^0.11.0 | no |
| 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.1 | no |
| 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-core | 0.12.3 |
create-hr-plugin | 0.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.
npxcaches. If a scaffold comes out single-widget after you asked for a suite, you ran a pre-0.8.0 CLI, which had no--templateflag and silently ignored the argument — and the manifest shape is frozen at your first publish. Confirm withnpx create-hr-plugin --help(it lists--templatefrom 0.8.0) ornpx create-hr-plugin@latest. Older CLIs also had no slug validation, so an invalidmanifest.idwent 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:ssrbuild. Suites need 0.11.0+, and a suite’snpm run dev:ssrneeds 0.12.3 — on 0.12.2 it previews unstyled, because the scaffolded widgets export nogetStaticAssets. Bump the pin and switchbuildtobuild:manifest && hr-widget-buildbefore 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.