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