Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: integrate the OpenNext server #140

Merged
merged 3 commits into from
Nov 25, 2024
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
vicb marked this conversation as resolved.
Show resolved Hide resolved

> [!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
vicb marked this conversation as resolved.
Show resolved Hide resolved
- 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;
}
vicb marked this conversation as resolved.
Show resolved Hide resolved
}

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
vicb marked this conversation as resolved.
Show resolved Hide resolved
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")
vicb marked this conversation as resolved.
Show resolved Hide resolved
?.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"),
vicb marked this conversation as resolved.
Show resolved Hide resolved
// 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
vicb marked this conversation as resolved.
Show resolved Hide resolved
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();")
vicb marked this conversation as resolved.
Show resolved Hide resolved
.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