Skip to content

Commit

Permalink
feat(cloudflare-module): use new workers static assets (#2800)
Browse files Browse the repository at this point in the history
  • Loading branch information
pi0 authored Oct 18, 2024
1 parent 89cc12b commit 9df404a
Show file tree
Hide file tree
Showing 12 changed files with 191 additions and 67 deletions.
2 changes: 1 addition & 1 deletion playground/nitro.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { defineNitroConfig } from "nitropack/config";

export default defineNitroConfig({
compatibilityDate: "2024-09-29",
compatibilityDate: "2024-09-19",
});
3 changes: 3 additions & 0 deletions playground/wrangler.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
name = "nitro-test"
compatibility_date = "2024-09-19"
assets = { directory = "./.output/public/", binding = "ASSETS" }
4 changes: 2 additions & 2 deletions src/presets/_types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 & {});
47 changes: 45 additions & 2 deletions src/presets/cloudflare/preset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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,
Expand Down
10 changes: 7 additions & 3 deletions src/presets/cloudflare/runtime/_module-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,26 @@ type MaybePromise<T> = T | Promise<T>;

export function createHandler<Env>(hooks: {
fetch: (
...params: Parameters<NonNullable<ExportedHandler<Env>["fetch"]>>
...params: [
...Parameters<NonNullable<ExportedHandler<Env>["fetch"]>>,
url: URL,
]
) => MaybePromise<Response | CF.Response | undefined>;
}) {
const nitroApp = useNitroApp();

return <ExportedHandler<Env>>{
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());
Expand Down
74 changes: 74 additions & 0 deletions src/presets/cloudflare/runtime/cloudflare-module-legacy.ts
Original file line number Diff line number Diff line change
@@ -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<Env>({
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));
};
60 changes: 10 additions & 50 deletions src/presets/cloudflare/runtime/cloudflare-module.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,27 @@
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
? wsAdapter(nitroApp.h3App.websocket)
: undefined;

interface Env {
__STATIC_CONTENT?: any;
ASSETS?: { fetch: typeof fetch };
}

export default createHandler<Env>({
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 (
Expand All @@ -33,42 +30,5 @@ export default createHandler<Env>({
) {
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));
};
2 changes: 1 addition & 1 deletion test/fixture/nitro.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { defineNitroConfig } from "nitropack/config";

export default defineNitroConfig({
compressPublicAssets: true,
compatibilityDate: "2024-09-29",
compatibilityDate: "2024-09-19",
imports: {
presets: [
{
Expand Down
5 changes: 2 additions & 3 deletions test/fixture/wrangler.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
name = "nitro-test"

[site]
bucket = ".output/public"
compatibility_date = "2024-09-19"
assets = { directory = "./.output/public/", binding = "ASSETS"}
31 changes: 31 additions & 0 deletions test/presets/cloudflare-module-legacy.test.ts
Original file line number Diff line number Diff line change
@@ -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;
};
});
});
10 changes: 9 additions & 1 deletion test/presets/cloudflare-module.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
});
Expand Down
10 changes: 6 additions & 4 deletions test/tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export async function setupTest(
isWorker: [
"cloudflare-worker",
"cloudflare-module",
"cloudflare-module-legacy",
"cloudflare-pages",
"vercel-edge",
"winterjs",
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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 () => {
Expand Down

0 comments on commit 9df404a

Please sign in to comment.