Skip to content

Commit

Permalink
feat: integrate the OpenNext server
Browse files Browse the repository at this point in the history
  • Loading branch information
vicb committed Nov 22, 2024
1 parent fd9408f commit 2efdc83
Show file tree
Hide file tree
Showing 16 changed files with 632 additions and 449 deletions.
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

0 comments on commit 2efdc83

Please sign in to comment.