dist/index.html was a polyglot — `#!/bin/sh`, a `<<\EOF` heredoc swallowing the whole document, the installer after the closing EOF. Both halves ran. The browser half was never honest, and could not be made honest. Anything before <!DOCTYPE html> that is not whitespace or a comment puts the document in quirks mode. Chromium, at 390 and 1280: compatMode BackCompat; document.head.children.length === 1, with all fifteen head elements — title, icons, viewport, description, every og: and twitter: tag — parsed into <body> where they do nothing; and "#!/bin/sh <<\EOF" as the first text node of the page, first string in body.innerText, pushing <main> down 24px. Proven by re-serving the identical live bytes with the 17-byte prefix stripped: BackCompat -> CSS1Compat, head.children 1 -> 15, at both widths. No arrangement of those bytes avoids it. Only whitespace and comments may precede a DOCTYPE; an HTML comment opens with `<!`, which a shell reads as a redirect from a file named `!--`, so a POSIX script cannot start there at all. The trick was unfixable, not unlucky. So the two resources become two files and `/` chooses between them by Accept, which is what HTTP has for this. worker.js is the whole rule: text/html gets dist/page.html, anything else — curl, wget, the /cli /mcp /dev shims that re-exec `curl -fsSL https://hanzo.sh` — gets dist/install.sh. `/` carries vary: accept and cache-control: no-store, because Cloudflare honours Vary only on Accept-Encoding and a shared cache holding one representation for the other kind of client breaks either the page or the install command. scripts/postbuild.js replaces the polyglot rewrite: it moves the document off index.html (Static Assets serve an exact match BEFORE the Worker runs, so a file at that name would hand curl the HTML) and writes /install from install.sh, so the alias is one file in git instead of 12,746 duplicated bytes. wrangler.toml: routes moves ABOVE [assets]. TOML puts every key after a table header inside that table, so it was parsing as assets.routes and wrangler dropped it with a warning — the custom domain survived only because it was attached out of band. html_handling and not_found_handling are "none": exact match or the Worker, nothing silently rewritten, a mistyped path still 404s. The deploy gate now checks both representations against the live URL, plus a 200 for every path the head declares — the check that would have caught og-image.svg. Verified against the real runtime before pushing: wrangler dev --local, curl gets install.sh byte-for-byte, a browser gets page.html with <!DOCTYPE html> at byte 0, `sh -n -c "$(curl -fsSL /)"` parses under dash and bash, and /index.html /nonexistent-xyz /some/deep/path still 404. Co-authored-by: Hanzo Dev <[email protected]>
26 lines
1.1 KiB
JavaScript
26 lines
1.1 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Two things Vite cannot know about the shape hanzo.sh is served in.
|
|
*
|
|
* 1. The document must NOT be dist/index.html. Static Assets serve an exact
|
|
* match before the Worker ever runs, so a file at that name would be handed
|
|
* to `curl hanzo.sh | sh` as HTML. It moves to page.html; worker.js decides
|
|
* which of page.html and install.sh `/` means.
|
|
* 2. /install is a published alias of /install.sh (some deployed callers ask
|
|
* for it). One file in git, two names in dist — copied here rather than
|
|
* committed twice, and made from the same bytes so they cannot drift.
|
|
*/
|
|
|
|
import { copyFileSync, renameSync } from 'fs'
|
|
import { join, dirname } from 'path'
|
|
import { fileURLToPath } from 'url'
|
|
|
|
const dist = join(dirname(fileURLToPath(import.meta.url)), '..', 'dist')
|
|
|
|
renameSync(join(dist, 'index.html'), join(dist, 'page.html'))
|
|
copyFileSync(join(dist, 'install.sh'), join(dist, 'install'))
|
|
|
|
console.log('dist/page.html the document, <!DOCTYPE html> at byte 0')
|
|
console.log('dist/install.sh the installer, also served as /install')
|
|
console.log('/ whichever of the two the Accept header asks for')
|