Skip to content

Commit 840be1f

Browse files
PeterswisJustinvicb
authored andcommitted
feat: integrate the OpenNext server
1 parent 7408e41 commit 840be1f

File tree

16 files changed

+632
-449
lines changed

16 files changed

+632
-449
lines changed

examples/middleware/open-next.config.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,23 @@
11
import type { OpenNextConfig } from "@opennextjs/aws/types/open-next";
22

33
const config: OpenNextConfig = {
4-
default: {},
4+
default: {
5+
override: {
6+
wrapper: "cloudflare-streaming",
7+
converter: "edge",
8+
// Unused implementation
9+
incrementalCache: "dummy",
10+
tagCache: "dummy",
11+
queue: "dummy",
12+
},
13+
},
514

615
middleware: {
716
external: true,
817
override: {
918
wrapper: "cloudflare",
1019
converter: "edge",
20+
proxyExternalRequest: "fetch",
1121
},
1222
},
1323
};

examples/middleware/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,5 @@
2121
]
2222
},
2323
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
24-
"exclude": ["node_modules", "open-next.config.ts"]
24+
"exclude": ["node_modules", "open-next.config.ts", "worker.ts"]
2525
}

examples/middleware/wrangler.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
#:schema node_modules/wrangler/config-schema.json
22
name = "middleware"
3-
main = ".open-next/index.mjs"
4-
3+
main = ".open-next/worker.ts"
54
compatibility_date = "2024-09-23"
65
compatibility_flags = ["nodejs_compat"]
76

packages/cloudflare/README.md

