diff --git a/.changeset/template-spec-version-sync.md b/.changeset/template-spec-version-sync.md new file mode 100644 index 0000000000..ca530b2c3e --- /dev/null +++ b/.changeset/template-spec-version-sync.md @@ -0,0 +1,64 @@ +--- +"create-objectstack": patch +--- + +fix(create-objectstack): the blank template's `specVersion` stops shipping eleven majors stale, and the version-time sync covers every declared surface on every template (#9264) + +The one bundled template declared the platform it targets in **two** places that +disagreed by eleven majors: + +| file | key | was | +|:--|:--|:--| +| `objectstack.manifest.json` | `specVersion` | `^6.0.0` | +| `objectstack.config.ts` | `engines.protocol` | `^17` | + +`scripts/sync-template-versions.mjs` re-stamped the config key and the template's +`@objectstack/*` dependency ranges, and **never opened the manifest at all**. So +`engines.protocol` tracked every major bump while `specVersion` sat at the value +it held when the script was written — and a green `sync-template-versions` run +was never evidence about it, because the script's failure mode was loud for the +keys it covered and mute for the key it did not. + +**This is not confined to the registry contract.** `create-objectstack` copies +the manifest into every scaffolded project, rewriting `name`, `displayName` and +`namespace` and dropping `description` — it has never touched `specVersion`. So +every project scaffolded since v7 was stamped with a `^6.0.0` spec range while +installing `@objectstack/spec@^17.0.0`. + +**The two keys are two facts, and the fix keeps them apart.** `engines.protocol` +is the ADR-0087 D1 runtime handshake range and carries the protocol major +(`^17`). `specVersion` is documented by `TemplateManifestSchema` as the +"Compatible `@objectstack/spec` semver range" and carries the package range +(`^17.0.0`) — the same value the script already writes into the template's own +`@objectstack/spec` dependency, so the manifest and the `package.json` now state +one fact once. They agree on the major only because the spec package's major and +the protocol major are kept in lockstep; they are stamped from two different +values. + +Deleting the key was not available: `specVersion` is **required** by +`TemplateManifestSchema`, and every shipped manifest is parsed against it by +`check:template-manifests`. + +**Two structural changes, because one-key-one-file coverage is what let this +sit:** + +- the sync script's file list is now **discovered**, not hard-coded — templates + are found by walking `src/templates/`, the same way `check-template-manifests` + finds the manifests it parses, so a second template is covered on the day it + lands; +- **every stamp is required**. A template whose file is missing, whose stamp is + absent, or whose `package.json` declares no `@objectstack/*` dependency is a + hard failure naming the path — never a skip. A skipped stamp is + indistinguishable from a synced one in the log, which is the invisibility this + fixes. + +The manifest is rewritten as **text** rather than parsed and re-serialized: +`objectstack.manifest.json` keeps `scaffold.variables` compact on one line, and +`JSON.stringify(…, null, 2)` would reformat unrelated structure on every release. + +CI coverage lands as four per-template ratchets in `template-consistency.test.ts`, +generalized off `blank` onto the same directory walk — including the invariant +that catches this exact class: the manifest's `specVersion` must equal the +`@objectstack/spec` range the template actually installs. Either file alone can +be self-consistently stale; only comparing them catches a stamp that covered one +and not the other. diff --git a/packages/create-objectstack/src/template-consistency.test.ts b/packages/create-objectstack/src/template-consistency.test.ts index 241b9ef513..dacb7a0c7c 100644 --- a/packages/create-objectstack/src/template-consistency.test.ts +++ b/packages/create-objectstack/src/template-consistency.test.ts @@ -72,46 +72,128 @@ const REPO_READ_ENV: NodeJS.ProcessEnv = (() => { return env; })(); -describe('blank template package.json', () => { - const templatePkg = JSON.parse( - fs.readFileSync( - path.join(pkgRoot, 'src', 'templates', 'blank', 'package.json'), - 'utf8', - ), - ); - - it('pins every @objectstack/* dep to the current major', () => { - const allDeps = { ...templatePkg.dependencies, ...templatePkg.devDependencies }; - const stackDeps = Object.entries(allDeps).filter(([name]) => - name.startsWith('@objectstack/'), - ); - expect(stackDeps.length).toBeGreaterThan(0); - for (const [name, range] of stackDeps) { - const match = /^\^(\d+)\./.exec(String(range)); - expect(match, `${name} range "${range}" must be ^.x`).not.toBeNull(); +// ── Declared version surfaces, per bundled template (#9264) ───────────────── +// +// Every bundled template declares the platform it targets in THREE places, and +// each one is committed to git, shipped in the tarball and copied into every +// scaffolded project. `scripts/sync-template-versions.mjs` re-stamps all three +// at version time; these ratchets are the CI half, because that script runs on +// a changesets/action release PR that gets no CI at all. +// +// The template list is DISCOVERED, not written down — the same directory walk +// the sync script and `check-template-manifests.ts` both use. A hand-kept list +// is precisely what failed here: coverage of one key in one file is how +// `specVersion` sat at `^6.0.0` while `engines.protocol` tracked every major up +// to `^17`, eleven majors of drift behind a green sync run. +const TEMPLATES_DIR = path.join(pkgRoot, 'src', 'templates'); +const bundledTemplates = fs + .readdirSync(TEMPLATES_DIR, { withFileTypes: true }) + .filter((e) => e.isDirectory() && e.name !== 'node_modules' && e.name !== 'dist') + .map((e) => e.name) + .sort(); + +describe('bundled template declared version surfaces', () => { + // Vacuous-green guard: describe.each over an empty list is a silent pass, and + // "the templates directory moved" must not read as "every template is clean". + it('discovers at least one bundled template', () => { + expect( + bundledTemplates.length, + `no template directories under ${path.relative(pkgRoot, TEMPLATES_DIR)} — ` + + 'the per-template ratchets below would all pass vacuously', + ).toBeGreaterThan(0); + }); + + describe.each(bundledTemplates)('%s', (template) => { + const templateDir = path.join(TEMPLATES_DIR, template); + const readTemplateFile = (name: string) => + fs.readFileSync(path.join(templateDir, name), 'utf8'); + + it('package.json pins every @objectstack/* dep to the current major', () => { + const templatePkg = JSON.parse(readTemplateFile('package.json')); + const allDeps = { ...templatePkg.dependencies, ...templatePkg.devDependencies }; + const stackDeps = Object.entries(allDeps).filter(([name]) => + name.startsWith('@objectstack/'), + ); + expect(stackDeps.length).toBeGreaterThan(0); + for (const [name, range] of stackDeps) { + const match = /^\^(\d+)\./.exec(String(range)); + expect(match, `${name} range "${range}" must be ^.x`).not.toBeNull(); + expect( + Number(match![1]), + `${name} pins ^${match![1]}.x but create-objectstack is v${ownMajor} — ` + + 'bump the template with the release (scaffold-time sync only fixes ' + + 'generated projects, not this committed baseline)', + ).toBe(ownMajor); + } + }); + + // NOTE the file: this stamp lives in `objectstack.config.ts`, inside the + // `defineStack({ manifest: … })` literal. It is NOT in + // `objectstack.manifest.json` — the two were conflated in this suite's own + // naming and in the sync script's log strings, and that conflation is part + // of how the sibling key below went unwatched for eleven majors. + it("objectstack.config.ts stamps engines.protocol at the scaffolder's major (ADR-0087 D1)", () => { + const config = readTemplateFile('objectstack.config.ts'); + const match = /engines:\s*\{\s*protocol:\s*'\^(\d+)'\s*\}/.exec(config); + expect( + match, + `${template}/objectstack.config.ts must stamp engines.protocol (ADR-0087 D1)`, + ).not.toBeNull(); expect( Number(match![1]), - `${name} pins ^${match![1]}.x but create-objectstack is v${ownMajor} — ` + - 'bump the template with the release (scaffold-time sync only fixes ' + - 'generated projects, not this committed baseline)', + `${template} stamps engines.protocol '^${match![1]}' but create-objectstack is v${ownMajor} — ` + + 'scripts/sync-template-versions.mjs re-stamps this at version time; keep them in lockstep', ).toBe(ownMajor); - } - }); -}); + }); -describe('blank template manifest engines.protocol (ADR-0087 D1)', () => { - it('stamps the current protocol major so the handshake covers fresh scaffolds', () => { - const config = fs.readFileSync( - path.join(pkgRoot, 'src', 'templates', 'blank', 'objectstack.config.ts'), - 'utf8', - ); - const match = /engines:\s*\{\s*protocol:\s*'\^(\d+)'\s*\}/.exec(config); - expect(match, 'template manifest must stamp engines.protocol (ADR-0087 D1)').not.toBeNull(); - expect( - Number(match![1]), - `template stamps engines.protocol '^${match![1]}' but create-objectstack is v${ownMajor} — ` + - 'scripts/sync-template-versions.mjs re-stamps this at version time; keep them in lockstep', - ).toBe(ownMajor); + // The key #9264 is about. Required by TemplateManifestSchema, read by the + // template registry, and copied verbatim into every scaffolded project — + // `create-objectstack` rewrites name/displayName/namespace and drops + // description, and has never touched this one. + it('objectstack.manifest.json declares specVersion at the current @objectstack/spec range', () => { + const manifest = JSON.parse(readTemplateFile('objectstack.manifest.json')); + expect( + typeof manifest.specVersion, + `${template}/objectstack.manifest.json must declare specVersion — it is REQUIRED by ` + + 'TemplateManifestSchema (packages/spec/src/cloud/template-manifest.zod.ts)', + ).toBe('string'); + + const match = /^\^(\d+)\.\d+\.\d+$/.exec(manifest.specVersion); + expect( + match, + `specVersion "${manifest.specVersion}" must be a ^.0.0 package range — it is the ` + + 'compatible @objectstack/spec range, not the protocol major that engines.protocol carries', + ).not.toBeNull(); + expect( + Number(match![1]), + `${template} declares specVersion "${manifest.specVersion}" but create-objectstack is ` + + `v${ownMajor} — scripts/sync-template-versions.mjs re-stamps this at version time`, + ).toBe(ownMajor); + }); + + // The invariant that makes the two files one fact rather than two: the + // manifest's declared spec range and the dependency a scaffolded project + // actually installs must agree. Either alone can be self-consistently + // stale; only comparing them catches a stamp that covered one and not the + // other, which is the exact failure this card is about. + it('specVersion agrees with the @objectstack/spec dependency the template installs', () => { + const manifest = JSON.parse(readTemplateFile('objectstack.manifest.json')); + const templatePkg = JSON.parse(readTemplateFile('package.json')); + const specDep = + templatePkg.dependencies?.['@objectstack/spec'] ?? + templatePkg.devDependencies?.['@objectstack/spec']; + + expect( + specDep, + `${template}/package.json must depend on @objectstack/spec for its manifest's ` + + 'specVersion to be checkable against something', + ).toBeDefined(); + expect( + manifest.specVersion, + `${template} declares specVersion "${manifest.specVersion}" but installs ` + + `@objectstack/spec "${specDep}" — one fact written twice, and they disagree`, + ).toBe(specDep); + }); }); }); diff --git a/packages/create-objectstack/src/templates/blank/objectstack.manifest.json b/packages/create-objectstack/src/templates/blank/objectstack.manifest.json index bc0e00e002..7ce5cc7f1c 100644 --- a/packages/create-objectstack/src/templates/blank/objectstack.manifest.json +++ b/packages/create-objectstack/src/templates/blank/objectstack.manifest.json @@ -2,7 +2,7 @@ "$schema": "https://schemas.objectstack.dev/template-manifest.json", "name": "blank", "namespace": "blank", - "specVersion": "^6.0.0", + "specVersion": "^17.0.0", "displayName": "Blank Starter", "description": "Minimal ObjectStack environment with a single object — a clean slate for building.", "category": "starter", diff --git a/scripts/check-cross-package-test-inputs.mjs b/scripts/check-cross-package-test-inputs.mjs index b52870dd7c..c6a0374cdc 100644 --- a/scripts/check-cross-package-test-inputs.mjs +++ b/scripts/check-cross-package-test-inputs.mjs @@ -276,7 +276,17 @@ const CROSS_PACKAGE_TEST_INPUTS = { 'create-objectstack': { // src/template-consistency.test.ts reads doc frontmatter by repo-relative // path to decide which templates are internal. - globs: ['content/**'], + // + // `sync-template-versions.mjs` is named in a comment rather than read, the + // same shape as `check-nul-bytes.mjs` above and settled the same way: a + // mention forces a declaration, and declaring the file is cheaper than + // rewording prose to dodge the scanner. Here the coupling is real on top of + // being cheap — that script STAMPS the three per-template version surfaces + // (`package.json` @objectstack/* ranges, `objectstack.config.ts` + // `engines.protocol`, `objectstack.manifest.json` `specVersion`) that the + // ratchets in that test assert, so a change to the stamper is exactly the + // change those ratchets exist to catch (#9264). + globs: ['content/**', 'scripts/sync-template-versions.mjs'], }, }; diff --git a/scripts/sync-template-versions.mjs b/scripts/sync-template-versions.mjs index f5af6fd2be..122acd4f22 100644 --- a/scripts/sync-template-versions.mjs +++ b/scripts/sync-template-versions.mjs @@ -1,75 +1,233 @@ // Copyright (c) 2026 ObjectStack contributors. Apache-2.0 license. // -// Re-sync the create-objectstack blank template's @objectstack/* dependency -// ranges with the scaffolder's own package version. Runs as part of the root -// `version` script (changesets/action calls `pnpm run version` when preparing -// the release PR), so a version bump can never ship with the template pinning -// a stale range — the drift class behind #2907: the template froze at ^6.0.0 -// while the registry published 14.x, and every fresh `npm create objectstack` -// project landed eight majors behind the docs. The scaffold-time dep rewrite -// (pkg-utils.ts) and the ratchet test (template-consistency.test.ts) both -// guard this too, but release PRs opened by changesets/action with the default -// GITHUB_TOKEN do not trigger CI, so fixing the file at version time is the -// only spot that cannot be skipped. - -import { readFileSync, writeFileSync } from 'node:fs'; +// Re-sync every bundled create-objectstack template's declared version surfaces +// with the scaffolder's own package version. Runs as part of the root `version` +// script (changesets/action calls `pnpm run version` when preparing the release +// PR), so a version bump can never ship with a template pinning a stale range — +// the drift class behind #2907: the template froze at ^6.0.0 while the registry +// published 14.x, and every fresh `npm create objectstack` project landed eight +// majors behind the docs. The scaffold-time dep rewrite (pkg-utils.ts) and the +// ratchet tests (template-consistency.test.ts) both guard this too, but release +// PRs opened by changesets/action with the default GITHUB_TOKEN do not trigger +// CI, so fixing the files at version time is the only spot that cannot be +// skipped. +// +// THREE SURFACES, NOT ONE, AND THE MISSING ONE IS WHY (#9264). This script used +// to stamp two keys in two hard-coded `blank/` paths and never opened +// `objectstack.manifest.json` at all. So `specVersion` — a REQUIRED field of +// TemplateManifestSchema, copied verbatim into every scaffolded project by +// `create-objectstack` — sat at `^6.0.0` while the config's `engines.protocol` +// tracked every major up to ^17. Eleven majors of drift, and a green +// `sync-template-versions` run was never evidence about it: the script's failure +// mode was loud for the keys it covered and MUTE for the key it did not. +// +// Two structural consequences, both deliberate: +// +// * the file list is DISCOVERED, never hard-coded. Templates are found by +// walking `src/templates/`, the same way `check-template-manifests.ts` +// finds the manifests it parses, and for the same stated reason: a template +// added tomorrow is covered on the day it lands, without anyone remembering +// this script exists. One-key-one-file coverage is what let #9264 sit. +// * every stamp is REQUIRED. A template whose file is missing, or whose file +// carries no stamp to sync, is a hard failure naming the path — never a +// skip. A silent skip is indistinguishable from a green run, which is +// precisely the invisibility this card was filed about. +// +// TWO VALUES, TWO MEANINGS — do not collapse them. `engines.protocol` is the +// ADR-0087 D1 runtime handshake range and carries the PROTOCOL major (`^17`). +// `specVersion` is documented by TemplateManifestSchema as the "Compatible +// @objectstack/spec semver range" and carries the PACKAGE range (`^17.0.0`) — +// the same value this script writes into the template's own +// `@objectstack/spec` dependency, so the manifest and the package.json state +// one fact once. They agree on the major today only because the spec package's +// major and the protocol major are kept in lockstep; they are still two +// different declarations and are stamped from two different values. + +import { readFileSync, writeFileSync, readdirSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; -import { dirname, join } from 'node:path'; +import { dirname, join, relative } from 'node:path'; const root = dirname(dirname(fileURLToPath(import.meta.url))); const scaffolderPkgPath = join(root, 'packages/create-objectstack/package.json'); -const templatePkgPath = join( - root, - 'packages/create-objectstack/src/templates/blank/package.json', -); + +/** + * Where the bundled templates live. A DIRECTORY, walked, never a hand-kept + * list — see the header. Mirrors `check-template-manifests.ts`'s TEMPLATE_ROOT. + */ +const TEMPLATE_ROOT = join(root, 'packages/create-objectstack/src/templates'); + +const rel = (p) => relative(root, p); const version = JSON.parse(readFileSync(scaffolderPkgPath, 'utf8')).version; if (!/^\d+\.\d+\.\d+/.test(String(version))) { console.error(`✗ sync-template-versions: cannot parse create-objectstack version '${version}'`); process.exit(1); } -const range = `^${String(version).split('.')[0]}.0.0`; - -const templatePkg = JSON.parse(readFileSync(templatePkgPath, 'utf8')); -let changed = 0; -for (const deps of [templatePkg.dependencies, templatePkg.devDependencies]) { - if (!deps) continue; - for (const dep of Object.keys(deps)) { - if (dep.startsWith('@objectstack/') && deps[dep] !== range) { - console.log(` ${dep}: ${deps[dep]} → ${range}`); - deps[dep] = range; - changed++; +const major = String(version).split('.')[0]; +/** The `@objectstack/*` package range: dependencies AND the manifest's `specVersion`. */ +const range = `^${major}.0.0`; + +/** + * The text stamps, as a table over (file, key, pattern, replacement). + * + * Table-driven rather than one block per key so that adding a fourth declared + * version surface is a row, and so that all of them share ONE failure contract + * — the shape whose absence let `specVersion` drift unnoticed (#9264). + * + * Rewritten as TEXT, not parse/re-serialize, and that matters for the manifest: + * `objectstack.manifest.json` keeps `scaffold.variables` compact on one line, so + * `JSON.stringify(…, null, 2)` would reformat 42 bytes of unrelated structure + * on every release. A targeted replace touches the value and nothing else. + */ +const TEXT_STAMPS = [ + { + file: 'objectstack.config.ts', + key: 'engines.protocol', + value: `^${major}`, + // ADR-0087 D1 — the runtime refuses an incompatible package at the boundary + // with the exact migration command. Scaffolds populate it by default; this + // is the ratchet that closes grandfathering. + pattern: /engines:\s*\{\s*protocol:\s*'[^']*'\s*\}/, + replacement: `engines: { protocol: '^${major}' }`, + }, + { + file: 'objectstack.manifest.json', + key: 'specVersion', + value: range, + // Required by TemplateManifestSchema and read by the template registry; + // `create-objectstack` copies it verbatim into every scaffolded project + // (it rewrites name/displayName/namespace and drops description, and has + // never touched this key), so a stale value ships to real users. + pattern: /("specVersion"\s*:\s*)"[^"]*"/, + replacement: `$1"${range}"`, + }, +]; + +/** Every bundled template directory, sorted. Deliberately not a curated list. */ +function findTemplateDirs() { + return readdirSync(TEMPLATE_ROOT, { withFileTypes: true }) + .filter((e) => e.isDirectory() && e.name !== 'node_modules' && e.name !== 'dist') + .map((e) => e.name) + .sort(); +} + +const templates = findTemplateDirs(); + +// Vacuous-green guard, same rationale as check-template-manifests.ts: zero +// templates is far more likely to mean "the directory moved" than "we ship no +// templates", and a run that stamped nothing must not report success. +if (templates.length === 0) { + console.error( + `✗ sync-template-versions: no template directories under ${rel(TEMPLATE_ROOT)}.\n` + + ' Every release stamps at least one bundled template, so this is almost certainly a\n' + + ' moved directory rather than an empty one. A sync that rewrote nothing must not pass.', + ); + process.exit(1); +} + +/** Problems are collected so one run names every unstamped file, not just the first. */ +const problems = []; + +for (const template of templates) { + // ── package.json: every @objectstack/* range → the package range ────────── + const templatePkgPath = join(TEMPLATE_ROOT, template, 'package.json'); + let templatePkg; + try { + templatePkg = JSON.parse(readFileSync(templatePkgPath, 'utf8')); + } catch (err) { + problems.push( + `${rel(templatePkgPath)} could not be read as JSON (${err.message}). Every bundled ` + + 'template ships a package.json pinning the @objectstack/* ranges a scaffolded project installs.', + ); + templatePkg = null; + } + + if (templatePkg) { + let stackDeps = 0; + let changed = 0; + for (const deps of [templatePkg.dependencies, templatePkg.devDependencies]) { + if (!deps) continue; + for (const dep of Object.keys(deps)) { + if (!dep.startsWith('@objectstack/')) continue; + stackDeps++; + if (deps[dep] !== range) { + console.log(` ${template}/package.json ${dep}: ${deps[dep]} → ${range}`); + deps[dep] = range; + changed++; + } + } + } + + if (stackDeps === 0) { + // Zero matches is the silent-skip shape this script exists to refuse: it + // reads exactly like "already in lockstep" and means the opposite. + problems.push( + `${rel(templatePkgPath)} declares no @objectstack/* dependency, so nothing was synced. ` + + 'A bundled template installs the platform it scaffolds against — add the deps, or drop ' + + 'the template directory.', + ); + } else if (changed === 0) { + console.log( + `✓ ${template}/package.json already pins ${range} across ${stackDeps} @objectstack/* dep(s)` + + ` — in lockstep with create-objectstack@${version}`, + ); + } else { + writeFileSync(templatePkgPath, JSON.stringify(templatePkg, null, 2) + '\n'); + console.log( + `✓ ${template}/package.json: ${changed} of ${stackDeps} @objectstack/* range(s) → ${range}` + + ` (lockstep with create-objectstack@${version})`, + ); + } + } + + // ── the text stamps ─────────────────────────────────────────────────────── + for (const stamp of TEXT_STAMPS) { + const path = join(TEMPLATE_ROOT, template, stamp.file); + let src; + try { + src = readFileSync(path, 'utf8'); + } catch (err) { + problems.push( + `${rel(path)} could not be read (${err.message}), so ${stamp.key} was not synced. ` + + 'Every bundled template declares it; a template that genuinely should not be stamped ' + + 'is a decision to record in TEXT_STAMPS, not a file to skip.', + ); + continue; + } + + // Absence is a hard failure, never a skip — the #9264 lesson. Tested before + // the replace so "key missing" and "value already correct" stay distinct: + // both produce an unchanged string, and only one of them is fine. + if (!stamp.pattern.test(src)) { + problems.push( + `${rel(path)} has no ${stamp.key} stamp to sync (expected to match ${stamp.pattern}). ` + + `It should declare ${stamp.key} = ${stamp.value}.`, + ); + continue; + } + + const stamped = src.replace(stamp.pattern, stamp.replacement); + if (stamped === src) { + console.log(`✓ ${template}/${stamp.file} already stamps ${stamp.key} '${stamp.value}'`); + } else { + writeFileSync(path, stamped); + console.log(`✓ ${template}/${stamp.file}: ${stamp.key} → '${stamp.value}'`); } } } -if (changed === 0) { - console.log(`✓ blank template already pins ${range} — in lockstep with create-objectstack@${version}`); -} else { - writeFileSync(templatePkgPath, JSON.stringify(templatePkg, null, 2) + '\n'); - console.log(`✓ blank template: ${changed} @objectstack/* range(s) → ${range} (lockstep with create-objectstack@${version})`); +if (problems.length > 0) { + console.error(`\n✗ sync-template-versions: ${problems.length} unstamped surface(s).\n`); + for (const p of problems) console.error(` • ${p}`); + console.error( + '\n Each of these is a declared version surface that would have shipped stale. The script\n' + + ' fails rather than skipping, because a skipped stamp is indistinguishable from a synced\n' + + ' one in the log — which is how specVersion drifted eleven majors (#9264).\n', + ); + process.exit(1); } -// Also re-stamp the manifest's `engines.protocol` range in the template stack -// config (ADR-0087 D1 — scaffolds populate the handshake field by default, the -// ratchet that closes grandfathering). Same lockstep rationale as above. -const templateConfigPath = join( - root, - 'packages/create-objectstack/src/templates/blank/objectstack.config.ts', +console.log( + `\n✓ sync-template-versions: ${templates.length} template(s) in lockstep with ` + + `create-objectstack@${version} — deps and specVersion at ${range}, engines.protocol at '^${major}'.`, ); -const major = String(version).split('.')[0]; -const config = readFileSync(templateConfigPath, 'utf8'); -const stamped = config.replace( - /engines:\s*\{\s*protocol:\s*'[^']*'\s*\}/, - `engines: { protocol: '^${major}' }`, -); -if (stamped === config) { - if (!config.includes(`engines: { protocol: '^${major}' }`)) { - console.error('✗ sync-template-versions: template objectstack.config.ts has no engines.protocol stamp to sync'); - process.exit(1); - } - console.log(`✓ blank template manifest already stamps engines.protocol '^${major}'`); -} else { - writeFileSync(templateConfigPath, stamped); - console.log(`✓ blank template manifest: engines.protocol → '^${major}'`); -} diff --git a/turbo.json b/turbo.json index 62d0178b01..cbff026e2d 100644 --- a/turbo.json +++ b/turbo.json @@ -188,7 +188,8 @@ "!dist/**", "!coverage/**", "!.turbo/**", - "$TURBO_ROOT$/content/**" + "$TURBO_ROOT$/content/**", + "$TURBO_ROOT$/scripts/sync-template-versions.mjs" ] }, "test:e2e": {