Repository navigation
fix(web): a release works on Cloudflare Pages — the terminal opens (node_modules is served), the sign-in page keeps its headers - #111
Merged
Conversation
…e sees a terminal and no script answered with HTML The first deploy to Cloudflare Pages (app.levelcode.ai, 2026-10-19) went live without its terminal: the client loads xterm, vscode-textmate and vscode-oniguruma from static/node_modules, wrangler never uploads a folder by that name, and Pages answered each of those scripts with index.html (200, text/html), so the browser refused to run them. Every check had passed against serve.mjs, a plain file server. web/lib/pages.mjs is what Pages does differently, and serve.mjs --dist <release> --pages answers that way: only what `wrangler pages deploy` uploads (its IGNORE_LIST: **/node_modules, **/.git, **/.DS_Store, its own files at the root; symbolic links skipped), _redirects first (200 rewrites, 3xx redirects, :name and * placeholders, first match wins), .html redirected to the address without it and served there, and index.html with 200 for an address that has no file. e2e.mjs, e2e-look.mjs and e2e-signin-tab.mjs pass --pages through. e2e.mjs gains two checks: in a release, LevelCode Shell: New Terminal opens a terminal (its library loaded); and no script, JSON or WebAssembly response was HTML (cdp.mjs now keeps each response's content type). Against the release that is live — same build id, 5bfb4df6caa1, rebuilt from the same client — `e2e.mjs --pages` fails both, as production did (30/32). The fix is the next commit. pages.mjs also holds the release's answer to the folder (NODE_MODULES_COPY, toRedirectsFile), which the next commit's build writes; pages.test.js runs that rule against every kind of path.
…les is served, the sign-in page keeps its headers Three things a release did on Pages that it does not do on a plain file server (the previous commit shows them): - node_modules. The build stages static/node_modules a second time as static/node-modules, and writes a _redirects with one rule, /_/:id/static/node_modules/* -> /_/:id/static/node-modules/:splat (200, a rewrite), so Pages serves the client's files at the address it asks for, for this build and for a previous one kept online. Other hosts serve node_modules and ignore _redirects. The build refuses a node_modules folder anywhere else in a release. (+959 files, +35 MB: 1,840 files are uploaded, of Pages' 20,000.) - /callback. Pages serves callback.html at /callback and matches _headers by the address asked for: the sign-in page went without its default-src 'none' policy and no-store there. Every page's rule is now also written without `.html` (index.html's is `/`, which has its own). - levelcode-themes. A client built from a checkout that has LevelCode's extensions bakes it in; the build still staged a second copy (without a package.nls.json), which the workbench skipped with an error per translation lookup. It is staged only when the client does not have it. Checked with the client the live release was built from: e2e.mjs --pages 32/32 (the terminal opens; no script, JSON or WebAssembly answered with HTML), e2e-look.mjs --pages 26/26 (the web themes are in use: the baked theme's default is outranked by following the system), e2e-signin-tab.mjs --pages 20/20 (the sign-in page reached through Pages' .html redirect), e2e.mjs without --pages 32/32. Unit gate 127 files on Node 24 and Node 18. Not run on Cloudflare itself.
Contributor
There was a problem hiding this comment.
🟢 Approval recommended
No blocking issues were identified in the focused build, routing, header, and regression-test changes.
No open issues 🎉
What changed in this PR
Fixes LevelCode web releases on Cloudflare Pages so terminal dependencies load and the sign-in callback retains its security headers.
Changes:
- Copies client dependencies to an uploadable path and adds a rewrite rule.
- Preserves extensionless page headers and avoids staging duplicate themes.
- Adds Pages emulation and regression checks.
| File | Description |
|---|---|
| web/test/unit/pages.test.js | Tests upload filtering, rewrites, and page routing. |
| web/test/unit/headers.test.js | Checks headers for /callback. |
| web/test/e2e.mjs | Checks terminal loading and unexpected HTML responses. |
| web/test/e2e-signin-tab.mjs | Enables Pages-mode sign-in checks. |
| web/test/e2e-look.mjs | Enables Pages-mode appearance checks. |
| web/test/cdp.mjs | Records response MIME types. |
| web/serve.mjs | Adds Pages-compatible serving mode. |
| web/lib/pages.mjs | Defines Pages routing and upload behavior. |
| web/lib/headers.mjs | Adds extensionless page header rules. |
| scripts/build-web.mjs | Stages dependencies, writes rewrites, and avoids duplicate themes. |
| docs/WEB.md | Documents Pages deployment and verification. |
Review effort: Balanced
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
On the first deploy to Cloudflare Pages (app.levelcode.ai), the terminal did not open. The browser console showed:
Refused to execute script from …/static/node_modules/@xterm/xterm/lib/xterm.js because its MIME type ('text/html') is not executable(and the same forvscode-textmateandvscode-oniguruma);extensions/levelcode-themes/package.nls.json.The cause.
wrangler pages deploynever uploads a folder namednode_modules:**/node_modulesis in its ignore list, inpackages/wrangler/src/pages/validate.ts, and no option keeps it. The editor's web client loads its terminal library, its syntax colouring and more fromstatic/node_modules. Pages answers an address it has no file for withindex.htmland status 200, so each of those scripts came back as the page.Commits
test(web):serve.mjs --dist <release> --pagesanswers the way Pages does (web/lib/pages.mjs):_redirectsrules apply first;.htmladdresses are redirected to the address without it;e2e.mjs,e2e-look.mjsande2e-signin-tab.mjstake--pages.e2e.mjschecks that a terminal opens and that no script, JSON or WebAssembly response was HTML. Against the live release (rebuilt from the same client: same build id,5bfb4df6caa1) it fails both, 30/32, as production did.fix(web):static/node_modulesagain asstatic/node-modules, and writes_redirectswith a single rewrite rule,/_/:id/static/node_modules/* /_/:id/static/node-modules/:splat 200. Other hosts ignore_redirectsand servenode_modulesdirectly. The build refuses anode_modulesfolder anywhere else in a release./callback: Pages servescallback.htmlat/callback, and matches_headersrules by the address requested. Every page's header rule is now also written without.html, so the sign-in page keeps its strict policy andno-store.levelcode-themes: it is no longer staged a second time when the client already has it baked in. That stops the translation-lookup errors.docs/WEB.md: a new section, "On Cloudflare Pages".Verified
With the client the live release was built from:
e2e.mjs --pagese2e-look.mjs --pagese2e-signin-tab.mjs --pages.htmlredirecte2e.mjs, without--pagesThe release is 1,840 uploaded files, of Pages' 20,000 (+959 files, +35 MB for the copy). The largest file is still 24.6 MiB, under the 25 MiB limit.
Not run
On Cloudflare itself: everything above runs against the local emulation of Pages.
Left as it is
The baked
levelcode-themesstill declares the desktop's default theme. In practice the browser edition's themes win, because the editor follows the system's light or dark setting (e2e-lookconfirms it). They would lose only if a user turned that off.🤖 Generated with Claude Code