Lines changed: 12 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ You can use [`create-next-app`](https://nextjs.org/docs/pages/api-reference/cli/
2727
```toml
2828
#:schema node_modules/wrangler/config-schema.json
2929
name = "<your-app-name>"
30-
main = ".open-next/index.mjs"
30+
main = ".open-next/worker.ts"
3131

3232
compatibility_date = "2024-09-23"
3333
compatibility_flags = ["nodejs_compat"]
@@ -44,8 +44,12 @@ import type { OpenNextConfig } from "open-next/types/open-next";
4444
const config: OpenNextConfig = {
4545
default: {
4646
override: {
47-
wrapper: "cloudflare",
47+
wrapper: "cloudflare-streaming",
4848
converter: "edge",
49+
// Unused implementation
50+
incrementalCache: "dummy",
51+
tagCache: "dummy",
52+
queue: "dummy",
4953
},
5054
},
5155

@@ -54,42 +58,20 @@ const config: OpenNextConfig = {
5458
override: {
5559
wrapper: "cloudflare",
5660
converter: "edge",
61+
proxyExternalRequest: "fetch",
5762
},
5863
},
59-
60-
dangerous: {
61-
disableTagCache: true,
62-
disableIncrementalCache: true,
63-
},
6464
};
6565

6666
export default config;
6767
```
6868

69-
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`:
70-
71-
- Create the binding
72-
73-
```bash
74-
npx wrangler kv namespace create NEXT_CACHE_WORKERS_KV
75-
# or
76-
pnpm wrangler kv namespace create NEXT_CACHE_WORKERS_KV
77-
# or
78-
yarn wrangler kv namespace create NEXT_CACHE_WORKERS_KV
79-
# or
80-
bun wrangler kv namespace create NEXT_CACHE_WORKERS_KV
81-
```
82-
83-
- Paste the snippet to your `wrangler.toml`:
84-
85-
```bash
86-
[[kv_namespaces]]
87-
binding = "NEXT_CACHE_WORKERS_KV"
88-
id = "..."
89-
```
69+
## Know issues
9070

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

9476
## Local development
9577

packages/cloudflare/env.d.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,13 @@ declare global {
66
SKIP_NEXT_APP_BUILD?: string;
77
NEXT_PRIVATE_DEBUG_CACHE?: string;
88
__OPENNEXT_KV_BINDING_NAME: string;
9-
[key: string]: string | Fetcher;
9+
OPEN_NEXT_ORIGIN: string;
1010
}
1111
}
12+
13+
interface Window {
14+
[key: string]: string | Fetcher;
15+
}
1216
}
1317

1418
export {};

packages/cloudflare/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
"vitest": "catalog:"
6161
},
6262
"dependencies": {
63-
"@opennextjs/aws": "https://pkg.pr.new/@opennextjs/aws@5c0e121",
63+
"@opennextjs/aws": "https://pkg.pr.new/@opennextjs/aws@0ac604e",
6464
"ts-morph": "catalog:"
6565
},
6666
"peerDependencies": {

packages/cloudflare/src/cli/build/build-worker.ts renamed to packages/cloudflare/src/cli/build/bundle-server.ts

Lines changed: 52 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { readFileSync } from "node:fs";
1+
import fs from "node:fs";
22
import { readFile, writeFile } from "node:fs/promises";
3-
import { dirname, join } from "node:path";
3+
import path from "node:path";
44
import { fileURLToPath } from "node:url";
55

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

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

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

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

35-
copyPackageCliFiles(packageDistDir, config);
36-
37-
const workerEntrypoint = join(config.paths.internal.templates, "worker.ts");
38-
const workerOutputFile = join(config.paths.output.root, "index.mjs");
33+
copyPackageCliFiles(packageDistDir, config, openNextOptions);
3934

4035
const nextConfigStr =
41-
readFileSync(join(config.paths.output.standaloneApp, "/server.js"), "utf8")?.match(
42-
/const nextConfig = ({.+?})\n/
43-
)?.[1] ?? {};
36+
fs
37+
.readFileSync(path.join(config.paths.output.standaloneApp, "/server.js"), "utf8")
38+
?.match(/const nextConfig = ({.+?})\n/)?.[1] ?? {};
4439

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

4742
patchWranglerDeps(config);
4843
updateWebpackChunksFile(config);
4944

45+
const { appBuildOutputPath, appPath, outputDir, monorepoRoot } = openNextOptions;
46+
const outputPath = path.join(outputDir, "server-functions", "default");
47+
const packagePath = path.relative(monorepoRoot, appBuildOutputPath);
48+
const openNextServer = path.join(outputPath, packagePath, `index.mjs`);
49+
const openNextServerBundle = path.join(outputPath, packagePath, `handler.mjs`);
50+
5051
await build({
51-
entryPoints: [workerEntrypoint],
52+
entryPoints: [openNextServer],
5253
bundle: true,
53-
outfile: workerOutputFile,
54+
outfile: openNextServerBundle,
5455
format: "esm",
5556
target: "esnext",
5657
minify: false,
@@ -60,15 +61,15 @@ export async function buildWorker(config: Config): Promise<void> {
6061
// Note: we apply an empty shim to next/dist/compiled/ws because it generates two `eval`s:
6162
// eval("require")("bufferutil");
6263
// eval("require")("utf-8-validate");
63-
"next/dist/compiled/ws": join(config.paths.internal.templates, "shims", "empty.ts"),
64+
"next/dist/compiled/ws": path.join(config.paths.internal.templates, "shims", "empty.ts"),
6465
// Note: we apply an empty shim to next/dist/compiled/edge-runtime since (amongst others) it generated the following `eval`:
6566
// eval(getModuleCode)(module, module.exports, throwingRequire, params.context, ...Object.values(params.scopedContext));
6667
// which comes from https://github.com/vercel/edge-runtime/blob/6e96b55f/packages/primitives/src/primitives/load.js#L57-L63
6768
// QUESTION: Why did I encountered this but mhart didn't?
68-
"next/dist/compiled/edge-runtime": join(config.paths.internal.templates, "shims", "empty.ts"),
69+
"next/dist/compiled/edge-runtime": path.join(config.paths.internal.templates, "shims", "empty.ts"),
6970
// `@next/env` is a library Next.js uses for loading dotenv files, for obvious reasons we need to stub it here
7071
// source: https://github.com/vercel/next.js/tree/0ac10d79720/packages/next-env
71-
"@next/env": join(config.paths.internal.templates, "shims", "env.ts"),
72+
"@next/env": path.join(config.paths.internal.templates, "shims", "env.ts"),
7273
},
7374
define: {
7475
// config file used by Next.js, see: https://github.com/vercel/next.js/blob/68a7128/packages/next/src/build/utils.ts#L2137-L2139
@@ -86,15 +87,11 @@ export async function buildWorker(config: Config): Promise<void> {
8687
// We need to set platform to node so that esbuild doesn't complain about the node imports
8788
platform: "node",
8889
banner: {
90+
// `__dirname` is used by unbundled js files (which don't inherit the `__dirname` present in the `define` field)
91+
// so we also need to set it on the global scope
92+
// Note: this was hit in the `next/dist/compiled/@opentelemetry/api` module
8993
js: `
90-
${
91-
/*
92-
`__dirname` is used by unbundled js files (which don't inherit the `__dirname` present in the `define` field)
93-
so we also need to set it on the global scope
94-
Note: this was hit in the `next/dist/compiled/@opentelemetry/api` module
95-
*/ ""
96-
}
97-
globalThis.__dirname ??= "";
94+
globalThis.__dirname ??= "";
9895
9996
// Do not crash on cache not supported
10097
// https://github.com/cloudflare/workerd/pull/2434
@@ -106,15 +103,15 @@ globalThis.fetch = (input, init) => {
106103
}
107104
return curFetch(input, init);
108105
};
109-
import { Readable } from 'node:stream';
106+
import __cf_stream from 'node:stream';
110107
fetch = globalThis.fetch;
111108
const CustomRequest = class extends globalThis.Request {
112109
constructor(input, init) {
113110
if (init) {
114111
delete init.cache;
115112
if (init.body?.__node_stream__ === true) {
116113
// https://github.com/cloudflare/workerd/issues/2746
117-
init.body = Readable.toWeb(init.body);
114+
init.body = __cf_stream.Readable.toWeb(init.body);
118115
}
119116
}
120117
super(input, init);
@@ -128,9 +125,18 @@ globalThis.__dangerous_ON_edge_converter_returns_request = true;
128125
},
129126
});
130127

