refactor: cut over to the Astro build
Merges refactor/task-20-cutover. Task 20 steps 1, 2, and 5; publishing is
not included.
The hand-written site is gone: 32 files deleted, including app.js,
responsive.css, and all ten route index.html files. Twelve more could not
be deleted -- the Astro pages import them and the build fails without
them -- so they moved to legacy/ verbatim, outside the reach of
check-tokens.mjs, which sweeps src/ and would demand a token migration
these files have not had.
Before anything was deleted, rendered-text-diff swept all ten routes plus
both Portuguese pages at full parity, 0 missing and 0 extra. That
comparison stops being possible once the legacy files are gone, which is
why it ran first. computed-style-diff on /full-guide/ is unchanged at 32.
verify.mjs no longer reads app.js and holds at 84 assertions.
audit-ui.mjs reads dist/. Docs across README, AGENTS.md, GATES.md, the
architecture context, and the operations guide now describe the built
site rather than the hand-written one.
origin/pages is unchanged at 37a1e480c6.
The publish job is still gated to manual dispatch.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+20
-13
@@ -1,18 +1,20 @@
|
||||
import { readFileSync, readdirSync, statSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
|
||||
const base = '/ai-for-dummies/';
|
||||
const read = (path) => readFileSync(new URL(`../${path}`, import.meta.url), 'utf8');
|
||||
// The legacy hand-written pages are gone; the built output is the site now.
|
||||
const pages = [
|
||||
'index.html',
|
||||
'full-guide/index.html',
|
||||
'summary/index.html',
|
||||
'models/index.html',
|
||||
'agents/index.html',
|
||||
'skills/index.html',
|
||||
'rules/index.html',
|
||||
'skills-review/index.html',
|
||||
'hands-on/starter/index.html',
|
||||
'hands-on/rules/index.html',
|
||||
'dist/index.html',
|
||||
'dist/full-guide/index.html',
|
||||
'dist/summary/index.html',
|
||||
'dist/models/index.html',
|
||||
'dist/agents/index.html',
|
||||
'dist/skills/index.html',
|
||||
'dist/rules/index.html',
|
||||
'dist/skills-review/index.html',
|
||||
'dist/hands-on/starter/index.html',
|
||||
'dist/hands-on/rules/index.html',
|
||||
];
|
||||
const walk = (path) =>
|
||||
readdirSync(path).flatMap((name) => {
|
||||
@@ -61,8 +63,13 @@ for (const [kind, values] of Object.entries(baselineValues)) {
|
||||
const missing = values.filter((value) => !builtValues[kind].has(value));
|
||||
if (missing.length) throw new Error(`built CSS lost ${kind}: ${missing.join(', ')}`);
|
||||
}
|
||||
// Legacy fixtures still ship directly. Resolve every var() from the stylesheets
|
||||
// that page actually links; Astro's tokens cannot mask a broken standalone page.
|
||||
// The hands-on labs still ship their own stylesheet rather than going through
|
||||
// Astro. Resolve every var() from the stylesheets each page actually links, so
|
||||
// Astro's tokens cannot mask a broken standalone page.
|
||||
// Astro emits base-absolute hrefs (`/ai-for-dummies/_astro/x.css`); those are
|
||||
// rooted at `dist/`, not at the page's own directory.
|
||||
const resolveHref = (page, href) =>
|
||||
href.startsWith(base) ? join('dist', href.slice(base.length)) : join(dirname(page), href);
|
||||
for (const page of pages) {
|
||||
const html = read(page);
|
||||
const linked = [...html.matchAll(/<link[^>]+rel="stylesheet"[^>]+href="([^"]+)"/gi)].map(
|
||||
@@ -70,7 +77,7 @@ for (const page of pages) {
|
||||
);
|
||||
const styles = linked
|
||||
.filter((href) => !/^https?:/i.test(href))
|
||||
.map((href) => read(join(dirname(page), href)));
|
||||
.map((href) => read(resolveHref(page, href)));
|
||||
const defined = new Set(styles.flatMap((css) => [...definitions(css)]));
|
||||
const unresolved = new Set(
|
||||
styles.flatMap((css) =>
|
||||
|
||||
+10
-26
@@ -46,7 +46,7 @@ const source = {
|
||||
read('src/content/providers/claude.json'),
|
||||
read('src/content/providers/gemini.json'),
|
||||
].join('\n'),
|
||||
starter: read('hands-on/starter/app.js'),
|
||||
starter: read('public/hands-on/starter/app.js'),
|
||||
voteService: read('vote-service/main.go'),
|
||||
ndoReview: read('src/content/reviews/ndo-repro.md'),
|
||||
};
|
||||
@@ -73,32 +73,16 @@ if (rendered(html.starter) !== snapshot('hands-on/starter.txt'))
|
||||
if (rendered(html.labRules) !== snapshot('hands-on/rules.txt'))
|
||||
throw new Error('rules lab rendered-text snapshot changed');
|
||||
|
||||
const braceMatch = (source, open) => {
|
||||
let depth = 0;
|
||||
let quote = '';
|
||||
let escaped = false;
|
||||
for (let index = open; index < source.length; index += 1) {
|
||||
const character = source[index];
|
||||
if (quote) {
|
||||
if (escaped) escaped = false;
|
||||
else if (character === '\\') escaped = true;
|
||||
else if (character === quote) quote = '';
|
||||
continue;
|
||||
}
|
||||
if (character === "'" || character === '"' || character === '`') quote = character;
|
||||
else if (character === '{') depth += 1;
|
||||
else if (character === '}' && --depth === 0) return index;
|
||||
}
|
||||
throw new Error('could not brace-match legacy Portuguese translations');
|
||||
};
|
||||
const legacyGuide = read('app.js');
|
||||
const translationsStart = legacyGuide.indexOf('const translations =');
|
||||
const objectStart = legacyGuide.indexOf('{', translationsStart);
|
||||
const objectEnd = braceMatch(legacyGuide, objectStart);
|
||||
const translations = Function(`return (${legacyGuide.slice(objectStart, objectEnd + 1)})`)();
|
||||
const portuguese = Object.values(translations.pt).map(rendered);
|
||||
// The 102 Portuguese strings were extracted verbatim from the legacy
|
||||
// `app.js` `translations.pt` object at cutover, before that file was deleted.
|
||||
// This is a legacy capture, not a snapshot of the Astro build: it still asserts
|
||||
// against an independent source, which is the whole point of the check.
|
||||
const portuguese = JSON.parse(read('.agents/snapshots/full-guide-pt.json')).map(rendered);
|
||||
if (portuguese.length !== 102)
|
||||
throw new Error('full-guide Portuguese source no longer has 102 translated strings');
|
||||
throw new Error('full-guide Portuguese snapshot no longer has 102 translated strings');
|
||||
// Without this, trimming the snapshot would make the check below pass vacuously.
|
||||
if (portuguese.some((value) => !value.trim()))
|
||||
throw new Error('full-guide Portuguese snapshot has an empty entry');
|
||||
if (!portuguese.every((value) => rendered(html.guide).includes(value)))
|
||||
throw new Error('built full-guide lost a Portuguese translation');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user