@jxsuite/starters
v1.7.0
Published
Starter site templates for new Jx projects
Downloads
1,975
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
both its Electrobun and Chromium window layers.
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.bun run docs:generate, becausedocs/studio/projects/starters.mdis rendered fromregistry.jsonbyscripts/docs/generators/starters.ts. Hand-editing it is a bug; CI diffs the page.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. ElectroBun inlines the bun-side JS graph, so
import.meta.dirname resolves to app/bun/ at runtime. registry.json and
sites/ must therefore be staged to bun/registry.json and bun/sites by
build.copy in packages/desktop/electrobun.config.ts (specs/desktop.md §7.4).
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.
