Skip to content
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
5 changes: 5 additions & 0 deletions .changeset/silver-phones-brush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@opennextjs/cloudflare": minor
---

feat: turbopack support
4 changes: 2 additions & 2 deletions examples/common/config-e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ export function configurePlaywright(
if (isCI) {
// Do not build on CI - there is a preceding build step
command = `pnpm preview:worker -- --port ${port} --inspector-port ${inspectorPort} ${env}`;
timeout = 200_000;
timeout = 800_000;
} else {
timeout = 500_000;
timeout = 800_000;
command = `pnpm preview -- --port ${port} --inspector-port ${inspectorPort} ${env}`;
}
} else {
Expand Down
2 changes: 1 addition & 1 deletion examples/e2e/app-router/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"scripts": {
"openbuild": "node ../../packages/open-next/dist/index.js build --streaming --build-command \"npx turbo build\"",
"dev": "next dev --turbopack --port 3001",
"build": "next build",
"build": "next build --turbopack",
"start": "next start --port 3001",
"lint": "next lint",
"clean": "rm -rf .turbo node_modules .next .open-next",
Expand Down
18 changes: 7 additions & 11 deletions packages/cloudflare/src/cli/build/bundle-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,12 @@ export async function bundleServer(buildOpts: BuildOptions, projectOpts: Project
copyPackageCliFiles(packageDistDir, buildOpts);

const { appPath, outputDir, monorepoRoot, debug } = buildOpts;
const baseManifestPath = path.join(
outputDir,
"server-functions/default",
getPackagePath(buildOpts),
".next"
);
const serverFiles = path.join(baseManifestPath, "required-server-files.json");
const dotNextPath = path.join(outputDir, "server-functions/default", getPackagePath(buildOpts), ".next");
const serverFiles = path.join(dotNextPath, "required-server-files.json");
const nextConfig = JSON.parse(fs.readFileSync(serverFiles, "utf-8")).config;

const useTurbopack = fs.existsSync(path.join(dotNextPath, "server/chunks/[turbopack]_runtime.js"));

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

await patchWebpackRuntime(buildOpts);
Expand Down Expand Up @@ -141,13 +138,12 @@ export async function bundleServer(buildOpts: BuildOptions, projectOpts: Project
// Note: we need the __non_webpack_require__ variable declared as it is used by next-server:
// https://github.com/vercel/next.js/blob/be0c3283/packages/next/src/server/next-server.ts#L116-L119
__non_webpack_require__: "require",
// The 2 following defines are used to reduce the bundle size by removing unnecessary code
// Next uses different precompiled renderers (i.e. `app-page.runtime.prod.js`) based on if you use `TURBOPACK` or some experimental React features
...(useTurbopack ? {} : { "process.env.TURBOPACK": "false" }),
// We make sure that environment variables that Next.js expects are properly defined
"process.env.NEXT_RUNTIME": '"nodejs"',
"process.env.NODE_ENV": '"production"',
// The 2 following defines are used to reduce the bundle size by removing unnecessary code
// Next uses different precompiled renderers (i.e. `app-page.runtime.prod.js`) based on if you use `TURBOPACK` or some experimental React features
// Turbopack is not supported for build at the moment, so we disable it
"process.env.TURBOPACK": "false",
// This define should be safe to use for Next 14.2+, earlier versions (13.5 and less) will cause trouble
"process.env.__NEXT_EXPERIMENTAL_REACT": `${needsExperimentalReact(nextConfig)}`,
// Fix `res.validate` in Next 15.4 (together with the `route-module` patch)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import type { Plugin } from "esbuild";

import { getOpenNextConfig } from "../../../api/config.js";
import { patchResRevalidate } from "../patches/plugins/res-revalidate.js";
import { patchTurbopackRuntime } from "../patches/plugins/turbopack.js";
import { patchUseCacheIO } from "../patches/plugins/use-cache.js";
import { normalizePath } from "../utils/index.js";
import { copyWorkerdPackages } from "../utils/workerd.js";
Expand Down Expand Up @@ -210,6 +211,7 @@ async function generateBundle(
// Cloudflare specific patches
patchResRevalidate,
patchUseCacheIO,
patchTurbopackRuntime,
...additionalCodePatches,
]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ export function patchVercelOgLibrary(buildOpts: BuildOptions) {
for (const traceInfoPath of globSync(path.join(appBuildOutputPath, ".next/server/**/*.nft.json"), {
windowsPathsNoEscape: true,
})) {
let edgeFilePatched = false;

const traceInfo: TraceInfo = JSON.parse(readFileSync(traceInfoPath, { encoding: "utf8" }));
const tracedNodePath = traceInfo.files.find((p) => p.endsWith("@vercel/og/index.node.js"));

Expand All @@ -40,17 +42,23 @@ export function patchVercelOgLibrary(buildOpts: BuildOptions) {
);

copyFileSync(tracedEdgePath, outputEdgePath);
}

if (!edgeFilePatched) {
edgeFilePatched = true;
// Change font fetches in the library to use imports.
const node = parseFile(outputEdgePath);
const { edits, matches } = patchVercelOgFallbackFont(node);
writeFileSync(outputEdgePath, node.commitEdits(edits));

const fontFileName = matches[0]!.getMatch("PATH")!.text();
renameSync(path.join(outputDir, fontFileName), path.join(outputDir, `${fontFileName}.bin`));
if (matches.length > 0) {
const fontFileName = matches[0]!.getMatch("PATH")!.text();
renameSync(path.join(outputDir, fontFileName), path.join(outputDir, `${fontFileName}.bin`));
}
}

// Change node imports for the library to edge imports.
// This is only useful when turbopack is not used to bundle the function.
const routeFilePath = traceInfoPath.replace(appBuildOutputPath, packagePath).replace(".nft.json", "");

const node = parseFile(routeFilePath);
Expand Down
89 changes: 89 additions & 0 deletions packages/cloudflare/src/cli/build/patches/plugins/turbopack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { patchCode } from "@opennextjs/aws/build/patch/astCodePatcher.js";
import type { CodePatcher } from "@opennextjs/aws/build/patch/codePatcher.js";
import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js";

const inlineChunksRule = `
rule:
kind: call_expression
pattern: require(resolved)
fix:
requireChunk(chunkPath)
`;

export const patchTurbopackRuntime: CodePatcher = {
name: "inline-turbopack-chunks",
patches: [
{
versions: ">=15.0.0",
pathFilter: getCrossPlatformPathRegex(String.raw`\[turbopack\]_runtime\.js$`, {
escape: false,
}),
contentFilter: /loadRuntimeChunkPath/,
patchCode: async ({ code, tracedFiles }) => {
let patched = patchCode(code, inlineExternalImportRule);
patched = patchCode(patched, inlineChunksRule);

return `${patched}\n${inlineChunksFn(tracedFiles)}`;
},
},
],
};

function getInlinableChunks(tracedFiles: string[]): string[] {
const chunks = new Set<string>();
for (const file of tracedFiles) {
if (file === "[turbopack]_runtime.js") {
continue;
}
if (file.includes(".next/server/chunks/")) {
chunks.add(file);
}
}
return Array.from(chunks);
}

function inlineChunksFn(tracedFiles: string[]) {
// From the outputs, we extract every chunks
const chunks = getInlinableChunks(tracedFiles);
return `
function requireChunk(chunkPath) {
switch(chunkPath) {
${chunks
.map(
(chunk) =>
` case "${
// we only want the path after /path/to/.next/
chunk.replace(/.*\/\.next\//, "")
}": return require("${chunk}");`
)
.join("\n")}
default:
throw new Error(\`Not found \${chunkPath}\`);
}
}
`;
}

// Turbopack imports `og` via `externalImport`.
// We patch it to:
// - add the explicit path so that the file is inlined by wrangler
// - use the edge version of the module instead of the node version.
//
// Modules that are not inlined (no added to the switch), would generate an error similar to:
// Failed to load external module path/to/module: Error: No such module "path/to/module"
const inlineExternalImportRule = `
rule:
pattern: "$RAW = await import($ID)"
inside:
regex: "externalImport"
kind: function_declaration
stopBy: end
fix: |-
switch ($ID) {
case "next/dist/compiled/@vercel/og/index.node.js":
$RAW = await import("next/dist/compiled/@vercel/og/index.edge.js");
break;
default:
$RAW = await import($ID);
}
`;