feat(03-04): Worker [assets] binding + one-command deploy + D-14 build-artifact gate

- wrangler.jsonc assets block serves ../web/dist; run_worker_first keeps /api/* + /telegram/* on the Worker (D-13)
- root npm run deploy = build theme → web → bot wrangler deploy + set-webhook (D-15); dev = build + wrangler dev --port 8787 --local (D-12)
- vite.config.ts dual-build: __ARAHARAH_HARNESS__ define keyed on --mode harness, separate dist/dist-harness outDirs
- main.ts guard unchanged; ambient constant declared once in web/src/vite-env.d.ts
- build-artifact.test.mjs BA1..BA6: zero CDN refs, self-hosted fonts, MapLibre worker, inlined style, no mock installer in ANY emitted file (HTML included)
This commit is contained in:
araharah-executor
2026-10-23 00:15:26 +08:00
parent d573acc583
commit 8b39ea70c3
9 changed files with 203 additions and 31 deletions
+4 -1
View File
@@ -7,9 +7,12 @@
# Dependencies # Dependencies
node_modules/ node_modules/
# Build output # Build output (any depth covers theme/dist and web/dist — regenerated
# output must NEVER be committed, including theme/dist which the web
# build consumes at build time without vendoring)
dist/ dist/
theme/dist/ theme/dist/
web/dist-harness/
# Secrets / local env # Secrets / local env
.env .env
+1 -1
View File
@@ -5,7 +5,7 @@
"scripts": { "scripts": {
"test": "node test/runner.mjs", "test": "node test/runner.mjs",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"dev": "wrangler dev", "dev": "wrangler dev --port 8787 --local",
"deploy": "wrangler deploy && node scripts/set-webhook.mjs", "deploy": "wrangler deploy && node scripts/set-webhook.mjs",
"commands": "node scripts/register-commands.mjs" "commands": "node scripts/register-commands.mjs"
}, },
+9
View File
@@ -6,5 +6,14 @@
// workers_dev: serve on the <name>.<account>.workers.dev URL — the WEBHOOK_URL // workers_dev: serve on the <name>.<account>.workers.dev URL — the WEBHOOK_URL
// target for this phase (Phase 3's custom route/domain may supersede) // target for this phase (Phase 3's custom route/domain may supersede)
"workers_dev": true, "workers_dev": true,
// Workers static assets binding (D-13, D-14): serves ../web/dist. The
// Worker fetch still owns /api/* and /telegram/* first via
// run_worker_first (a GLOB ARRAY, not a boolean).
"assets": {
"directory": "../web/dist",
"binding": "ASSETS",
"not_found_handling": "none"
},
"run_worker_first": ["/api/*", "/telegram/*"],
"kv_namespaces": [{ "binding": "chat_sessions", "id": "59f58a54bc44483e89872b20a005a5eb" }] "kv_namespaces": [{ "binding": "chat_sessions", "id": "59f58a54bc44483e89872b20a005a5eb" }]
} }
+5 -2
View File
@@ -8,7 +8,9 @@
"web" "web"
], ],
"scripts": { "scripts": {
"build": "npm run build --workspace theme", "build": "npm run build -w theme && npm run build -w web",
"deploy": "npm run build && npm run deploy -w bot",
"dev": "npm run build && npm run dev -w bot",
"sheet": "npm run sheet --workspace theme", "sheet": "npm run sheet --workspace theme",
"gallery": "npm run gallery --workspace theme", "gallery": "npm run gallery --workspace theme",
"audit": "npm run audit --workspace theme", "audit": "npm run audit --workspace theme",
@@ -21,7 +23,8 @@
"web:test": "npm test -w web", "web:test": "npm test -w web",
"web:typecheck": "npm run typecheck -w web", "web:typecheck": "npm run typecheck -w web",
"web:dev": "npm run dev -w web", "web:dev": "npm run dev -w web",
"web:build": "npm run build -w web", "web:build": "npm run build -w theme && npm run build -w web",
"web:build:harness": "npm run build:harness -w web",
"web:capture": "npm run capture -w web" "web:capture": "npm run capture -w web"
} }
} }
+1
View File
@@ -7,6 +7,7 @@
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "vite build",
"build:harness": "vite build --mode harness",
"capture": "node scripts/capture.mjs" "capture": "node scripts/capture.mjs"
}, },
"dependencies": { "dependencies": {
+4 -6
View File
@@ -17,12 +17,10 @@
* storage API anywhere on the path. * storage API anywhere on the path.
*/ */
/* Ambient harness constant. Its canonical declaration home is /* Ambient harness constant `__ARAHARAH_HARNESS__` is declared in
web/src/vite-env.d.ts (plan 03-04 task 2); until that file exists it is web/src/vite-env.d.ts (its canonical home, plan 03-04) and supplied by
declared here — single `declare const`, never re-typed later. Vite's Vite's `define` (vite.config.ts): true ONLY in --mode harness builds,
`define` (vite.config.ts) supplies true ONLY in --mode harness builds false everywhere else, so plain dev runs cannot hit a ReferenceError. */
and false everywhere else, so a plain dev run cannot hit a ReferenceError. */
declare const __ARAHARAH_HARNESS__: boolean;
import "./shell.css"; import "./shell.css";
import { bootMap } from "./map/map.ts"; import { bootMap } from "./map/map.ts";
+10
View File
@@ -0,0 +1,10 @@
/*
* vite-env.d.ts — ambient declarations for the build pipeline.
*
* __ARAHARAH_HARNESS__ is injected by vite.config.ts's `define` (03-04
* dual-build): true only in `vite build --mode harness` / --mode harness
* dev, statically false in every production build. Declared here ONCE so
* typecheck passes in both modes; main.ts references it without a local
* re-declaration.
*/
declare const __ARAHARAH_HARNESS__: boolean;
+135
View File
@@ -0,0 +1,135 @@
#!/usr/bin/env node
/*
* build-artifact.test.mjs — the D-14 build-artifact gate (T-03-20, T-03-21).
* ------------------------------------------------------------------------------
* Reads web/dist (the PRODUCTION build only — the [assets] binding serves
* exactly this directory) and proves the shipped bundle is fully
* self-contained and free of the Mock-Telegram installer:
*
* BA1 no emitted .js/.css file references a third-party CDN
* (cdn.tailwindcss.com, fonts.googleapis.com, fonts.gstatic.com,
* unpkg.com, cdn.jsdelivr.net) — T-03-20: a CDN slip would leak
* user navigation requests to third parties
* BA2 at least one self-hosted .woff2 font was copied through by Vite's
* url() resolution of @araharah/theme/dist/rams.css
* BA3 a hashed MapLibre worker chunk exists under web/dist/assets
* BA4 the built JS carries the inlined Rams style ("liberty-rams" or
* "ne2_shaded" — build-time import, never a runtime fetch)
* BA5 the PRODUCTION bundle contains "installMockWebApp"/"__ARAHARAH_MOCK__"
* in NO emitted file — .js AND .html (web/index.html is the ONE entry
* shared by both builds, so an unguarded runner-page profile block
* would ship the mock and the fetch stubs as inline HTML that a
* JS-only scan would miss entirely). Scans web/dist ONLY: the harness
* build emits to web/dist-harness/ and is deliberately NOT clean —
* BA5 pointed there would invert the gate it exists to enforce.
* BA6 no emitted HTML carries the runner page's /api/session or
* /api/location-fallback fetch stubs (same dead-code-elimination proof)
*
* If web/dist does not exist, every case fails with a build-first message.
*/
import { strict as assert } from "node:assert";
import { readdirSync, readFileSync, existsSync } from "node:fs";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import path from "node:path";
import { test } from "./harness.mjs";
const WEB = path.join(fileURLToPath(new URL(import.meta.url)), "..", "..");
const DIST = join(WEB, "dist");
const DIST_ASSETS = join(DIST, "assets");
const CDN_HOSTS = [
"cdn.tailwindcss.com",
"fonts.googleapis.com",
"fonts.gstatic.com",
"unpkg.com",
"cdn.jsdelivr.net",
];
function claimBuildFirst() {
return new Error(
"web/dist does not exist — run `npm run build -w theme && npm run build -w web` before this suite",
);
}
/* walk web/dist recursively — DirEnt-based, no dependency */
function walk(dir) {
const out = [];
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const full = join(dir, entry.name);
if (entry.isDirectory()) out.push(...walk(full));
else out.push(full);
}
return out;
}
function readIfExists(file) {
return existsSync(file) ? readFileSync(file, "utf8") : null;
}
const allFiles = existsSync(DIST) ? walk(DIST) : [];
const jsFiles = allFiles.filter((f) => f.endsWith(".js"));
const cssFiles = allFiles.filter((f) => f.endsWith(".css"));
const htmlFiles = allFiles.filter((f) => f.endsWith(".html"));
test("BA1 no emitted .js/.css file references a third-party CDN (D-14)", () => {
assert.ok(existsSync(DIST), claimBuildFirst().message);
for (const file of [...jsFiles, ...cssFiles]) {
const content = readFileSync(file, "utf8");
for (const host of CDN_HOSTS) {
assert.ok(
!content.includes(host),
`${file} references CDN host ${host} (D-14 forbids CDN fallbacks)`,
);
}
}
});
test("BA2 web/dist carries self-hosted woff2 fonts copied at build time", () => {
assert.ok(existsSync(DIST), claimBuildFirst().message);
const woff2 = allFiles.filter((f) => f.endsWith(".woff2"));
assert.ok(woff2.length >= 1, `no .woff2 under web/dist — apply BA2's contingency (public/fonts copy or postbuild script), never a CDN link`);
});
test("BA3 web/dist/assets contains a hashed MapLibre worker chunk", () => {
assert.ok(existsSync(DIST_ASSETS), claimBuildFirst().message);
const worker = readdirSync(DIST_ASSETS).filter((f) => f.endsWith(".js") && /worker/i.test(f));
assert.ok(worker.length >= 1, `no MapLibre worker chunk in web/dist/assets (found: ${readdirSync(DIST_ASSETS).join(", ")})`);
});
test("BA4 built JS carries the inlined Rams style JSON (liberty-rams/ne2_shaded)", () => {
assert.ok(existsSync(DIST_ASSETS), claimBuildFirst().message);
const bundle = jsFiles
.filter((f) => f.startsWith(DIST_ASSETS))
.map((f) => readFileSync(f, "utf8"))
.join("");
const hasStyle = bundle.includes("liberty-rams") || bundle.includes("ne2_shaded");
assert.ok(hasStyle, "inlined Rams style marker absent from the JS bundle — style JSON was not inlined at build time");
});
test("BA5 production bundle carries NO Mock-Telegram installer in ANY emitted file (.js + .html)", () => {
assert.ok(existsSync(DIST), claimBuildFirst().message);
const jsAssets = jsFiles.map((f) => readFileSync(f, "utf8")).join("");
const allHtml = htmlFiles.map((f) => readFileSync(f, "utf8")).join("");
for (const token of ["installMockWebApp", "__ARAHARAH_MOCK__"]) {
assert.ok(
!jsAssets.includes(token),
`installMockWebApp gate: "${token}" found in a web/dist JS asset — the mock survived tree-shaking`,
);
assert.ok(
!allHtml.includes(token),
`installMockWebApp gate: "${token}" found in an emitted web/dist HTML (${htmlFiles.join(", ")}) — the runner page's profile block shipped`,
);
}
});
test("BA6 production HTML carries NO runner-page fetch stub endpoints", () => {
assert.ok(existsSync(DIST), claimBuildFirst().message);
const allHtml = htmlFiles.map((f) => readFileSync(f, "utf8")).join("");
for (const endpoint of ["/api/session", "/api/location-fallback"]) {
assert.ok(
!allHtml.includes(endpoint),
`runner-page fetch stub "${endpoint}" present in emitted web/dist HTML — an unguarded profile block shipped`,
);
}
});
+34 -21
View File
@@ -1,23 +1,36 @@
import { defineConfig } from "vite"; import { defineConfig } from "vite";
export default defineConfig(({ mode }) => ({ export default defineConfig(({ mode }) => {
base: "/", const isHarness = mode === "harness";
// 03-03: the harness constant (declared ambient in main.ts; canonical home return {
// is web/src/vite-env.d.ts under plan 03-04) is REAL only in --mode harness base: "/",
// builds and statically false everywhere else — with a bare ambient global // 03-04 dual-build contract (the capture-harness fix, T-03-21):
// referenced in main.ts, this define keeps plain `vite dev` from throwing a // `vite build --mode harness` emits to dist-harness/ WITH the mock
// ReferenceError while the fallback mock install there still fires via // installer + fetch stubs; every other build emits to dist/ WITHOUT
// import.meta.env.DEV. 03-04's dual-build config keys on the same mode. // them. The constant is REAL only in the harness mode and statically
define: { // false everywhere else — including plain `vite dev`, where this
__ARAHARAH_HARNESS__: JSON.stringify(mode === "harness"), // define keeps main.ts's ambient reference from throwing a
}, // ReferenceError while the fallback mock still fires via
build: { // import.meta.env.DEV.
outDir: "dist", //
emptyOutDir: true, // Vite replaces the IDENTIFIER textually at build time, so the
target: "es2022", // harness bundle compiles `if (VITE_HARNESS) installMockWebApp()`
}, // down to `if (true) …`. The production build never defines it,
server: { // so the identifier is unreferenced and the branch is tree-shaken.
port: 5173, //
strictPort: true, // The two builds must NEVER share an outDir: sharing one would make
}, // them clobber each other and make the BA5 gate meaningless.
})); define: {
__ARAHARAH_HARNESS__: JSON.stringify(isHarness),
},
build: {
outDir: isHarness ? "dist-harness" : "dist",
emptyOutDir: true,
target: "es2022",
},
server: {
port: 5173,
strictPort: true,
},
};
});