@opennextjs/cloudflare 1.19.1 → 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.
- package/dist/api/overrides/tag-cache/do-sharded-tag-cache.d.ts +1 -1
- package/dist/api/overrides/tag-cache/do-sharded-tag-cache.js +3 -3
- package/dist/cli/build/patches/plugins/load-manifest.d.ts +37 -0
- package/dist/cli/build/patches/plugins/load-manifest.js +185 -10
- package/dist/cli/build/patches/plugins/turbopack.js +179 -25
- package/dist/cli/commands/build.js +7 -0
- package/dist/cli/commands/deploy.js +4 -1
- package/dist/cli/commands/populate-cache.js +4 -1
- package/dist/cli/commands/preview.js +4 -1
- package/dist/cli/commands/utils/utils.js +10 -3
- package/dist/cli/templates/init.js +0 -5
- package/dist/cli/utils/create-open-next-config.d.ts +1 -0
- package/dist/cli/utils/create-open-next-config.js +4 -3
- package/dist/cli/utils/is-interactive.d.ts +8 -0
- package/dist/cli/utils/is-interactive.js +13 -0
- package/package.json +6 -5
|
@@ -140,7 +140,7 @@ declare class ShardedDOTagCache implements NextModeTagCache {
|
|
|
140
140
|
* @returns An array of objects with the tag and the last revalidation time
|
|
141
141
|
*/
|
|
142
142
|
getFromRegionalCache(opts: CacheTagKeyOptions): Promise<CachedTagValue[]>;
|
|
143
|
-
putToRegionalCache(optsKey: CacheTagKeyOptions, stub: DurableObjectStub<DOShardedTagCache>): Promise<void>;
|
|
143
|
+
putToRegionalCache(optsKey: CacheTagKeyOptions, stub: DurableObjectStub<DOShardedTagCache>, prefetchedTagData?: Record<string, TagData>): Promise<void>;
|
|
144
144
|
/**
|
|
145
145
|
* Deletes the regional cache for the given tags
|
|
146
146
|
* This is used to ensure that the cache is cleared when the tags are revalidated
|
|
@@ -211,14 +211,14 @@ class ShardedDOTagCache {
|
|
|
211
211
|
return [];
|
|
212
212
|
}
|
|
213
213
|
}
|
|
214
|
-
async putToRegionalCache(optsKey, stub) {
|
|
214
|
+
async putToRegionalCache(optsKey, stub, prefetchedTagData) {
|
|
215
215
|
if (!this.opts.regionalCache)
|
|
216
216
|
return;
|
|
217
217
|
const cache = await this.getCacheInstance();
|
|
218
218
|
if (!cache)
|
|
219
219
|
return;
|
|
220
220
|
const tags = optsKey.tags;
|
|
221
|
-
const tagData = await stub.getTagData(tags);
|
|
221
|
+
const tagData = prefetchedTagData ?? (await stub.getTagData(tags));
|
|
222
222
|
await Promise.all(tags.map(async (tag) => {
|
|
223
223
|
let data = tagData[tag];
|
|
224
224
|
if (data === undefined) {
|
|
@@ -316,7 +316,7 @@ class ShardedDOTagCache {
|
|
|
316
316
|
for (const tag of remainingTags) {
|
|
317
317
|
result.set(tag, tagData[tag] ?? null);
|
|
318
318
|
}
|
|
319
|
-
getCloudflareContext().ctx.waitUntil(this.putToRegionalCache({ doId, tags: remainingTags }, stub));
|
|
319
|
+
getCloudflareContext().ctx.waitUntil(this.putToRegionalCache({ doId, tags: remainingTags }, stub, tagData));
|
|
320
320
|
}));
|
|
321
321
|
return result;
|
|
322
322
|
}
|
|
@@ -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
|
|
80
|
+
const manifestPaths = await glob(join(baseDir, "**/*_client-reference-manifest.js"), {
|
|
79
81
|
windowsPathsNoEscape: true,
|
|
80
82
|
});
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
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
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
1
3
|
import { patchCode } from "@opennextjs/aws/build/patch/astCodePatcher.js";
|
|
2
4
|
import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js";
|
|
3
5
|
const inlineChunksRule = `
|
|
@@ -7,6 +9,179 @@ rule:
|
|
|
7
9
|
fix:
|
|
8
10
|
requireChunk(chunkPath)
|
|
9
11
|
`;
|
|
12
|
+
/**
|
|
13
|
+
* Discover Turbopack external module mappings by reading symlinks in .next/node_modules/.
|
|
14
|
+
*
|
|
15
|
+
* Turbopack externalizes packages listed in serverExternalPackages and creates hashed
|
|
16
|
+
* identifiers (e.g. "shiki-43d062b67f27bbdc") with symlinks in .next/node_modules/ pointing
|
|
17
|
+
* to the real packages (e.g. ../../node_modules/shiki). At runtime, externalImport() does
|
|
18
|
+
* `await import("shiki-43d062b67f27bbdc/wasm")` which fails because the bundler can't
|
|
19
|
+
* statically analyze those hashed names. This function discovers the mappings so we can
|
|
20
|
+
* generate explicit switch cases for the bundler.
|
|
21
|
+
*
|
|
22
|
+
* @param filePath Absolute path to the Turbopack runtime file being patched
|
|
23
|
+
* (e.g. `/abs/path/to/.open-next/server-functions/default/.../.next/server/chunks/ssr/[turbopack]_runtime.js`).
|
|
24
|
+
* This must be the actual file in `.open-next/` (not `buildOptions.appBuildOutputPath`)
|
|
25
|
+
* because the `.next/node_modules/` symlinks are in the traced copy, not the original.
|
|
26
|
+
* @returns A map from hashed identifiers to real package names (e.g. "shiki-43d062b67f27bbdc" -> "shiki").
|
|
27
|
+
*/
|
|
28
|
+
function discoverExternalModuleMappings(filePath) {
|
|
29
|
+
// filePath is like: .../.next/server/chunks/ssr/[turbopack]_runtime.js
|
|
30
|
+
// We need: .../.next/node_modules/
|
|
31
|
+
const dotNextDir = filePath.replace(/\/server\/chunks\/.*$/, "");
|
|
32
|
+
const nodeModulesDir = path.join(dotNextDir, "node_modules");
|
|
33
|
+
const mappings = new Map();
|
|
34
|
+
if (!fs.existsSync(nodeModulesDir)) {
|
|
35
|
+
return mappings;
|
|
36
|
+
}
|
|
37
|
+
for (const entry of fs.readdirSync(nodeModulesDir, { withFileTypes: true })) {
|
|
38
|
+
try {
|
|
39
|
+
if (entry.isSymbolicLink()) {
|
|
40
|
+
const entryPath = path.join(nodeModulesDir, entry.name);
|
|
41
|
+
const target = fs.readlinkSync(entryPath);
|
|
42
|
+
// target is like "../../node_modules/shiki" — extract package name
|
|
43
|
+
const match = target.match(/node_modules\/(.+)$/);
|
|
44
|
+
if (match?.[1]) {
|
|
45
|
+
mappings.set(entry.name, match[1]);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// skip entries we can't read
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return mappings;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Build a dynamic inlineExternalImportRule that includes cases for all discovered
|
|
57
|
+
* Turbopack external module hashes, mapping them back to their real package names.
|
|
58
|
+
*
|
|
59
|
+
* We use a switch for exact matches (including bare + subpath cases) and a fallback
|
|
60
|
+
* for the default case. Since switch/case can only match exact strings, we enumerate
|
|
61
|
+
* known subpaths from the traced files to cover cases like "shiki-hash/wasm".
|
|
62
|
+
*/
|
|
63
|
+
function buildExternalImportRule(mappings, tracedFiles, runtimeCode) {
|
|
64
|
+
// Tracks module names that already have a switch case, to avoid duplicates.
|
|
65
|
+
const casedModules = new Set();
|
|
66
|
+
const cases = [];
|
|
67
|
+
function addCase(moduleName, importPath) {
|
|
68
|
+
if (!casedModules.has(moduleName)) {
|
|
69
|
+
casedModules.add(moduleName);
|
|
70
|
+
cases.push(`case "${moduleName}":\n $RAW = await import("${importPath}");\n break;`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Always include the @vercel/og rewrite
|
|
74
|
+
addCase("next/dist/compiled/@vercel/og/index.node.js", "next/dist/compiled/@vercel/og/index.edge.js");
|
|
75
|
+
// Add case for each discovered external module mapping (bare import)
|
|
76
|
+
for (const [hashedName, realName] of mappings) {
|
|
77
|
+
addCase(hashedName, realName);
|
|
78
|
+
}
|
|
79
|
+
// Discover subpath imports from the traced chunk files.
|
|
80
|
+
// Chunks reference external modules like "shiki-hash/wasm" — scan for these patterns.
|
|
81
|
+
const subpathCases = discoverExternalSubpaths(mappings, tracedFiles);
|
|
82
|
+
for (const [hashedSubpath, realSubpath] of subpathCases) {
|
|
83
|
+
addCase(hashedSubpath, realSubpath);
|
|
84
|
+
}
|
|
85
|
+
// Discover bare external imports from chunk files (e.g. externalImport("shiki")).
|
|
86
|
+
// These need explicit switch cases so the bundler can statically resolve them.
|
|
87
|
+
const bareImports = discoverBareExternalImports(tracedFiles, runtimeCode);
|
|
88
|
+
for (const [moduleName, realName] of bareImports) {
|
|
89
|
+
addCase(moduleName, realName);
|
|
90
|
+
}
|
|
91
|
+
// Indent each case line by 4 spaces to align with the switch body in the YAML fix block.
|
|
92
|
+
const indentedCases = cases
|
|
93
|
+
.flatMap((c) => c.split("\n"))
|
|
94
|
+
.map((line) => ` ${line}`)
|
|
95
|
+
.join("\n");
|
|
96
|
+
return `
|
|
97
|
+
rule:
|
|
98
|
+
pattern: "$RAW = await import($ID)"
|
|
99
|
+
inside:
|
|
100
|
+
regex: "externalImport"
|
|
101
|
+
kind: function_declaration
|
|
102
|
+
stopBy: end
|
|
103
|
+
fix: |-
|
|
104
|
+
switch ($ID) {
|
|
105
|
+
${indentedCases}
|
|
106
|
+
default:
|
|
107
|
+
$RAW = await import($ID);
|
|
108
|
+
}
|
|
109
|
+
`;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Scan traced chunk files for bare external module imports (e.g. `externalImport("shiki")`).
|
|
113
|
+
*
|
|
114
|
+
* In some Turbopack versions, externalized packages are referenced by their real names
|
|
115
|
+
* (not hashed). The default `await import(id)` with a variable `id` can't be statically
|
|
116
|
+
* analyzed by the bundler (ESBuild). By adding explicit switch cases with string literals,
|
|
117
|
+
* we make these imports statically discoverable so they get bundled into the worker.
|
|
118
|
+
*/
|
|
119
|
+
function discoverBareExternalImports(tracedFiles, runtimeCode) {
|
|
120
|
+
const bareImports = new Map();
|
|
121
|
+
// Turbopack assigns `externalImport` to a property on the context prototype,
|
|
122
|
+
// e.g. `contextPrototype.y = externalImport`. The property name could change between versions,
|
|
123
|
+
// so we extract it dynamically from the runtime code rather than hardcoding it.
|
|
124
|
+
const propMatch = runtimeCode.match(/contextPrototype\.(\w+)\s*=\s*externalImport/);
|
|
125
|
+
if (!propMatch?.[1]) {
|
|
126
|
+
return bareImports;
|
|
127
|
+
}
|
|
128
|
+
const externalImportAlias = propMatch[1];
|
|
129
|
+
// Chunks call externalImport as e.g. `.y("shiki")` — build a regex using the discovered property name.
|
|
130
|
+
const externalImportRegexp = new RegExp(`\\.${externalImportAlias}\\("([^"]+)"\\)`, "g");
|
|
131
|
+
const chunkFiles = tracedFiles.filter((f) => f.includes(".next/server/chunks/"));
|
|
132
|
+
for (const filePath of chunkFiles) {
|
|
133
|
+
try {
|
|
134
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
135
|
+
for (const externalImportMatch of content.matchAll(externalImportRegexp)) {
|
|
136
|
+
const moduleName = externalImportMatch[1];
|
|
137
|
+
if (moduleName) {
|
|
138
|
+
// Identity mapping — the module name is already the real name
|
|
139
|
+
bareImports.set(moduleName, moduleName);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
// skip files we can't read
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return bareImports;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Scan traced chunk files for external module subpath imports.
|
|
151
|
+
* E.g. find "shiki-43d062b67f27bbdc/wasm" in chunk code and map it to "shiki/wasm".
|
|
152
|
+
*
|
|
153
|
+
* Only scans files with "[externals]" in the name since those are the chunks that
|
|
154
|
+
* contain externalImport calls.
|
|
155
|
+
*/
|
|
156
|
+
function discoverExternalSubpaths(mappings, tracedFiles) {
|
|
157
|
+
const subpaths = new Map();
|
|
158
|
+
const externalChunks = tracedFiles.filter((f) => f.includes("[externals]"));
|
|
159
|
+
for (const [hashedName, realName] of mappings) {
|
|
160
|
+
// Build a regex to find quoted subpath imports of this hashed module in chunk source code.
|
|
161
|
+
// E.g. for hashedName "shiki-43d062b67f27bbdc", this matches strings like
|
|
162
|
+
// "shiki-43d062b67f27bbdc/wasm" or "shiki-43d062b67f27bbdc/engine/javascript".
|
|
163
|
+
// The hashedName is escaped to safely use it as a literal in the regex pattern.
|
|
164
|
+
const escaped = getCrossPlatformPathRegex(hashedName, { escape: true });
|
|
165
|
+
const pattern = new RegExp(`"(${escaped}/[^"]*)"`, "g");
|
|
166
|
+
for (const filePath of externalChunks) {
|
|
167
|
+
try {
|
|
168
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
169
|
+
for (const match of content.matchAll(pattern)) {
|
|
170
|
+
const fullHashedPath = match[1];
|
|
171
|
+
if (fullHashedPath) {
|
|
172
|
+
const subpath = fullHashedPath.slice(hashedName.length);
|
|
173
|
+
const realSubpath = realName + subpath;
|
|
174
|
+
subpaths.set(fullHashedPath, realSubpath);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
// skip files we can't read
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return subpaths;
|
|
184
|
+
}
|
|
10
185
|
export const patchTurbopackRuntime = {
|
|
11
186
|
name: "inline-turbopack-chunks",
|
|
12
187
|
patches: [
|
|
@@ -16,8 +191,10 @@ export const patchTurbopackRuntime = {
|
|
|
16
191
|
escape: false,
|
|
17
192
|
}),
|
|
18
193
|
contentFilter: /loadRuntimeChunkPath/,
|
|
19
|
-
patchCode: async ({ code, tracedFiles }) => {
|
|
20
|
-
|
|
194
|
+
patchCode: async ({ code, tracedFiles, filePath }) => {
|
|
195
|
+
const mappings = discoverExternalModuleMappings(filePath);
|
|
196
|
+
const externalImportRule = buildExternalImportRule(mappings, tracedFiles, code);
|
|
197
|
+
let patched = patchCode(code, externalImportRule);
|
|
21
198
|
patched = patchCode(patched, inlineChunksRule);
|
|
22
199
|
return `${patched}\n${inlineChunksFn(tracedFiles)}`;
|
|
23
200
|
},
|
|
@@ -53,26 +230,3 @@ ${chunks
|
|
|
53
230
|
}
|
|
54
231
|
`;
|
|
55
232
|
}
|
|
56
|
-
// Turbopack imports `og` via `externalImport`.
|
|
57
|
-
// We patch it to:
|
|
58
|
-
// - add the explicit path so that the file is inlined by wrangler
|
|
59
|
-
// - use the edge version of the module instead of the node version.
|
|
60
|
-
//
|
|
61
|
-
// Modules that are not inlined (no added to the switch), would generate an error similar to:
|
|
62
|
-
// Failed to load external module path/to/module: Error: No such module "path/to/module"
|
|
63
|
-
const inlineExternalImportRule = `
|
|
64
|
-
rule:
|
|
65
|
-
pattern: "$RAW = await import($ID)"
|
|
66
|
-
inside:
|
|
67
|
-
regex: "externalImport"
|
|
68
|
-
kind: function_declaration
|
|
69
|
-
stopBy: end
|
|
70
|
-
fix: |-
|
|
71
|
-
switch ($ID) {
|
|
72
|
-
case "next/dist/compiled/@vercel/og/index.node.js":
|
|
73
|
-
$RAW = await import("next/dist/compiled/@vercel/og/index.edge.js");
|
|
74
|
-
break;
|
|
75
|
-
default:
|
|
76
|
-
$RAW = await import($ID);
|
|
77
|
-
}
|
|
78
|
-
`;
|
|
@@ -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);
|
|
@@ -15,7 +15,10 @@ export async function deployCommand(args) {
|
|
|
15
15
|
const { config } = await retrieveCompiledConfig();
|
|
16
16
|
const buildOpts = getNormalizedOptions(config);
|
|
17
17
|
const wranglerConfig = await readWranglerConfig(args);
|
|
18
|
-
const envVars = await getEnvFromPlatformProxy(
|
|
18
|
+
const envVars = await getEnvFromPlatformProxy({
|
|
19
|
+
configPath: args.wranglerConfigPath,
|
|
20
|
+
environment: args.env,
|
|
21
|
+
}, buildOpts);
|
|
19
22
|
await populateCache(buildOpts, config, wranglerConfig, {
|
|
20
23
|
target: "remote",
|
|
21
24
|
environment: args.env,
|
|
@@ -29,7 +29,10 @@ async function populateCacheCommand(target, args) {
|
|
|
29
29
|
const { config } = await retrieveCompiledConfig();
|
|
30
30
|
const buildOpts = getNormalizedOptions(config);
|
|
31
31
|
const wranglerConfig = await readWranglerConfig(args);
|
|
32
|
-
const envVars = await getEnvFromPlatformProxy(
|
|
32
|
+
const envVars = await getEnvFromPlatformProxy({
|
|
33
|
+
configPath: args.wranglerConfigPath,
|
|
34
|
+
environment: args.env,
|
|
35
|
+
}, buildOpts);
|
|
33
36
|
await populateCache(buildOpts, config, wranglerConfig, {
|
|
34
37
|
target,
|
|
35
38
|
environment: args.env,
|
|
@@ -13,7 +13,10 @@ export async function previewCommand(args) {
|
|
|
13
13
|
const { config } = await retrieveCompiledConfig();
|
|
14
14
|
const buildOpts = getNormalizedOptions(config);
|
|
15
15
|
const wranglerConfig = await readWranglerConfig(args);
|
|
16
|
-
const envVars = await getEnvFromPlatformProxy(
|
|
16
|
+
const envVars = await getEnvFromPlatformProxy({
|
|
17
|
+
configPath: args.wranglerConfigPath,
|
|
18
|
+
environment: args.env,
|
|
19
|
+
}, buildOpts);
|
|
17
20
|
await populateCache(buildOpts, config, wranglerConfig, {
|
|
18
21
|
target: args.remote ? "remote" : "local",
|
|
19
22
|
environment: args.env,
|
|
@@ -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
|
-
|
|
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(
|
|
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
|
|
@@ -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,
|
|
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,
|
|
27
|
-
let content = readFileSync(join(getPackageTemplatesDirPath(),
|
|
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,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.
|
|
4
|
+
"version": "1.19.3",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"opennextjs-cloudflare": "dist/cli/index.js"
|
|
@@ -44,7 +44,8 @@
|
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"@ast-grep/napi": "^0.40.5",
|
|
46
46
|
"@dotenvx/dotenvx": "1.31.0",
|
|
47
|
-
"@opennextjs/aws": "3.10.
|
|
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",
|
|
@@ -57,7 +58,7 @@
|
|
|
57
58
|
"@eslint/js": "^9.11.1",
|
|
58
59
|
"@tsconfig/strictest": "^2.0.5",
|
|
59
60
|
"@types/mock-fs": "^4.13.4",
|
|
60
|
-
"@types/node": "^22.
|
|
61
|
+
"@types/node": "^22.12.0",
|
|
61
62
|
"@types/picomatch": "^4.0.0",
|
|
62
63
|
"@types/yargs": "^17.0.33",
|
|
63
64
|
"diff": "^8.0.2",
|
|
@@ -73,10 +74,10 @@
|
|
|
73
74
|
"rimraf": "^6.0.1",
|
|
74
75
|
"typescript": "^5.9.3",
|
|
75
76
|
"typescript-eslint": "^8.48.0",
|
|
76
|
-
"vitest": "^
|
|
77
|
+
"vitest": "^4.1.4"
|
|
77
78
|
},
|
|
78
79
|
"peerDependencies": {
|
|
79
|
-
"next": ">=15.5.15 || >=16.2.3",
|
|
80
|
+
"next": ">=15.5.15 <16 || >=16.2.3",
|
|
80
81
|
"wrangler": "^4.65.0"
|
|
81
82
|
},
|
|
82
83
|
"scripts": {
|