|
1 | 1 | import { NextjsAppPaths } from "../../nextjsPaths";
|
2 | 2 | import { build, Plugin } from "esbuild";
|
3 |
| -import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs"; |
| 3 | +import { readdirSync, readFileSync, writeFileSync } from "node:fs"; |
4 | 4 | import { cp, readFile, writeFile } from "node:fs/promises";
|
5 | 5 |
|
6 |
| -import { globSync } from "glob"; |
7 | 6 | import { resolve } from "node:path";
|
8 | 7 |
|
9 |
| -const fixRequiresESBuildPlugin: Plugin = { |
10 |
| - name: "replaceRelative", |
11 |
| - setup(build) { |
12 |
| - // Note: we (empty) shim require-hook modules as they generate problematic code that uses requires |
13 |
| - build.onResolve({ filter: /^\.\/require-hook$/ }, (args) => ({ |
14 |
| - path: `${__dirname}/templates/shims/empty.ts`, |
15 |
| - })); |
16 |
| - }, |
17 |
| -}; |
| 8 | +import { patchRequire } from "./patches/investigated/patchRequire"; |
| 9 | +import { patchUrl } from "./patches/investigated/patchUrl"; |
| 10 | + |
| 11 | +import { patchReadFile } from "./patches/to-investigate/patchReadFile"; |
| 12 | +import { patchFindDir } from "./patches/to-investigate/patchFindDir"; |
| 13 | +import { inlineNextRequire } from "./patches/to-investigate/inlineNextRequire"; |
| 14 | +import { inlineEvalManifest } from "./patches/to-investigate/inlineEvalManifest"; |
18 | 15 |
|
19 | 16 | /**
|
20 | 17 | * Using the Next.js build output in the `.next` directory builds a workerd compatible output
|
@@ -129,175 +126,12 @@ async function updateWorkerBundledCode(
|
129 | 126 |
|
130 | 127 | let patchedCode = originalCode;
|
131 | 128 |
|
132 |
| - // ESBuild does not support CJS format |
133 |
| - // See https://github.com/evanw/esbuild/issues/1921 and linked issues |
134 |
| - // Some of the solutions are based on `module.createRequire()` not implemented in workerd. |
135 |
| - patchedCode = patchedCode |
136 |
| - .replace(/__require\d?\(/g, "require(") |
137 |
| - .replace(/__require\d?\./g, "require."); |
138 |
| - |
139 |
| - // The next-server code gets the buildId from the filesystem, resulting in a `[unenv] fs.readFileSync is not implemented yet!` error |
140 |
| - // so we add an early return to the `getBuildId` function so that the `readyFileSync` is never encountered |
141 |
| - // (source: https://github.com/vercel/next.js/blob/15aeb92efb34c09a36/packages/next/src/server/next-server.ts#L438-L451) |
142 |
| - // Note: we could/should probably just patch readFileSync here or something! |
143 |
| - patchedCode = patchedCode.replace( |
144 |
| - "getBuildId() {", |
145 |
| - `getBuildId() { |
146 |
| - return ${JSON.stringify( |
147 |
| - readFileSync( |
148 |
| - `${nextjsAppPaths.standaloneAppDotNextDir}/BUILD_ID`, |
149 |
| - "utf-8" |
150 |
| - ) |
151 |
| - )}; |
152 |
| - ` |
153 |
| - ); |
154 |
| - |
155 |
| - // Same as above, the next-server code loads the manifests with `readyFileSync` and we want to avoid that |
156 |
| - // (source: https://github.com/vercel/next.js/blob/15aeb92e/packages/next/src/server/load-manifest.ts#L34-L56) |
157 |
| - // Note: we could/should probably just patch readFileSync here or something! |
158 |
| - const manifestJsons = globSync( |
159 |
| - `${nextjsAppPaths.standaloneAppDotNextDir}/**/*-manifest.json` |
160 |
| - ).map((file) => file.replace(nextjsAppPaths.standaloneAppDir + "/", "")); |
161 |
| - patchedCode = patchedCode.replace( |
162 |
| - /function loadManifest\((.+?), .+?\) {/, |
163 |
| - `$& |
164 |
| - ${manifestJsons |
165 |
| - .map( |
166 |
| - (manifestJson) => ` |
167 |
| - if ($1.endsWith("${manifestJson}")) { |
168 |
| - return ${readFileSync( |
169 |
| - `${nextjsAppPaths.standaloneAppDir}/${manifestJson}`, |
170 |
| - "utf-8" |
171 |
| - )}; |
172 |
| - } |
173 |
| - ` |
174 |
| - ) |
175 |
| - .join("\n")} |
176 |
| - throw new Error("Unknown loadManifest: " + $1); |
177 |
| - ` |
178 |
| - ); |
179 |
| - |
180 |
| - // This solves the fact that the workerd URL parsing is not compatible with the node.js one |
181 |
| - // VERY IMPORTANT: this required the following dependency to be part of the application!!!! (this is very bad!!!) |
182 |
| - // "node-url": "npm:url@^0.11.4" |
183 |
| - // Hopefully this should not be necessary after this unenv PR lands: https://github.com/unjs/unenv/pull/292 |
184 |
| - patchedCode = patchedCode.replace( |
185 |
| - / ([a-zA-Z0-9_]+) = require\("url"\);/g, |
186 |
| - ` $1 = require("url"); |
187 |
| - const nodeUrl = require("node-url"); |
188 |
| - $1.parse = nodeUrl.parse.bind(nodeUrl); |
189 |
| - $1.format = nodeUrl.format.bind(nodeUrl); |
190 |
| - $1.pathToFileURL = (path) => { |
191 |
| - console.log("url.pathToFileURL", path); |
192 |
| - return new URL("file://" + path); |
193 |
| - } |
194 |
| - ` |
195 |
| - ); |
196 |
| - |
197 |
| - // The following avoid various Next.js specific files `require`d at runtime since we can just read |
198 |
| - // and inline their content during build time |
199 |
| - const pagesManifestFile = `${nextjsAppPaths.standaloneAppServerDir}/pages-manifest.json`; |
200 |
| - const appPathsManifestFile = `${nextjsAppPaths.standaloneAppServerDir}/app-paths-manifest.json`; |
201 |
| - |
202 |
| - const pagesManifestFiles = existsSync(pagesManifestFile) |
203 |
| - ? Object.values(JSON.parse(readFileSync(pagesManifestFile, "utf-8"))).map( |
204 |
| - (file) => ".next/server/" + file |
205 |
| - ) |
206 |
| - : []; |
207 |
| - const appPathsManifestFiles = existsSync(appPathsManifestFile) |
208 |
| - ? Object.values( |
209 |
| - JSON.parse(readFileSync(appPathsManifestFile, "utf-8")) |
210 |
| - ).map((file) => ".next/server/" + file) |
211 |
| - : []; |
212 |
| - const allManifestFiles = pagesManifestFiles.concat(appPathsManifestFiles); |
213 |
| - |
214 |
| - const htmlPages = allManifestFiles.filter((file) => file.endsWith(".html")); |
215 |
| - const pageModules = allManifestFiles.filter((file) => file.endsWith(".js")); |
216 |
| - |
217 |
| - patchedCode = patchedCode.replace( |
218 |
| - /const pagePath = getPagePath\(.+?\);/, |
219 |
| - `$& |
220 |
| - ${htmlPages |
221 |
| - .map( |
222 |
| - (htmlPage) => ` |
223 |
| - if (pagePath.endsWith("${htmlPage}")) { |
224 |
| - return ${JSON.stringify( |
225 |
| - readFileSync( |
226 |
| - `${nextjsAppPaths.standaloneAppDir}/${htmlPage}`, |
227 |
| - "utf-8" |
228 |
| - ) |
229 |
| - )}; |
230 |
| - } |
231 |
| - ` |
232 |
| - ) |
233 |
| - .join("\n")} |
234 |
| - ${pageModules |
235 |
| - .map( |
236 |
| - (module) => ` |
237 |
| - if (pagePath.endsWith("${module}")) { |
238 |
| - return require("${nextjsAppPaths.standaloneAppDir}/${module}"); |
239 |
| - } |
240 |
| - ` |
241 |
| - ) |
242 |
| - .join("\n")} |
243 |
| - throw new Error("Unknown pagePath: " + pagePath); |
244 |
| - ` |
245 |
| - ); |
246 |
| - |
247 |
| - // Here we patch `findDir` so that the next server can detect whether the `app` or `pages` directory exists |
248 |
| - // (source: https://github.com/vercel/next.js/blob/ba995993/packages/next/src/lib/find-pages-dir.ts#L4-L13) |
249 |
| - // (usage source: https://github.com/vercel/next.js/blob/ba995993/packages/next/src/server/next-server.ts#L450-L451) |
250 |
| - // Note: `findDir` uses `fs.existsSync` under the hood, so patching that should be enough to make this work |
251 |
| - patchedCode = patchedCode.replace( |
252 |
| - "function findDir(dir, name) {", |
253 |
| - `function findDir(dir, name) { |
254 |
| - if (dir.endsWith(".next/server")) { |
255 |
| - if (name === "app") return ${existsSync( |
256 |
| - `${nextjsAppPaths.standaloneAppServerDir}/app` |
257 |
| - )}; |
258 |
| - if (name === "pages") return ${existsSync( |
259 |
| - `${nextjsAppPaths.standaloneAppServerDir}/pages` |
260 |
| - )}; |
261 |
| - } |
262 |
| - throw new Error("Unknown findDir call: " + dir + " " + name); |
263 |
| - ` |
264 |
| - ); |
265 |
| - |
266 |
| - // `evalManifest` relies on readFileSync so we need to patch the function so that it instead returns the content of the manifest files |
267 |
| - // which are known at build time |
268 |
| - // (source: https://github.com/vercel/next.js/blob/b1e32c5d1f/packages/next/src/server/load-manifest.ts#L72) |
269 |
| - // Note: we could/should probably just patch readFileSync here or something, but here the issue is that after the readFileSync call |
270 |
| - // there is a vm `runInNewContext` call which we also don't support (source: https://github.com/vercel/next.js/blob/b1e32c5d1f/packages/next/src/server/load-manifest.ts#L88) |
271 |
| - const manifestJss = globSync( |
272 |
| - `${nextjsAppPaths.standaloneAppDotNextDir}/**/*_client-reference-manifest.js` |
273 |
| - ).map((file) => file.replace(`${nextjsAppPaths.standaloneAppDir}/`, "")); |
274 |
| - patchedCode = patchedCode.replace( |
275 |
| - /function evalManifest\((.+?), .+?\) {/, |
276 |
| - `$& |
277 |
| - ${manifestJss |
278 |
| - .map( |
279 |
| - (manifestJs) => ` |
280 |
| - if ($1.endsWith("${manifestJs}")) { |
281 |
| - require("${nextjsAppPaths.standaloneAppDir}/${manifestJs}"); |
282 |
| - return { |
283 |
| - __RSC_MANIFEST: { |
284 |
| - "${manifestJs |
285 |
| - .replace(".next/server/app", "") |
286 |
| - .replace( |
287 |
| - "_client-reference-manifest.js", |
288 |
| - "" |
289 |
| - )}": globalThis.__RSC_MANIFEST["${manifestJs |
290 |
| - .replace(".next/server/app", "") |
291 |
| - .replace("_client-reference-manifest.js", "")}"], |
292 |
| - }, |
293 |
| - }; |
294 |
| - } |
295 |
| - ` |
296 |
| - ) |
297 |
| - .join("\n")} |
298 |
| - throw new Error("Unknown evalManifest: " + $1); |
299 |
| - ` |
300 |
| - ); |
| 129 | + patchedCode = patchRequire(patchedCode); |
| 130 | + patchedCode = patchReadFile(patchedCode, nextjsAppPaths); |
| 131 | + patchedCode = patchUrl(patchedCode); |
| 132 | + patchedCode = inlineNextRequire(patchedCode, nextjsAppPaths); |
| 133 | + patchedCode = patchFindDir(patchedCode, nextjsAppPaths); |
| 134 | + patchedCode = inlineEvalManifest(patchedCode, nextjsAppPaths); |
301 | 135 |
|
302 | 136 | await writeFile(workerOutputFile, patchedCode);
|
303 | 137 | }
|
@@ -338,3 +172,13 @@ async function updateWebpackChunksFile(nextjsAppPaths: NextjsAppPaths) {
|
338 | 172 |
|
339 | 173 | writeFileSync(webpackRuntimeFile, updatedFileContent);
|
340 | 174 | }
|
| 175 | + |
| 176 | +const fixRequiresESBuildPlugin: Plugin = { |
| 177 | + name: "replaceRelative", |
| 178 | + setup(build) { |
| 179 | + // Note: we (empty) shim require-hook modules as they generate problematic code that uses requires |
| 180 | + build.onResolve({ filter: /^\.\/require-hook$/ }, (args) => ({ |
| 181 | + path: `${__dirname}/templates/shims/empty.ts`, |
| 182 | + })); |
| 183 | + }, |
| 184 | +}; |
0 commit comments