Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
12 changes: 11 additions & 1 deletion examples/middleware/open-next.config.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
import type { OpenNextConfig } from "@opennextjs/aws/types/open-next";

const config: OpenNextConfig = {
default: {},
default: {
override: {
wrapper: "cloudflare-streaming",
converter: "edge",
// Unused implementation
incrementalCache: "dummy",
tagCache: "dummy",
queue: "dummy",
},
},

middleware: {
external: true,
override: {
wrapper: "cloudflare",
converter: "edge",
proxyExternalRequest: "fetch",
},
},
};
Expand Down
2 changes: 1 addition & 1 deletion examples/middleware/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,5 @@
]
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules", "open-next.config.ts"]
"exclude": ["node_modules", "open-next.config.ts", "worker.ts"]
}
3 changes: 1 addition & 2 deletions examples/middleware/wrangler.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
#:schema node_modules/wrangler/config-schema.json
name = "middleware"
main = ".open-next/index.mjs"

main = ".open-next/worker.ts"
compatibility_date = "2024-09-23"
compatibility_flags = ["nodejs_compat"]

Expand Down
42 changes: 12 additions & 30 deletions packages/cloudflare/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ You can use [`create-next-app`](https://nextjs.org/docs/pages/api-reference/cli/
```toml
#:schema node_modules/wrangler/config-schema.json
name = "<your-app-name>"
main = ".open-next/index.mjs"
main = ".open-next/worker.ts"

compatibility_date = "2024-09-23"
compatibility_flags = ["nodejs_compat"]
Expand All @@ -44,8 +44,12 @@ import type { OpenNextConfig } from "open-next/types/open-next";
const config: OpenNextConfig = {
default: {
override: {
wrapper: "cloudflare",
wrapper: "cloudflare-streaming",
converter: "edge",
// Unused implementation
incrementalCache: "dummy",
tagCache: "dummy",
queue: "dummy",
},
},

Expand All @@ -54,42 +58,20 @@ const config: OpenNextConfig = {
override: {
wrapper: "cloudflare",
converter: "edge",
proxyExternalRequest: "fetch",
},
},

dangerous: {
disableTagCache: true,
disableIncrementalCache: true,
},
};

export default config;
```

You can enable Incremental Static Regeneration ([ISR](https://nextjs.org/docs/app/building-your-application/data-fetching/incremental-static-regeneration)) by adding a KV binding named `NEXT_CACHE_WORKERS_KV` to your `wrangler.toml`:

- Create the binding

```bash
npx wrangler kv namespace create NEXT_CACHE_WORKERS_KV
# or
pnpm wrangler kv namespace create NEXT_CACHE_WORKERS_KV
# or
yarn wrangler kv namespace create NEXT_CACHE_WORKERS_KV
# or
bun wrangler kv namespace create NEXT_CACHE_WORKERS_KV
```

- Paste the snippet to your `wrangler.toml`:

```bash
[[kv_namespaces]]
binding = "NEXT_CACHE_WORKERS_KV"
id = "..."
```
## Know issues

> [!WARNING]
> The current support for ISR is limited.
- Next cache is not supported in the experimental branch yet
- `▲ [WARNING] Suspicious assignment to defined constant "process.env.NODE_ENV" [assign-to-define]` can safely be ignored
- You should test with cache disabled in the developper tools
- Maybe more, still experimental...

## Local development

Expand Down
6 changes: 5 additions & 1 deletion packages/cloudflare/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@ declare global {
SKIP_NEXT_APP_BUILD?: string;
NEXT_PRIVATE_DEBUG_CACHE?: string;
__OPENNEXT_KV_BINDING_NAME: string;
[key: string]: string | Fetcher;
OPEN_NEXT_ORIGIN: string;
}
}

interface Window {
[key: string]: string | Fetcher;
}
}

export {};
2 changes: 1 addition & 1 deletion packages/cloudflare/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
"vitest": "catalog:"
},
"dependencies": {
"@opennextjs/aws": "https://pkg.pr.new/@opennextjs/aws@5c0e121",
"@opennextjs/aws": "https://pkg.pr.new/@opennextjs/aws@0ac604e",
"ts-morph": "catalog:"
},
"peerDependencies": {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { readFileSync } from "node:fs";
import fs from "node:fs";
import { readFile, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import path from "node:path";
import { fileURLToPath } from "node:url";

import type { BuildOptions } from "@opennextjs/aws/build/helper.js";
import { build, Plugin } from "esbuild";

import { Config } from "../config";
Expand All @@ -20,37 +21,37 @@ import { patchWranglerDeps } from "./patches/to-investigate/wrangler-deps";
import { copyPrerenderedRoutes } from "./utils";

/** The dist directory of the Cloudflare adapter package */
const packageDistDir = join(dirname(fileURLToPath(import.meta.url)), "..");
const packageDistDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "..");

/**
* Using the Next.js build output in the `.next` directory builds a workerd compatible output
*
* @param outputDir the directory where to save the output
* @param config
* Bundle the Open Next server.
*/
export async function buildWorker(config: Config): Promise<void> {
export async function bundleServer(config: Config, openNextOptions: BuildOptions): Promise<void> {
// Copy over prerendered assets (e.g. SSG routes)
copyPrerenderedRoutes(config);

copyPackageCliFiles(packageDistDir, config);

const workerEntrypoint = join(config.paths.internal.templates, "worker.ts");
const workerOutputFile = join(config.paths.output.root, "index.mjs");
copyPackageCliFiles(packageDistDir, config, openNextOptions);

const nextConfigStr =
readFileSync(join(config.paths.output.standaloneApp, "/server.js"), "utf8")?.match(
/const nextConfig = ({.+?})\n/
)?.[1] ?? {};
fs
.readFileSync(path.join(config.paths.output.standaloneApp, "/server.js"), "utf8")
?.match(/const nextConfig = ({.+?})\n/)?.[1] ?? {};

console.log(`\x1b[35m⚙️ Bundling the worker file...\n\x1b[0m`);
console.log(`\x1b[35m⚙️ Bundling the OpenNext server...\n\x1b[0m`);

patchWranglerDeps(config);
updateWebpackChunksFile(config);

const { appBuildOutputPath, appPath, outputDir, monorepoRoot } = openNextOptions;
const outputPath = path.join(outputDir, "server-functions", "default");
const packagePath = path.relative(monorepoRoot, appBuildOutputPath);
const openNextServer = path.join(outputPath, packagePath, `index.mjs`);
const openNextServerBundle = path.join(outputPath, packagePath, `handler.mjs`);

await build({
entryPoints: [workerEntrypoint],
entryPoints: [openNextServer],
bundle: true,
outfile: workerOutputFile,
outfile: openNextServerBundle,
format: "esm",
target: "esnext",
minify: false,
Expand All @@ -60,15 +61,15 @@ export async function buildWorker(config: Config): Promise<void> {
// Note: we apply an empty shim to next/dist/compiled/ws because it generates two `eval`s:
// eval("require")("bufferutil");
// eval("require")("utf-8-validate");
"next/dist/compiled/ws": join(config.paths.internal.templates, "shims", "empty.ts"),
"next/dist/compiled/ws": path.join(config.paths.internal.templates, "shims", "empty.ts"),
// Note: we apply an empty shim to next/dist/compiled/edge-runtime since (amongst others) it generated the following `eval`:
// eval(getModuleCode)(module, module.exports, throwingRequire, params.context, ...Object.values(params.scopedContext));
// which comes from https://github.com/vercel/edge-runtime/blob/6e96b55f/packages/primitives/src/primitives/load.js#L57-L63
// QUESTION: Why did I encountered this but mhart didn't?
"next/dist/compiled/edge-runtime": join(config.paths.internal.templates, "shims", "empty.ts"),
"next/dist/compiled/edge-runtime": path.join(config.paths.internal.templates, "shims", "empty.ts"),
// `@next/env` is a library Next.js uses for loading dotenv files, for obvious reasons we need to stub it here
// source: https://github.com/vercel/next.js/tree/0ac10d79720/packages/next-env
"@next/env": join(config.paths.internal.templates, "shims", "env.ts"),
"@next/env": path.join(config.paths.internal.templates, "shims", "env.ts"),
},
define: {
// config file used by Next.js, see: https://github.com/vercel/next.js/blob/68a7128/packages/next/src/build/utils.ts#L2137-L2139
Expand All @@ -86,15 +87,11 @@ export async function buildWorker(config: Config): Promise<void> {
// We need to set platform to node so that esbuild doesn't complain about the node imports
platform: "node",
banner: {
// `__dirname` is 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
js: `
${
/*
`__dirname` is 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.__dirname ??= "";

// Do not crash on cache not supported
// https://github.com/cloudflare/workerd/pull/2434
Expand All @@ -106,15 +103,15 @@ globalThis.fetch = (input, init) => {
}
return curFetch(input, init);
};
import { Readable } from 'node:stream';
import __cf_stream from 'node:stream';
fetch = globalThis.fetch;
const CustomRequest = class extends globalThis.Request {
constructor(input, init) {
if (init) {
delete init.cache;
if (init.body?.__node_stream__ === true) {
// https://github.com/cloudflare/workerd/issues/2746
init.body = Readable.toWeb(init.body);
init.body = __cf_stream.Readable.toWeb(init.body);
}
}
super(input, init);
Expand All @@ -128,9 +125,18 @@ globalThis.__dangerous_ON_edge_converter_returns_request = true;
},
});

await updateWorkerBundledCode(workerOutputFile, config);
await updateWorkerBundledCode(openNextServerBundle, config, openNextOptions);

console.log(`\x1b[35mWorker saved in \`${workerOutputFile}\` 🚀\n\x1b[0m`);
const isMonorepo = monorepoRoot !== appPath;
if (isMonorepo) {
const packagePosixPath = packagePath.split(path.sep).join(path.posix.sep);
fs.writeFileSync(
path.join(outputPath, "handler.mjs"),
`export * from "./${packagePosixPath}/handler.mjs";`
);
}

console.log(`\x1b[35mWorker saved in \`${openNextServerBundle}\` 🚀\n\x1b[0m`);
}

/**
Expand All @@ -141,7 +147,11 @@ globalThis.__dangerous_ON_edge_converter_returns_request = true;
* @param workerOutputFile
* @param config
*/
async function updateWorkerBundledCode(workerOutputFile: string, config: Config): Promise<void> {
async function updateWorkerBundledCode(
workerOutputFile: string,
config: Config,
openNextOptions: BuildOptions
): Promise<void> {
const originalCode = await readFile(workerOutputFile, "utf8");

let patchedCode = originalCode;
Expand All @@ -151,10 +161,15 @@ async function updateWorkerBundledCode(workerOutputFile: string, config: Config)
patchedCode = inlineNextRequire(patchedCode, config);
patchedCode = patchFindDir(patchedCode, config);
patchedCode = inlineEvalManifest(patchedCode, config);
patchedCode = await patchCache(patchedCode, config);
patchedCode = await patchCache(patchedCode, openNextOptions);
patchedCode = inlineMiddlewareManifestRequire(patchedCode, config);
patchedCode = patchExceptionBubbling(patchedCode);

patchedCode = patchedCode
// workers do not support dynamic require nor require.resolve
.replace("patchAsyncStorage();", "//patchAsyncStorage();")
.replace('require.resolve("./cache.cjs")', '"unused"');

await writeFile(workerOutputFile, patchedCode);
}

Expand All @@ -164,10 +179,10 @@ function createFixRequiresESBuildPlugin(config: Config): Plugin {
setup(build) {
// Note: we (empty) shim require-hook modules as they generate problematic code that uses requires
build.onResolve({ filter: /^\.\/require-hook$/ }, () => ({
path: join(config.paths.internal.templates, "shims", "empty.ts"),
path: path.join(config.paths.internal.templates, "shims", "empty.ts"),
}));
build.onResolve({ filter: /\.\/lib\/node-fs-methods$/ }, () => ({
path: join(config.paths.internal.templates, "shims", "empty.ts"),
path: path.join(config.paths.internal.templates, "shims", "empty.ts"),
}));
},
};
Expand Down
Loading