11import { patchCode } from "@opennextjs/aws/build/patch/astCodePatcher.js" ;
22import type { CodePatcher } from "@opennextjs/aws/build/patch/codePatcher.js" ;
33import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js" ;
4+ import fs from "node:fs" ;
5+ import path from "node:path" ;
46
57const inlineChunksRule = `
68rule:
1012 requireChunk(chunkPath)
1113` ;
1214
15+ /**
16+ * Discover Turbopack external module mappings by reading symlinks in .next/node_modules/.
17+ *
18+ * Turbopack externalizes packages listed in serverExternalPackages and creates hashed
19+ * identifiers (e.g. "shiki-43d062b67f27bbdc") with symlinks in .next/node_modules/ pointing
20+ * to the real packages (e.g. ../../node_modules/shiki). At runtime, externalImport() does
21+ * `await import("shiki-43d062b67f27bbdc/wasm")` which fails on workerd because those hashed
22+ * names are not real modules. This function discovers the mappings so we can intercept them.
23+ */
24+ function discoverExternalModuleMappings ( filePath : string ) : Map < string , string > {
25+ // filePath is like: .../.next/server/chunks/ssr/[turbopack]_runtime.js
26+ // We need: .../.next/node_modules/
27+ const dotNextDir = filePath . replace ( / \/ s e r v e r \/ c h u n k s \/ .* $ / , "" ) ;
28+ const nodeModulesDir = path . join ( dotNextDir , "node_modules" ) ;
29+
30+ const mappings = new Map < string , string > ( ) ;
31+
32+ if ( ! fs . existsSync ( nodeModulesDir ) ) {
33+ return mappings ;
34+ }
35+
36+ for ( const entry of fs . readdirSync ( nodeModulesDir ) ) {
37+ const entryPath = path . join ( nodeModulesDir , entry ) ;
38+ try {
39+ const stat = fs . lstatSync ( entryPath ) ;
40+ if ( stat . isSymbolicLink ( ) ) {
41+ const target = fs . readlinkSync ( entryPath ) ;
42+ // target is like "../../node_modules/shiki" — extract package name
43+ const match = target . match ( / n o d e _ m o d u l e s \/ ( .+ ) $ / ) ;
44+ if ( match ?. [ 1 ] ) {
45+ mappings . set ( entry , match [ 1 ] ) ;
46+ }
47+ }
48+ } catch {
49+ // skip entries we can't read
50+ }
51+ }
52+
53+ return mappings ;
54+ }
55+
56+ /**
57+ * Build a dynamic inlineExternalImportRule that includes cases for all discovered
58+ * Turbopack external module hashes, mapping them back to their real package names.
59+ *
60+ * We use a switch for exact matches (including bare + subpath cases) and a fallback
61+ * for the default case. Since switch/case can only match exact strings, we enumerate
62+ * known subpaths from the traced files to cover cases like "shiki-hash/wasm".
63+ */
64+ function buildExternalImportRule ( mappings : Map < string , string > , tracedFiles : string [ ] ) : string {
65+ const cases : string [ ] = [ ] ;
66+
67+ // Always include the @vercel /og rewrite
68+ cases . push ( ` case "next/dist/compiled/@vercel/og/index.node.js":
69+ $RAW = await import("next/dist/compiled/@vercel/og/index.edge.js");
70+ break;` ) ;
71+
72+ // Add case for each discovered external module mapping (bare import)
73+ for ( const [ hashedName , realName ] of mappings ) {
74+ cases . push ( ` case "${ hashedName } ":
75+ $RAW = await import("${ realName } ");
76+ break;` ) ;
77+ }
78+
79+ // Discover subpath imports from the traced chunk files.
80+ // Chunks reference external modules like "shiki-hash/wasm" — scan for these patterns.
81+ const subpathCases = discoverExternalSubpaths ( mappings , tracedFiles ) ;
82+ for ( const [ hashedSubpath , realSubpath ] of subpathCases ) {
83+ cases . push ( ` case "${ hashedSubpath } ":
84+ $RAW = await import("${ realSubpath } ");
85+ break;` ) ;
86+ }
87+
88+ return `
89+ rule:
90+ pattern: "$RAW = await import($ID)"
91+ inside:
92+ regex: "externalImport"
93+ kind: function_declaration
94+ stopBy: end
95+ fix: |-
96+ switch ($ID) {
97+ ${ cases . join ( "\n" ) }
98+ default:
99+ $RAW = await import($ID);
100+ }
101+ ` ;
102+ }
103+
104+ /**
105+ * Scan traced chunk files for external module subpath imports.
106+ * E.g. find "shiki-43d062b67f27bbdc/wasm" in chunk code and map it to "shiki/wasm".
107+ *
108+ * Only scans files with "[externals]" in the name since those are the chunks that
109+ * contain externalImport calls.
110+ */
111+ function discoverExternalSubpaths ( mappings : Map < string , string > , tracedFiles : string [ ] ) : Map < string , string > {
112+ const subpaths = new Map < string , string > ( ) ;
113+
114+ const externalChunks = tracedFiles . filter ( ( f ) => f . includes ( "[externals]" ) ) ;
115+
116+ for ( const [ hashedName , realName ] of mappings ) {
117+ const pattern = new RegExp ( `"(${ hashedName . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, "\\$&" ) } /[^"]*)"` , "g" ) ;
118+
119+ for ( const filePath of externalChunks ) {
120+ try {
121+ const content = fs . readFileSync ( filePath , "utf-8" ) ;
122+ for ( const match of content . matchAll ( pattern ) ) {
123+ const fullHashedPath = match [ 1 ] ;
124+ if ( fullHashedPath ) {
125+ const subpath = fullHashedPath . slice ( hashedName . length ) ;
126+ const realSubpath = realName + subpath ;
127+ subpaths . set ( fullHashedPath , realSubpath ) ;
128+ }
129+ }
130+ } catch {
131+ // skip files we can't read
132+ }
133+ }
134+ }
135+
136+ return subpaths ;
137+ }
138+
13139export const patchTurbopackRuntime : CodePatcher = {
14140 name : "inline-turbopack-chunks" ,
15141 patches : [
@@ -19,8 +145,10 @@ export const patchTurbopackRuntime: CodePatcher = {
19145 escape : false ,
20146 } ) ,
21147 contentFilter : / l o a d R u n t i m e C h u n k P a t h / ,
22- patchCode : async ( { code, tracedFiles } ) => {
23- let patched = patchCode ( code , inlineExternalImportRule ) ;
148+ patchCode : async ( { code, tracedFiles, filePath } ) => {
149+ const mappings = discoverExternalModuleMappings ( filePath ) ;
150+ const externalImportRule = buildExternalImportRule ( mappings , tracedFiles ) ;
151+ let patched = patchCode ( code , externalImportRule ) ;
24152 patched = patchCode ( patched , inlineChunksRule ) ;
25153
26154 return `${ patched } \n${ inlineChunksFn ( tracedFiles ) } ` ;
@@ -63,27 +191,3 @@ ${chunks
63191 }
64192` ;
65193}
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