@opennextjs/cloudflare 1.19.2 → 1.19.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,3 +6,40 @@
6
6
  import { type BuildOptions } from "@opennextjs/aws/build/helper.js";
7
7
  import type { ContentUpdater, Plugin } from "@opennextjs/aws/plugins/content-updater.js";
8
8
  export declare function inlineLoadManifest(updater: ContentUpdater, buildOpts: BuildOptions): Plugin;
9
+ /**
10
+ * Factor out large manifest values into separate variables.
11
+ *
12
+ * @param manifest The manifest code.
13
+ * @param key The key to factor out.
14
+ * @param values A map to store the factored values (indexed by variable name).
15
+ * @param prefixMap Map of short hash prefix → full hash, updated in place for
16
+ * collision resolution across calls.
17
+ * @returns The manifest code with large values factored out.
18
+ */
19
+ export declare function factorManifestValue(manifest: string, key: string, values: Map<string, string>, prefixMap: Map<string, string>): string;
20
+ /**
21
+ * Factor out large object values into separate variables.
22
+ *
23
+ * @param valueText The JS source text of the module mapping object.
24
+ * @param sharedVars Map to accumulate shared variable declarations.
25
+ * @param prefixMap Map of short hash prefix → full hash, updated in place for
26
+ * collision resolution across calls.
27
+ * @returns The rewritten value text with chunks arrays replaced by variable refs.
28
+ */
29
+ export declare function factorObjectValues(valueText: string, sharedVars: Map<string, string>, prefixMap: Map<string, string>): string;
30
+ /**
31
+ * Get or create a short variable name for a value, resolving collisions.
32
+ *
33
+ * Computes a SHA1 hash of the value, then finds the shortest unique prefix
34
+ * (minimum {@link MIN_PREFIX_LENGTH} hex chars). When a new hash collides with
35
+ * an existing prefix, the new entry is given a longer prefix — existing entries
36
+ * are never renamed.
37
+ *
38
+ * This allows saving space in the generated code (A full SHA1 is 40 hex chars) because
39
+ * identifiers are not minimized by the Open Next build process.
40
+ *
41
+ * @param value The value to hash.
42
+ * @param prefixMap Map of short prefix → full hash, updated in place.
43
+ * @returns The variable name (`v<shortPrefix>`).
44
+ */
45
+ export declare function getOrCreateVarName(value: string, prefixMap: Map<string, string>): string;
@@ -3,10 +3,12 @@
3
3
  *
4
4
  * They rely on `readFileSync` that is not supported by workerd.
5
5
  */
6
+ import crypto from "node:crypto";
6
7
  import { readFile } from "node:fs/promises";
7
8
  import { join, posix, relative, sep } from "node:path";
9
+ import { Lang, parse } from "@ast-grep/napi";
8
10
  import { getPackagePath } from "@opennextjs/aws/build/helper.js";
9
- import { patchCode } from "@opennextjs/aws/build/patch/astCodePatcher.js";
11
+ import { applyRule, patchCode } from "@opennextjs/aws/build/patch/astCodePatcher.js";
10
12
  import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js";
11
13
  import { glob } from "glob";
12
14
  import { normalizePath } from "../../../utils/normalize-path.js";
