| 1 | // Exercise the built Worker in workerd before it can be deployed. Storage is |
| 2 | // temporary and local; no service bindings or Cloudflare credentials are used. |
| 3 | import assert from "node:assert/strict"; |
| 4 | import { readFileSync, readdirSync } from "node:fs"; |
| 5 | import { join, resolve } from "node:path"; |
| 6 | import { Miniflare, convertV4MiniflareOptions } from "miniflare"; |
| 7 | |
| 8 | const root = resolve("build/server"); |
| 9 | const config = JSON.parse(readFileSync(join(root, "wrangler.json"), "utf8")); |
| 10 | const source = readFileSync(join(root, "index.js"), "utf8"); |
| 11 | const build = source.match(/searchParams\.set\("_g1t_build", "([^"]+)"\)/)?.[1]; |
| 12 | assert.ok(build, "The page cache must be separated by build"); |
| 13 | const css = "body{color:red}"; |
| 14 | // Seed R2 and the page cache through a test-only entry, keeping all R2 I/O |
| 15 | // inside workerd. The application entry itself is the unmodified build. |
| 16 | const seed = `import app from './index.js'; |
| 17 | export default { async fetch(request, env, ctx) { |
| 18 | if (new URL(request.url).pathname === '/__seed') { |
| 19 | await env.DOWNLOADS.put('web-assets/assets/app-old12345.css', ${JSON.stringify(css)}, {httpMetadata:{contentType:'text/css'}}); |
| 20 | const cache = new URL('/pricing.data', request.url); |
| 21 | cache.searchParams.set('_g1t_build', ${JSON.stringify(build)}); |
| 22 | await caches.default.put(new Request(cache), new Response('wrong-build-cache', {headers:{'cache-control':'public, max-age=300','x-g1t-kept-at':String(Date.now())}})); |
| 23 | return new Response('seeded'); |
| 24 | } |
| 25 | return app.fetch(request, env, ctx); |
| 26 | }};`; |
| 27 | const files = [join(root, "index.js"), ...readdirSync(join(root, "assets")).filter((name) => name.endsWith(".js")).map((name) => join(root, "assets", name))]; |
| 28 | const runtime = new Miniflare(convertV4MiniflareOptions({ |
| 29 | name: "web-deploy-check", cf: false, modulesRoot: root, |
| 30 | modules: [{ type: "ESModule", path: join(root, "deploy-check.mjs"), contents: seed }, ...files.map((path) => ({ type: "ESModule", path }))], |
| 31 | compatibilityDate: config.compatibility_date, |
| 32 | compatibilityFlags: config.compatibility_flags, |
| 33 | r2Buckets: ["DOWNLOADS"], |
| 34 | })); |
| 35 | try { |
| 36 | const origin = await runtime.ready; |
| 37 | const request = (path, init) => fetch(new URL(path, origin), { ...init, signal: AbortSignal.timeout(15_000) }); |
| 38 | assert.equal((await request("/__seed")).status, 200); |
| 39 | const old = await request("/assets/app-old12345.css"); |
| 40 | assert.equal(old.status, 200); |
| 41 | assert.equal(old.headers.get("content-type"), "text/css"); |
| 42 | assert.equal(await old.text(), css); |
| 43 | const missing = await request("/assets/app-missing0.css"); |
| 44 | assert.equal(missing.status, 404); |
| 45 | assert.equal(missing.headers.get("cache-control"), "no-store"); |
| 46 | assert.doesNotMatch(missing.headers.get("content-type") ?? "", /html/); |
| 47 | for (const path of ["/pricing.data", "/flagon-io/-/home.data?tab=one&_routes=root"]) { |
| 48 | const answer = await request(path, { headers: { "x-g1t-build": "previous-build" } }); |
| 49 | assert.equal(answer.status, 202); |
| 50 | const body = await answer.text(); |
| 51 | assert.match(body, /reload/); |
| 52 | assert.ok(body.includes(path.split(".data")[0])); |
| 53 | assert.doesNotMatch(body, /_routes|wrong-build-cache/); |
| 54 | } |
| 55 | console.log("Deployment safety: old CSS survives, missing assets stay isolated, old navigations bypass cache and loaders."); |
| 56 | } finally { |
| 57 | await runtime.dispose(); |
| 58 | } |