@jxsuite/starters
v3.1.4
Published
Starter site templates for new Jx projects
Readme
@jxsuite/starters
The catalogue of ready-made starter sites a user can clone when creating a new Jx project, either from Studio's New Project picker or with bun create @jxsuite <directory> --template <id>. The package is two things: registry.json, the picker metadata, and sites/, one complete buildable Jx project tree per starter.
The TypeScript module itself is metadata-only. It reads the registry and resolves a starter's directory; the copy-and-rewrite of a clone happens in @jxsuite/create's generate.ts.
API
import { getStarter, getStarterDir, listStarters, SITES_DIR } from "@jxsuite/starters";
listStarters(); // StarterMeta[] — parsed once, memoized (same array instance)
getStarter("restaurant"); // StarterMeta | undefined
getStarterDir("restaurant"); // <SITES_DIR>/restaurant; throws on an unknown idSITES_DIR is join(import.meta.dirname, "sites"), so the same code resolves from the monorepo and from the copy staged inside the packaged desktop app. getStarterDir throws Unknown starter: "<id>" rather than handing back a path that does not exist. The registry is also exported raw as @jxsuite/starters/registry.json, for consumers that want the array without the module. The package publishes raw TypeScript: exports["."] is ./index.ts, there is no build step, and the module imports only node:fs and node:path.
The registry contract
registry.json is a flat JSON array. Every entry carries all eight fields; there are no optional keys and no defaults (index.ts, StarterMeta):
| Field | Contract |
| ------------- | ------------------------------------------------------------ |
| id | Unique; also the sites/<id> directory name |
| name | Display name in the picker |
| industry | Archetype label, e.g. Restaurant & Food |
| tagline | One line under the name |
| description | Longer copy for the showcase |
| features | Non-empty string[] of Jx features the starter demonstrates |
| accent | Hex colour, /^#[0-9a-fA-F]{3,8}$/ |
| thumbnail | data:image/… URI (generated, see below) |
tests/registry.test.ts asserts each of those, that ids are unique, and that every id maps to a sites/<id>/project.json. Never restate the number of starters in prose: scripts/docs/check-site-claims.ts reads registry.json and fails the root README.md or any sites/jxsuite.com/pages/** page claiming a different count (digits and number words alike). The same gate holds the templates page's ::starter-card slugs to the registry ids, both directions, so a starter added without a card is caught too. It does not scan this file, so here the habit is the only guard.
The registry is the source several unrelated gates read by path: the docs generator, the marketing-claims count check, the packaged-desktop bundle verifier, and the screenshot lane's shot selector.
Layout of sites/
Each sites/<id>/ is a complete, buildable Jx project. It contains components/, layouts/, pages/, content/, public/, project.json, a package.json, and two committed generated entry schemas:
sites/restaurant/
project.json # validated by parseProjectConfig and by its own bundle
project.schema.json # generated by `bun run schema:generate-all`
document.schema.json # generated by `bun run schema:generate-all`
components/ layouts/ pages/ content/ public/
package.json # PUBLISHED @jxsuite/* ranges; rewritten by release-please
images.json # OPTIONAL: authoring-only Pexels manifest, stripped from clones
.gitignore # .cache, dist, node_modulestests/validate.test.ts holds the tree to its schemas: the two committed bundles must be self-contained (no relative $refs survive), project.json must satisfy both parseProjectConfig and its own project.schema.json, every {components,layouts,pages}/**/*.json must validate against document.schema.json (and at least one must exist), and every content/**/*.md must start with frontmatter. tests/validate-projects.test.ts runs each starter through @jxsuite/schema's validateProjectFile as well.
Starters are not uniform by design: the museum starter is the multilingual one (i18n.locales: ["en", "fr-CA", "ar"] with routing: "prefix-except-default"), and real-estate's listings collection uses format: "Csv" where the others use Markdown.
Consumption: cloned, never depended on
Nothing a user ships depends on this package. @jxsuite/create imports getStarterDir lazily (so blank projects never load the large starters package), copies the tree, and then re-stamps identity:
bun create @jxsuite my-site --template restaurant- Excluded from the copy:
node_modules,dist,.cache,.jx-cache,.git, andimages.json(packages/create/generate.ts). project.jsonis preserved, with onlyname/url/description/build.adapterre-stamped (plus design tokens when the quickstart supplies them). Content, components, layouts and public assets are copied verbatim.package.jsonis rebuilt from scratch with current dependency ranges, so a clone never inherits whatever the in-repo starter pinned.--templateresolves built-in template ids first, then registry starter ids; an unknown id falls back toblank.
Two other consumers read the registry live: the dev server exposes it to Studio's picker at GET /__studio/starters, returning listStarters() verbatim (specs/server.md §4.1), and the desktop app imports listStarters directly in its Chromium window layer and its Electron hub.
Authoring or modifying a starter
Add the
sites/<id>/tree and aregistry.jsonentry with all eight fields.bun run schema:generate-allregenerates everyproject.schema.jsonanddocument.schema.json. Always via this script, never by hand: it runsschema:clean-rootsfirst (see below).bun run schema:verifyproves the result in CI, so a stale entry schema is a red build..github/workflows/schemas.ymlalso regenerates it and pushes the fix to your branch, with a comment naming the JSON Pointers that moved.Nothing for the docs:
docs/studio/projects/starters.mdis rendered fromregistry.jsonbyscripts/docs/generators/starters.tswhen the site builds, and is never committed (bun run docs:generatepreviews it).bun run screenshots:thumbnails. Thethumbnaildata URI is not hand-authored. The script builds the starter with image optimization off, servesdist/, screenshots the hero viewport in headless Chromium, and writes the small JPEG intoregistry.jsonplus a full-res JPEG intosites/jxsuite.com/public/starters/<id>.jpg. (This is whyregistry.jsonis large.)Photos, if the starter ships any:
PEXELS_API_KEY=… bun packages/starters/scripts/fetch-photos.ts <site-id> [--force]images.jsonholds{ site, thumbnail?, images }, where each image is{ role, query, orientation?, photoId? }. The pinnedphotoIdmakes re-runs fetch the same images;--forcere-picks even pinned ones. The script upserts a per-site block inCREDITS.mdbetween<!-- site:<id>:start -->and<!-- site:<id>:end -->. Committed output is plain image files, so scaffolded projects have no Pexels or network dependency.bun test --isolatefrompackages/starters. Per-file coverage is gated at 99% lines / 99% functions (bunfig.toml);sites/,scripts/andtests/are excluded from coverage.
Adding a starter needs no desktop config change: packages/desktop/scripts/verify-bundle.ts derives its checklist from the staged registry, so every new id is covered automatically.
Hazards
The shadowed core. A starter's package.json pins published @jxsuite/* ranges. It must, because it is a template a user scaffolds from, and starters are not bun workspace members, so workspace:^ is unavailable. Iterating a starter in Studio runs bun install in that root, which materialises a real @jxsuite/schema beside a workspace far ahead of it. That has already produced a starter document schema narrower than the starter's own content, unnoticed for six weeks (scripts/check-shadowed-core.ts carries the full post-mortem). bun run schema:generate-all therefore runs schema:clean-roots (bun scripts/check-shadowed-core.ts --fix) first, removing only node_modules/@jxsuite/* and a stray lockfile. Third-party dependencies stay, because the install is what makes the starter preview, and a workspace symlink is never removed.
Never put that cleanup in a starter's package.json. This package publishes sites/, so those manifests ship to end users; a postinstall or preschema hook there would run on their machine and delete the dependencies they just installed. The monorepo has the workspace being shadowed, so the monorepo owns the cleanup.
Do not hand-edit the @jxsuite/* ranges in sites/*/package.json. release-please rewrites them inside its release commit via extra-files; the gate is bun run templates:check (templates:sync to repair).
Packaged-desktop path contract. The packaged application's Bun backend is a single bundle, so import.meta.dirname no longer resolves to this package. registry.json and sites/ are staged beside the bundle (BACKEND_DATA in packages/desktop/scripts/lib/backend-bundle.ts), and bun run smoke in packages/desktop fails if the staged copy cannot list the starters (specs/desktop.md §7).
The screenshot lane ignores this package's source. It watches only registry.json and the six starter roots a shot actually opens (.github/workflows/screenshots.yml); a registry change arms exactly one shot, new-project, because that modal's gallery is listStarters().
Versioning
Published to npm as @jxsuite/starters. Like every @jxsuite package, it ships TypeScript source and follows the monorepo's release train (the starters release-please component). Within the monorepo, @jxsuite/create depends on it via workspace:^. Because consumption is a one-time clone, publishing a new version never changes an existing project.
