@opennextjs/aws 3.9.15 → 3.10.0

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.
Files changed (38) hide show
  1. package/dist/adapters/cache.d.ts +3 -1
  2. package/dist/adapters/cache.js +84 -18
  3. package/dist/adapters/composable-cache.d.ts +12 -0
  4. package/dist/adapters/composable-cache.js +82 -2
  5. package/dist/adapters/dynamo-provider.js +2 -0
  6. package/dist/adapters/revalidate.js +2 -1
  7. package/dist/build/compileCache.js +2 -3
  8. package/dist/build/copyTracedFiles.d.ts +1 -0
  9. package/dist/build/copyTracedFiles.js +20 -0
  10. package/dist/build/createAssets.js +4 -1
  11. package/dist/build/helper.d.ts +7 -12
  12. package/dist/build/helper.js +19 -57
  13. package/dist/build/patch/patches/patchNextServer.d.ts +1 -0
  14. package/dist/build/patch/patches/patchNextServer.js +19 -0
  15. package/dist/build/patch/patches/patchOriginalNextConfig.js +4 -15
  16. package/dist/core/routing/cacheInterceptor.js +2 -1
  17. package/dist/core/routing/i18n/index.js +4 -2
  18. package/dist/overrides/proxyExternalRequest/fetch.js +10 -1
  19. package/dist/overrides/tagCache/dummy.js +3 -0
  20. package/dist/overrides/tagCache/dynamodb-lite.js +94 -8
  21. package/dist/overrides/tagCache/dynamodb-nextMode.d.ts +2 -1
  22. package/dist/overrides/tagCache/dynamodb-nextMode.js +112 -27
  23. package/dist/overrides/tagCache/dynamodb.js +104 -22
  24. package/dist/overrides/tagCache/fs-dev-nextMode.d.ts +2 -1
  25. package/dist/overrides/tagCache/fs-dev-nextMode.js +37 -6
  26. package/dist/overrides/tagCache/fs-dev.js +40 -8
  27. package/dist/types/cache.d.ts +21 -0
  28. package/dist/types/global.d.ts +5 -2
  29. package/dist/types/next-types.d.ts +1 -0
  30. package/dist/types/overrides.d.ts +42 -1
  31. package/dist/utils/cache.d.ts +22 -2
  32. package/dist/utils/cache.js +44 -4
  33. package/dist/utils/promise.js +2 -0
  34. package/dist/utils/requestCache.d.ts +20 -0
  35. package/dist/utils/requestCache.js +27 -0
  36. package/dist/utils/semver.d.ts +10 -0
  37. package/dist/utils/semver.js +55 -0
  38. package/package.json +6 -6
@@ -11,7 +11,9 @@ export default class Cache {
11
11
  getFetchCache(key: string, softTags?: string[], tags?: string[]): Promise<CacheHandlerValue | null>;
12
12
  getIncrementalCache(key: string): Promise<CacheHandlerValue | null>;
13
13
  set(key: string, data?: IncrementalCacheValue, ctx?: IncrementalCacheContext): Promise<void>;
14
- revalidateTag(tags: string | string[]): Promise<void>;
14
+ revalidateTag(tags: string | string[], durations?: {
15
+ expire?: number;
16
+ }): Promise<void>;
15
17
  private updateTagsOnSet;
16
18
  private extractRevalidateForSet;
17
19
  }
@@ -1,5 +1,6 @@
1
- import { getTagsFromValue, hasBeenRevalidated, writeTags } from "../utils/cache";
1
+ import { getTagsFromValue, hasBeenRevalidated, isStale, writeTags, } from "../utils/cache";
2
2
  import { isBinaryContentType } from "../utils/binary";
3
+ import { compareSemver } from "../utils/semver";
3
4
  import { debug, error, warn } from "./logger";
4
5
  export const SOFT_TAG_PREFIX = "_N_T_/";
5
6
  function isFetchCache(options) {
@@ -57,8 +58,11 @@ export default class Cache {
57
58
  }
58
59
  }
59
60
  }
61
+ const _isStale = cachedEntry.shouldBypassTagCache
62
+ ? false
63
+ : await isStale(key, _tags, _lastModified);
60
64
  return {
61
- lastModified: _lastModified,
65
+ lastModified: _isStale ? 1 : _lastModified,
62
66
  value: cachedEntry.value,
63
67
  };
64
68
  }
