From 9df404a0c90b5d16b243dbc741e832aa14d4a512 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Sat, 19 Oct 2024 01:55:44 +0900 Subject: [PATCH] feat(cloudflare-module): use new workers static assets (#2800) --- playground/nitro.config.ts | 2 +- playground/wrangler.toml | 3 + src/presets/_types.gen.ts | 4 +- src/presets/cloudflare/preset.ts | 47 +++++++++++- .../cloudflare/runtime/_module-handler.ts | 10 ++- .../runtime/cloudflare-module-legacy.ts | 74 +++++++++++++++++++ .../cloudflare/runtime/cloudflare-module.ts | 60 +++------------ test/fixture/nitro.config.ts | 2 +- test/fixture/wrangler.toml | 5 +- test/presets/cloudflare-module-legacy.test.ts | 31 ++++++++ test/presets/cloudflare-module.test.ts | 10 ++- test/tests.ts | 10 ++- 12 files changed, 191 insertions(+), 67 deletions(-) create mode 100644 playground/wrangler.toml create mode 100644 src/presets/cloudflare/runtime/cloudflare-module-legacy.ts create mode 100644 test/presets/cloudflare-module-legacy.test.ts diff --git a/playground/nitro.config.ts b/playground/nitro.config.ts index 35a05d1f45..2d5eb0abf7 100644 --- a/playground/nitro.config.ts +++ b/playground/nitro.config.ts @@ -1,5 +1,5 @@ import { defineNitroConfig } from "nitropack/config"; export default defineNitroConfig({ - compatibilityDate: "2024-09-29", + compatibilityDate: "2024-09-19", }); diff --git a/playground/wrangler.toml b/playground/wrangler.toml new file mode 100644 index 0000000000..90d5480708 --- /dev/null +++ b/playground/wrangler.toml @@ -0,0 +1,3 @@ +name = "nitro-test" +compatibility_date = "2024-09-19" +assets = { directory = "./.output/public/", binding = "ASSETS" } diff --git a/src/presets/_types.gen.ts b/src/presets/_types.gen.ts index f7ebdeed95..ba3dba7f07 100644 --- a/src/presets/_types.gen.ts +++ b/src/presets/_types.gen.ts @@ -16,6 +16,6 @@ export interface PresetOptions { vercel: VercelOptions; } -export type PresetName = "alwaysdata" | "aws-amplify" | "aws-lambda" | "aws-lambda-streaming" | "azure" | "azure-functions" | "azure-swa" | "base-worker" | "bun" | "cleavr" | "cli" | "cloudflare" | "cloudflare-module" | "cloudflare-pages" | "cloudflare-pages-static" | "cloudflare-worker" | "deno" | "deno-deploy" | "deno-server" | "digital-ocean" | "edgio" | "firebase" | "flight-control" | "genezio" | "github-pages" | "gitlab-pages" | "heroku" | "iis" | "iis-handler" | "iis-node" | "koyeb" | "layer0" | "netlify" | "netlify-builder" | "netlify-edge" | "netlify-legacy" | "netlify-static" | "nitro-dev" | "nitro-prerender" | "node" | "node-cluster" | "node-listener" | "node-server" | "platform-sh" | "render-com" | "service-worker" | "static" | "stormkit" | "vercel" | "vercel-edge" | "vercel-static" | "winterjs" | "zeabur" | "zeabur-static" | "zerops" | "zerops-static"; +export type PresetName = "alwaysdata" | "aws-amplify" | "aws-lambda" | "aws-lambda-streaming" | "azure" | "azure-functions" | "azure-swa" | "base-worker" | "bun" | "cleavr" | "cli" | "cloudflare" | "cloudflare-module" | "cloudflare-module-legacy" | "cloudflare-pages" | "cloudflare-pages-static" | "cloudflare-worker" | "deno" | "deno-deploy" | "deno-server" | "digital-ocean" | "edgio" | "firebase" | "flight-control" | "genezio" | "github-pages" | "gitlab-pages" | "heroku" | "iis" | "iis-handler" | "iis-node" | "koyeb" | "layer0" | "netlify" | "netlify-builder" | "netlify-edge" | "netlify-legacy" | "netlify-static" | "nitro-dev" | "nitro-prerender" | "node" | "node-cluster" | "node-listener" | "node-server" | "platform-sh" | "render-com" | "service-worker" | "static" | "stormkit" | "vercel" | "vercel-edge" | "vercel-static" | "winterjs" | "zeabur" | "zeabur-static" | "zerops" | "zerops-static"; -export type PresetNameInput = "alwaysdata" | "aws-amplify" | "awsAmplify" | "aws_amplify" | "aws-lambda" | "awsLambda" | "aws_lambda" | "aws-lambda-streaming" | "awsLambdaStreaming" | "aws_lambda_streaming" | "azure" | "azure-functions" | "azureFunctions" | "azure_functions" | "azure-swa" | "azureSwa" | "azure_swa" | "base-worker" | "baseWorker" | "base_worker" | "bun" | "cleavr" | "cli" | "cloudflare" | "cloudflare-module" | "cloudflareModule" | "cloudflare_module" | "cloudflare-pages" | "cloudflarePages" | "cloudflare_pages" | "cloudflare-pages-static" | "cloudflarePagesStatic" | "cloudflare_pages_static" | "cloudflare-worker" | "cloudflareWorker" | "cloudflare_worker" | "deno" | "deno-deploy" | "denoDeploy" | "deno_deploy" | "deno-server" | "denoServer" | "deno_server" | "digital-ocean" | "digitalOcean" | "digital_ocean" | "edgio" | "firebase" | "flight-control" | "flightControl" | "flight_control" | "genezio" | "github-pages" | "githubPages" | "github_pages" | "gitlab-pages" | "gitlabPages" | "gitlab_pages" | "heroku" | "iis" | "iis-handler" | "iisHandler" | "iis_handler" | "iis-node" | "iisNode" | "iis_node" | "koyeb" | "layer0" | "netlify" | "netlify-builder" | "netlifyBuilder" | "netlify_builder" | "netlify-edge" | "netlifyEdge" | "netlify_edge" | "netlify-legacy" | "netlifyLegacy" | "netlify_legacy" | "netlify-static" | "netlifyStatic" | "netlify_static" | "nitro-dev" | "nitroDev" | "nitro_dev" | "nitro-prerender" | "nitroPrerender" | "nitro_prerender" | "node" | "node-cluster" | "nodeCluster" | "node_cluster" | "node-listener" | "nodeListener" | "node_listener" | "node-server" | "nodeServer" | "node_server" | "platform-sh" | "platformSh" | "platform_sh" | "render-com" | "renderCom" | "render_com" | "service-worker" | "serviceWorker" | "service_worker" | "static" | "stormkit" | "vercel" | "vercel-edge" | "vercelEdge" | "vercel_edge" | "vercel-static" | "vercelStatic" | "vercel_static" | "winterjs" | "zeabur" | "zeabur-static" | "zeaburStatic" | "zeabur_static" | "zerops" | "zerops-static" | "zeropsStatic" | "zerops_static" | (string & {}); +export type PresetNameInput = "alwaysdata" | "aws-amplify" | "awsAmplify" | "aws_amplify" | "aws-lambda" | "awsLambda" | "aws_lambda" | "aws-lambda-streaming" | "awsLambdaStreaming" | "aws_lambda_streaming" | "azure" | "azure-functions" | "azureFunctions" | "azure_functions" | "azure-swa" | "azureSwa" | "azure_swa" | "base-worker" | "baseWorker" | "base_worker" | "bun" | "cleavr" | "cli" | "cloudflare" | "cloudflare-module" | "cloudflareModule" | "cloudflare_module" | "cloudflare-module-legacy" | "cloudflareModuleLegacy" | "cloudflare_module_legacy" | "cloudflare-pages" | "cloudflarePages" | "cloudflare_pages" | "cloudflare-pages-static" | "cloudflarePagesStatic" | "cloudflare_pages_static" | "cloudflare-worker" | "cloudflareWorker" | "cloudflare_worker" | "deno" | "deno-deploy" | "denoDeploy" | "deno_deploy" | "deno-server" | "denoServer" | "deno_server" | "digital-ocean" | "digitalOcean" | "digital_ocean" | "edgio" | "firebase" | "flight-control" | "flightControl" | "flight_control" | "genezio" | "github-pages" | "githubPages" | "github_pages" | "gitlab-pages" | "gitlabPages" | "gitlab_pages" | "heroku" | "iis" | "iis-handler" | "iisHandler" | "iis_handler" | "iis-node" | "iisNode" | "iis_node" | "koyeb" | "layer0" | "netlify" | "netlify-builder" | "netlifyBuilder" | "netlify_builder" | "netlify-edge" | "netlifyEdge" | "netlify_edge" | "netlify-legacy" | "netlifyLegacy" | "netlify_legacy" | "netlify-static" | "netlifyStatic" | "netlify_static" | "nitro-dev" | "nitroDev" | "nitro_dev" | "nitro-prerender" | "nitroPrerender" | "nitro_prerender" | "node" | "node-cluster" | "nodeCluster" | "node_cluster" | "node-listener" | "nodeListener" | "node_listener" | "node-server" | "nodeServer" | "node_server" | "platform-sh" | "platformSh" | "platform_sh" | "render-com" | "renderCom" | "render_com" | "service-worker" | "serviceWorker" | "service_worker" | "static" | "stormkit" | "vercel" | "vercel-edge" | "vercelEdge" | "vercel_edge" | "vercel-static" | "vercelStatic" | "vercel_static" | "winterjs" | "zeabur" | "zeabur-static" | "zeaburStatic" | "zeabur_static" | "zerops" | "zerops-static" | "zeropsStatic" | "zerops_static" | (string & {}); diff --git a/src/presets/cloudflare/preset.ts b/src/presets/cloudflare/preset.ts index 0aad2c5a41..aec20020f4 100644 --- a/src/presets/cloudflare/preset.ts +++ b/src/presets/cloudflare/preset.ts @@ -105,10 +105,10 @@ const cloudflare = defineNitroPreset( } ); -const cloudflareModule = defineNitroPreset( +const cloudflareModuleLegacy = defineNitroPreset( { extends: "base-worker", - entry: "./runtime/cloudflare-module", + entry: "./runtime/cloudflare-module-legacy", exportConditions: ["workerd"], commands: { preview: "npx wrangler dev ./server/index.mjs --site ./public", @@ -139,14 +139,57 @@ const cloudflareModule = defineNitroPreset( }, }, }, + { + name: "cloudflare-module-legacy" as const, + aliases: ["cloudflare-module"] as const, + compatibilityDate: "2024-05-07", + url: import.meta.url, + } +); + +const cloudflareModule = defineNitroPreset( + { + extends: "base-worker", + entry: "./runtime/cloudflare-module", + exportConditions: ["workerd"], + commands: { + preview: "npx wrangler dev ./server/index.mjs --assets ./public/", + deploy: "npx wrangler deploy", + }, + rollupConfig: { + output: { + format: "esm", + exports: "named", + inlineDynamicImports: false, + }, + }, + wasm: { + lazy: false, + esmImport: true, + }, + hooks: { + async compiled(nitro: Nitro) { + await writeFile( + resolve(nitro.options.output.dir, "package.json"), + JSON.stringify({ private: true, main: "./server/index.mjs" }, null, 2) + ); + await writeFile( + resolve(nitro.options.output.dir, "package-lock.json"), + JSON.stringify({ lockfileVersion: 1 }, null, 2) + ); + }, + }, + }, { name: "cloudflare-module" as const, + compatibilityDate: "2024-09-19", url: import.meta.url, } ); export default [ cloudflare, + cloudflareModuleLegacy, cloudflareModule, cloudflarePages, cloudflarePagesStatic, diff --git a/src/presets/cloudflare/runtime/_module-handler.ts b/src/presets/cloudflare/runtime/_module-handler.ts index 92a9512a1c..95cf1fa188 100644 --- a/src/presets/cloudflare/runtime/_module-handler.ts +++ b/src/presets/cloudflare/runtime/_module-handler.ts @@ -8,22 +8,26 @@ type MaybePromise = T | Promise; export function createHandler(hooks: { fetch: ( - ...params: Parameters["fetch"]>> + ...params: [ + ...Parameters["fetch"]>>, + url: URL, + ] ) => MaybePromise; }) { const nitroApp = useNitroApp(); return >{ async fetch(request, env, context) { + const url = new URL(request.url); + // Preset-specific logic if (hooks.fetch) { - const res = await hooks.fetch(request, env, context); + const res = await hooks.fetch(request, env, context, url); if (res) { return res; } } - const url = new URL(request.url); let body; if (requestHasBody(request as unknown as Request)) { body = Buffer.from(await request.arrayBuffer()); diff --git a/src/presets/cloudflare/runtime/cloudflare-module-legacy.ts b/src/presets/cloudflare/runtime/cloudflare-module-legacy.ts new file mode 100644 index 0000000000..21b2c64e8e --- /dev/null +++ b/src/presets/cloudflare/runtime/cloudflare-module-legacy.ts @@ -0,0 +1,74 @@ +import "#nitro-internal-pollyfills"; +import { + getAssetFromKV, + mapRequestToAsset, +} from "@cloudflare/kv-asset-handler"; +import wsAdapter from "crossws/adapters/cloudflare"; +import { withoutBase } from "ufo"; +import { useNitroApp, useRuntimeConfig } from "nitropack/runtime"; +import { getPublicAssetMeta } from "#nitro-internal-virtual/public-assets"; +import { createHandler } from "./_module-handler"; + +// @ts-ignore Bundled by Wrangler +// See https://github.com/cloudflare/kv-asset-handler#asset_manifest-required-for-es-modules +import manifest from "__STATIC_CONTENT_MANIFEST"; + +const nitroApp = useNitroApp(); + +const ws = import.meta._websocket + ? wsAdapter(nitroApp.h3App.websocket) + : undefined; + +interface Env { + __STATIC_CONTENT?: any; +} + +export default createHandler({ + async fetch(request, env, context) { + // Websocket upgrade + // https://crossws.unjs.io/adapters/cloudflare + if ( + import.meta._websocket && + request.headers.get("upgrade") === "websocket" + ) { + return ws!.handleUpgrade(request as any, env, context); + } + + try { + // https://github.com/cloudflare/kv-asset-handler#es-modules + return await getAssetFromKV( + { + request: request as unknown as Request, + waitUntil(promise) { + return context.waitUntil(promise); + }, + }, + { + cacheControl: assetsCacheControl, + mapRequestToAsset: baseURLModifier, + ASSET_NAMESPACE: env.__STATIC_CONTENT, + ASSET_MANIFEST: JSON.parse(manifest), + } + ); + } catch { + // Ignore + } + }, +}); + +function assetsCacheControl(_request: Request) { + const url = new URL(_request.url); + const meta = getPublicAssetMeta(url.pathname); + if (meta.maxAge) { + return { + browserTTL: meta.maxAge, + edgeTTL: meta.maxAge, + }; + } + return {}; +} + +const baseURLModifier = (request: Request) => { + const url = withoutBase(request.url, useRuntimeConfig().app.baseURL); + return mapRequestToAsset(new Request(url, request)); +}; diff --git a/src/presets/cloudflare/runtime/cloudflare-module.ts b/src/presets/cloudflare/runtime/cloudflare-module.ts index 990abda0a2..32bc0efd23 100644 --- a/src/presets/cloudflare/runtime/cloudflare-module.ts +++ b/src/presets/cloudflare/runtime/cloudflare-module.ts @@ -1,18 +1,10 @@ import "#nitro-internal-pollyfills"; -import { useNitroApp, useRuntimeConfig } from "nitropack/runtime"; -import { getPublicAssetMeta } from "#nitro-internal-virtual/public-assets"; -import { - getAssetFromKV, - mapRequestToAsset, -} from "@cloudflare/kv-asset-handler"; +import type { fetch } from "@cloudflare/workers-types"; import wsAdapter from "crossws/adapters/cloudflare"; -import { withoutBase } from "ufo"; +import { useNitroApp } from "nitropack/runtime"; +import { isPublicAssetURL } from "#nitro-internal-virtual/public-assets"; import { createHandler } from "./_module-handler"; -// @ts-ignore Bundled by Wrangler -// See https://github.com/cloudflare/kv-asset-handler#asset_manifest-required-for-es-modules -import manifest from "__STATIC_CONTENT_MANIFEST"; - const nitroApp = useNitroApp(); const ws = import.meta._websocket @@ -20,11 +12,16 @@ const ws = import.meta._websocket : undefined; interface Env { - __STATIC_CONTENT?: any; + ASSETS?: { fetch: typeof fetch }; } export default createHandler({ - async fetch(request, env, context) { + fetch(request, env, context, url) { + // Static assets fallback (optional binding) + if (env.ASSETS && isPublicAssetURL(url.pathname)) { + return env.ASSETS.fetch(request); + } + // Websocket upgrade // https://crossws.unjs.io/adapters/cloudflare if ( @@ -33,42 +30,5 @@ export default createHandler({ ) { return ws!.handleUpgrade(request as any, env, context); } - - try { - // https://github.com/cloudflare/kv-asset-handler#es-modules - return await getAssetFromKV( - { - request: request as unknown as Request, - waitUntil(promise) { - return context.waitUntil(promise); - }, - }, - { - cacheControl: assetsCacheControl, - mapRequestToAsset: baseURLModifier, - ASSET_NAMESPACE: env.__STATIC_CONTENT, - ASSET_MANIFEST: JSON.parse(manifest), - } - ); - } catch { - // Ignore - } }, }); - -function assetsCacheControl(_request: Request) { - const url = new URL(_request.url); - const meta = getPublicAssetMeta(url.pathname); - if (meta.maxAge) { - return { - browserTTL: meta.maxAge, - edgeTTL: meta.maxAge, - }; - } - return {}; -} - -const baseURLModifier = (request: Request) => { - const url = withoutBase(request.url, useRuntimeConfig().app.baseURL); - return mapRequestToAsset(new Request(url, request)); -}; diff --git a/test/fixture/nitro.config.ts b/test/fixture/nitro.config.ts index 90ec1699ac..097b9f27bf 100644 --- a/test/fixture/nitro.config.ts +++ b/test/fixture/nitro.config.ts @@ -3,7 +3,7 @@ import { defineNitroConfig } from "nitropack/config"; export default defineNitroConfig({ compressPublicAssets: true, - compatibilityDate: "2024-09-29", + compatibilityDate: "2024-09-19", imports: { presets: [ { diff --git a/test/fixture/wrangler.toml b/test/fixture/wrangler.toml index f3ddf5fd9b..84d615edf3 100644 --- a/test/fixture/wrangler.toml +++ b/test/fixture/wrangler.toml @@ -1,4 +1,3 @@ name = "nitro-test" - -[site] -bucket = ".output/public" +compatibility_date = "2024-09-19" +assets = { directory = "./.output/public/", binding = "ASSETS"} diff --git a/test/presets/cloudflare-module-legacy.test.ts b/test/presets/cloudflare-module-legacy.test.ts new file mode 100644 index 0000000000..6ac40d3c36 --- /dev/null +++ b/test/presets/cloudflare-module-legacy.test.ts @@ -0,0 +1,31 @@ +import { Miniflare } from "miniflare"; +import { resolve } from "pathe"; +import { Response as _Response } from "undici"; +import { describe } from "vitest"; + +import { setupTest, testNitro } from "../tests"; + +describe("nitro:preset:cloudflare-module", async () => { + const ctx = await setupTest("cloudflare-module-legacy", {}); + + testNitro(ctx, () => { + const mf = new Miniflare({ + modules: true, + scriptPath: resolve(ctx.outDir, "server/index.mjs"), + modulesRules: [{ type: "CompiledWasm", include: ["**/*.wasm"] }], + sitePath: resolve(ctx.outDir, "public"), + compatibilityFlags: ["streams_enable_constructors"], + bindings: { ...ctx.env }, + }); + + return async ({ url, headers, method, body }) => { + const res = await mf.dispatchFetch("http://localhost" + url, { + headers: headers || {}, + method: method || "GET", + redirect: "manual", + body, + }); + return res as unknown as Response; + }; + }); +}); diff --git a/test/presets/cloudflare-module.test.ts b/test/presets/cloudflare-module.test.ts index c1f1842edc..927cf441f6 100644 --- a/test/presets/cloudflare-module.test.ts +++ b/test/presets/cloudflare-module.test.ts @@ -13,7 +13,15 @@ describe("nitro:preset:cloudflare-module", async () => { modules: true, scriptPath: resolve(ctx.outDir, "server/index.mjs"), modulesRules: [{ type: "CompiledWasm", include: ["**/*.wasm"] }], - sitePath: resolve(ctx.outDir, "public"), + assets: { + directory: resolve(ctx.outDir, "public"), + routingConfig: { has_user_worker: true }, + assetConfig: { + // https://developers.cloudflare.com/workers/static-assets/routing/#routing-configuration + html_handling: "auto-trailing-slash" /* default */, + not_found_handling: "none" /* default */, + }, + }, compatibilityFlags: ["streams_enable_constructors"], bindings: { ...ctx.env }, }); diff --git a/test/tests.ts b/test/tests.ts index 1d23a26968..ccb94269df 100644 --- a/test/tests.ts +++ b/test/tests.ts @@ -86,6 +86,7 @@ export async function setupTest( isWorker: [ "cloudflare-worker", "cloudflare-module", + "cloudflare-module-legacy", "cloudflare-pages", "vercel-edge", "winterjs", @@ -226,8 +227,11 @@ export function testNitro( const { data: helloData } = await callHandler({ url: "/api/hello" }); expect(helloData).to.toMatchObject({ message: "Hello API" }); - const { data: heyData } = await callHandler({ url: "/api/hey" }); - expect(heyData).to.have.string("Hey API"); + if (ctx.nitro?.options.serveStatic) { + // /api/hey is expected to be prerendered + const { data: heyData } = await callHandler({ url: "/api/hey" }); + expect(heyData).to.have.string("Hey API"); + } const { data: kebabData } = await callHandler({ url: "/api/kebab" }); expect(kebabData).to.have.string("hello-world"); @@ -339,8 +343,6 @@ export function testNitro( }, }); expect(status).toBe(503); - const { data: heyData } = await callHandler({ url: "/api/hey" }); - expect(heyData).to.have.string("Hey API"); }); it("universal import.meta", async () => {