Files
Claude 057162e45b 🔖 fix: sync package-lock, and stamp the version fallbacks from the build
Junie's review on #16 caught the lock, and it was worse than reported:
package-lock.json said 0.3.0, so it had already missed the 0.4.0 release. It
does not ship (npm excludes it from tarballs, and `files` lists only dist, src,
FRAMEWORKS.md and CHANGELOG.md) and `npm ci` never complained because it only
diffs dependencies, not the project's own version — which is exactly why it
drifted two releases without anyone noticing. Regenerated with
`npm install --package-lock-only`.

The other half was the two version strings in docs/index.html: the brand badge
and the "It is still 0.x" line. Both are no-JavaScript fallbacks — page.js
overwrites them from meta.js — so they are right in a browser and stale in a
text reader or a scraper. Bumping them by hand is what failed on 0.4.0 and
again here, and it is the "release facts hand-bumped beside their generator"
finding from the code review.

So build-docs.mjs now stamps them, next to the meta.js it already writes. The
docs-sync gate turns a forgotten bump into a CI failure instead of a review
comment. Both regexes are asserted: renaming the markup fails the build with
the pattern that stopped matching, rather than silently stamping nothing —
verified by renaming the id and watching it exit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAcqrz3FcadkYr3xG32CjK
2026-08-20 16:34:49 +00:00

110 lines
5.2 KiB
JavaScript

/**
* Builds the assets the landing page needs, into docs/.
*
* The page is served by GitHub Pages straight from the repository, so everything it loads has
* to be committed — there is no build step on the hosting side. Everything it loads is also
* local: the previous page pulled Tailwind, CodeMirror and Babel from three CDNs, and its
* playground died silently when the unpinned `@babel/standalone` URL rolled over to Babel 8
* and the plugin list it passed stopped existing. Vendoring the compiler pins it to the
* version in package.json and to a lockfile.
*/
import { build } from 'esbuild';
import { copyFile, mkdir, readFile, writeFile, stat } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import path from 'node:path';
import { collectDiagnostics } from './diagnostics.mjs';
const root = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
const docs = path.join(root, 'docs');
const vendor = path.join(docs, 'vendor');
const size = async (file) => {
const { size: bytes } = await stat(file);
return `${(bytes / 1024).toFixed(0)} KB`;
};
await mkdir(vendor, { recursive: true });
const pkg = JSON.parse(await readFile(path.join(root, 'package.json'), 'utf8'));
// 1. The library itself, as a browser global the playground can pull names out of.
await build({
entryPoints: [path.join(root, 'src/index.ts')],
bundle: true,
format: 'iife',
globalName: 'Cereale',
minify: true,
target: 'es2022',
tsconfigRaw: {
compilerOptions: { experimentalDecorators: false, useDefineForClassFields: true, target: 'es2022' },
},
outfile: path.join(docs, 'cereale.js'),
});
// 2. Facts the page would otherwise hard-code and then get wrong. Everything else it needs —
// the decorator count, the export list — it derives from the bundle at runtime.
await writeFile(
path.join(docs, 'meta.js'),
`// Generated by scripts/build-docs.mjs — do not edit.\n` +
`window.CEREALE_META = ${JSON.stringify({ version: pkg.version, node: pkg.engines.node }, null, 2)};\n`
);
// 2b. The same version, stamped into the two spots in index.html that page.js later
// overwrites from meta.js. Those are the no-JavaScript fallbacks: correct in a browser,
// stale in a text reader or a scraper, and hand-bumped until now — they went stale on
// 0.4.0 and again on 0.4.1. Stamping them here means the docs-sync gate catches the
// drift instead of a reviewer. The regexes are asserted, so if the markup is renamed
// the build fails loudly rather than silently stamping nothing.
const indexPath = path.join(docs, 'index.html');
let index = await readFile(indexPath, 'utf8');
const stamps = [
[/(<span class="badge" id="version-badge">)v[\d.]+(<\/span>)/, `$1v${pkg.version}$2`],
[/(<span class="js-version">)[\d.]+(<\/span>)/g, `$1${pkg.version}$2`],
];
for (const [re, replacement] of stamps) {
if (!re.test(index)) {
console.error(`build-docs: nothing in docs/index.html matched ${re} — the version fallback markup moved.`);
process.exit(1);
}
index = index.replace(re, replacement);
}
await writeFile(indexPath, index);
// 3. The compiler errors the page quotes, produced by actually running the compiler. If a
// snippet the page calls a compile error ever compiles, this fails the build.
const { byCase, problems } = await collectDiagnostics(root);
if (problems.length > 0) {
console.error('The landing page makes a claim the compiler does not support:\n' +
problems.map((p) => ` - ${p}`).join('\n'));
process.exit(1);
}
await writeFile(
path.join(docs, 'diagnostics.js'),
`// Generated by scripts/build-docs.mjs from real tsc output — do not edit.\n` +
`window.CEREALE_DIAGNOSTICS = ${JSON.stringify(byCase, null, 2)};\n`
);
// 4. The playground's TypeScript compiler, pinned by package.json rather than by a CDN URL.
const babel = path.join(root, 'node_modules/@babel/standalone/babel.min.js');
await copyFile(babel, path.join(vendor, 'babel.min.js'));
await writeFile(
path.join(vendor, 'README.md'),
`# Vendored assets\n\n` +
`Generated by \`npm run build:docs\`. Do not edit by hand.\n\n` +
`- \`babel.min.js\` — @babel/standalone ${JSON.parse(await readFile(path.join(root, 'node_modules/@babel/standalone/package.json'), 'utf8')).version}, ` +
`used by the playground to compile TypeScript with standard decorators in the browser. ` +
`Vendored rather than loaded from a CDN: the previous page referenced an unpinned unpkg URL, ` +
`which silently began serving Babel 8 and broke the playground.\n\n` +
`- \`../.nojekyll\` — opts the directory out of Jekyll, so GitHub Pages serves it verbatim. ` +
`Jekyll ignores paths beginning with an underscore and carries default \`vendor/\` exclusions, ` +
`and the failure mode is an asset that silently does not publish — for this page, the ` +
`playground's compiler 404ing while everything else looks fine.\n`
);
// 5. Written rather than committed by hand so it cannot be lost in a docs/ rewrite.
await writeFile(path.join(docs, '.nojekyll'), '');
console.log(`docs/cereale.js ${await size(path.join(docs, 'cereale.js'))}`);
console.log(`docs/meta.js ${await size(path.join(docs, 'meta.js'))} (v${pkg.version})`);
console.log(`docs/vendor/babel.min.js ${await size(path.join(vendor, 'babel.min.js'))}`);