Skip to content

Commit 608893e

Browse files
vicbJosh Kahn
andauthored
Factor manifest code to reduce the bundle size (#1215)
Co-authored-by: Josh Kahn <jkahn@cloudflare.com>
1 parent 32594d6 commit 608893e

3 files changed

Lines changed: 486 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.
Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
import { describe, expect, test } from "vitest";
2+
3+
import { factorManifestValue, factorObjectValues, getOrCreateVarName } from "./load-manifest.js";
4+
5+
describe("getOrCreateVarName", () => {
6+
test("returns a variable name starting with 'v' followed by a 3-char prefix", () => {
7+
const prefixMap = new Map<string, string>();
8+
const varName = getOrCreateVarName("some-value-long-enough-for-hashing", prefixMap);
9+
expect(varName).toMatch(/^v[0-9a-f]{3}$/);
10+
});
11+
12+
test("returns the same variable name for the same value", () => {
13+
const prefixMap = new Map<string, string>();
14+
const value = "some-value-long-enough-for-hashing";
15+
const first = getOrCreateVarName(value, prefixMap);
16+
const second = getOrCreateVarName(value, prefixMap);
17+
expect(second).toBe(first);
18+
expect(prefixMap.size).toBe(1);
19+
});
20+
21+
test("returns different variable names for different values", () => {
22+
const prefixMap = new Map<string, string>();
23+
const a = getOrCreateVarName("value-a-that-is-long-enough-to-be-factored", prefixMap);
24+
const b = getOrCreateVarName("value-b-that-is-long-enough-to-be-factored", prefixMap);
25+
expect(a).not.toBe(b);
26+
expect(prefixMap.size).toBe(2);
27+
});
28+
29+
// SHA1("test-value-135-padding-to-make-it-long") = 8aa7da...
30+
// SHA1("test-value-152-padding-to-make-it-long") = 8aae79...
31+
// Both share the 3-char prefix "8aa".
32+
test("lengthens the new entry on 3-char collision without renaming the first", () => {
33+
const prefixMap = new Map<string, string>();
34+
const first = getOrCreateVarName("test-value-135-padding-to-make-it-long", prefixMap);
35+
const second = getOrCreateVarName("test-value-152-padding-to-make-it-long", prefixMap);
36+
37+
// The first entry keeps its short 3-char prefix.
38+
expect(first).toBe("v8aa");
39+
// The second entry gets a longer prefix to avoid collision.
40+
expect(second).toBe("v8aae");
41+
expect(prefixMap.size).toBe(2);
42+
});
43+
44+
// SHA1("test-value-241-...") = 47b8f8...
45+
// SHA1("test-value-404-...") = 47b6fc...
46+
// SHA1("test-value-748-...") = 47bac4...
47+
// All three share the 3-char prefix "47b".
48+
test("handles three-way collision at 3-char prefix", () => {
49+
const prefixMap = new Map<string, string>();
50+
const first = getOrCreateVarName("test-value-241-padding-to-make-it-long", prefixMap);
51+
const second = getOrCreateVarName("test-value-404-padding-to-make-it-long", prefixMap);
52+
const third = getOrCreateVarName("test-value-748-padding-to-make-it-long", prefixMap);
53+
54+
// First takes "47b".
55+
expect(first).toBe("v47b");
56+
// Second collides at "47b", gets "47b6".
57+
expect(second).toBe("v47b6");
58+
// Third collides at "47b" (taken by first), gets "47ba".
59+
expect(third).toBe("v47ba");
60+
expect(prefixMap.size).toBe(3);
61+
});
62+
63+
// SHA1("test-value-179-...") = 6ce8d80f...
64+
// SHA1("test-value-548-...") = 6ce8335e...
65+
// Both share the 4-char prefix "6ce8".
66+
test("handles collision that requires more than 4 chars to resolve", () => {
67+
const prefixMap = new Map<string, string>();
68+
const first = getOrCreateVarName("test-value-179-padding-to-make-it-long", prefixMap);
69+
const second = getOrCreateVarName("test-value-548-padding-to-make-it-long", prefixMap);
70+
71+
// First takes "6ce".
72+
expect(first).toBe("v6ce");
73+
// Second collides at "6ce", tries "6ce8" — still collides, resolves to "6ce83".
74+
expect(second).toBe("v6ce8");
75+
expect(prefixMap.size).toBe(2);
76+
});
77+
78+
test("updates prefixMap in place", () => {
79+
const prefixMap = new Map<string, string>();
80+
getOrCreateVarName("value-a-that-is-long-enough-to-be-factored", prefixMap);
81+
expect(prefixMap.size).toBe(1);
82+
const [prefix, fullHash] = [...prefixMap.entries()][0]!;
83+
expect(prefix).toHaveLength(3);
84+
expect(fullHash).toHaveLength(40);
85+
});
86+
});
87+
88+
describe("factorManifestValue", () => {
89+
const makeManifest = (key: string, value: string) =>
90+
`globalThis.__RSC_MANIFEST["/page"] = { "${key}": ${value} };`;
91+
92+
test("factors out large values into a variable", () => {
93+
const values = new Map<string, string>();
94+
const prefixMap = new Map<string, string>();
95+
const largeValue = JSON.stringify({ a: "x".repeat(50) });
96+
const manifest = makeManifest("clientModules", largeValue);
97+
98+
const result = factorManifestValue(manifest, "clientModules", values, prefixMap);
99+
100+
// The manifest should reference a variable instead of the inline value.
101+
expect(result).not.toContain(largeValue);
102+
expect(values.size).toBe(1);
103+
const [varName, storedValue] = [...values.entries()][0]!;
104+
expect(varName).toMatch(/^v[0-9a-f]{3,}$/);
105+
expect(storedValue).toBe(largeValue);
106+
expect(result).toContain(varName);
107+
expect(prefixMap.size).toBe(1);
108+
});
109+
110+
test("leaves small values untouched", () => {
111+
const values = new Map<string, string>();
112+
const prefixMap = new Map<string, string>();
113+
const smallValue = '"small"';
114+
const manifest = makeManifest("clientModules", smallValue);
115+
116+
const result = factorManifestValue(manifest, "clientModules", values, prefixMap);
117+
118+
expect(result).toBe(manifest);
119+
expect(values.size).toBe(0);
120+
expect(prefixMap.size).toBe(0);
121+
});
122+
123+
test("returns original manifest when key is not found", () => {
124+
const values = new Map<string, string>();
125+
const prefixMap = new Map<string, string>();
126+
const manifest = makeManifest("clientModules", '"some-value"');
127+
128+
const result = factorManifestValue(manifest, "nonExistentKey", values, prefixMap);
129+
130+
expect(result).toBe(manifest);
131+
expect(values.size).toBe(0);
132+
});
133+
134+
test("reuses variable name for identical values across manifests", () => {
135+
const values = new Map<string, string>();
136+
const prefixMap = new Map<string, string>();
137+
const largeValue = JSON.stringify({ a: "x".repeat(50) });
138+
const manifest1 = makeManifest("clientModules", largeValue);
139+
const manifest2 = makeManifest("clientModules", largeValue);
140+
141+
const result1 = factorManifestValue(manifest1, "clientModules", values, prefixMap);
142+
const result2 = factorManifestValue(manifest2, "clientModules", values, prefixMap);
143+
144+
// Both should reference the same variable.
145+
const varName = [...values.keys()][0]!;
146+
expect(result1).toContain(varName);
147+
expect(result2).toContain(varName);
148+
// Only one entry in the values map (same content, same variable).
149+
expect(values.size).toBe(1);
150+
expect(prefixMap.size).toBe(1);
151+
});
152+
153+
test("factors multiple keys from the same manifest with shared prefixMap", () => {
154+
const values = new Map<string, string>();
155+
const prefixMap = new Map<string, string>();
156+
const largeA = JSON.stringify({ a: "a".repeat(50) });
157+
const largeB = JSON.stringify({ b: "b".repeat(50) });
158+
const manifest = `globalThis.__RSC_MANIFEST["/page"] = { "clientModules": ${largeA}, "ssrModuleMapping": ${largeB} };`;
159+
160+
let result = factorManifestValue(manifest, "clientModules", values, prefixMap);
161+
result = factorManifestValue(result, "ssrModuleMapping", values, prefixMap);
162+
163+
expect(values.size).toBe(2);
164+
expect(prefixMap.size).toBe(2);
165+
// Both variable names should appear in the result.
166+
for (const varName of values.keys()) {
167+
expect(result).toContain(varName);
168+
}
169+
// Neither large value should appear inline.
170+
expect(result).not.toContain(largeA);
171+
expect(result).not.toContain(largeB);
172+
});
173+
});
174+
175+
describe("factorObjectValues", () => {
176+
test("deduplicates repeated large chunks arrays", () => {
177+
const sharedVars = new Map<string, string>();
178+
const prefixMap = new Map<string, string>();
179+
const chunksArray = JSON.stringify(["chunk-a-long-name.js", "chunk-b-long-name.js"]);
180+
// Two entries with the same chunks array.
181+
const input = `{
182+
"mod1": { "id": "1", "chunks": ${chunksArray} },
183+
"mod2": { "id": "2", "chunks": ${chunksArray} }
184+
}`;
185+
186+
const result = factorObjectValues(input, sharedVars, prefixMap);
187+
188+
// The chunks array should be replaced by a variable reference.
189+
expect(sharedVars.size).toBe(1);
190+
const [varName, storedValue] = [...sharedVars.entries()][0]!;
191+
expect(varName).toMatch(/^v[0-9a-f]{3,}$/);
192+
expect(storedValue).toBe(chunksArray);
193+
// Both occurrences should use the same variable.
194+
const varOccurrences = result.split(varName).length - 1;
195+
expect(varOccurrences).toBe(2);
196+
expect(prefixMap.size).toBe(1);
197+
});
198+
199+
test("skips small chunks arrays", () => {
200+
const sharedVars = new Map<string, string>();
201+
const prefixMap = new Map<string, string>();
202+
const input = `{
203+
"mod1": { "id": "1", "chunks": ["a"] }
204+
}`;
205+
206+
const result = factorObjectValues(input, sharedVars, prefixMap);
207+
208+
expect(result).toBe(input);
209+
expect(sharedVars.size).toBe(0);
210+
expect(prefixMap.size).toBe(0);
211+
});
212+
213+
test("handles distinct chunks arrays with different variable names", () => {
214+
const sharedVars = new Map<string, string>();
215+
const prefixMap = new Map<string, string>();
216+
const chunksA = JSON.stringify(["chunk-alpha-long-name.js", "chunk-beta-long-name.js"]);
217+
const chunksB = JSON.stringify(["chunk-gamma-long-name.js", "chunk-delta-long-name.js"]);
218+
const input = `{
219+
"mod1": { "id": "1", "chunks": ${chunksA} },
220+
"mod2": { "id": "2", "chunks": ${chunksB} }
221+
}`;
222+
223+
const result = factorObjectValues(input, sharedVars, prefixMap);
224+
225+
expect(sharedVars.size).toBe(2);
226+
expect(prefixMap.size).toBe(2);
227+
// Both variable names should appear in the result.
228+
for (const varName of sharedVars.keys()) {
229+
expect(result).toContain(varName);
230+
}
231+
});
232+
233+
test("shares the prefixMap with factorManifestValue", () => {
234+
const values = new Map<string, string>();
235+
const sharedVars = new Map<string, string>();
236+
const prefixMap = new Map<string, string>();
237+
238+
// First, factor a manifest value.
239+
const largeValue = JSON.stringify({ a: "x".repeat(50) });
240+
const manifest = `globalThis.__RSC_MANIFEST["/page"] = { "clientModules": ${largeValue} };`;
241+
factorManifestValue(manifest, "clientModules", values, prefixMap);
242+
expect(prefixMap.size).toBe(1);
243+
244+
// Then, factor chunks using the same prefixMap.
245+
const chunksArray = JSON.stringify(["chunk-a-long-name.js", "chunk-b-long-name.js"]);
246+
const input = `{ "mod1": { "id": "1", "chunks": ${chunksArray} } }`;
247+
factorObjectValues(input, sharedVars, prefixMap);
248+
249+
// The prefixMap should now have 2 entries.
250+
expect(prefixMap.size).toBe(2);
251+
// The variable names should be different.
252+
const allVarNames = [...values.keys(), ...sharedVars.keys()];
253+
expect(new Set(allVarNames).size).toBe(2);
254+
});
255+
256+
test("returns input unchanged when no chunks pairs are found", () => {
257+
const sharedVars = new Map<string, string>();
258+
const prefixMap = new Map<string, string>();
259+
const input = `{ "mod1": { "id": "1", "name": "test" } }`;
260+
261+
const result = factorObjectValues(input, sharedVars, prefixMap);
262+
263+
expect(result).toBe(input);
264+
expect(sharedVars.size).toBe(0);
265+
});
266+
});

0 commit comments

Comments
 (0)