@@ -77,21 +81,27 @@ export default class Cache {
77
81
  const cacheData = cachedEntry.value;
78
82
  const meta = cacheData.meta;
79
83
  const tags = getTagsFromValue(cacheData);
80
- const _lastModified = cachedEntry.lastModified ?? Date.now();
84
+ let _lastModified = cachedEntry.lastModified ?? Date.now();
81
85
  const _hasBeenRevalidated = cachedEntry.shouldBypassTagCache
82
86
  ? false
83
87
  : await hasBeenRevalidated(key, tags, cachedEntry);
84
88
  if (_hasBeenRevalidated)
85
89
  return null;
90
+ const _isStale = cachedEntry.shouldBypassTagCache
91
+ ? false
92
+ : await isStale(key, tags, _lastModified);
86
93
  const store = globalThis.__openNextAls.getStore();
87
94
  if (store) {
88
- store.lastModified = _lastModified;
95
+ store.lastModified = _isStale ? 1 : _lastModified;
96
+ _lastModified = store.lastModified;
89
97
  }
90
98
  if (cacheData?.type === "route") {
91
99
  return {
92
100
  lastModified: _lastModified,
93
101
  value: {
94
- kind: globalThis.isNextAfter15 ? "APP_ROUTE" : "ROUTE",
102
+ kind: compareSemver(globalThis.nextVersion, ">=", "15.0.0")
103
+ ? "APP_ROUTE"
104
+ : "ROUTE",
95
105
  body: Buffer.from(cacheData.body ?? Buffer.alloc(0), isBinaryContentType(String(meta?.headers?.["content-type"]))
96
106
  ? "base64"
97
107
  : "utf8"),
@@ -101,7 +111,8 @@ export default class Cache {
101
111
  };
102
112
  }
103
113
  if (cacheData?.type === "page" || cacheData?.type === "app") {
104
- if (globalThis.isNextAfter15 && cacheData?.type === "app") {
114
+ if (compareSemver(globalThis.nextVersion, ">=", "15.0.0") &&
115
+ cacheData?.type === "app") {
105
116
  const segmentData = new Map();
106
117
  if (cacheData.segmentData) {
107
118
  for (const [segmentPath, segmentContent] of Object.entries(cacheData.segmentData ?? {})) {
@@ -124,7 +135,9 @@ export default class Cache {
124
135
  return {
125
136
  lastModified: _lastModified,
126
137
  value: {
127
- kind: globalThis.isNextAfter15 ? "PAGES" : "PAGE",
138
+ kind: compareSemver(globalThis.nextVersion, ">=", "15.0.0")
139
+ ? "PAGES"
140
+ : "PAGE",
128
141
  html: cacheData.html,
129
142
  pageData: cacheData.type === "page" ? cacheData.json : cacheData.rsc,
130
143
  status: meta?.status,
@@ -209,7 +222,13 @@ export default class Cache {
209
222
  break;
210
223
  }
211
224
  case "APP_PAGE": {
212
- const { html, rscData, headers, status } = data;
225
+ const { html, rscData, headers, status, segmentData, postponed } = data;
226
+ const segmentToWrite = {};
227
+ if (segmentData) {
228
+ for (const [segmentPath, segmentContent,] of segmentData.entries()) {
229
+ segmentToWrite[segmentPath] = segmentContent.toString("utf8");
230
+ }
231
+ }
213
232
  await globalThis.incrementalCache.set(key, {
214
233
  type: "app",
215
234
  html,
@@ -217,8 +236,10 @@ export default class Cache {
217
236
  meta: {
218
237
  status,
219
238
  headers,
239
+ postponed,
220
240
  },
221
241
  revalidate,
242
+ segmentData: segmentData ? segmentToWrite : undefined,
222
243
  }, "cache");
223
244
  break;
224
245
  }
@@ -248,7 +269,7 @@ export default class Cache {
248
269
  detachedPromise?.resolve();
249
270
  }
250
271
  }
251
- async revalidateTag(tags) {
272
+ async revalidateTag(tags, durations) {
252
273
  const config = globalThis.openNextConfig.dangerous;
253
274
  if (config?.disableTagCache || config?.disableIncrementalCache) {
254
275
  return;
@@ -260,7 +281,25 @@ export default class Cache {
260
281
  try {
261
282
  if (globalThis.tagCache.mode === "nextMode") {
262
283
  const paths = (await globalThis.tagCache.getPathsByTags?.(_tags)) ?? [];
263
- await writeTags(_tags);
284
+ const now = Date.now();
285
+ const tagsToWrite = _tags.map((tag) => {
286
+ if (durations) {
287
+ // Use provided durations
288
+ return {
289
+ tag,
290
+ stale: now,
291
+ expire: durations.expire !== undefined
292
+ ? now + durations.expire * 1000
293
+ : undefined,
294
+ };
295
+ }
296
+ // Immediate expiration, default behavior before next 16, now only with {expire: 0}
297
+ return {
298
+ tag,
299
+ expire: now,
300
+ };
301
+ });
302
+ await writeTags(tagsToWrite);
264
303
  if (paths.length > 0) {
265
304
  // TODO: we should introduce a new method in cdnInvalidationHandler to invalidate paths by tags for cdn that supports it
266
305
  // It also means that we'll need to provide the tags used in every request to the wrapper or converter.
@@ -283,10 +322,25 @@ export default class Cache {
283
322
  // Find all keys with the given tag
284
323
  const paths = await globalThis.tagCache.getByTag(tag);
285
324
  debug("Items", paths);
286
- const toInsert = paths.map((path) => ({
287
- path,
288
- tag,
289
- }));
325
+ const now = Date.now();
326
+ const toInsert = paths.map((path) => {
327
+ const baseEntry = { path, tag };
328
+ if (durations) {
329
+ // Use provided durations
330
+ return {
331
+ ...baseEntry,
332
+ stale: now,
333
+ expire: durations.expire !== undefined
334
+ ? now + durations.expire * 1000
335
+ : undefined,
336
+ };
337
+ }
338
+ // Default behavior: immediate expiration
339
+ return {
340
+ ...baseEntry,
341
+ expire: now,
342
+ };
343
+ });
290
344
  // If the tag is a soft tag, we should also revalidate the hard tags
291
345
  if (tag.startsWith(SOFT_TAG_PREFIX)) {
292
346
  for (const path of paths) {
@@ -297,10 +351,22 @@ export default class Cache {
297
351
  for (const hardTag of hardTags) {
298
352
  const _paths = await globalThis.tagCache.getByTag(hardTag);
299
353
  debug({ hardTag, _paths });
300
- toInsert.push(..._paths.map((path) => ({
301
- path,
302
- tag: hardTag,
303
- })));
354
+ toInsert.push(..._paths.map((path) => {
355
+ const baseEntry = { path, tag: hardTag };
356
+ if (durations) {
357
+ return {
358
+ ...baseEntry,
359
+ stale: now,
360
+ expire: durations.expire !== undefined
361
+ ? now + durations.expire * 1000
362
+ : undefined,
363
+ };
364
+ }
365
+ return {
366
+ ...baseEntry,
367
+ expire: now,
368
+ };
369
+ }));
304
370
  }
305
371
  }
306
372
  }
@@ -21,5 +21,17 @@ declare const _default: {
21
21
  */
22
22
  expireTags(...tags: string[]): Promise<void>;
23
23
  receiveExpiredTags(...tags: string[]): Promise<void>;
24
+ /**
25
+ * Added in Next.js 16. Updates tags with optional stale/expire durations.
26
+ * Mirrors the logic in `Cache.revalidateTag` but without CDN invalidation
27
+ * since composable cache keys are not URL paths.
28
+ *
29
+ * When `durations` is provided, marks tags as stale immediately and optionally
30
+ * sets an expiry timestamp. When omitted, immediately expires tags (no grace period).
31
+ * durations.expire is in seconds, but we convert it to milliseconds for storage and comparison.
32
+ */
33
+ updateTags(tags: string[], durations?: {
34
+ expire?: number;
35
+ }): Promise<void>;
24
36
  };
25
37
  export default _default;
@@ -1,4 +1,4 @@
1
- import { writeTags } from "../utils/cache";
1
+ import { isStale, writeTags } from "../utils/cache";
2
2
  import { fromReadableStream, toReadableStream } from "../utils/stream";
3
3
  import { debug } from "./logger";
4
4
  const pendingWritePromiseMap = new Map();
@@ -21,14 +21,22 @@ export default {
21
21
  return undefined;
22
22
  }
23
23
  debug("composable cache result", result);
24
- // We need to check if the tags associated with this entry has been revalidated
24
+ let revalidate = result.value.revalidate;
25
25
  if (globalThis.tagCache.mode === "nextMode" &&
26
26
  result.value.tags.length > 0) {
27
+ // We need to check if the tags associated with this entry has been revalidated
27
28
  const hasBeenRevalidated = result.shouldBypassTagCache
28
29
  ? false
29
30
  : await globalThis.tagCache.hasBeenRevalidated(result.value.tags, result.lastModified);
30
31
  if (hasBeenRevalidated)
31
32
  return undefined;
33
+ // Check if tags are stale – entry is valid but needs background revalidation
34
+ const isCacheStale = result.shouldBypassTagCache
35
+ ? false
36
+ : await isStale(cacheKey, result.value.tags, result.lastModified);
37
+ if (isCacheStale) {
38
+ revalidate = -1;
39
+ }
32
40
  }
33
41
  else if (globalThis.tagCache.mode === "original" ||
34
42
  globalThis.tagCache.mode === undefined) {
@@ -37,9 +45,17 @@ export default {
37
45
  : (await globalThis.tagCache.getLastModified(cacheKey, result.lastModified)) === -1;
38
46
  if (hasBeenRevalidated)
39
47
  return undefined;
48
+ // Check if tags are stale – entry is valid but needs background revalidation
49
+ const isCacheStale = result.shouldBypassTagCache
50
+ ? false
51
+ : await isStale(cacheKey, result.value.tags, result.lastModified);
52
+ if (isCacheStale) {
53
+ revalidate = -1;
54
+ }
40
55
  }
41
56
  return {
42
57
  ...result.value,
58
+ revalidate,
43
59
  value: toReadableStream(result.value.value),
44
60
  };
45
61
  }
@@ -118,4 +134,68 @@ export default {
118
134
  // This function does absolutely nothing
119
135
  return;
120
136
  },
137
+ /**
138
+ * Added in Next.js 16. Updates tags with optional stale/expire durations.
139
+ * Mirrors the logic in `Cache.revalidateTag` but without CDN invalidation
140
+ * since composable cache keys are not URL paths.
141
+ *
142
+ * When `durations` is provided, marks tags as stale immediately and optionally
143
+ * sets an expiry timestamp. When omitted, immediately expires tags (no grace period).
144
+ * durations.expire is in seconds, but we convert it to milliseconds for storage and comparison.
145
+ */
146
+ async updateTags(tags, durations) {
147
+ const config = globalThis.openNextConfig.dangerous;
148
+ if (config?.disableTagCache || config?.disableIncrementalCache) {
149
+ return;
150
+ }
151
+ if (tags.length === 0) {
152
+ return;
153
+ }
154
+ try {
155
+ const now = Date.now();
156
+ if (globalThis.tagCache.mode === "nextMode") {
157
+ const tagsToWrite = tags.map((tag) => {
158
+ if (durations) {
159
+ return {
160
+ tag,
161
+ stale: now,
162
+ expire: durations.expire !== undefined
163
+ ? now + durations.expire * 1000
164
+ : undefined,
165
+ };
166
+ }
167
+ // Default: immediate expiry, no grace period
168
+ return { tag, expire: now };
169
+ });
170
+ await writeTags(tagsToWrite);
171
+ }
172
+ else {
173
+ // Original mode: resolve tag → path mappings first
174
+ const originalTagCache = globalThis.tagCache;
175
+ const pathsPerTag = await Promise.all(tags.map(async (tag) => {
176
+ const paths = await originalTagCache.getByTag(tag);
177
+ return paths.map((path) => {
178
+ if (durations) {
179
+ return {
180
+ path,
181
+ tag,
182
+ stale: now,
183
+ expire: durations.expire !== undefined
184
+ ? now + durations.expire * 1000
185
+ : undefined,
186
+ };
187
+ }
188
+ return { path, tag, expire: now };
189
+ });
190
+ }));
191
+ const toWrite = pathsPerTag.flat();
192
+ if (toWrite.length > 0) {
193
+ await writeTags(toWrite);
194
+ }
195
+ }
196
+ }
197
+ catch (e) {
198
+ debug("Failed to update tags", e);
199
+ }
200
+ },
121
201
  };
@@ -30,6 +30,8 @@ async function insert(requestType) {
30
30
  tag: item.tag.S,
31
31
  path: item.path.S,
32
32
  revalidatedAt: Number.parseInt(item.revalidatedAt.N),
33
+ ...(item.stale && { stale: Number.parseInt(item.stale.N) }),
34
+ ...(item.expire && { expire: Number.parseInt(item.expire.N) }),
33
35
  }));
34
36
  await tagCache.writeTags(parsedData);
35
37
  return {
@@ -4,6 +4,7 @@ import path from "node:path";
4
4
  import { createGenericHandler } from "../core/createGenericHandler.js";
5
5
  import { debug, error } from "./logger.js";
6
6
  const prerenderManifest = loadPrerenderManifest();
7
+ const ALLOWED_STATUS_CODES = [200, 307, 308, 404];
7
8
  const defaultHandler = async (event) => {
8
9
  const failedRecords = [];
9
10
  for (const record of event.records) {
@@ -32,7 +33,7 @@ const defaultHandler = async (event) => {
32
33
  headers: res.headers,
33
34
  statusCode: res.statusCode,
34
35
  });
35
- if (res.statusCode !== 200 ||
36
+ if (!ALLOWED_STATUS_CODES.includes(res.statusCode ?? 0) ||
36
37
  res.headers["x-nextjs-cache"] !== "REVALIDATED") {
37
38
  failedRecords.push(record);
38
39
  }
@@ -11,7 +11,6 @@ export function compileCache(options, format = "cjs") {
11
11
  const { config } = options;
12
12
  const ext = format === "cjs" ? "cjs" : "mjs";
13
13
  const compiledCacheFile = path.join(options.buildDir, `cache.${ext}`);
14
- const isAfter15 = buildHelper.compareSemver(options.nextVersion, ">=", "15.0.0");
15
14
  // Normal cache
16
15
  buildHelper.esbuildSync({
17
16
  external: ["next", "styled-jsx", "react", "@aws-sdk/*"],
@@ -23,7 +22,7 @@ export function compileCache(options, format = "cjs") {
23
22
  js: [
24
23
  `globalThis.disableIncrementalCache = ${config.dangerous?.disableIncrementalCache ?? false};`,
25
24
  `globalThis.disableDynamoDBCache = ${config.dangerous?.disableTagCache ?? false};`,
26
- `globalThis.isNextAfter15 = ${isAfter15};`,
25
+ `globalThis.nextVersion = "${options.nextVersion}";`,
27
26
  ].join(""),
28
27
  },
29
28
  }, options);
@@ -41,7 +40,7 @@ export function compileCache(options, format = "cjs") {
41
40
  js: [
42
41
  `globalThis.disableIncrementalCache = ${config.dangerous?.disableIncrementalCache ?? false};`,
43
42
  `globalThis.disableDynamoDBCache = ${config.dangerous?.disableTagCache ?? false};`,
44
- `globalThis.isNextAfter15 = ${isAfter15};`,
43
+ `globalThis.nextVersion = "${options.nextVersion}";`,
45
44
  ].join(""),
46
45
  },
47
46
  }, options);
@@ -6,6 +6,7 @@
6
6
  */
7
7
  export declare function copyFileAndMakeOwnerWritable(src: string, dest: string): void;
8
8
  export declare function isExcluded(srcPath: string): boolean;
9
+ export declare function isNonLinuxPlatformPackage(srcPath: string): boolean;
9
10
  interface CopyTracedFilesOptions {
10
11
  buildOutputPath: string;
11
12
  packagePath: string;
@@ -40,6 +40,12 @@ export function isExcluded(srcPath) {
40
40
  escape: false,
41
41
  })));
42
42
  }
43
+ const NON_LINUX_PLATFORMS = ["darwin", "win32", "freebsd"];
44
+ const platformPattern = NON_LINUX_PLATFORMS.join("|");
45
+ const nonLinuxPlatformRegex = getCrossPlatformPathRegex(`/node_modules/(?:@[^/]+/)?(?:[^/]+-)?(${platformPattern})-[^/]+/`, { escape: false });
46
+ export function isNonLinuxPlatformPackage(srcPath) {
47
+ return nonLinuxPlatformRegex.test(srcPath);
48
+ }
43
49
  function copyPatchFile(outputDir) {
44
50
  const patchFile = path.join(__dirname, "patch", "patchedAsyncStorage.js");
45
51
  const outputPatchFile = path.join(outputDir, "patchedAsyncStorage.cjs");
@@ -190,11 +196,22 @@ File ${serverPath} does not exist
190
196
  const tracedFiles = [];
191
197
  const erroredFiles = [];
192
198
  //Actually copy the files
199
+ const skippedPlatformPackages = new Set();
193
200
  filesToCopy.forEach((to, from) => {
194
201
  // We don't want to copy excluded packages (e.g. sharp)
195
202
  if (isExcluded(from)) {
196
203
  return;
197
204
  }
205
+ // Skip non-Linux platform-specific native binaries (e.g. @swc/core-darwin-arm64)
206
+ // Set OPEN_NEXT_SKIP_PLATFORM_FILTER=true to bypass this check
207
+ if (!process.env.OPEN_NEXT_SKIP_PLATFORM_FILTER &&
208
+ isNonLinuxPlatformPackage(from)) {
209
+ const match = from.match(/node_modules\/(?:\.pnpm\/.*\/node_modules\/)?((?:@[^/]+\/)?[^/]+)/);
210
+ if (match) {
211
+ skippedPlatformPackages.add(match[1]);
212
+ }
213
+ return;
214
+ }
198
215
  tracedFiles.push(to);
199
216
  mkdirSync(path.dirname(to), { recursive: true });
200
217
  let symlink = null;
@@ -229,6 +246,9 @@ File ${serverPath} does not exist
229
246
  }
230
247
  }
231
248
  });
249
+ if (skippedPlatformPackages.size > 0) {
250
+ logger.debug("Skipped non-Linux platform packages:", [...skippedPlatformPackages].join(", "));
251
+ }
232
252
  readdirSync(standaloneNextDir)
233
253
  .filter((fileOrDir) => !statSync(path.join(standaloneNextDir, fileOrDir)).isDirectory())
234
254
  .forEach((file) => {
@@ -39,7 +39,10 @@ export function createStaticAssets(options, { useBasePath = false } = {}) {
39
39
  fs.copyFileSync(path.join(appBuildOutputPath, ".next/BUILD_ID"), path.join(outputPath, "BUILD_ID"));
40
40
  fs.cpSync(path.join(appBuildOutputPath, ".next/static"), path.join(outputPath, "_next", "static"), { recursive: true });
41
41
  if (fs.existsSync(appPublicPath)) {
42
- fs.cpSync(appPublicPath, outputPath, { recursive: true });
42
+ fs.cpSync(appPublicPath, outputPath, {
43
+ recursive: true,
44
+ dereference: true,
45
+ });
43
46
  }
44
47
  const appSrcPath = fs.existsSync(path.join(appPath, "src"))
45
48
  ? "src/app"
@@ -62,16 +62,8 @@ export declare function getHtmlPages(dotNextPath: string): Set<string>;
62
62
  export declare function getBuildId(options: BuildOptions): string;
63
63
  export declare function getOpenNextVersion(): string;
64
64
  export declare function getNextVersion(appPath: string): string;
65
- export type SemverOp = "=" | ">=" | "<=" | ">" | "<";
66
- /**
67
- * Compare two semver versions.
68
- *
69
- * @param v1 - First version. Can be "latest", otherwise it should be a valid semver version in the format of `major.minor.patch`. Usually is the next version from the package.json without canary suffix. If minor or patch are missing, they are considered 0.
70
- * @param v2 - Second version. Should not be "latest", it should be a valid semver version in the format of `major.minor.patch`. If minor or patch are missing, they are considered 0.
71
- * @example
72
- * compareSemver("2.0.0", ">=", "1.0.0") === true
73
- */
74
- export declare function compareSemver(v1: string, operator: SemverOp, v2: string): boolean;
65
+ export type { SemverOp } from "../utils/semver.js";
66
+ export { compareSemver } from "../utils/semver.js";
75
67
  /**
76
68
  * Check if a Next.js version is supported by the Next.js team.
77
69
  * Major versions are supported for 2 years following their initial release.
@@ -95,9 +87,12 @@ export declare function copyEnvFile(appPath: string, packagePath: string, output
95
87
  * Finds the path to the Next configuration file if it exists.
96
88
  *
97
89
  * @param appPath The directory to check for the Next config file
98
- * @returns The full path to Next config file if it exists, undefined otherwise
90
+ * @returns An object with the full path to Next config file alongside a flag indicating whether the file is in typescript if it exists, undefined otherwise
99
91
  */
100
- export declare function findNextConfig({ appPath, }: Pick<BuildOptions, "appPath">): string | undefined;
92
+ export declare function findNextConfig({ appPath, }: Pick<BuildOptions, "appPath">): {
93
+ path: string;
94
+ isTypescript: boolean;
95
+ } | undefined;
101
96
  /**
102
97
  * Check we are in a Nextjs app by looking for the Nextjs config file.
103
98
  */
@@ -214,61 +214,7 @@ export function getNextVersion(appPath) {
214
214
  // Drop the -canary.n suffix
215
215
  return version.split("-")[0];
216
216
  }
217
- /**
218
- * Compare two semver versions.
219
- *
220
- * @param v1 - First version. Can be "latest", otherwise it should be a valid semver version in the format of `major.minor.patch`. Usually is the next version from the package.json without canary suffix. If minor or patch are missing, they are considered 0.
221
- * @param v2 - Second version. Should not be "latest", it should be a valid semver version in the format of `major.minor.patch`. If minor or patch are missing, they are considered 0.
222
- * @example
223
- * compareSemver("2.0.0", ">=", "1.0.0") === true
224
- */
225
- export function compareSemver(v1, operator, v2) {
226
- // - = 0 when versions are equal
227
- // - > 0 if v1 > v2
228
- // - < 0 if v2 > v1
229
- let versionDiff = 0;
230
- if (v1 === "latest") {
231
- versionDiff = 1;
232
- }
233
- else {
234
- if (/^[^\d]/.test(v1)) {
235
- // biome-ignore lint/style/noParameterAssign:
236
- v1 = v1.substring(1);
237
- }
238
- if (/^[^\d]/.test(v2)) {
239
- // biome-ignore lint/style/noParameterAssign:
240
- v2 = v2.substring(1);
241
- }
242
- const [major1, minor1 = 0, patch1 = 0] = v1.split(".").map(Number);
243
- const [major2, minor2 = 0, patch2 = 0] = v2.split(".").map(Number);
244
- if (Number.isNaN(major1) || Number.isNaN(major2)) {
245
- throw new Error("The major version is required.");
246
- }
247
- if (major1 !== major2) {
248
- versionDiff = major1 - major2;
249
- }
250
- else if (minor1 !== minor2) {
251
- versionDiff = minor1 - minor2;
252
- }
253
- else if (patch1 !== patch2) {
254
- versionDiff = patch1 - patch2;
255
- }
256
- }
257
- switch (operator) {
258
- case "=":
259
- return versionDiff === 0;
260
- case ">=":
261
- return versionDiff >= 0;
262
- case "<=":
263
- return versionDiff <= 0;
264
- case ">":
265
- return versionDiff > 0;
266
- case "<":
267
- return versionDiff < 0;
268
- default:
269
- throw new Error(`Unsupported operator: ${operator}`);
270
- }
271
- }
217
+ export { compareSemver } from "../utils/semver.js";
272
218
  /**
273
219
  * Next.js major version release dates (YYYY-MM-DD).
274
220
  * Each major version is supported for 2 years from its release date.
@@ -352,10 +298,26 @@ export function copyEnvFile(appPath, packagePath, outputPath) {
352
298
  * Finds the path to the Next configuration file if it exists.
353
299
  *
354
300
  * @param appPath The directory to check for the Next config file
355
- * @returns The full path to Next config file if it exists, undefined otherwise
301
+ * @returns An object with the full path to Next config file alongside a flag indicating whether the file is in typescript if it exists, undefined otherwise
356
302
  */
357
303
  export function findNextConfig({ appPath, }) {
358
- return ["js", "cjs", "mjs", "ts"].find((ext) => fs.existsSync(path.join(appPath, `next.config.${ext}`)));
304
+ const extensions = [
305
+ { ext: "ts", isTypescript: true },
306
+ { ext: "mts", isTypescript: true },
307
+ { ext: "cts", isTypescript: true },
308
+ { ext: "js", isTypescript: false },
309
+ { ext: "mjs", isTypescript: false },
310
+ { ext: "cjs", isTypescript: false },
311
+ ];
312
+ for (const { ext, isTypescript } of extensions) {
313
+ const configPath = path.join(appPath, `next.config.${ext}`);
314
+ if (fs.existsSync(configPath)) {
315
+ return {
316
+ path: configPath,
317
+ isTypescript,
318
+ };
319
+ }
320
+ }
359
321
  }
360
322
  /**
361
323
  * Check we are in a Nextjs app by looking for the Nextjs config file.
@@ -2,6 +2,7 @@ import type { CodePatcher } from "../codePatcher.js";
2
2
  export declare const disablePreloadingRule = "\nrule:\n kind: statement_block\n inside:\n kind: if_statement\n any:\n - has:\n kind: member_expression\n pattern: this.nextConfig.experimental.preloadEntriesOnStart\n stopBy: end\n - has:\n kind: binary_expression\n pattern: appDocumentPreloading === true\n stopBy: end\nfix:\n '{}'\n";
3
3
  export declare const removeMiddlewareManifestRule = "\nrule:\n kind: statement_block\n inside:\n kind: method_definition\n has:\n kind: property_identifier\n regex: ^getMiddlewareManifest$\nfix:\n '{return null;}'\n";
4
4
  export declare const emptyHandleNextImageRequestRule = "\nrule:\n kind: assignment_expression\n pattern: this.handleNextImageRequest = $VALUE\n inside:\n kind: method_definition\n stopBy: end\n has:\n kind: property_identifier\n regex: ^constructor$\n inside:\n kind: class_body\nfix:\n this.handleNextImageRequest = async (req, res, parsedUrl) => false\n";
5
+ export declare const provideInternalWaitUntil = "\nrule:\n kind: return_statement\n inside:\n kind: method_definition\n stopBy: end\n has:\n kind: property_identifier\n regex: getInternalWaitUntil\n\nfix:\n return globalThis.__openNextAls.getStore()?.waitUntil;\n";
5
6
  /**
6
7
  * Swaps the body for a throwing implementation
7
8
  *
@@ -47,6 +47,19 @@ rule:
47
47
  fix:
48
48
  this.handleNextImageRequest = async (req, res, parsedUrl) => false
49
49
  `;
50
+ export const provideInternalWaitUntil = `
51
+ rule:
52
+ kind: return_statement
53
+ inside:
54
+ kind: method_definition
55
+ stopBy: end
56
+ has:
57
+ kind: property_identifier
58
+ regex: getInternalWaitUntil
59
+
60
+ fix:
61
+ return globalThis.__openNextAls.getStore()?.waitUntil;
62
+ `;
50
63
  /**
51
64
  * Swaps the body for a throwing implementation
52
65
  *
@@ -119,6 +132,12 @@ export const patchNextServer = {
119
132
  contentFilter: /getMiddlewareManifest/,
120
133
  patchCode: createPatchCode(removeMiddlewareManifestRule),
121
134
  },
135
+ {
136
+ versions: ">=16.0.0",
137
+ pathFilter,
138
+ contentFilter: /getInternalWaitUntil/,
139
+ patchCode: createPatchCode(provideInternalWaitUntil),
140
+ },
122
141
  ...babelPatches,
123
142
  ],
124
143
  };
@@ -45,25 +45,14 @@ export async function patchOriginalNextConfig(options) {
45
45
  * @returns
46
46
  */
47
47
  async function importNextConfigFromSource(buildOptions) {
48
- // Find the `next.config file with any supported extension
49
- const tsExtensions = [".ts", ".mts", ".cts"];
50
- const possibleExtensions = [...tsExtensions, ".mjs", ".js", ".cjs"];
51
- let configPath;
52
- let configExtension;
53
- for (const ext of possibleExtensions) {
54
- const testPath = path.join(buildOptions.appPath, `next.config${ext}`);
55
- if (fs.existsSync(testPath)) {
56
- configPath = testPath;
57
- configExtension = ext;
58
- break;
59
- }
60
- }
61
- if (!configPath || !configExtension) {
48
+ const nextConfigDetails = buildHelper.findNextConfig(buildOptions);
49
+ if (!nextConfigDetails) {
62
50
  throw new Error("Could not find next.config file");
63
51
  }
52
+ const { path: configPath, isTypescript: configIsTs } = nextConfigDetails;
64
53
  let configToImport;
65
54
  // Only compile if the extension is a TypeScript extension
66
- if (tsExtensions.includes(configExtension)) {
55
+ if (configIsTs) {
67
56
  await build({
68
57
  entryPoints: [configPath],
69
58
  outfile: path.join(buildOptions.tempBuildDir, "next.config.mjs"),