@@ -75,19 +77,62 @@ async function getEvalManifestRule(buildOpts) {
75
77
  const { outputDir } = buildOpts;
76
78
  const baseDir = join(outputDir, "server-functions/default", getPackagePath(buildOpts), ".next");
77
79
  const appDir = join(baseDir, "server/app");
78
- const manifests = await glob(join(baseDir, "**/*_client-reference-manifest.js"), {
80
+ const manifestPaths = await glob(join(baseDir, "**/*_client-reference-manifest.js"), {
79
81
  windowsPathsNoEscape: true,
80
82
  });
81
- // Sort by path length descending so longer (more specific) paths match first,
82
- // preventing suffix collisions in the `.endsWith()` chain (see #1156).
83
- const sortedManifests = [...manifests].sort((a, b) => b.length - a.length);
84
- const returnManifests = sortedManifests
85
- .map((manifest) => {
86
- const endsWith = normalizePath(relative(baseDir, manifest));
87
- const key = normalizePath("/" + relative(appDir, manifest)).replace("_client-reference-manifest.js", "");
83
+ // Map of factored large objects (variable name -> {...})
84
+ const factoredObjects = new Map();
85
+ // Map of manifest path -> factored manifest content
86
+ const factoredManifest = new Map();
87
+ // Shared map of short hash prefix -> full SHA1 hash, used for collision resolution.
88
+ const prefixMap = new Map();
89
+ for (const path of manifestPaths) {
90
+ if (path.endsWith("page_client-reference-manifest.js")) {
91
+ // `page_client-reference-manifest.js` files could contain large repeated values.
92
+ // Factor out large values into separate variables to reduce the overall size of the generated code.
93
+ let manifest = await readFile(path, "utf-8");
94
+ for (const key of [
95
+ "clientModules",
96
+ "ssrModuleMapping",
97
+ "edgeSSRModuleMapping",
98
+ "rscModuleMapping",
99
+ "entryCSSFiles",
100
+ "entryJSFiles",
101
+ ]) {
102
+ manifest = factorManifestValue(manifest, key, factoredObjects, prefixMap);
103
+ }
104
+ factoredManifest.set(path, manifest);
105
+ }
106
+ }
107
+ // Map of factored values in an object
108
+ const factoredValues = new Map();
109
+ for (const [varName, value] of factoredObjects) {
110
+ factoredObjects.set(varName, factorObjectValues(value, factoredValues, prefixMap));
111
+ }
112
+ // Prepend chunks variable declarations before the factored values
113
+ const factoredValueCode = [...factoredValues.entries()]
114
+ .map(([name, val]) => `const ${name} = ${val};`)
115
+ .join("\n");
116
+ const factoredObjectCode = [...factoredObjects.entries()]
117
+ .map(([varName, value]) => `const ${varName} = ${value};`)
118
+ .join("\n");
119
+ const returnManifests = manifestPaths
120
+ // Sort by path length descending so longer (more specific) paths match first,
121
+ // preventing suffix collisions in the `.endsWith()` chain (see #1156).
122
+ .toSorted((a, b) => b.length - a.length)
123
+ .map((path) => {
124
+ let manifest;
125
+ if (factoredManifest.has(path)) {
126
+ manifest = factoredManifest.get(path);
127
+ }
128
+ else {
129
+ manifest = `require(${JSON.stringify(path)});`;
130
+ }
131
+ const endsWith = normalizePath(relative(baseDir, path));
132
+ const key = normalizePath("/" + relative(appDir, path)).replace("_client-reference-manifest.js", "");
88
133
  return `
89
134
  if ($PATH.endsWith("${endsWith}")) {
90
- require(${JSON.stringify(manifest)});
135
+ ${manifest}
91
136
  return {
92
137
  __RSC_MANIFEST: {
93
138
  "${key}": globalThis.__RSC_MANIFEST["${key}"],
@@ -104,6 +149,9 @@ function evalManifest($PATH, $$$ARGS) {
104
149
  }`,
105
150
  },
106
151
  fix: `
152
+ ${factoredValueCode}
153
+ ${factoredObjectCode}
154
+
107
155
  function evalManifest($PATH, $$$ARGS) {
108
156
  $PATH = $PATH.replaceAll(${JSON.stringify(sep)}, ${JSON.stringify(posix.sep)});
109
157
  ${returnManifests}
@@ -116,3 +164,130 @@ function evalManifest($PATH, $$$ARGS) {
116
164
  }`,
117
165
  };
118
166
  }
167
+ /**
168
+ * Factor out large manifest values into separate variables.
169
+ *
170
+ * @param manifest The manifest code.
171
+ * @param key The key to factor out.
172
+ * @param values A map to store the factored values (indexed by variable name).
173
+ * @param prefixMap Map of short hash prefix → full hash, updated in place for
174
+ * collision resolution across calls.
175
+ * @returns The manifest code with large values factored out.
176
+ */
177
+ export function factorManifestValue(manifest, key, values, prefixMap) {
178
+ const valueName = "VALUE";
179
+ // ASTGrep rule to extract the value of a specific key from the manifest object in the evalManifest function.
180
+ //
181
+ // globalThis.__RSC_MANIFEST["/path/to/page"] = {
182
+ // // ...
183
+ // key: $VALUE
184
+ // // ...
185
+ // }
186
+ const extractValueRule = `
187
+ rule:
188
+ kind: pair
189
+ all:
190
+ - has:
191
+ field: key
192
+ pattern: '"${key}"'
193
+ - has:
194
+ field: value
195
+ pattern: $${valueName}
196
+ inside:
197
+ pattern: globalThis.__RSC_MANIFEST[$$$_] = { $$$ };
198
+ stopBy: end
199
+ `;
200
+ const rootNode = parse(Lang.JavaScript, manifest).root();
201
+ const { matches } = applyRule(extractValueRule, rootNode, { once: true });
202
+ if (matches.length === 1 && matches[0]?.getMatch(valueName)) {
203
+ const match = matches[0];
204
+ const value = match.getMatch(valueName).text();
205
+ if (value.length > 30) {
206
+ // Factor out large values into separate variables.
207
+ const varName = getOrCreateVarName(value, prefixMap);
208
+ values.set(varName, value);
209
+ // Replace the value in the manifest with the variable reference.
210
+ return rootNode.commitEdits([match.replace(`"${key}": ${varName}`)]);
211
+ }
212
+ }
213
+ // Return the original manifest if the value is not found or is small enough to not warrant factoring out.
214
+ return manifest;
215
+ }
216
+ /**
217
+ * Factor out large object values into separate variables.
218
+ *
219
+ * @param valueText The JS source text of the module mapping object.
220
+ * @param sharedVars Map to accumulate shared variable declarations.
221
+ * @param prefixMap Map of short hash prefix → full hash, updated in place for
222
+ * collision resolution across calls.
223
+ * @returns The rewritten value text with chunks arrays replaced by variable refs.
224
+ */
225
+ export function factorObjectValues(valueText, sharedVars, prefixMap) {
226
+ const rootNode = parse(Lang.JavaScript, valueText).root();
227
+ // Find all "chunks": [...] pairs
228
+ const chunksRule = `
229
+ rule:
230
+ kind: pair
231
+ all:
232
+ - has:
233
+ field: key
234
+ pattern: '"chunks"'
235
+ - has:
236
+ field: value
237
+ kind: array
238
+ pattern: $CHUNKS
239
+ `;
240
+ const { matches } = applyRule(chunksRule, rootNode, { once: false });
241
+ const edits = [];
242
+ for (const match of matches) {
243
+ const chunksNode = match.getMatch("CHUNKS");
244
+ if (!chunksNode)
245
+ continue;
246
+ const chunksText = chunksNode.text();
247
+ if (chunksText.length <= 30)
248
+ continue; // Skip small arrays
249
+ const varName = getOrCreateVarName(chunksText, prefixMap);
250
+ sharedVars.set(varName, chunksText);
251
+ edits.push({ match, replacement: `"chunks": ${varName}` });
252
+ }
253
+ return edits.length === 0
254
+ ? valueText
255
+ : rootNode.commitEdits(edits.map((e) => e.match.replace(e.replacement)));
256
+ }
257
+ /** Minimum number of hex characters used for short hash prefixes. */
258
+ const MIN_PREFIX_LENGTH = 3;
259
+ /**
260
+ * Get or create a short variable name for a value, resolving collisions.
261
+ *
262
+ * Computes a SHA1 hash of the value, then finds the shortest unique prefix
263
+ * (minimum {@link MIN_PREFIX_LENGTH} hex chars). When a new hash collides with
264
+ * an existing prefix, the new entry is given a longer prefix — existing entries
265
+ * are never renamed.
266
+ *
267
+ * This allows saving space in the generated code (A full SHA1 is 40 hex chars) because
268
+ * identifiers are not minimized by the Open Next build process.
269
+ *
270
+ * @param value The value to hash.
271
+ * @param prefixMap Map of short prefix → full hash, updated in place.
272
+ * @returns The variable name (`v<shortPrefix>`).
273
+ */
274
+ export function getOrCreateVarName(value, prefixMap) {
275
+ const sha1 = crypto.createHash("sha1").update(value).digest("hex");
276
+ // Find the shortest prefix (>= MIN_PREFIX_LENGTH) that doesn't collide
277
+ // with any existing prefix. Only the new entry is lengthened.
278
+ for (let len = MIN_PREFIX_LENGTH; len <= sha1.length; len++) {
279
+ const candidate = sha1.slice(0, len);
280
+ const existing = prefixMap.get(candidate);
281
+ if (existing === undefined) {
282
+ prefixMap.set(candidate, sha1);
283
+ return `v${candidate}`;
284
+ }
285
+ if (existing === sha1) {
286
+ // Same content seen again — reuse the existing variable.
287
+ return `v${candidate}`;
288
+ }
289
+ // A different hash occupies this exact prefix — lengthen and retry.
290
+ }
291
+ // Unreachable: two different SHA1 hashes always diverge before 40 chars.
292
+ throw new Error("Failed to find a unique prefix");
293
+ }
@@ -2,6 +2,7 @@ import logger from "@opennextjs/aws/logger.js";
2
2
  import { build as buildImpl } from "../build/build.js";
3
3
  import { askConfirmation } from "../utils/ask-confirmation.js";
4
4
  import { createWranglerConfigFile, findWranglerConfig } from "../utils/create-wrangler-config.js";
5
+ import { isNonInteractiveOrCI } from "../utils/is-interactive.js";
5
6
  import { compileConfig, getNormalizedOptions, nextAppDir, printHeaders, readWranglerConfig, withWranglerOptions, withWranglerPassthroughArgs, } from "./utils/utils.js";
6
7
  /**
7
8
  * Implementation of the `opennextjs-cloudflare build` command.
@@ -18,6 +19,12 @@ export async function buildCommand(args) {
18
19
  // nor when `--skipWranglerConfigCheck` is used.
19
20
  if (!projectOpts.wranglerConfigPath && !args.skipWranglerConfigCheck) {
20
21
  if (!findWranglerConfig(projectOpts.sourceDir)) {
22
+ // In non-interactive environments (CI, Cloudflare Workers Builds,
23
+ // Docker, etc.) the prompt would hang or crash. Fail fast with a
24
+ // clear message pointing at the existing escape hatch.
25
+ if (isNonInteractiveOrCI()) {
26
+ throw new Error("No `wrangler.(toml|json|jsonc)` config file found.\n\nCreate one at the project root before running the build, or skip this check with `--skipWranglerConfigCheck` or `SKIP_WRANGLER_CONFIG_CHECK=yes`.");
27
+ }
21
28
  const confirmCreate = "No `wrangler.(toml|json|jsonc)` config file found, do you want to create one?";
22
29
  if (await askConfirmation(confirmCreate)) {
23
30
  await createWranglerConfigFile(projectOpts.sourceDir);
@@ -9,7 +9,8 @@ import logger from "@opennextjs/aws/logger.js";
9
9
  import { unstable_readConfig } from "wrangler";
10
10
  import { ensureCloudflareConfig } from "../../build/utils/ensure-cf-config.js";
11
11
  import { askConfirmation } from "../../utils/ask-confirmation.js";
12
- import { createOpenNextConfigFile, findOpenNextConfig } from "../../utils/create-open-next-config.js";
12
+ import { createOpenNextConfigFile, findOpenNextConfig, OPEN_NEXT_CONFIG_FILE_NAME, } from "../../utils/create-open-next-config.js";
13
+ import { isNonInteractiveOrCI } from "../../utils/is-interactive.js";
13
14
  export const nextAppDir = process.cwd();
14
15
  /**
15
16
  * Print headers and warnings for the CLI.
@@ -41,9 +42,15 @@ export async function compileConfig(configPath) {
41
42
  }
42
43
  configPath ??= findOpenNextConfig(nextAppDir);
43
44
  if (!configPath) {
44
- const answer = await askConfirmation("Missing required `open-next.config.ts` file, do you want to create one?");
45
+ // In non-interactive environments (CI, Cloudflare Workers Builds,
46
+ // Docker, etc.) there is no TTY to answer a prompt — the build would
47
+ // hang or crash. Fail fast with an actionable message instead.
48
+ if (isNonInteractiveOrCI()) {
49
+ throw new Error(`No \`${OPEN_NEXT_CONFIG_FILE_NAME}\` file was found in the project root.\n\nThis file is required for OpenNext Cloudflare builds.\nRun \`opennextjs-cloudflare migrate\` to create it, or see https://opennext.js.org/cloudflare/get-started for setup guidance.\nCommit it and re-run the build.`);
50
+ }
51
+ const answer = await askConfirmation(`Missing required \`${OPEN_NEXT_CONFIG_FILE_NAME}\` file, do you want to create one?`);
45
52
  if (!answer) {
46
- throw new Error("The `open-next.config.ts` file is required, aborting!");
53
+ throw new Error(`The \`${OPEN_NEXT_CONFIG_FILE_NAME}\` file is required, aborting!`);
47
54
  }
48
55
  configPath = createOpenNextConfigFile(nextAppDir, { cache: false });
49
56
  }
@@ -37,11 +37,6 @@ function init(request, env) {
37
37
  populateProcessEnv(url, env);
38
38
  }
39
39
  function initRuntime() {
40
- // Some packages rely on `process.version` and `process.versions.node` (i.e. Jose@4)
41
- // TODO: Remove when https://github.com/unjs/unenv/pull/493 is merged
42
- Object.assign(process, { version: process.version || "v22.14.0" });
43
- // @ts-expect-error Node type does not match workerd
44
- Object.assign(process.versions, { node: "22.14.0", ...process.versions });
45
40
  globalThis.__dirname ??= "";
46
41
  globalThis.__filename ??= "";
47
42
  // Some packages rely on `import.meta.url` but it is undefined in workerd
@@ -1,3 +1,4 @@
1
+ export declare const OPEN_NEXT_CONFIG_FILE_NAME = "open-next.config.ts";
1
2
  /**
2
3
  * Finds the path to the OpenNext configuration file if it exists.
3
4
  *
@@ -2,6 +2,7 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { patchCode } from "@opennextjs/aws/build/patch/astCodePatcher.js";
4
4
  import { getPackageTemplatesDirPath } from "../../utils/get-package-templates-dir-path.js";
5
+ export const OPEN_NEXT_CONFIG_FILE_NAME = "open-next.config.ts";
5
6
  /**
6
7
  * Finds the path to the OpenNext configuration file if it exists.
7
8
  *
@@ -9,7 +10,7 @@ import { getPackageTemplatesDirPath } from "../../utils/get-package-templates-di
9
10
  * @returns The full path to open-next.config.ts if it exists, undefined otherwise
10
11
  */
11
12
  export function findOpenNextConfig(appDir) {
12
- const openNextConfigPath = join(appDir, "open-next.config.ts");
13
+ const openNextConfigPath = join(appDir, OPEN_NEXT_CONFIG_FILE_NAME);
13
14
  if (existsSync(openNextConfigPath)) {
14
15
  return openNextConfigPath;
15
16
  }
@@ -23,8 +24,8 @@ export function findOpenNextConfig(appDir) {
23
24
  * @returns The path to the created configuration file
24
25
  */
25
26
  export function createOpenNextConfigFile(appDir, options) {
26
- const openNextConfigPath = join(appDir, "open-next.config.ts");
27
- let content = readFileSync(join(getPackageTemplatesDirPath(), "open-next.config.ts"), "utf8");
27
+ const openNextConfigPath = join(appDir, OPEN_NEXT_CONFIG_FILE_NAME);
28
+ let content = readFileSync(join(getPackageTemplatesDirPath(), OPEN_NEXT_CONFIG_FILE_NAME), "utf8");
28
29
  if (!options.cache) {
29
30
  content = patchCode(content, commentOutR2ImportRule);
30
31
  content = patchCode(content, commentOutIncrementalCacheRule);
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Whether the current process is running in an interactive terminal.
3
+ */
4
+ export declare function isInteractive(): boolean;
5
+ /**
6
+ * Whether prompts should be suppressed.
7
+ */
8
+ export declare function isNonInteractiveOrCI(): boolean;
@@ -0,0 +1,13 @@
1
+ import ci from "ci-info";
2
+ /**
3
+ * Whether the current process is running in an interactive terminal.
4
+ */
5
+ export function isInteractive() {
6
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
7
+ }
8
+ /**
9
+ * Whether prompts should be suppressed.
10
+ */
11
+ export function isNonInteractiveOrCI() {
12
+ return !isInteractive() || ci.isCI;
13
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@opennextjs/cloudflare",
3
3
  "description": "Cloudflare builder for next apps",
4
- "version": "1.19.2",
4
+ "version": "1.19.3",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "opennextjs-cloudflare": "dist/cli/index.js"
@@ -45,6 +45,7 @@
45
45
  "@ast-grep/napi": "^0.40.5",
46
46
  "@dotenvx/dotenvx": "1.31.0",
47
47
  "@opennextjs/aws": "3.10.2",
48
+ "ci-info": "^4.2.0",
48
49
  "cloudflare": "^4.4.1",
49
50
  "comment-json": "^4.5.1",
50
51
  "enquirer": "^2.4.1",