131-
await updateWorkerBundledCode(workerOutputFile, config);
128+
await updateWorkerBundledCode(openNextServerBundle, config, openNextOptions);
132129

133-
console.log(`\x1b[35mWorker saved in \`${workerOutputFile}\` 🚀\n\x1b[0m`);
130+
const isMonorepo = monorepoRoot !== appPath;
131+
if (isMonorepo) {
132+
const packagePosixPath = packagePath.split(path.sep).join(path.posix.sep);
133+
fs.writeFileSync(
134+
path.join(outputPath, "handler.mjs"),
135+
`export * from "./${packagePosixPath}/handler.mjs";`
136+
);
137+
}
138+
139+
console.log(`\x1b[35mWorker saved in \`${openNextServerBundle}\` 🚀\n\x1b[0m`);
134140
}
135141

136142
/**
@@ -141,7 +147,11 @@ globalThis.__dangerous_ON_edge_converter_returns_request = true;
141147
* @param workerOutputFile
142148
* @param config
143149
*/
144-
async function updateWorkerBundledCode(workerOutputFile: string, config: Config): Promise<void> {
150+
async function updateWorkerBundledCode(
151+
workerOutputFile: string,
152+
config: Config,
153+
openNextOptions: BuildOptions
154+
): Promise<void> {
145155
const originalCode = await readFile(workerOutputFile, "utf8");
146156

147157
let patchedCode = originalCode;
@@ -151,10 +161,15 @@ async function updateWorkerBundledCode(workerOutputFile: string, config: Config)
151161
patchedCode = inlineNextRequire(patchedCode, config);
152162
patchedCode = patchFindDir(patchedCode, config);
153163
patchedCode = inlineEvalManifest(patchedCode, config);
154-
patchedCode = await patchCache(patchedCode, config);
164+
patchedCode = await patchCache(patchedCode, openNextOptions);
155165
patchedCode = inlineMiddlewareManifestRequire(patchedCode, config);
156166
patchedCode = patchExceptionBubbling(patchedCode);
157167

168+
patchedCode = patchedCode
169+
// workers do not support dynamic require nor require.resolve
170+
.replace("patchAsyncStorage();", "//patchAsyncStorage();")
171+
.replace('require.resolve("./cache.cjs")', '"unused"');
172+
158173
await writeFile(workerOutputFile, patchedCode);
159174
}
160175

@@ -164,10 +179,10 @@ function createFixRequiresESBuildPlugin(config: Config): Plugin {
164179
setup(build) {
165180
// Note: we (empty) shim require-hook modules as they generate problematic code that uses requires
166181
build.onResolve({ filter: /^\.\/require-hook$/ }, () => ({
167-
path: join(config.paths.internal.templates, "shims", "empty.ts"),
182+
path: path.join(config.paths.internal.templates, "shims", "empty.ts"),
168183
}));
169184
build.onResolve({ filter: /\.\/lib\/node-fs-methods$/ }, () => ({
170-
path: join(config.paths.internal.templates, "shims", "empty.ts"),
185+
path: path.join(config.paths.internal.templates, "shims", "empty.ts"),
171186
}));
172187
},
173188
};

0 commit comments

Comments
 (0)