diff --git a/.changeset/eleven-sloths-punch.md b/.changeset/eleven-sloths-punch.md new file mode 100644 index 00000000..ba3cba92 --- /dev/null +++ b/.changeset/eleven-sloths-punch.md @@ -0,0 +1,5 @@ +--- +"@opennextjs/cloudflare": patch +--- + +Extract the worker init code to a separate file diff --git a/packages/cloudflare/src/cli/build/build.ts b/packages/cloudflare/src/cli/build/build.ts index b0e857e2..3c02b5e1 100644 --- a/packages/cloudflare/src/cli/build/build.ts +++ b/packages/cloudflare/src/cli/build/build.ts @@ -12,6 +12,7 @@ import type { ProjectOptions } from "../project-options.js"; import { bundleServer } from "./bundle-server.js"; import { compileCacheAssetsManifestSqlFile } from "./open-next/compile-cache-assets-manifest.js"; import { compileEnvFiles } from "./open-next/compile-env-files.js"; +import { compileInit } from "./open-next/compile-init.js"; import { compileDurableObjects } from "./open-next/compileDurableObjects.js"; import { createServerBundle } from "./open-next/createServerBundle.js"; import { createWranglerConfigIfNotExistent } from "./utils/index.js"; @@ -63,6 +64,9 @@ export async function build( // Compile .env files compileEnvFiles(options); + // Compile workerd init + compileInit(options); + // Compile middleware await createMiddleware(options, { forceOnlyBuildOnce: true }); diff --git a/packages/cloudflare/src/cli/build/bundle-server.ts b/packages/cloudflare/src/cli/build/bundle-server.ts index 6b7d6296..954eaa34 100644 --- a/packages/cloudflare/src/cli/build/bundle-server.ts +++ b/packages/cloudflare/src/cli/build/bundle-server.ts @@ -144,46 +144,6 @@ export async function bundleServer(buildOpts: BuildOptions): Promise { "process.env.__NEXT_EXPERIMENTAL_REACT": `${needsExperimentalReact(nextConfig)}`, }, platform: "node", - banner: { - js: ` -// Used by unbundled js files (which don't inherit the __dirname present in the define field) -// so we also need to set it on the global scope -// Note: this was hit in the next/dist/compiled/@opentelemetry/api module -globalThis.__dirname ??= ""; -globalThis.__filename ??= ""; - -// Do not crash on cache not supported -// https://github.com/cloudflare/workerd/pull/2434 -// compatibility flag "cache_option_enabled" -> does not support "force-cache" -const curFetch = globalThis.fetch; -globalThis.fetch = (input, init) => { - if (init) { - delete init.cache; - } - return curFetch(input, init); -}; -import __cf_stream from 'node:stream'; -fetch = globalThis.fetch; -const CustomRequest = class extends globalThis.Request { - constructor(input, init) { - if (init) { - delete init.cache; - // https://github.com/cloudflare/workerd/issues/2746 - // https://github.com/cloudflare/workerd/issues/3245 - Object.defineProperty(init, "body", { - value: init.body instanceof __cf_stream.Readable ? ReadableStream.from(init.body) : init.body - }); - } - super(input, init); - } -}; -globalThis.Request = CustomRequest; -Request = globalThis.Request; -// Makes the edge converter returns either a Response or a Request. -globalThis.__dangerous_ON_edge_converter_returns_request = true; -globalThis.__BUILD_TIMESTAMP_MS__ = ${Date.now()}; -`, - }, }); fs.writeFileSync(openNextServerBundle + ".meta.json", JSON.stringify(result.metafile, null, 2)); diff --git a/packages/cloudflare/src/cli/build/open-next/compile-env-files.ts b/packages/cloudflare/src/cli/build/open-next/compile-env-files.ts index 1709b5cd..e8043779 100644 --- a/packages/cloudflare/src/cli/build/open-next/compile-env-files.ts +++ b/packages/cloudflare/src/cli/build/open-next/compile-env-files.ts @@ -9,7 +9,7 @@ import { extractProjectEnvVars } from "../utils/index.js"; * Compiles the values extracted from the project's env files to the output directory for use in the worker. */ export function compileEnvFiles(buildOpts: BuildOptions) { - const envDir = path.join(buildOpts.outputDir, "env"); + const envDir = path.join(buildOpts.outputDir, "cloudflare"); fs.mkdirSync(envDir, { recursive: true }); ["production", "development", "test"].forEach((mode) => fs.appendFileSync( diff --git a/packages/cloudflare/src/cli/build/open-next/compile-init.ts b/packages/cloudflare/src/cli/build/open-next/compile-init.ts new file mode 100644 index 00000000..e0a1a829 --- /dev/null +++ b/packages/cloudflare/src/cli/build/open-next/compile-init.ts @@ -0,0 +1,27 @@ +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +import type { BuildOptions } from "@opennextjs/aws/build/helper"; +import { build } from "esbuild"; + +/** + * Compiles the initialization code for the workerd runtime + */ +export async function compileInit(options: BuildOptions) { + const currentDir = path.join(path.dirname(fileURLToPath(import.meta.url))); + const templatesDir = path.join(currentDir, "../../templates"); + const initPath = path.join(templatesDir, "init.js"); + + await build({ + entryPoints: [initPath], + outdir: path.join(options.outputDir, "cloudflare"), + bundle: false, + minify: false, + format: "esm", + target: "esnext", + platform: "node", + define: { + __BUILD_TIMESTAMP_MS__: JSON.stringify(Date.now()), + }, + }); +} diff --git a/packages/cloudflare/src/cli/templates/init.ts b/packages/cloudflare/src/cli/templates/init.ts new file mode 100644 index 00000000..00e84c07 --- /dev/null +++ b/packages/cloudflare/src/cli/templates/init.ts @@ -0,0 +1,128 @@ +/** + * Initialization for the workerd runtime. + * + * The file must be imported at the top level the worker. + */ + +import { AsyncLocalStorage } from "node:async_hooks"; +import process from "node:process"; +import stream from "node:stream"; + +// @ts-expect-error: resolved by wrangler build +import * as nextEnvVars from "./next-env.mjs"; + +const cloudflareContextALS = new AsyncLocalStorage(); + +// Note: this symbol needs to be kept in sync with `src/api/get-cloudflare-context.ts` +Object.defineProperty(globalThis, Symbol.for("__cloudflare-context__"), { + get() { + return cloudflareContextALS.getStore(); + }, +}); + +/** + * Executes the handler with the Cloudflare context. + */ +export async function runWithCloudflareRequestContext( + request: Request, + env: CloudflareEnv, + ctx: ExecutionContext, + handler: () => Promise +): Promise { + init(request, env); + + return cloudflareContextALS.run({ env, ctx, cf: request.cf }, handler); +} + +let initialized = false; + +/** + * Initializes the runtime on the first call, + * no-op on subsequent invocations. + */ +function init(request: Request, env: CloudflareEnv) { + if (initialized) { + return; + } + initialized = true; + + const url = new URL(request.url); + + initRuntime(); + populateProcessEnv(url, env); +} + +function initRuntime() { + // Some packages rely on `process.version` and `process.versions.node` (i.e. Jose@4) + // TODO: Remove when https://github.com/unjs/unenv/pull/493 is merged + Object.assign(process, { version: process.version || "v22.14.0" }); + // @ts-expect-error Node type does not match workerd + Object.assign(process.versions, { node: "22.14.0", ...process.versions }); + + globalThis.__dirname ??= ""; + globalThis.__filename ??= ""; + + // Do not crash on cache not supported + // https://github.com/cloudflare/workerd/pull/2434 + // compatibility flag "cache_option_enabled" -> does not support "force-cache" + const __original_fetch = globalThis.fetch; + + globalThis.fetch = (input, init) => { + if (init) { + delete (init as { cache: unknown }).cache; + } + return __original_fetch(input, init); + }; + + const CustomRequest = class extends globalThis.Request { + constructor(input: RequestInfo | URL, init?: RequestInit) { + if (init) { + delete (init as { cache: unknown }).cache; + // https://github.com/cloudflare/workerd/issues/2746 + // https://github.com/cloudflare/workerd/issues/3245 + Object.defineProperty(init, "body", { + // @ts-ignore + value: init.body instanceof stream.Readable ? ReadableStream.from(init.body) : init.body, + }); + } + super(input, init); + } + }; + + Object.assign(globalThis, { + Request: CustomRequest, + //@ts-expect-error Inline at build time by ESBuild + __BUILD_TIMESTAMP_MS__: __BUILD_TIMESTAMP_MS__, + }); +} + +/** + * Populate process.env with: + * - the environment variables and secrets from the cloudflare platform + * - the variables from Next .env* files + * - the origin resolver information + */ +function populateProcessEnv(url: URL, env: CloudflareEnv) { + for (const [key, value] of Object.entries(env)) { + if (typeof value === "string") { + process.env[key] = value; + } + } + + const mode = env.NEXTJS_ENV ?? "production"; + if (nextEnvVars[mode]) { + for (const key in nextEnvVars[mode]) { + process.env[key] ??= nextEnvVars[mode][key]; + } + } + + // Set the default Origin for the origin resolver. + // This is only needed for an external middleware bundle + process.env.OPEN_NEXT_ORIGIN = JSON.stringify({ + default: { + host: url.hostname, + protocol: url.protocol.slice(0, -1), + port: url.port, + }, + }); +} diff --git a/packages/cloudflare/src/cli/templates/worker.ts b/packages/cloudflare/src/cli/templates/worker.ts index eb49fb62..f265c289 100644 --- a/packages/cloudflare/src/cli/templates/worker.ts +++ b/packages/cloudflare/src/cli/templates/worker.ts @@ -1,34 +1,16 @@ -import { AsyncLocalStorage } from "node:async_hooks"; -import process from "node:process"; - -import type { CloudflareContext } from "../../api"; -// @ts-expect-error: resolved by wrangler build -import * as nextEnvVars from "./env/next-env.mjs"; - -const cloudflareContextALS = new AsyncLocalStorage(); - -// Note: this symbol needs to be kept in sync with `src/api/get-cloudflare-context.ts` -Object.defineProperty(globalThis, Symbol.for("__cloudflare-context__"), { - get() { - return cloudflareContextALS.getStore(); - }, -}); +//@ts-expect-error: Will be resolved by wrangler build +import { runWithCloudflareRequestContext } from "./cloudflare/init.js"; //@ts-expect-error: Will be resolved by wrangler build export { DOQueueHandler } from "./.build/durable-objects/queue.js"; //@ts-expect-error: Will be resolved by wrangler build export { DOShardedTagCache } from "./.build/durable-objects/sharded-tag-cache.js"; -// Populate process.env on the first request -let processEnvPopulated = false; - export default { async fetch(request, env, ctx) { - return cloudflareContextALS.run({ env, ctx, cf: request.cf }, async () => { + return runWithCloudflareRequestContext(request, env, ctx, async () => { const url = new URL(request.url); - populateProcessEnv(url, env); - // Serve images in development. // Note: "/cdn-cgi/image/..." requests do not reach production workers. if (url.pathname.startsWith("/cdn-cgi/image/")) { @@ -57,45 +39,3 @@ export default { }); }, } as ExportedHandler; - -/** - * Populate process.env with: - * - the environment variables and secrets from the cloudflare platform - * - the variables from Next .env* files - * - the origin resolver information - */ -function populateProcessEnv(url: URL, env: CloudflareEnv) { - if (processEnvPopulated) { - return; - } - - // Some packages rely on `process.version` and `process.versions.node` (i.e. Jose@4) - // TODO: Remove when https://github.com/unjs/unenv/pull/493 is merged - Object.assign(process, { version: process.version || "v22.14.0" }); - // @ts-expect-error Node type does not match workerd - Object.assign(process.versions, { node: "22.14.0", ...process.versions }); - - processEnvPopulated = true; - - for (const [key, value] of Object.entries(env)) { - if (typeof value === "string") { - process.env[key] = value; - } - } - - const mode = env.NEXTJS_ENV ?? "production"; - if (nextEnvVars[mode]) { - for (const key in nextEnvVars[mode]) { - process.env[key] ??= nextEnvVars[mode][key]; - } - } - - // Set the default Origin for the origin resolver. - process.env.OPEN_NEXT_ORIGIN = JSON.stringify({ - default: { - host: url.hostname, - protocol: url.protocol.slice(0, -1), - port: url.port, - }, - }); -}