Skip to content

Commit 8371be5

Browse files
committed
Factor large repeated values in manifests
1 parent c4b1687 commit 8371be5

2 files changed

Lines changed: 102 additions & 13 deletions

File tree

.changeset/dry-forks-melt.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@opennextjs/cloudflare": patch
3+
---
4+
5+
Factor large repeated values in manifests
6+
7+
This reduce the size of the generated code.

packages/cloudflare/src/cli/build/patches/plugins/load-manifest.ts

Lines changed: 95 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44
* They rely on `readFileSync` that is not supported by workerd.
55
*/
66

7+
import crypto from "node:crypto";
78
import { readFile } from "node:fs/promises";
89
import { join, posix, relative, sep } from "node:path";
910

11+
import { Lang, parse } from "@ast-grep/napi";
1012
import { 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";
1214
import type { ContentUpdater, Plugin } from "@opennextjs/aws/plugins/content-updater.js";
1315
import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js";
1416
import { 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 `
114142
if ($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+
133163
function 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

Comments
 (0)