44 * They rely on `readFileSync` that is not supported by workerd.
55 */
66
7+ import crypto from "node:crypto" ;
78import { readFile } from "node:fs/promises" ;
89import { join , posix , relative , sep } from "node:path" ;
910
11+ import { Lang , parse } from "@ast-grep/napi" ;
1012import { type BuildOptions , getPackagePath } from "@opennextjs/aws/build/helper.js" ;
11- import { patchCode , type RuleConfig } from "@opennextjs/aws/build/patch/astCodePatcher.js" ;
13+ import { applyRule , patchCode , type RuleConfig } from "@opennextjs/aws/build/patch/astCodePatcher.js" ;
1214import type { ContentUpdater , Plugin } from "@opennextjs/aws/plugins/content-updater.js" ;
1315import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js" ;
1416import { glob } from "glob" ;
@@ -96,23 +98,49 @@ async function getEvalManifestRule(buildOpts: BuildOptions) {
9698
9799 const baseDir = join ( outputDir , "server-functions/default" , getPackagePath ( buildOpts ) , ".next" ) ;
98100 const appDir = join ( baseDir , "server/app" ) ;
99- const manifests = await glob ( join ( baseDir , "**/*_client-reference-manifest.js" ) , {
101+ const manifestPaths = await glob ( join ( baseDir , "**/*_client-reference-manifest.js" ) , {
100102 windowsPathsNoEscape : true ,
101103 } ) ;
102104
103- // Sort by path length descending so longer (more specific) paths match first,
104- // preventing suffix collisions in the `.endsWith()` chain (see #1156).
105- const sortedManifests = [ ...manifests ] . sort ( ( a , b ) => b . length - a . length ) ;
106- const returnManifests = sortedManifests
107- . map ( ( manifest ) => {
108- const endsWith = normalizePath ( relative ( baseDir , manifest ) ) ;
109- const key = normalizePath ( "/" + relative ( appDir , manifest ) ) . replace (
110- "_client-reference-manifest.js" ,
111- ""
112- ) ;
105+ // Map of factored large values (variable name -> value)
106+ const factoredValues = new Map < string , string > ( ) ;
107+ // Map of manifest path -> factored manifest content
108+ const factoredManifest = new Map < string , string > ( ) ;
109+ for ( const path of manifestPaths ) {
110+ if ( path . endsWith ( "page_client-reference-manifest.js" ) ) {
111+ // `page_client-reference-manifest.js` files could contain large repeated values.
112+ // Factor out large values into separate variables to reduce the overall size of the generated code.
113+ let manifest = await readFile ( path , "utf-8" ) ;
114+ manifest = factorManifestValue ( manifest , "clientModules" , factoredValues ) ;
115+ manifest = factorManifestValue ( manifest , "ssrModuleMapping" , factoredValues ) ;
116+ manifest = factorManifestValue ( manifest , "edgeSSRModuleMapping" , factoredValues ) ;
117+ manifest = factorManifestValue ( manifest , "rscModuleMapping" , factoredValues ) ;
118+ factoredManifest . set ( path , manifest ) ;
119+ }
120+ }
121+
122+ const factoredValuesCode = [ ...factoredValues . entries ( ) ]
123+ . map ( ( [ varName , value ] ) => `const ${ varName } = ${ value } ;` )
124+ . join ( "\n" ) ;
125+
126+ const returnManifests = manifestPaths
127+ // Sort by path length descending so longer (more specific) paths match first,
128+ // preventing suffix collisions in the `.endsWith()` chain (see #1156).
129+ . toSorted ( ( a , b ) => b . length - a . length )
130+ . map ( ( path ) => {
131+ let manifest : string ;
132+
133+ if ( factoredManifest . has ( path ) ) {
134+ manifest = factoredManifest . get ( path ) ! ;
135+ } else {
136+ manifest = `require(${ JSON . stringify ( path ) } );` ;
137+ }
138+
139+ const endsWith = normalizePath ( relative ( baseDir , path ) ) ;
140+ const key = normalizePath ( "/" + relative ( appDir , path ) ) . replace ( "_client-reference-manifest.js" , "" ) ;
113141 return `
114142if ($PATH.endsWith("${ endsWith } ")) {
115- require( ${ JSON . stringify ( manifest ) } );
143+ ${ manifest }
116144 return {
117145 __RSC_MANIFEST: {
118146 "${ key } ": globalThis.__RSC_MANIFEST["${ key } "],
@@ -130,6 +158,8 @@ function evalManifest($PATH, $$$ARGS) {
130158}` ,
131159 } ,
132160 fix : `
161+ ${ factoredValuesCode }
162+
133163function evalManifest($PATH, $$$ARGS) {
134164 $PATH = $PATH.replaceAll(${ JSON . stringify ( sep ) } , ${ JSON . stringify ( posix . sep ) } );
135165 ${ returnManifests }
@@ -142,3 +172,55 @@ function evalManifest($PATH, $$$ARGS) {
142172}` ,
143173 } satisfies RuleConfig ;
144174}
175+
176+ /**
177+ * Factor out large manifest values into separate variables.
178+ *
179+ * @param manifest The manifest code
180+ * @param key The key to factor out
181+ * @param values A map to store the factored values (indexed by variable name)
182+ * @returns The manifest code with large values factored out
183+ */
184+ function factorManifestValue ( manifest : string , key : string , values : Map < string , string > ) : string {
185+ const valueName = "VALUE" ;
186+ // ASTGrep rule to extract the value of a specific key from the manifest object in the evalManifest function.
187+ //
188+ // globalThis.__RSC_MANIFEST["/path/to/page"] = {
189+ // // ...
190+ // key: $VALUE
191+ // // ...
192+ // }
193+ const extractValueRule = `
194+ rule:
195+ kind: pair
196+ all:
197+ - has:
198+ field: key
199+ pattern: '"${ key } "'
200+ - has:
201+ field: value
202+ pattern: $${ valueName }
203+ inside:
204+ pattern: globalThis.__RSC_MANIFEST[$$$_] = { $$$ };
205+ stopBy: end
206+ fix: '"${ key } ": $${ valueName } '
207+ ` ;
208+
209+ const rootNode = parse ( Lang . JavaScript , manifest ) . root ( ) ;
210+ const { matches } = applyRule ( extractValueRule , rootNode , { once : true } ) ;
211+ if ( matches . length === 1 && matches [ 0 ] ?. getMatch ( valueName ) ) {
212+ const match = matches [ 0 ] ;
213+ const value = match . getMatch ( valueName ) ! . text ( ) ;
214+ if ( value . length > 30 ) {
215+ // Factor out large values into separate variables.
216+ // The value is factored out in a variable name `v_${hash}`.
217+ const valueVarName = `v_${ crypto . createHash ( "sha1" ) . update ( value ) . digest ( "hex" ) } ` ;
218+ values . set ( valueVarName , value ) ;
219+ // Replace the value in the manifest with the variable reference.
220+ return rootNode . commitEdits ( [ match . replace ( `"${ key } ": ${ valueVarName } ` ) ] ) ;
221+ }
222+ }
223+
224+ // return the original manifest if the value is not found or is small enough to not warrant factoring out.
225+ return manifest ;
226+ }
0 commit comments