@opennextjs/cloudflare 1.19.0 → 1.19.2

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.
@@ -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
  }
@@ -36,6 +36,15 @@ export function withFilter({ tagCache, filterFn }) {
36
36
  }
37
37
  return tagCache.writeTags(filteredTags);
38
38
  },
39
+ isStale: tagCache.isStale
40
+ ? async (tags, lastModified) => {
41
+ const filteredTags = tags.filter(filterFn);
42
+ if (filteredTags.length === 0) {
43
+ return false;
44
+ }
45
+ return tagCache.isStale(filteredTags, lastModified);
46
+ }
47
+ : undefined,
39
48
  };
40
49
  }
41
50
  /**
@@ -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
- let patched = patchCode(code, inlineExternalImportRule);
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
- `;
@@ -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(config, buildOpts);
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(config, buildOpts);
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(config, buildOpts);
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,
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.0",
4
+ "version": "1.19.2",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "opennextjs-cloudflare": "dist/cli/index.js"
@@ -44,7 +44,7 @@
44
44
  "dependencies": {
45
45
  "@ast-grep/napi": "^0.40.5",
46
46
  "@dotenvx/dotenvx": "1.31.0",
47
- "@opennextjs/aws": "3.10.1",
47
+ "@opennextjs/aws": "3.10.2",
48
48
  "cloudflare": "^4.4.1",
49
49
  "comment-json": "^4.5.1",
50
50
  "enquirer": "^2.4.1",
@@ -57,7 +57,7 @@
57
57
  "@eslint/js": "^9.11.1",
58
58
  "@tsconfig/strictest": "^2.0.5",
59
59
  "@types/mock-fs": "^4.13.4",
60
- "@types/node": "^22.2.0",
60
+ "@types/node": "^22.12.0",
61
61
  "@types/picomatch": "^4.0.0",
62
62
  "@types/yargs": "^17.0.33",
63
63
  "diff": "^8.0.2",
@@ -73,10 +73,10 @@
73
73
  "rimraf": "^6.0.1",
74
74
  "typescript": "^5.9.3",
75
75
  "typescript-eslint": "^8.48.0",
76
- "vitest": "^2.1.1"
76
+ "vitest": "^4.1.4"
77
77
  },
78
78
  "peerDependencies": {
79
- "next": ">=15.5.15 || >=16.2.3",
79
+ "next": ">=15.5.15 <16 || >=16.2.3",
80
80
  "wrangler": "^4.65.0"
81
81
  },
82
82
  "scripts": {