Status: Accepted (DEV-2213 / DEV-2222; supersedes ADR-0021 decision 10)
ADR-0021 versioned the docs examples but left starters as one hand-maintained
scaffold per framework, rewritten to any selected major (15–19) purely by
re-pinning package.json at runtime (applyHandsontableVersion). That
conflates content version with package version exactly the way ADR-0021's
decision 1 fixed for docs: an 18-authored starter pinned to 15 works only
while the source happens to avoid every 17+/18+ API. Its decision 10
deliberately deferred the problem to "a separate empirical pass".
That pass ran (DEV-2107 era + the starter matrix): the breakage is real and
structural, not incidental — handsontable/themes exists only from 17.0.0
(the five theme-API recipes), @handsontable/angular-wrapper has no 15.x
release at all, wrapper-16's HotTableComponent is standalone: false, and
DEV-2200's best-practice modernization (no CSS imports, JS theme: object)
is impossible below 17 because nothing injects core CSS there. A single
master source cannot be simultaneously correct for 15 and idiomatic for 18+.
Meanwhile PR #144's first cut of per-bucket container contexts baked one
node_modules per (framework, bucket) — 24 contexts — which doubled the image
and stalled wrangler deploy ~1 h in exporting layers (norm ~8 min).
-
Starter buckets mirror docs buckets, keyed by major. One generated bucket per supported major (
MIN_BUCKET_MAJOR = 15… npmdist-tags.latest's major) plusnext, underapps/authoring/public/starter-examples/<bucket>/. Unlike docs buckets (major.minor, one per release branch), starter buckets are major-only — per-minor keys would mean ~12 live branches instead of one per major. -
Bucket source =
prod-examples/<major>branch when it exists, else master;nextis always master. Resolution happens in.github/workflows/import-starters.yml's prepare job viagit ls-remote. Pipeline code always runs from master; the branch contributes onlyexamples/**(sparse checkout). Cutting a branch redirects its bucket with zero workflow changes, which also defines the rollout for a new major: its bucket tracks master (absorbing modernization on every re-import) until the branch is cut, so the snapshot is born current. -
Frozen branches take cherry-picks only — compat/build/security fixes, applied newest → oldest as far as relevant. Policy, eligible classes, and the dependency-minimal rule live in
docs/backport-policy.md. The split this enforces today: master (17+/next buckets) is import-free and themes via the JStheme:object (DEV-2200);prod-examples/15|16keephandsontable/styles/*.cssimports + stringthemeName, because neitherinjectCoreCssnorhandsontable/themesexists below 17.0.0. -
Pins are resolved, never authored. Branch sources keep
"latest";pipeline/import.mjsresolves the bucket'shotVersionfrom the npm registry (newest stable of the major; a major with no stable release fails generation), rewrites every dependency whose name containshandsontable(except@handsontable/pikaday) in lockstep, and regenerates the lockfile. The rewrite is byte-identical toapplyHandsontableVersion's runtime re-application — pinned by the byte-identity test inpipeline/import.test.mjs, which is what makes the Tier-2 frozen-install fast path possible at all. -
Bucket membership is floored, not branched.
minCoreMajorinconfig/frameworks.jsonfilters frameworks out of too-old buckets (angular < 16: no wrapper release; the five theme-API recipes < 17: nohandsontable/themesexport). Directories for excluded frameworks stay on the frozen branches — the importer skips them; deleting one that a bucket does require hard-fails that bucket and, because the publish job gates on the whole matrix, blocks the generated PR for every bucket. -
One baked node_modules seed per framework, a fingerprint per (framework, bucket) — the #146 single-seed design, replacing #144's per-bucket bake that hit the image-size wall.
scripts/prepare-container.mjsbakes only the seed bucket's (default 18) node_modules and emits asha256(package.json + pnpm-lock.yaml)fingerprint for every bucket intoworkers/api/src/frameworks.generated.ts, all pointing at the one seed. This is correct because the seed is a warm cache, not a contract: a pristine session's frozenpnpm install --frozen-lockfilereconciles the seed to the submitted self-consistent package.json + lock, downloading only the delta (today the HOT core/wrapper pins, ~1.3 s). A fingerprint match hard-fails rather than retrying non-frozen, sopipeline/frameworks-generated.test.mjsrecomputes every fingerprint against the bucket artifacts (drift test,checked >= 40— arithmetic over the bucket set, and the tripwire when a bucket is added or dropped). Consequence of the seed model: every dependency a frozen branch diverges from the seed grows that bucket's boot download — hence the dependency-minimal backport rule. -
Regeneration is event-driven. Pushes touching
examples/**on master orprod-examples/**, a weekly cron (re-pins after patch releases), and manual per-bucket dispatch all funnel into the same generatedchore/starter-example-bucketsPR (create-pull-request pinned by SHA — the job holds write permissions). The publish job rebuildscatalog.jsonand the container fingerprints in the same commit, keeping decision 6's drift test green by construction.
- ADR-0021 decision 10 is superseded: starter support per major is now
empirically enforced by construction (bucket membership + frozen branches
- the e2e starter matrix) instead of assumed. The matrix run of 2026-08-12 is the baseline: all starters green at 15–18.
- The version dropdown serves genuinely different content per major for starters, same as docs examples — the last consumer of the "one source, any pin" model is gone.
- Branch count grows by one per Handsontable major. The static-asset ceiling concern from ADR-0021 decision 11 applies to starter buckets too, but at ~40 files per bucket it is decades away for starters; docs buckets remain the binding constraint.
- The three copies of the major floor (
version.tsDEFAULT_MIN_MAJOR,import.mjsMIN_BUCKET_MAJOR,import-starters.ymlMIN_MAJOR) must move together; each site cross-references the others. - Writing to a
prod-examples/**branch is production-adjacent: it regenerates a served bucket on merge of the generated PR. Branch pushes go through per-round PRs for review.