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 all 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
14 changes: 12 additions & 2 deletions 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-node",
converter: "edge",
// Unused implementation
incrementalCache: "dummy",
tagCache: "dummy",
queue: "dummy",
},
},

middleware: {
external: true,
override: {
wrapper: "cloudflare",
wrapper: "cloudflare-edge",
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
44 changes: 13 additions & 31 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,52 +44,34 @@ import type { OpenNextConfig } from "open-next/types/open-next";
const config: OpenNextConfig = {
default: {
override: {
wrapper: "cloudflare",
wrapper: "cloudflare-node",
converter: "edge",
// Unused implementation
incrementalCache: "dummy",
tagCache: "dummy",
queue: "dummy",
},
},

middleware: {
external: true,
override: {
wrapper: "cloudflare",
wrapper: "cloudflare-edge",
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 = "..."
```
## Known 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 developer 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;
}
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@2202f36",
"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")
?.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 @@ -87,14 +88,11 @@ export async function buildWorker(config: Config): Promise<void> {
platform: "node",
banner: {
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 ??= "";
// __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 ??= "";

// Do not crash on cache not supported
// https://github.com/cloudflare/workerd/pull/2434
Expand All @@ -106,15 +104,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 +126,18 @@ globalThis.__dangerous_ON_edge_converter_returns_request = true;
},
});

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

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 \`${workerOutputFile}\` 🚀\n\x1b[0m`);
console.log(`\x1b[35mWorker saved in \`${openNextServerBundle}\` 🚀\n\x1b[0m`);
}

/**
Expand All @@ -141,7 +148,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 +162,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 +180,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