Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/eleven-sloths-punch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@opennextjs/cloudflare": patch
---

Extract the worker init code to a separate file
4 changes: 4 additions & 0 deletions packages/cloudflare/src/cli/build/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 });

Expand Down
40 changes: 0 additions & 40 deletions packages/cloudflare/src/cli/build/bundle-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,46 +144,6 @@ export async function bundleServer(buildOpts: BuildOptions): Promise<void> {
"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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
27 changes: 27 additions & 0 deletions packages/cloudflare/src/cli/build/open-next/compile-init.ts
Original file line number Diff line number Diff line change
@@ -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()),
},
});
}
133 changes: 133 additions & 0 deletions packages/cloudflare/src/cli/templates/init.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/**
* 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<Response>
): Promise<Response> {
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 });

// 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 __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,
// This is only needed for an external middleware bundle
__dangerous_ON_edge_converter_returns_request: true,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to add that actually for an external middleware ? This means that the output of the middleware will be a Request.
It's fine when everything was in 1 worker, but if we have multiple worker it won't work as expected (or it will require a custom converter/wrapper to forward the request)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's right, I'll remove this.
Thanks for catching that.

//@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,
},
});
}
66 changes: 3 additions & 63 deletions packages/cloudflare/src/cli/templates/worker.ts
Original file line number Diff line number Diff line change
@@ -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<CloudflareContext>();

// 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/")) {
Expand Down Expand Up @@ -57,45 +39,3 @@ export default {
});
},
} as ExportedHandler<CloudflareEnv>;

/**
* 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,
},
});
}