feat: scaffold the repository from the lecture theme (Phase 0) - #12
Conversation
Copies the build, test and release machinery from QuantEcon/quantecon-theme.mystmd at 47e37ed885a40afb4fa42dd27ad458357616f68e and adapts it here. Decision D1 makes this repository a self-contained sibling of the lecture theme rather than a package sharing its code, so the scaffold is copied and then owned here. Copied and renamed: package `@quantecon/report-theme` at 0.1.0, zip `quantecon-theme-report.zip`, `template.yml` title "QuantEcon Report Theme", the Makefile THEME variable, and the release asset names. No occurrence of `@quantecon/lecture-theme` or "QuantEcon Lecture Theme" survives the copy, and a unit test guards that. Adapted, with the reasons in the changed files: - `release.yml` attaches two assets per tag — the theme zip and `plugins/compliance.mjs`. The plugin is built in Phase 1b (#5), so a dedicated step reports its absence as a warning rather than failing, and says which issue makes it real. - `preview.yml` targets `compliance-lecture-style`'s `mystmd` branch, probing for it with `git ls-remote` on every run and falling back to the local visual fixture until the branch exists. - No live-compute stack: `@myst-theme/jupyter`, the Thebe asset copy in `prod:build` and all three `patches/` entries exist to make executable notebook cells work, and report sites have no code cells. - `theme.spec.ts` asserts structure rather than pixels — there is no report UI to baseline until Phase 2 (#6) — so the `visual` job is green from the first PR. The platform-suffixed baselines and `/update-snapshots` plumbing are in place for the Phase 3 harness (#9). - The FOUC guard keeps the grid and font assertions and drops the nav-panel one, which returns with the sidebar in Phase 2. Verified locally on node v26.8.1 (the .nvmrc pins 24, which CI uses): `npm install`, `npm run compile`, `npm run test:unit`, `npm run prod:build` and `make build-zip` all succeed; the zip contains a `template.yml` stamped 0.1.0 from package.json and no node_modules; and `myst start` serves the fixture through the built bundle with no warnings. Part of #3 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
🎭 Visual regression resultsDetails
|
There was a problem hiding this comment.
🟡 Changes recommended
The Vercel server entrypoint uses ESM syntax without the repository being configured as ESM, and the template install/engine settings are inconsistent with the repo’s documented/CI expectations.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Scaffolds quantecon-theme-report.mystmd as a self-contained sibling of the lecture theme by copying/adapting the build, test, preview, and release machinery plus a minimal Remix/MyST theme implementation so CI and consumers have a working baseline.
Changes:
- Adds Remix theme runtime (routes, loaders, root document + critical CSS) and Tailwind styling.
- Introduces CI/release/preview/update-snapshots GitHub Actions workflows and Playwright-based rendering/FOUC guards.
- Adds release-bundle template (
template/,template.yml, Makefile) plus unit tests guarding version/name stamping and scaffold renames.
File summaries
| File | Description |
|---|---|
| tsconfig.json | TypeScript configuration for the scaffold |
| package.json | Project deps/scripts/overrides/engines |
| server.js | Vercel adapter request handler entrypoint |
| remix.env.d.ts | Remix type references |
| remix.config.dev.js | Dev Remix config and bundling rules |
| remix.config.prod.js | Prod Remix config (publicPath/build dirs) |
| tailwind.config.js | Tailwind theme extensions + font stack |
| styles/app.css | Tailwind entry + grid/base styles |
| scripts/relative-css-asset-urls.mjs | Rewrites built CSS asset URLs to be relative |
| app/root.tsx | Document + critical CSS + theme session integration |
| app/links.ts | Self-hosted KaTeX + Source Sans 3 stylesheet links |
| app/entry.client.tsx | Client hydration entry |
| app/entry.server.tsx | Server render entry |
| app/backend/loaders.server.ts | Server-side loaders for MyST config/page JSON |
| app/components/Page.tsx | Page renderer (grid + MyST AST render) |
| app/components/ErrorPage.tsx | Error boundary page rendering |
| app/routes/_index.tsx | Index route loader/meta/links |
| app/routes/$.tsx | Catch-all page route loader/meta |
| app/routes/api.theme.tsx | Theme toggle API action passthrough |
| app/routes/myst-theme[.css].ts | Theme CSS loader route |
| app/routes/[favicon.ico].tsx | Favicon loader route |
| app/routes/[robots.txt].tsx | Robots.txt loader route |
| playwright.config.ts | Playwright projects + webServer wiring |
| tests/visual/serve.sh | Fixture server script for Playwright/preview |
| tests/visual/README.md | Visual/FOUC harness documentation |
| tests/visual/theme.spec.ts | Chromium rendering structure assertions |
| tests/visual/fouc.spec.ts | WebKit first-paint/FOUC guard |
| tests/visual/fixture/myst.yml.in | Fixture MyST config template |
| tests/visual/fixture/intro.md | Fixture landing page content |
| tests/visual/fixture/series.md | Fixture secondary page content |
| tests/unit/template-stamp.test.mjs | Unit tests guarding stamping/rename invariants |
| template.yml | Theme bundle manifest for MyST |
| template/package.json | Shipped theme runtime package manifest |
| template/server.js | Shipped Express server entry for the bundle |
| template/README.md | Shipped bundle README |
| template/LICENSE | Shipped bundle license |
| template/.gitignore | Shipped bundle gitignore |
| Makefile | Local bundle assembly + zip build targets |
| README.md | Repo overview + development commands |
| CHANGELOG.md | Initial changelog scaffold (Keep a Changelog) |
| CONTRIBUTING.md | Contribution + release process docs |
| SECURITY.md | Security posture + audit triage/overrides rationale |
| .nvmrc | Node version pin |
| .npmrc | legacy-peer-deps rationale for Remix v1 pin |
| .gitignore | Ignores for build outputs/fixtures/reports |
| .eslintrc.js | Remix ESLint config |
| .github/workflows/ci.yml | CI: build/typecheck/unit + Playwright suites |
| .github/workflows/preview.yml | PR preview build/deploy to gh-pages |
| .github/workflows/release.yml | Tag-driven release build + assets publish |
| .github/workflows/update-snapshots.yml | PR comment-driven snapshot refresh |
Review details
- Files reviewed: 46/52 changed files
- Comments generated: 3
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| import { createRequestHandler } from "@remix-run/vercel"; | ||
| import * as build from "@remix-run/dev/server-build"; | ||
|
|
||
| export default createRequestHandler({ build, mode: process.env.NODE_ENV }); |
There was a problem hiding this comment.
The diagnosis is right and the remedy is not, so no change here. This file is never executed. The release workflow assembles the bundle by copying from template/, and the server it ships is template/server.js, which is CommonJS throughout; the root server.js exists only to satisfy @remix-run/vercel, and this repository deploys previews to GitHub Pages rather than Vercel. Converting it to CommonJS would preserve a file that does nothing while keeping the high-severity @vercel/node advisory chain it drags in, which SECURITY.md already documents along with the intention to remove the Vercel adapter with the Phase 2 shell (#6). Deleting the file and the dependency is the fix that pays, and it belongs with that work rather than with a scaffold PR whose value is fidelity to the lecture theme, where this file is byte-identical.
The PR Preview job failed on `Failed to fetch http://localhost:3000/index.json after 3 attempts`. Trimming the app to the routes a live `myst start` needs cut four that only the static export path exercises, so the preview — the one job that runs `myst build --html` — was the first thing to notice. Restores `($project)_.($a).($b).($c).($d).$slug[.json].tsx` (per-page JSON, plus `myst.xref.json` and `myst.search.json`), `[sitemap.xml].tsx`, `[sitemap_style.xsl].tsx` and `[objects.inv].tsx`, with the three loaders they call. `getMystSearchJson` is typed as a plain JSON object rather than `MystSearchIndex`, whose package this theme does not depend on directly. Reproduced and confirmed locally: a `myst build --html` of the fixture against the built bundle now serves 200 for every crawled route and writes both pages, the sitemap, `objects.inv` and the xref/search JSON, with the baseurl applied and the stylesheet asset URLs relative. Part of #3 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
…TING `template/package.json` declared `engines.node` as `>=16` while CONTRIBUTING.md says the built theme supports Node 20 or newer and cites that very field as the evidence. One of the two was wrong; 20 is the honest number, since the repo pins 24 in .nvmrc, requires 20 for the unit tests, and the engine the theme renders under has a higher floor than 16 anyway. Raised by Copilot on #12. The same mismatch exists in the lecture theme this scaffold was copied from and is filed separately there. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…npm ci The release bundle's package.json declared engines.node ">=16" while CONTRIBUTING.md cites that field as evidence for a Node 20 floor; 20 is the honest number (.nvmrc pins 24, the root package.json requires 20, the rendering engine's floor is above 16). And template.yml ran `npm install` although the release asset ships a lockfile (release.yml generates it and `files` lists it), so consumer installs were neither reproducible nor lockfile-preserving. `npm ci` fails without a lockfile, but the only lockfile-less path is a source checkout, which is not a usable template until `make build-theme` has run, and that target installs on its own (#162). The report theme carries the same two lines; its copy of the engines floor was corrected in QuantEcon/quantecon-theme-report.mystmd#12 and the install decision is QuantEcon/quantecon-theme-report.mystmd#13. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…npm ci (#191) * fix(bundle): declare Node >=20 and install the shipped lockfile with npm ci The release bundle's package.json declared engines.node ">=16" while CONTRIBUTING.md cites that field as evidence for a Node 20 floor; 20 is the honest number (.nvmrc pins 24, the root package.json requires 20, the rendering engine's floor is above 16). And template.yml ran `npm install` although the release asset ships a lockfile (release.yml generates it and `files` lists it), so consumer installs were neither reproducible nor lockfile-preserving. `npm ci` fails without a lockfile, but the only lockfile-less path is a source checkout, which is not a usable template until `make build-theme` has run, and that target installs on its own (#162). The report theme carries the same two lines; its copy of the engines floor was corrected in QuantEcon/quantecon-theme-report.mystmd#12 and the install decision is QuantEcon/quantecon-theme-report.mystmd#13. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs(changelog): record the bundle-manifest change Copilot review on #191: CONTRIBUTING.md requires every consumer-facing change under Unreleased. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Part of #3 (Phase 0).
Source
Copied from
QuantEcon/quantecon-theme.mystmdat47e37ed885a40afb4fa42dd27ad458357616f68e— the clone'smainat copy time, which is the same commit issue #3 names. Decision D1 makes this repository a self-contained sibling of the lecture theme rather than a package sharing its code, so the scaffold is copied once and then owned here: a later fix in one repository does not reach the other.What was copied
ci.yml,preview.yml,release.yml,update-snapshots.ymlMakefile,template.yml,template/(package.json,server.js,README.md,LICENSE,.gitignore,thumbnail.png)package.json,tsconfig.json,.eslintrc.js,remix.config.dev.js,remix.config.prod.js,remix.env.d.ts,server.js,tailwind.config.js,.nvmrc,.npmrc,.gitignoreadditionsscripts/relative-css-asset-urls.mjs(verbatim)app/root.tsx(with the inlined critical CSS),app/entry.client.tsx,app/entry.server.tsx,app/links.ts,app/backend/loaders.server.ts,app/components/{Page,ErrorPage}.tsx,app/routes/(index, catch-all,api.theme,myst-theme[.css],[favicon.ico],[robots.txt])styles/app.cssplaywright.config.ts,tests/visual/serve.sh,tests/visual/fouc.spec.ts,tests/visual/theme.spec.ts,tests/visual/README.md,tests/visual/fixture/,tests/unit/template-stamp.test.mjsCHANGELOG.md,CONTRIBUTING.md,SECURITY.md, aDevelopmentsection inREADME.mdWhat was renamed
@quantecon/lecture-theme@quantecon/report-theme(root andtemplate/package.json)2.3.10.1.0quantecon-theme.zipquantecon-theme-report.ziptitle: QuantEcon Lecture Themetitle: QuantEcon Report ThemeTHEME = quantecon-themeTHEME = quantecon-theme-report.deploy/quantecon-theme,dist/quantecon-theme.deploy/quantecon-theme-report,dist/quantecon-theme-reportgrep -rnover the working tree (excludingnode_modules/,.deploy/,build/,public/) finds no remaining occurrence of@quantecon/lecture-theme,QuantEcon Lecture Theme, or a barequantecon-themezip/package name. The only hits are the literal strings insidetests/unit/template-stamp.test.mjs, which is the guard that keeps it that way, and deliberate prose/comment references to the lecture repository (quantecon-theme.mystmd) recording where something came from.What was adapted, and why
release.ymlattaches two assets. The theme zip andplugins/compliance.mjs, per the issue. The plugin does not exist until Phase 1b (#5), so a dedicatedCheck for the compliance plugin bundlestep emits a::warning::naming that issue when the file is absent, andfail_on_unmatched_files: falseis set explicitly with a comment saying to flip it totrueonce #5 lands. A release that silently dropped the plugin would leave consumers pinning a 404, so the absence is loud rather than invisible. The tag-matches-package.jsonand changelog-section guards are unchanged.preview.ymlfalls back to the fixture. It targetsQuantEcon/compliance-lecture-style'smystmdbranch, which does not exist yet — I checked:git ls-remote --headson that repository returnsmainonly. The job probes for the branch withgit ls-remoteon every run and, when it is missing, copiestests/visual/fixtureand builds that instead, emitting a::notice::that points at compliance-lecture-style#28. The day the branch appears the preview switches to real content with no change to this file.No live-compute stack.
@myst-theme/jupyter, thebuild:thebestep inprod:build, and all threepatches/entries (@jupyter-widgets/controls,@myst-theme/jupyter,jupyterlab-plotly) exist to make executable notebook cells work. Report sites have no code cells, so all of it is dropped — which also removespatch-packageand thepostinstallhook.prod:buildis thereforeprod:copy && build:css && remix build && relative-css-asset-urls.mjs. This is the "adapt the scripts and say what you changed" case from the brief.test:unithas a new, real test. The lecture theme's unit suite guards that its patches applied; with no patches, that suite has nothing to assert, andnode --testerrors on a glob matching no files.tests/unit/template-stamp.test.mjsreplaces it with a guard over the thing this repository actually stamps: theVERSIONplaceholder intemplate/package.json, the line-initialversion:intemplate.yml,package.json/template.ymlagreement, the Makefile-and-workflow zip name, and the no-leaked-lecture-names check above. It runs inci.ymland again inrelease.ymlbefore the artifact is built, exactly as the lecture theme's did.theme.spec.tsasserts structure, not pixels. Phase 0 has no report UI: the app renders article content in the centred grid and nothing else, so screenshot baselines would lock in an empty page and be thrown away in Phase 2 (#6). Pixel baselines also cannot be seeded from here — they are platform-suffixed and CI needs-linuxones. So thevisualjob runs a rendering suite that is green from the first PR, and the platform-suffixedsnapshotPathTemplate, thedesktop-chrome/mobile-chromeprojects and the whole/update-snapshotspath stay wired up for the Phase 3 harness (#9) to seed with/update-new-snapshots.FOUC guard trimmed to what exists. The grid-not-collapsed and font-stack assertions are kept verbatim, along with the control case. The lecture theme's third assertion — that the off-canvas contents sidebar is parked off-screen on first paint — is dropped with the sidebar itself; the comment says it returns with the Phase 2 shell, together with its
translateX(-100%)critical-CSS rule.Single fixture, so a single
webServer. The lecture harness serves a second Thebe-disabled fixture on a second port to assert the live-compute toggle is absent. With no compute there is nothing to assert, soNO_THEBE_PORTand the secondwebServerentry are gone.Every route the static export path crawls is kept. I first trimmed the app to the routes a live
myst startneeds, and the preview job caught that:myst build --htmlalso fetches per-page.json,sitemap.xml,sitemap_style.xslandobjects.inv, and it fails hard when they 404. The second commit restores those four routes and their loaders.getMystSearchJsonis typed as a plain JSON object rather thanMystSearchIndex, whose package (@myst-theme/search) the lecture theme never declared as a direct dependency either.Two dependency
overridesnot carried across.ws— both copies in this tree (7.5.13,8.21.3) are already above their patched floors and nothing is flagged against them.sanitize-html— I tested the lecture theme's exact2.17.5pin here and it does not clear the package's advisories, while raising the total alert count from 69 to 71 by pulling more paths onto the forced copy, so it is omitted and deferred instead. Everything else in the table is carried across and each entry was checked to resolve in this tree.SECURITY.mdrecords the full triage against a realnpm auditrun rather than the lecture theme's numbers.template.ymlauthor as an object.name: QuantEconas a plain string makes mystmd try to split an organisation into given/family and warn on every build;name: {literal: QuantEcon}is the documented escape and the fixture build is now warning-free.Verified locally
Node here is v26.8.1 while
.nvmrcpins 24 (which is what all four workflows use). I used the local Node as instructed and did not install another; nothing in the run depended on the difference, but CI is the authority on the pinned version.npm install(no lockfile yet, so notnpm ci)package-lock.jsoncommittednpm run compile@types/lodash.throttleadded back —@myst-theme/site's sources are type-checked and import it)npm run test:unitnpm run prod:buildrewrote 74 asset URL(s) in 3 stylesheet(s), so that path is exercised toomake build-zip.deploy/quantecon-theme-report.zip; unzipped it containsbuild/,public/,server.js,package.json(name@quantecon/report-theme,VERSION→0.1.0),template.ymlstampedversion: 0.1.0,package-lock.json,CHANGELOG.md,LICENSE,README.md,thumbnail.png, and nonode_modulesTHEME_TEMPLATE=$PWD/.deploy/quantecon-theme-report bash tests/visual/serve.shstarts (✨ Starting QuantEcon Report Theme ✨), builds both fixture pages with no warnings, and serves 200s. Bothh1s, the table, the KaTeX stylesheets,#skip-to-articleand the inline critical CSS are present in the HTMLjs-yaml), jobsbuild/visual/fouc-guard,preview,release,update-snapshotsnpx playwright test --listloads the config and enumerates 6 tests acrossdesktop-chrome,mobile-chrome,webkit-foucCI has since confirmed the browser suites on the runner:
Visual regression (Chromium)ran 4 tests green andFOUC guard (WebKit)passed, both against the theme built from this branch.Locally I did not run the browser suites — no Playwright browser install, no visual or FOUC run — per the brief. Instead I checked the harness by inspection against the real served document, and one thing that check caught is worth naming: the FOUC control case's regex,
/<style>[^<]*:where\(\.simple-center-grid\)[^<]*<\/style>/, matches exactly one<style>block in the served HTML, so the control genuinely strips the critical CSS. It also caught that@myst-theme/site's footnotes section reuses.simple-center-grid, sotheme.spec.tsscopes its locator tomain > .simple-center-gridrather than asserting a bare count of one.Acceptance criteria
npm ci && npm run compile && npm run prod:buildon a clean checkout, andmake build-zipproduces the stamped zipnpm installbecause there was no lockfile.npm ciis what CI runs against the lockfile this PR commitsCIis green across all three jobs —Build & Typecheck,Visual regression (Chromium)(4 tests passed, with the 🎭 summary comment posted) andFOUC guard (WebKit).PR Previewis green and deployed, taking the fixture fallback path as designed.update-snapshots.ymltriggers on anissue_commentand GitHub runs that workflow from the default branch, so it cannot run until this merges — that is inherent, not a gap in this PRrelease.ymlend to end and attaches both assets## [0.1.0]section inCHANGELOG.md, which does not exist yet — the scaffold sits under## [Unreleased], and cutting the release moves it. The second asset cannot attach until Phase 1b (#5) builds itFAMILY.mdmerged inquantecon-theme.mystmdand linked from this READMEREADME.mdandCONTRIBUTING.mddescribe the conventions and say the document is still to landfile:specifierRemaining work
FAMILY.mdinquantecon-theme.mystmd, listing the conventions the family keeps in step (release zip layout andtemplate.ymlstamping, tag-pinned plugin URLs as release assets, the visual harness with platform-suffixed baselines, the FOUC guard, asset-URL rewriting, Keep-a-Changelog format,.mystmdnaming), then a link to it from this README. Not started here on purpose.-linuxand-darwinvisual baselines once Phase 2 gives them something to show (Phase 2: report theme shell — fonts, tokens, top bar, sidebar, footer #6, Phase 3: test harness — fixture, snapshots, overflow gate, static-build smoke, preview #9).v0.1.0— move the## [Unreleased]section under a dated heading first.@remix-run/verceladapter inserver.js: it is the only reason@vercel/node(and its high-severity advisories) is in the tree, and nothing in the published bundle uses it.🤖 Generated with Claude Code