Skip to content

Commit 68aed26

Browse files
vicbconico974
andauthored
feat: turbopack support (#983)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
1 parent aefcb03 commit 68aed26

File tree

7 files changed

+116
-16
lines changed

7 files changed

+116
-16
lines changed

.changeset/silver-phones-brush.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@opennextjs/cloudflare": minor
3+
---
4+
5+
feat: turbopack support

examples/common/config-e2e.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ export function configurePlaywright(
2727
if (isCI) {
2828
// Do not build on CI - there is a preceding build step
2929
command = `pnpm preview:worker -- --port ${port} --inspector-port ${inspectorPort} ${env}`;
30-
timeout = 200_000;
30+
timeout = 800_000;
3131
} else {
32-
timeout = 500_000;
32+
timeout = 800_000;
3333
command = `pnpm preview -- --port ${port} --inspector-port ${inspectorPort} ${env}`;
3434
}
3535
} else {

examples/e2e/app-router/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"scripts": {
66
"openbuild": "node ../../packages/open-next/dist/index.js build --streaming --build-command \"npx turbo build\"",
77
"dev": "next dev --turbopack --port 3001",
8-
"build": "next build",
8+
"build": "next build --turbopack",
99
"start": "next start --port 3001",
1010
"lint": "next lint",
1111
"clean": "rm -rf .turbo node_modules .next .open-next",

packages/cloudflare/src/cli/build/bundle-server.ts

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -50,15 +50,12 @@ export async function bundleServer(buildOpts: BuildOptions, projectOpts: Project
5050
copyPackageCliFiles(packageDistDir, buildOpts);
5151

5252
const { appPath, outputDir, monorepoRoot, debug } = buildOpts;
53-
const baseManifestPath = path.join(
54-
outputDir,
55-
"server-functions/default",
56-
getPackagePath(buildOpts),
57-
".next"
58-
);
59-
const serverFiles = path.join(baseManifestPath, "required-server-files.json");
53+
const dotNextPath = path.join(outputDir, "server-functions/default", getPackagePath(buildOpts), ".next");
54+
const serverFiles = path.join(dotNextPath, "required-server-files.json");
6055
const nextConfig = JSON.parse(fs.readFileSync(serverFiles, "utf-8")).config;
6156

57+
const useTurbopack = fs.existsSync(path.join(dotNextPath, "server/chunks/[turbopack]_runtime.js"));
58+
6259
console.log(`\x1b[35m⚙️ Bundling the OpenNext server...\n\x1b[0m`);
6360

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

packages/cloudflare/src/cli/build/open-next/createServerBundle.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import type { Plugin } from "esbuild";
2727

2828
import { getOpenNextConfig } from "../../../api/config.js";
2929
import { patchResRevalidate } from "../patches/plugins/res-revalidate.js";
30+
import { patchTurbopackRuntime } from "../patches/plugins/turbopack.js";
3031
import { patchUseCacheIO } from "../patches/plugins/use-cache.js";
3132
import { normalizePath } from "../utils/index.js";
3233
import { copyWorkerdPackages } from "../utils/workerd.js";
@@ -210,6 +211,7 @@ async function generateBundle(
210211
// Cloudflare specific patches
211212
patchResRevalidate,
212213
patchUseCacheIO,
214+
patchTurbopackRuntime,
213215
...additionalCodePatches,
214216
]);
215217

packages/cloudflare/src/cli/build/patches/ast/patch-vercel-og-library.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ export function patchVercelOgLibrary(buildOpts: BuildOptions) {
2424
for (const traceInfoPath of globSync(path.join(appBuildOutputPath, ".next/server/**/*.nft.json"), {
2525
windowsPathsNoEscape: true,
2626
})) {
27+
let edgeFilePatched = false;
28+
2729
const traceInfo: TraceInfo = JSON.parse(readFileSync(traceInfoPath, { encoding: "utf8" }));
2830
const tracedNodePath = traceInfo.files.find((p) => p.endsWith("@vercel/og/index.node.js"));
2931

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

4244
copyFileSync(tracedEdgePath, outputEdgePath);
45+
}
4346

47+
if (!edgeFilePatched) {
48+
edgeFilePatched = true;
4449
// Change font fetches in the library to use imports.
4550
const node = parseFile(outputEdgePath);
4651
const { edits, matches } = patchVercelOgFallbackFont(node);
4752
writeFileSync(outputEdgePath, node.commitEdits(edits));
4853

49-
const fontFileName = matches[0]!.getMatch("PATH")!.text();
50-
renameSync(path.join(outputDir, fontFileName), path.join(outputDir, `${fontFileName}.bin`));
54+
if (matches.length > 0) {
55+
const fontFileName = matches[0]!.getMatch("PATH")!.text();
56+
renameSync(path.join(outputDir, fontFileName), path.join(outputDir, `${fontFileName}.bin`));
57+
}
5158
}
5259

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

5664
const node = parseFile(routeFilePath);
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { patchCode } from "@opennextjs/aws/build/patch/astCodePatcher.js";
2+
import type { CodePatcher } from "@opennextjs/aws/build/patch/codePatcher.js";
3+
import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js";
4+
5+
const inlineChunksRule = `
6+
rule:
7+
kind: call_expression
8+
pattern: require(resolved)
9+
fix:
10+
requireChunk(chunkPath)
11+
`;
12+
13+
export const patchTurbopackRuntime: CodePatcher = {
14+
name: "inline-turbopack-chunks",
15+
patches: [
16+
{
17+
versions: ">=15.0.0",
18+
pathFilter: getCrossPlatformPathRegex(String.raw`\[turbopack\]_runtime\.js$`, {
19+
escape: false,
20+
}),
21+
contentFilter: /loadRuntimeChunkPath/,
22+
patchCode: async ({ code, tracedFiles }) => {
23+
let patched = patchCode(code, inlineExternalImportRule);
24+
patched = patchCode(patched, inlineChunksRule);
25+
26+
return `${patched}\n${inlineChunksFn(tracedFiles)}`;
27+
},
28+
},
29+
],
30+
};
31+
32+
function getInlinableChunks(tracedFiles: string[]): string[] {
33+
const chunks = new Set<string>();
34+
for (const file of tracedFiles) {
35+
if (file === "[turbopack]_runtime.js") {
36+
continue;
37+
}
38+
if (file.includes(".next/server/chunks/")) {
39+
chunks.add(file);
40+
}
41+
}
42+
return Array.from(chunks);
43+
}
44+
45+
function inlineChunksFn(tracedFiles: string[]) {
46+
// From the outputs, we extract every chunks
47+
const chunks = getInlinableChunks(tracedFiles);
48+
return `
49+
function requireChunk(chunkPath) {
50+
switch(chunkPath) {
51+
${chunks
52+
.map(
53+
(chunk) =>
54+
` case "${
55+
// we only want the path after /path/to/.next/
56+
chunk.replace(/.*\/\.next\//, "")
57+
}": return require("${chunk}");`
58+
)
59+
.join("\n")}
60+
default:
61+
throw new Error(\`Not found \${chunkPath}\`);
62+
}
63+
}
64+
`;
65+
}
66+
67+
// Turbopack imports `og` via `externalImport`.
68+
// We patch it to:
69+
// - add the explicit path so that the file is inlined by wrangler
70+
// - use the edge version of the module instead of the node version.
71+
//
72+
// Modules that are not inlined (no added to the switch), would generate an error similar to:
73+
// Failed to load external module path/to/module: Error: No such module "path/to/module"
74+
const inlineExternalImportRule = `
75+
rule:
76+
pattern: "$RAW = await import($ID)"
77+
inside:
78+
regex: "externalImport"
79+
kind: function_declaration
80+
stopBy: end
81+
fix: |-
82+
switch ($ID) {
83+
case "next/dist/compiled/@vercel/og/index.node.js":
84+
$RAW = await import("next/dist/compiled/@vercel/og/index.edge.js");
85+
break;
86+
default:
87+
$RAW = await import($ID);
88+
}
89+
`;

0 commit comments

Comments
 (0)