Skip to content

Commit 3b20bc6

Browse files
authored
refactor: migrate the requirePage patch to ast-grep (#287)
1 parent 1a2b815 commit 3b20bc6

File tree

6 files changed

+147
-59
lines changed

6 files changed

+147
-59
lines changed

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

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import { build, Plugin } from "esbuild";
1010

1111
import { patchOptionalDependencies } from "./patches/ast/optional-deps.js";
1212
import * as patches from "./patches/index.js";
13+
import inlineRequirePagePlugin from "./patches/plugins/require-page.js";
14+
import setWranglerExternal from "./patches/plugins/wrangler-external.js";
1315
import { normalizePath, patchCodeWithValidations } from "./utils/index.js";
1416

1517
/** The dist directory of the Cloudflare adapter package */
@@ -48,8 +50,18 @@ export async function bundleServer(buildOpts: BuildOptions): Promise<void> {
4850
format: "esm",
4951
target: "esnext",
5052
minify: false,
51-
plugins: [createFixRequiresESBuildPlugin(buildOpts)],
52-
external: ["./middleware/handler.mjs", "caniuse-lite"],
53+
plugins: [
54+
createFixRequiresESBuildPlugin(buildOpts),
55+
inlineRequirePagePlugin(buildOpts),
56+
setWranglerExternal(),
57+
],
58+
external: [
59+
"./middleware/handler.mjs",
60+
// Next optional dependencies.
61+
"caniuse-lite",
62+
"jimp",
63+
"probe-image-size",
64+
],
5365
alias: {
5466
// Note: we apply an empty shim to next/dist/compiled/ws because it generates two `eval`s:
5567
// eval("require")("bufferutil");
@@ -146,7 +158,6 @@ async function updateWorkerBundledCode(workerOutputFile: string, buildOpts: Buil
146158
["require", patches.patchRequire],
147159
["`buildId` function", (code) => patches.patchBuildId(code, buildOpts)],
148160
["`loadManifest` function", (code) => patches.patchLoadManifest(code, buildOpts)],
149-
["next's require", (code) => patches.inlineNextRequire(code, buildOpts)],
150161
["`findDir` function", (code) => patches.patchFindDir(code, buildOpts)],
151162
["`evalManifest` function", (code) => patches.inlineEvalManifest(code, buildOpts)],
152163
["cacheHandler", (code) => patches.patchCache(code, buildOpts)],

packages/cloudflare/src/cli/build/patches/ast/optional-deps.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { type SgNode } from "@ast-grep/napi";
33
import { applyRule } from "./util.js";
44

55
/**
6-
* Handle optional dependencies.
6+
* Handles optional dependencies.
77
*
88
* A top level `require(optionalDep)` would throw when the dep is not installed.
99
*
@@ -16,7 +16,7 @@ rule:
1616
pattern: $MOD
1717
kind: string_fragment
1818
stopBy: end
19-
regex: ^caniuse-lite(/|$)
19+
regex: ^(caniuse-lite|jimp|probe-image-size)(/|$)
2020
not:
2121
inside:
2222
kind: try_statement
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { existsSync, readFileSync } from "node:fs";
2+
import { readFile } from "node:fs/promises";
3+
import { join } from "node:path";
4+
5+
import { type BuildOptions, getPackagePath } from "@opennextjs/aws/build/helper.js";
6+
import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js";
7+
import type { PluginBuild } from "esbuild";
8+
9+
import { patchCode, type RuleConfig } from "../ast/util.js";
10+
11+
export default function inlineRequirePagePlugin(buildOpts: BuildOptions) {
12+
return {
13+
name: "inline-require-page",
14+
15+
setup: async (build: PluginBuild) => {
16+
build.onLoad(
17+
{
18+
filter: getCrossPlatformPathRegex(String.raw`/next/dist/server/require\.js$`, { escape: false }),
19+
},
20+
async ({ path }) => {
21+
const jsCode = await readFile(path, "utf8");
22+
if (/function requirePage\(/.test(jsCode)) {
23+
return { contents: patchCode(jsCode, getRule(buildOpts)) };
24+
}
25+
}
26+
);
27+
},
28+
};
29+
}
30+
31+
function getRule(buildOpts: BuildOptions) {
32+
const { outputDir } = buildOpts;
33+
const serverDir = join(outputDir, "server-functions/default", getPackagePath(buildOpts), ".next/server");
34+
35+
const pagesManifestFile = join(serverDir, "pages-manifest.json");
36+
const appPathsManifestFile = join(serverDir, "app-paths-manifest.json");
37+
38+
const pagesManifests: string[] = existsSync(pagesManifestFile)
39+
? Object.values(JSON.parse(readFileSync(pagesManifestFile, "utf-8")))
40+
: [];
41+
const appPathsManifests: string[] = existsSync(appPathsManifestFile)
42+
? Object.values(JSON.parse(readFileSync(appPathsManifestFile, "utf-8")))
43+
: [];
44+
const manifests = pagesManifests.concat(appPathsManifests);
45+
46+
const htmlFiles = manifests.filter((file) => file.endsWith(".html"));
47+
const jsFiles = manifests.filter((file) => file.endsWith(".js"));
48+
49+
// Inline fs access and dynamic require that are not supported by workerd.
50+
const fnBody = `
51+
// html
52+
${htmlFiles
53+
.map(
54+
(file) => `if (pagePath.endsWith("${file}")) {
55+
return ${JSON.stringify(readFileSync(join(serverDir, file), "utf-8"))};
56+
}`
57+
)
58+
.join("\n")}
59+
// js
60+
process.env.__NEXT_PRIVATE_RUNTIME_TYPE = isAppPath ? 'app' : 'pages';
61+
try {
62+
${jsFiles
63+
.map(
64+
(file) => `if (pagePath.endsWith("${file}")) {
65+
return require(${JSON.stringify(join(serverDir, file))});
66+
}`
67+
)
68+
.join("\n")}
69+
} finally {
70+
process.env.__NEXT_PRIVATE_RUNTIME_TYPE = '';
71+
}
72+
`;
73+
74+
return {
75+
rule: {
76+
pattern: `
77+
function requirePage($PAGE, $DIST_DIR, $IS_APPP_ATH) {
78+
const $_ = getPagePath($$$ARGS);
79+
$$$_BODY
80+
}`,
81+
},
82+
fix: `
83+
function requirePage($PAGE, $DIST_DIR, $IS_APPP_ATH) {
84+
const pagePath = getPagePath($$$ARGS);
85+
${fnBody}
86+
}`,
87+
} satisfies RuleConfig;
88+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/**
2+
* ESBuild plugin to mark files bundled by wrangler as external.
3+
*
4+
* `.wasm` and `.bin` will ultimately be bundled by wrangler.
5+
* We should only mark them as external in the adapter.
6+
*
7+
* However simply marking them as external would copy the import path to the bundle,
8+
* i.e. `import("./file.wasm?module")` and given than the bundle is generated in a
9+
* different location than the input files, the relative path would not be valid.
10+
*
11+
* This ESBuild plugin convert relative paths to absolute paths so that they are
12+
* still valid from inside the bundle.
13+
*
14+
* ref: https://developers.cloudflare.com/workers/wrangler/bundling/
15+
*/
16+
17+
import { dirname, resolve } from "node:path";
18+
19+
import type { PluginBuild } from "esbuild";
20+
21+
export default function setWranglerExternal() {
22+
return {
23+
name: "wrangler-externals",
24+
25+
setup: async (build: PluginBuild) => {
26+
const namespace = "wrangler-externals-plugin";
27+
28+
build.onResolve({ filter: /(\.bin|\.wasm\?module)$/ }, ({ path, importer }) => {
29+
return {
30+
path: resolve(dirname(importer), path),
31+
namespace,
32+
external: true,
33+
};
34+
});
35+
36+
build.onLoad({ filter: /.*/, namespace }, async ({ path }) => {
37+
return {
38+
contents: `export * from '${path}';`,
39+
};
40+
});
41+
},
42+
};
43+
}

packages/cloudflare/src/cli/build/patches/to-investigate/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
export * from "./inline-eval-manifest.js";
22
export * from "./inline-middleware-manifest-require.js";
3-
export * from "./inline-next-require.js";
43
export * from "./patch-exception-bubbling.js";
54
export * from "./patch-find-dir.js";
65
export * from "./patch-load-instrumentation-module.js";

packages/cloudflare/src/cli/build/patches/to-investigate/inline-next-require.ts

Lines changed: 0 additions & 53 deletions
This file was deleted.

0 commit comments

Comments
 (0)