@opennextjs/aws 3.5.7 → 3.6.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.
@@ -12,4 +12,5 @@ export default class Cache {
12
12
  set(key: string, data?: IncrementalCacheValue, ctx?: IncrementalCacheContext): Promise<void>;
13
13
  revalidateTag(tags: string | string[]): Promise<void>;
14
14
  private updateTagsOnSet;
15
+ private extractRevalidateForSet;
15
16
  }
@@ -29,7 +29,7 @@ export default class Cache {
29
29
  async getFetchCache(key, softTags, tags) {
30
30
  debug("get fetch cache", { key, softTags, tags });
31
31
  try {
32
- const cachedEntry = await globalThis.incrementalCache.get(key, true);
32
+ const cachedEntry = await globalThis.incrementalCache.get(key, "fetch");
33
33
  if (cachedEntry?.value === undefined)
34
34
  return null;
35
35
  const _tags = [...(tags ?? []), ...(softTags ?? [])];
@@ -65,7 +65,7 @@ export default class Cache {
65
65
  }
66
66
  async getIncrementalCache(key) {
67
67
  try {
68
- const cachedEntry = await globalThis.incrementalCache.get(key, false);
68
+ const cachedEntry = await globalThis.incrementalCache.get(key, "cache");
69
69
  if (!cachedEntry?.value) {
70
70
  return null;
71
71
  }
@@ -150,6 +150,7 @@ export default class Cache {
150
150
  await globalThis.incrementalCache.delete(key);
151
151
  }
152
152
  else {
153
+ const revalidate = this.extractRevalidateForSet(ctx);
153
154
  switch (data.kind) {
154
155
  case "ROUTE":
155
156
  case "APP_ROUTE": {
@@ -163,7 +164,8 @@ export default class Cache {
163
164
  status,
164
165
  headers,
165
166
  },
166
- }, false);
167
+ revalidate,
168
+ }, "cache");
167
169
  break;
168
170
  }
169
171
  case "PAGE":
@@ -179,14 +181,16 @@ export default class Cache {
179
181
  status,
180
182
  headers,
181
183
  },
182
- }, false);
184
+ revalidate,
185
+ }, "cache");
183
186
  }
184
187
  else {
185
188
  await globalThis.incrementalCache.set(key, {
186
189
  type: "page",
187
190
  html,
188
191
  json: pageData,
189
- }, false);
192
+ revalidate,
193
+ }, "cache");
190
194
  }
191
195
  break;
192
196
  }
@@ -200,17 +204,19 @@ export default class Cache {
200
204
  status,
201
205
  headers,
202
206
  },
203
- }, false);
207
+ revalidate,
208
+ }, "cache");
204
209
  break;
205
210
  }
206
211
  case "FETCH":
207
- await globalThis.incrementalCache.set(key, data, true);
212
+ await globalThis.incrementalCache.set(key, data, "fetch");
208
213
  break;
209
214
  case "REDIRECT":
210
215
  await globalThis.incrementalCache.set(key, {
211
216
  type: "redirect",
212
217
  props: data.props,
213
- }, false);
218
+ revalidate,
219
+ }, "cache");
214
220
  break;
215
221
  case "IMAGE":
216
222
  // Not implemented
@@ -233,8 +239,11 @@ export default class Cache {
233
239
  if (config?.disableTagCache || config?.disableIncrementalCache) {
234
240
  return;
235
241
  }
242
+ const _tags = Array.isArray(tags) ? tags : [tags];
243
+ if (_tags.length === 0) {
244
+ return;
245
+ }
236
246
  try {
237
- const _tags = Array.isArray(tags) ? tags : [tags];
238
247
  if (globalThis.tagCache.mode === "nextMode") {
239
248
  const paths = (await globalThis.tagCache.getPathsByTags?.(_tags)) ?? [];
240
249
  await globalThis.tagCache.writeTags(_tags);
@@ -320,7 +329,8 @@ export default class Cache {
320
329
  // Write derivedTags to the tag cache
321
330
  // If we use an in house version of getDerivedTags in build we should use it here instead of next's one
322
331
  const derivedTags = data?.kind === "FETCH"
323
- ? (ctx?.tags ?? data?.data?.tags ?? []) // before version 14 next.js used data?.data?.tags so we keep it for backward compatibility
332
+ ? //@ts-expect-error - On older versions of next, ctx was a number, but for these cases we use data?.data?.tags
333
+ (ctx?.tags ?? data?.data?.tags ?? []) // before version 14 next.js used data?.data?.tags so we keep it for backward compatibility
324
334
  : data?.kind === "PAGE"
325
335
  ? (data.headers?.["x-next-cache-tags"]?.split(",") ?? [])
326
336
  : [];
@@ -339,4 +349,19 @@ export default class Cache {
339
349
  })));
340
350
  }
341
351
  }
352
+ extractRevalidateForSet(ctx) {
353
+ if (ctx === undefined) {
354
+ return undefined;
355
+ }
356
+ if (typeof ctx === "number" || ctx === false) {
357
+ return ctx;
358
+ }
359
+ if ("revalidate" in ctx) {
360
+ return ctx.revalidate;
361
+ }
362
+ if ("cacheControl" in ctx) {
363
+ return ctx.cacheControl?.revalidate;
364
+ }
365
+ return undefined;
366
+ }
342
367
  }
@@ -0,0 +1,17 @@
1
+ import type { ComposableCacheEntry } from "../types/cache";
2
+ declare const _default: {
3
+ get(cacheKey: string): Promise<{
4
+ value: import("stream/web").ReadableStream<any>;
5
+ tags: string[];
6
+ stale: number;
7
+ timestamp: number;
8
+ expire: number;
9
+ revalidate: number;
10
+ } | undefined>;
11
+ set(cacheKey: string, pendingEntry: Promise<ComposableCacheEntry>): Promise<void>;
12
+ refreshTags(): Promise<void>;
13
+ getExpiration(...tags: string[]): Promise<number>;
14
+ expireTags(...tags: string[]): Promise<void>;
15
+ receiveExpiredTags(...tags: string[]): Promise<void>;
16
+ };
17
+ export default _default;
@@ -0,0 +1,89 @@
1
+ import { fromReadableStream, toReadableStream } from "../utils/stream";
2
+ import { debug } from "./logger";
3
+ export default {
4
+ async get(cacheKey) {
5
+ try {
6
+ const result = await globalThis.incrementalCache.get(cacheKey, "composable");
7
+ if (!result?.value?.value) {
8
+ return undefined;
9
+ }
10
+ debug("composable cache result", result);
11
+ // We need to check if the tags associated with this entry has been revalidated
12
+ if (globalThis.tagCache.mode === "nextMode" &&
13
+ result.value.tags.length > 0) {
14
+ const hasBeenRevalidated = await globalThis.tagCache.hasBeenRevalidated(result.value.tags, result.lastModified);
15
+ if (hasBeenRevalidated)
16
+ return undefined;
17
+ }
18
+ else if (globalThis.tagCache.mode === "original" ||
19
+ globalThis.tagCache.mode === undefined) {
20
+ const hasBeenRevalidated = (await globalThis.tagCache.getLastModified(cacheKey, result.lastModified)) === -1;
21
+ if (hasBeenRevalidated)
22
+ return undefined;
23
+ }
24
+ return {
25
+ ...result.value,
26
+ value: toReadableStream(result.value.value),
27
+ };
28
+ }
29
+ catch (e) {
30
+ debug("Cannot read composable cache entry");
31
+ return undefined;
32
+ }
33
+ },
34
+ async set(cacheKey, pendingEntry) {
35
+ const entry = await pendingEntry;
36
+ const valueToStore = await fromReadableStream(entry.value);
37
+ await globalThis.incrementalCache.set(cacheKey, {
38
+ ...entry,
39
+ value: valueToStore,
40
+ }, "composable");
41
+ if (globalThis.tagCache.mode === "original") {
42
+ const storedTags = await globalThis.tagCache.getByPath(cacheKey);
43
+ const tagsToWrite = entry.tags.filter((tag) => !storedTags.includes(tag));
44
+ if (tagsToWrite.length > 0) {
45
+ await globalThis.tagCache.writeTags(tagsToWrite.map((tag) => ({ tag, path: cacheKey })));
46
+ }
47
+ }
48
+ },
49
+ async refreshTags() {
50
+ // We don't do anything for now, do we want to do something here ???
51
+ return;
52
+ },
53
+ async getExpiration(...tags) {
54
+ if (globalThis.tagCache.mode === "nextMode") {
55
+ return globalThis.tagCache.getLastRevalidated(tags);
56
+ }
57
+ // We always return 0 here, original tag cache are handled directly in the get part
58
+ // TODO: We need to test this more, i'm not entirely sure that this is working as expected
59
+ return 0;
60
+ },
61
+ async expireTags(...tags) {
62
+ if (globalThis.tagCache.mode === "nextMode") {
63
+ return globalThis.tagCache.writeTags(tags);
64
+ }
65
+ const tagCache = globalThis.tagCache;
66
+ const revalidatedAt = Date.now();
67
+ // For the original mode, we have more work to do here.
68
+ // We need to find all paths linked to to these tags
69
+ const pathsToUpdate = await Promise.all(tags.map(async (tag) => {
70
+ const paths = await tagCache.getByTag(tag);
71
+ return paths.map((path) => ({
72
+ path,
73
+ tag,
74
+ revalidatedAt,
75
+ }));
76
+ }));
77
+ // We need to deduplicate paths, we use a set for that
78
+ const setToWrite = new Set();
79
+ for (const entry of pathsToUpdate.flat()) {
80
+ setToWrite.add(entry);
81
+ }
82
+ await globalThis.tagCache.writeTags(Array.from(setToWrite));
83
+ },
84
+ // This one is necessary for older versions of next
85
+ async receiveExpiredTags(...tags) {
86
+ // This function does absolutely nothing
87
+ return;
88
+ },
89
+ };
@@ -50,6 +50,12 @@ export async function defaultHandler(event, options) {
50
50
  process.env.HOST = headersHost;
51
51
  }
52
52
  const imageParams = validateImageParams(headers, queryString === null ? undefined : queryString);
53
+ // We return a 400 here if imageParams returns an errorMessage
54
+ // https://github.com/vercel/next.js/blob/512d8283054407ab92b2583ecce3b253c3be7b85/packages/next/src/server/next-server.ts#L937-L941
55
+ if ("errorMessage" in imageParams) {
56
+ error("Error during validation of image params", imageParams.errorMessage);
57
+ return buildFailureResponse(imageParams.errorMessage, options?.streamCreator, 400);
58
+ }
53
59
  let etag;
54
60
  // We don't cache any images, so in order to be able to return 304 responses, we compute an ETag from what is assumed to be static
55
61
  if (process.env.OPENNEXT_STATIC_ETAG) {
@@ -68,7 +74,8 @@ export async function defaultHandler(event, options) {
68
74
  return buildSuccessResponse(result, options?.streamCreator, etag);
69
75
  }
70
76
  catch (e) {
71
- return buildFailureResponse(e, options?.streamCreator);
77
+ error("Failed to optimize image", e);
78
+ return buildFailureResponse("Internal server error", options?.streamCreator);
72
79
  }
73
80
  }
74
81
  //////////////////////
@@ -81,9 +88,6 @@ function validateImageParams(headers, query) {
81
88
  // @ts-ignore
82
89
  { headers }, query, nextConfig, false);
83
90
  debug("image params", imageParams);
84
- if ("errorMessage" in imageParams) {
85
- throw new Error(imageParams.errorMessage);
86
- }
87
91
  return imageParams;
88
92
  }
89
93
  function computeEtag(imageParams) {
@@ -119,28 +123,26 @@ function buildSuccessResponse(result, streamCreator, etag) {
119
123
  headers,
120
124
  };
121
125
  }
122
- function buildFailureResponse(e, streamCreator) {
123
- debug(e);
126
+ function buildFailureResponse(errorMessage, streamCreator, statusCode = 500) {
127
+ debug(errorMessage, statusCode);
124
128
  if (streamCreator) {
125
129
  const response = new OpenNextNodeResponse(() => void 0, async () => void 0, streamCreator);
126
- response.writeHead(500, {
130
+ response.writeHead(statusCode, {
127
131
  Vary: "Accept",
128
132
  "Cache-Control": "public,max-age=60,immutable",
129
- "Content-Type": "application/json",
130
133
  });
131
- response.end(e?.message || e?.toString() || "An error occurred");
134
+ response.end(errorMessage);
132
135
  }
133
136
  return {
134
137
  type: "core",
135
138
  isBase64Encoded: false,
136
- statusCode: 500,
139
+ statusCode: statusCode,
137
140
  headers: {
138
141
  Vary: "Accept",
139
142
  // For failed images, allow client to retry after 1 minute.
140
143
  "Cache-Control": "public,max-age=60,immutable",
141
- "Content-Type": "application/json",
142
144
  },
143
- body: toReadableStream(e?.message || e?.toString() || "An error occurred"),
145
+ body: toReadableStream(errorMessage),
144
146
  };
145
147
  }
146
148
  const loader = await resolveImageLoader(globalThis.openNextConfig.imageOptimization?.loader ?? "s3");
@@ -4,6 +4,9 @@ import * as buildHelper from "./helper.js";
4
4
  *
5
5
  * @param options Build options.
6
6
  * @param format Output format.
7
- * @returns The path to the compiled file.
7
+ * @returns An object containing the paths to the compiled cache and composable cache files.
8
8
  */
9
- export declare function compileCache(options: buildHelper.BuildOptions, format?: "cjs" | "esm"): string;
9
+ export declare function compileCache(options: buildHelper.BuildOptions, format?: "cjs" | "esm"): {
10
+ cache: string;
11
+ composableCache: string;
12
+ };
@@ -5,17 +5,18 @@ import * as buildHelper from "./helper.js";
5
5
  *
6
6
  * @param options Build options.
7
7
  * @param format Output format.
8
- * @returns The path to the compiled file.
8
+ * @returns An object containing the paths to the compiled cache and composable cache files.
9
9
  */
10
10
  export function compileCache(options, format = "cjs") {
11
11
  const { config } = options;
12
12
  const ext = format === "cjs" ? "cjs" : "mjs";
13
- const outFile = path.join(options.buildDir, `cache.${ext}`);
13
+ const compiledCacheFile = path.join(options.buildDir, `cache.${ext}`);
14
14
  const isAfter15 = buildHelper.compareSemver(options.nextVersion, ">=", "15.0.0");
15
+ // Normal cache
15
16
  buildHelper.esbuildSync({
16
17
  external: ["next", "styled-jsx", "react", "@aws-sdk/*"],
17
18
  entryPoints: [path.join(options.openNextDistDir, "adapters", "cache.js")],
18
- outfile: outFile,
19
+ outfile: compiledCacheFile,
19
20
  target: ["node18"],
20
21
  format,
21
22
  banner: {
@@ -26,5 +27,26 @@ export function compileCache(options, format = "cjs") {
26
27
  ].join(""),
27
28
  },
28
29
  }, options);
29
- return outFile;
30
+ const compiledComposableCacheFile = path.join(options.buildDir, `composable-cache.${ext}`);
31
+ // Composable cache
32
+ buildHelper.esbuildSync({
33
+ external: ["next", "styled-jsx", "react", "@aws-sdk/*"],
34
+ entryPoints: [
35
+ path.join(options.openNextDistDir, "adapters", "composable-cache.js"),
36
+ ],
37
+ outfile: compiledComposableCacheFile,
38
+ target: ["node18"],
39
+ format,
40
+ banner: {
41
+ js: [
42
+ `globalThis.disableIncrementalCache = ${config.dangerous?.disableIncrementalCache ?? false};`,
43
+ `globalThis.disableDynamoDBCache = ${config.dangerous?.disableTagCache ?? false};`,
44
+ `globalThis.isNextAfter15 = ${isAfter15};`,
45
+ ].join(""),
46
+ },
47
+ }, options);
48
+ return {
49
+ cache: compiledCacheFile,
50
+ composableCache: compiledComposableCacheFile,
51
+ };
30
52
  }
@@ -17,6 +17,7 @@ export declare function getManifests(nextDir: string): {
17
17
  };
18
18
  export declare function copyTracedFiles({ buildOutputPath, packagePath, outputDir, routes, bundledNextServer, skipServerFiles, }: CopyTracedFilesOptions): Promise<{
19
19
  tracedFiles: string[];
20
+ nodePackages: Map<string, string>;
20
21
  manifests: {
21
22
  buildId: string;
22
23
  config: import("../types/next-types.js").NextConfig;
@@ -41,18 +41,38 @@ export async function copyTracedFiles({ buildOutputPath, packagePath, outputDir,
41
41
  const standaloneNextDir = path.join(standaloneDir, packagePath, ".next");
42
42
  const standaloneServerDir = path.join(standaloneNextDir, "server");
43
43
  const outputNextDir = path.join(outputDir, packagePath, ".next");
44
- const extractFiles = (files, from = standaloneNextDir) => files.map((f) => path.resolve(from, f));
45
- // On next 14+, we might not have to include those files
46
- // For next 13, we need to include them otherwise we get runtime error
47
- const requiredServerFiles = JSON.parse(readFileSync(path.join(dotNextDir, bundledNextServer
48
- ? "next-minimal-server.js.nft.json"
49
- : "next-server.js.nft.json"), "utf8"));
44
+ // Files to copy
45
+ // Map from files in the `.next/standalone` to files in the `.open-next` folder
50
46
  const filesToCopy = new Map();
47
+ // Node packages
48
+ // Map from folders in the project to folders in the `.open-next` folder
49
+ // The map might also include the mono-repo path.
50
+ const nodePackages = new Map();
51
+ /**
52
+ * Extracts files and node packages from a .nft.json file
53
+ * @param nftFile path to the .nft.json file relative to `.next/`
54
+ */
55
+ const processNftFile = (nftFile) => {
56
+ const subDir = path.dirname(nftFile);
57
+ const files = JSON.parse(readFileSync(path.join(dotNextDir, nftFile), "utf8")).files;
58
+ files.forEach((tracedPath) => {
59
+ const src = path.join(standaloneNextDir, subDir, tracedPath);
60
+ const dst = path.join(outputNextDir, subDir, tracedPath);
61
+ filesToCopy.set(src, dst);
62
+ const module = path.join(dotNextDir, subDir, tracedPath);
63
+ if (module.endsWith("package.json")) {
64
+ nodePackages.set(path.dirname(module), path.dirname(dst));
65
+ }
66
+ });
67
+ };
51
68
  // Files necessary by the server
52
69
  if (!skipServerFiles) {
53
- extractFiles(requiredServerFiles.files).forEach((f) => {
54
- filesToCopy.set(f, f.replace(standaloneDir, outputDir));
55
- });
70
+ // On next 14+, we might not have to include those files
71
+ // For next 13, we need to include them otherwise we get runtime error
72
+ const nftFile = bundledNextServer
73
+ ? "next-minimal-server.js.nft.json"
74
+ : "next-server.js.nft.json";
75
+ processNftFile(nftFile);
56
76
  }
57
77
  // create directory for pages
58
78
  if (existsSync(path.join(standaloneNextDir, "server/pages"))) {
@@ -69,13 +89,12 @@ export async function copyTracedFiles({ buildOutputPath, packagePath, outputDir,
69
89
  recursive: true,
70
90
  });
71
91
  const computeCopyFilesForPage = (pagePath) => {
72
- const fullFilePath = `server/${pagePath}.js`;
73
- let requiredFiles;
92
+ const serverPath = `server/${pagePath}.js`;
74
93
  try {
75
- requiredFiles = JSON.parse(readFileSync(path.join(standaloneNextDir, `${fullFilePath}.nft.json`), "utf8"));
94
+ processNftFile(`${serverPath}.nft.json`);
76
95
  }
77
96
  catch (e) {
78
- if (existsSync(path.join(standaloneNextDir, fullFilePath))) {
97
+ if (existsSync(path.join(dotNextDir, serverPath))) {
79
98
  //TODO: add a link to the docs
80
99
  throw new Error(`
81
100
  --------------------------------------------------------------------------------
@@ -88,18 +107,14 @@ See the docs for more information on how to bundle edge runtime functions.
88
107
  throw new Error(`
89
108
  --------------------------------------------------------------------------------
90
109
  We cannot find the route for ${pagePath}.
91
- File ${fullFilePath} does not exist
110
+ File ${serverPath} does not exist
92
111
  --------------------------------------------------------------------------------`);
93
112
  }
94
- const dir = path.dirname(fullFilePath);
95
- extractFiles(requiredFiles.files, path.join(standaloneNextDir, dir)).forEach((f) => {
96
- filesToCopy.set(f, f.replace(standaloneDir, outputDir));
97
- });
98
- if (!existsSync(path.join(standaloneNextDir, fullFilePath))) {
113
+ if (!existsSync(path.join(standaloneNextDir, serverPath))) {
99
114
  throw new Error(`This error should only happen for static 404 and 500 page from page router. Report this if that's not the case.,
100
- File ${fullFilePath} does not exist`);
115
+ File ${serverPath} does not exist`);
101
116
  }
102
- filesToCopy.set(path.join(standaloneNextDir, fullFilePath), path.join(outputNextDir, fullFilePath));
117
+ filesToCopy.set(path.join(standaloneNextDir, serverPath), path.join(outputNextDir, serverPath));
103
118
  };
104
119
  const safeComputeCopyFilesForPage = (pagePath, alternativePath) => {
105
120
  try {
@@ -148,12 +163,15 @@ File ${fullFilePath} does not exist
148
163
  routes.forEach((route) => {
149
164
  computeCopyFilesForPage(route);
150
165
  });
166
+ // Only files that are actually copied
167
+ const tracedFiles = [];
151
168
  //Actually copy the files
152
169
  filesToCopy.forEach((to, from) => {
153
170
  // We don't want to copy excluded packages (i.e sharp)
154
171
  if (isExcluded(from)) {
155
172
  return;
156
173
  }
174
+ tracedFiles.push(to);
157
175
  mkdirSync(path.dirname(to), { recursive: true });
158
176
  let symlink = null;
159
177
  // For pnpm symlink we need to do that
@@ -233,7 +251,8 @@ File ${fullFilePath} does not exist
233
251
  }
234
252
  logger.debug("copyTracedFiles:", Date.now() - tsStart, "ms");
235
253
  return {
236
- tracedFiles: Array.from(filesToCopy.values()),
254
+ tracedFiles,
255
+ nodePackages,
237
256
  manifests: getManifests(standaloneNextDir),
238
257
  };
239
258
  }
@@ -14,8 +14,7 @@ import { copyMiddlewareResources, generateEdgeBundle, } from "./edge/createEdgeB
14
14
  import * as buildHelper from "./helper.js";
15
15
  import { installDependencies } from "./installDeps.js";
16
16
  import { applyCodePatches } from "./patch/codePatcher.js";
17
- import { patchEnvVars, patchFetchCacheForISR, patchFetchCacheSetMissingWaitUntil, patchNextServer, patchUnstableCacheForISR, } from "./patch/patches/index.js";
18
- import { patchBackgroundRevalidation } from "./patch/patches/patchBackgroundRevalidation.js";
17
+ import { patchBackgroundRevalidation, patchEnvVars, patchFetchCacheForISR, patchFetchCacheSetMissingWaitUntil, patchNextServer, patchUnstableCacheForISR, patchUseCacheForISR, } from "./patch/patches/index.js";
19
18
  export async function createServerBundle(options, codeCustomization) {
20
19
  const { config } = options;
21
20
  const foundRoutes = new Set();
@@ -86,7 +85,10 @@ async function generateBundle(name, options, fnOptions, codeCustomization) {
86
85
  const outPackagePath = path.join(outputPath, packagePath);
87
86
  fs.mkdirSync(outPackagePath, { recursive: true });
88
87
  const ext = fnOptions.runtime === "deno" ? "mjs" : "cjs";
88
+ // Normal cache
89
89
  fs.copyFileSync(path.join(options.buildDir, `cache.${ext}`), path.join(outPackagePath, "cache.cjs"));
90
+ // Composable cache
91
+ fs.copyFileSync(path.join(options.buildDir, `composable-cache.${ext}`), path.join(outPackagePath, "composable-cache.cjs"));
90
92
  if (fnOptions.runtime === "deno") {
91
93
  addDenoJson(outputPath, packagePath);
92
94
  }
@@ -123,6 +125,7 @@ async function generateBundle(name, options, fnOptions, codeCustomization) {
123
125
  patchNextServer,
124
126
  patchEnvVars,
125
127
  patchBackgroundRevalidation,
128
+ patchUseCacheForISR,
126
129
  ...additionalCodePatches,
127
130
  ]);
128
131
  // Build Lambda code
@@ -135,6 +138,7 @@ async function generateBundle(name, options, fnOptions, codeCustomization) {
135
138
  const isBefore13413 = buildHelper.compareSemver(options.nextVersion, "<=", "13.4.13");
136
139
  const isAfter141 = buildHelper.compareSemver(options.nextVersion, ">=", "14.1");
137
140
  const isAfter142 = buildHelper.compareSemver(options.nextVersion, ">=", "14.2");
141
+ const isAfter152 = buildHelper.compareSemver(options.nextVersion, ">=", "15.2.0");
138
142
  const disableRouting = isBefore13413 || config.middleware?.external;
139
143
  const updater = new ContentUpdater(options);
140
144
  const additionalPlugins = codeCustomization?.additionalPlugins
@@ -160,6 +164,7 @@ async function generateBundle(name, options, fnOptions, codeCustomization) {
160
164
  ...(isAfter141
161
165
  ? ["experimentalIncrementalCacheHandler"]
162
166
  : ["stableIncrementalCache"]),
167
+ ...(isAfter152 ? [] : ["composableCache"]),
163
168
  ],
164
169
  }),
165
170
  openNextResolvePlugin({
@@ -1,4 +1,5 @@
1
1
  export { envVarRuleCreator, patchEnvVars } from "./patchEnvVar.js";
2
2
  export { patchNextServer } from "./patchNextServer.js";
3
- export { patchFetchCacheForISR, patchUnstableCacheForISR, } from "./patchFetchCacheISR.js";
3
+ export { patchFetchCacheForISR, patchUnstableCacheForISR, patchUseCacheForISR, } from "./patchFetchCacheISR.js";
4
4
  export { patchFetchCacheSetMissingWaitUntil } from "./patchFetchCacheWaitUntil.js";
5
+ export { patchBackgroundRevalidation } from "./patchBackgroundRevalidation.js";
@@ -1,4 +1,5 @@
1
1
  export { envVarRuleCreator, patchEnvVars } from "./patchEnvVar.js";
2
2
  export { patchNextServer } from "./patchNextServer.js";
3
- export { patchFetchCacheForISR, patchUnstableCacheForISR, } from "./patchFetchCacheISR.js";
3
+ export { patchFetchCacheForISR, patchUnstableCacheForISR, patchUseCacheForISR, } from "./patchFetchCacheISR.js";
4
4
  export { patchFetchCacheSetMissingWaitUntil } from "./patchFetchCacheWaitUntil.js";
5
+ export { patchBackgroundRevalidation } from "./patchBackgroundRevalidation.js";
@@ -1,5 +1,7 @@
1
1
  import type { CodePatcher } from "../codePatcher.js";
2
2
  export declare const fetchRule = "\nrule:\n kind: member_expression\n pattern: $WORK_STORE.isOnDemandRevalidate\n inside:\n kind: ternary_expression\n all:\n - has: {kind: 'null'}\n - has: \n kind: await_expression\n has:\n kind: call_expression\n all:\n - has:\n kind: member_expression\n has:\n kind: property_identifier\n field: property\n regex: get\n - has:\n kind: arguments\n has:\n kind: object\n has:\n kind: pair\n all:\n - has:\n kind: property_identifier\n field: key\n regex: softTags\n inside:\n kind: variable_declarator\n\nfix:\n ($WORK_STORE.isOnDemandRevalidate && !globalThis.__openNextAls?.getStore()?.isISRRevalidation)\n";
3
3
  export declare const unstable_cacheRule = "\nrule:\n kind: member_expression\n pattern: $STORE_OR_CACHE.isOnDemandRevalidate\n inside:\n kind: if_statement\n stopBy: end\n has:\n kind: statement_block\n has:\n kind: variable_declarator\n has: \n kind: await_expression\n has:\n kind: call_expression\n all:\n - has:\n kind: member_expression\n has:\n kind: property_identifier\n field: property\n regex: get\n - has:\n kind: arguments\n has:\n kind: object\n has:\n kind: pair\n all:\n - has:\n kind: property_identifier\n field: key\n regex: softTags\n stopBy: end\nfix:\n ($STORE_OR_CACHE.isOnDemandRevalidate && !globalThis.__openNextAls?.getStore()?.isISRRevalidation)\n";
4
+ export declare const useCacheRule = "\nrule:\n kind: member_expression\n pattern: $STORE_OR_CACHE.isOnDemandRevalidate\n inside:\n kind: binary_expression\n has: \n kind: member_expression\n pattern: $STORE_OR_CACHE.isDraftMode\n inside:\n kind: if_statement\n stopBy: end\n has: \n kind: return_statement\n any:\n - has:\n kind: 'true'\n - has:\n regex: '!0'\n stopBy: end\nfix:\n '($STORE_OR_CACHE.isOnDemandRevalidate && !globalThis.__openNextAls?.getStore()?.isISRRevalidation)'";
4
5
  export declare const patchFetchCacheForISR: CodePatcher;
5
6
  export declare const patchUnstableCacheForISR: CodePatcher;
7
+ export declare const patchUseCacheForISR: CodePatcher;
@@ -74,6 +74,28 @@ rule:
74
74
  fix:
75
75
  ($STORE_OR_CACHE.isOnDemandRevalidate && !globalThis.__openNextAls?.getStore()?.isISRRevalidation)
76
76
  `;
77
+ export const useCacheRule = `
78
+ rule:
79
+ kind: member_expression
80
+ pattern: $STORE_OR_CACHE.isOnDemandRevalidate
81
+ inside:
82
+ kind: binary_expression
83
+ has:
84
+ kind: member_expression
85
+ pattern: $STORE_OR_CACHE.isDraftMode
86
+ inside:
87
+ kind: if_statement
88
+ stopBy: end
89
+ has:
90
+ kind: return_statement
91
+ any:
92
+ - has:
93
+ kind: 'true'
94
+ - has:
95
+ regex: '!0'
96
+ stopBy: end
97
+ fix:
98
+ '($STORE_OR_CACHE.isOnDemandRevalidate && !globalThis.__openNextAls?.getStore()?.isISRRevalidation)'`;
77
99
  export const patchFetchCacheForISR = {
78
100
  name: "patch-fetch-cache-for-isr",
79
101
  patches: [
@@ -100,3 +122,16 @@ export const patchUnstableCacheForISR = {
100
122
  },
101
123
  ],
102
124
  };
125
+ export const patchUseCacheForISR = {
126
+ name: "patch-use-cache-for-isr",
127
+ patches: [
128
+ {
129
+ versions: ">=15.3.0",
130
+ field: {
131
+ pathFilter: getCrossPlatformPathRegex(String.raw `(server/chunks/.*\.js|\.runtime\..*\.js|use-cache/use-cache-wrapper\.js)$`, { escape: false }),
132
+ contentFilter: /\.isOnDemandRevalidate/,
133
+ patchCode: createPatchCode(useCacheRule, Lang.JavaScript),
134
+ },
135
+ },
136
+ ],
137
+ };
@@ -1,5 +1,6 @@
1
1
  import { FunctionsConfigManifest, MiddlewareManifest, NextConfig, PrerenderManifest, } from "../../adapters/config/index.js";
2
2
  import { emptyReadableStream } from "../../utils/stream.js";
3
+ import { getQueryFromSearchParams } from "../../overrides/converters/utils.js";
3
4
  import { localizePath } from "./i18n/index.js";
4
5
  import { convertBodyToReadableStream, getMiddlewareMatch, isExternal, } from "./util.js";
5
6
  const middlewareManifest = MiddlewareManifest;
@@ -103,7 +104,7 @@ export async function handleMiddleware(internalEvent, initialSearch, middlewareL
103
104
  // NOTE: the header was added to `req` from above
104
105
  const rewriteUrl = responseHeaders.get("x-middleware-rewrite");
105
106
  let isExternalRewrite = false;
106
- let middlewareQueryString = internalEvent.query;
107
+ let middlewareQuery = internalEvent.query;
107
108
  let newUrl = internalEvent.url;
108
109
  if (rewriteUrl) {
109
110
  newUrl = rewriteUrl;
@@ -113,15 +114,12 @@ export async function handleMiddleware(internalEvent, initialSearch, middlewareL
113
114
  }
114
115
  else {
115
116
  const rewriteUrlObject = new URL(rewriteUrl);
116
- // Reset the query params if the middleware is a rewrite
117
- middlewareQueryString = middlewareQueryString.__nextDataReq
118
- ? {
119
- __nextDataReq: middlewareQueryString.__nextDataReq,
120
- }
121
- : {};
122
- rewriteUrlObject.searchParams.forEach((v, k) => {
123
- middlewareQueryString[k] = v;
124
- });
117
+ // Search params from the rewritten URL override the original search params
118
+ middlewareQuery = getQueryFromSearchParams(rewriteUrlObject.searchParams);
119
+ // We still need to add internal search params to the query string for pages router on older versions of Next.js
120
+ if ("__nextDataReq" in internalEvent.query) {
121
+ middlewareQuery.__nextDataReq = internalEvent.query.__nextDataReq;
122
+ }
125
123
  }
126
124
  }
127
125
  // If the middleware wants to directly return a response (i.e. not using `NextResponse.next()` or `NextResponse.rewrite()`)
@@ -145,7 +143,7 @@ export async function handleMiddleware(internalEvent, initialSearch, middlewareL
145
143
  headers: { ...internalEvent.headers, ...reqHeaders },
146
144
  body: internalEvent.body,
147
145
  method: internalEvent.method,
148
- query: middlewareQueryString,
146
+ query: middlewareQuery,
149
147
  cookies: internalEvent.cookies,
150
148
  remoteAddress: internalEvent.remoteAddress,
151
149
  isExternalRewrite,
package/dist/core/util.js CHANGED
@@ -15,6 +15,7 @@ overrideNextjsRequireHooks(NextConfig);
15
15
  applyNextjsRequireHooksOverride();
16
16
  //#endOverride
17
17
  const cacheHandlerPath = require.resolve("./cache.cjs");
18
+ const composableCacheHandlerPath = require.resolve("./composable-cache.cjs");
18
19
  // @ts-ignore
19
20
  const nextServer = new NextServer.default({
20
21
  //#override requestHandlerHost
@@ -42,6 +43,11 @@ const nextServer = new NextServer.default({
42
43
  //#override experimentalIncrementalCacheHandler
43
44
  incrementalCacheHandlerPath: cacheHandlerPath,
44
45
  //#endOverride
46
+ //#override composableCache
47
+ cacheHandlers: {
48
+ default: composableCacheHandlerPath,
49
+ },
50
+ //#endOverride
45
51
  },
46
52
  },
47
53
  customServer: false,
@@ -30,8 +30,8 @@ function buildS3Key(key, extension) {
30
30
  return path.posix.join(CACHE_BUCKET_KEY_PREFIX ?? "", extension === "fetch" ? "__fetch" : "", NEXT_BUILD_ID ?? "", extension === "fetch" ? key : `${key}.${extension}`);
31
31
  }
32
32
  const incrementalCache = {
33
- async get(key, isFetch) {
34
- const result = await awsFetch(buildS3Key(key, isFetch ? "fetch" : "cache"), {
33
+ async get(key, cacheType) {
34
+ const result = await awsFetch(buildS3Key(key, cacheType ?? "cache"), {
35
35
  method: "GET",
36
36
  });
37
37
  if (result.status === 404) {
@@ -46,8 +46,8 @@ const incrementalCache = {
46
46
  lastModified: new Date(result.headers.get("last-modified") ?? "").getTime(),
47
47
  };
48
48
  },
49
- async set(key, value, isFetch) {
50
- const response = await awsFetch(buildS3Key(key, isFetch ? "fetch" : "cache"), {
49
+ async set(key, value, cacheType) {
50
+ const response = await awsFetch(buildS3Key(key, cacheType ?? "cache"), {
51
51
  method: "PUT",
52
52
  body: JSON.stringify(value),
53
53
  });
@@ -15,10 +15,10 @@ function buildS3Key(key, extension) {
15
15
  return path.posix.join(CACHE_BUCKET_KEY_PREFIX ?? "", extension === "fetch" ? "__fetch" : "", NEXT_BUILD_ID ?? "", extension === "fetch" ? key : `${key}.${extension}`);
16
16
  }
17
17
  const incrementalCache = {
18
- async get(key, isFetch) {
18
+ async get(key, cacheType) {
19
19
  const result = await s3Client.send(new GetObjectCommand({
20
20
  Bucket: CACHE_BUCKET_NAME,
21
- Key: buildS3Key(key, isFetch ? "fetch" : "cache"),
21
+ Key: buildS3Key(key, cacheType ?? "cache"),
22
22
  }));
23
23
  const cacheData = JSON.parse((await result.Body?.transformToString()) ?? "{}");
24
24
  return {
@@ -26,10 +26,10 @@ const incrementalCache = {
26
26
  lastModified: result.LastModified?.getTime(),
27
27
  };
28
28
  },
29
- async set(key, value, isFetch) {
29
+ async set(key, value, cacheType) {
30
30
  await s3Client.send(new PutObjectCommand({
31
31
  Bucket: CACHE_BUCKET_NAME,
32
- Key: buildS3Key(key, isFetch ? "fetch" : "cache"),
32
+ Key: buildS3Key(key, cacheType ?? "cache"),
33
33
  Body: JSON.stringify(value),
34
34
  }));
35
35
  },
@@ -1,6 +1,7 @@
1
1
  declare const _default: {
2
2
  name: string;
3
3
  mode: "nextMode";
4
+ getLastRevalidated: (tags: string[]) => Promise<number>;
4
5
  hasBeenRevalidated: (tags: string[], lastModified?: number) => Promise<boolean>;
5
6
  writeTags: (tags: string[]) => Promise<void>;
6
7
  };
@@ -52,6 +52,10 @@ function buildDynamoObject(tag, revalidatedAt) {
52
52
  export default {
53
53
  name: "ddb-nextMode",
54
54
  mode: "nextMode",
55
+ getLastRevalidated: async (tags) => {
56
+ // Not supported for now
57
+ return 0;
58
+ },
55
59
  hasBeenRevalidated: async (tags, lastModified) => {
56
60
  if (globalThis.openNextConfig.dangerous?.disableTagCache) {
57
61
  return false;
@@ -1,4 +1,5 @@
1
1
  import fs from "node:fs";
2
+ // TODO: fix this for monorepo
2
3
  const tagFile = "../../dynamodb-provider/dynamodb-cache.json";
3
4
  const tagContent = fs.readFileSync(tagFile, "utf-8");
4
5
  let tags = JSON.parse(tagContent);
@@ -26,7 +27,7 @@ const tagCache = {
26
27
  tags = unchangedTags.concat(newTags.map((tag) => ({
27
28
  tag: { S: tag.tag },
28
29
  path: { S: tag.path },
29
- revalidatedAt: { N: String(tag.revalidatedAt) },
30
+ revalidatedAt: { N: String(tag.revalidatedAt ?? 1) },
30
31
  })));
31
32
  },
32
33
  };
@@ -3,6 +3,7 @@ import path from "node:path";
3
3
  import chalk from "chalk";
4
4
  import { loadAppPathRoutesManifest, loadAppPathsManifest, loadAppPathsManifestKeys, loadBuildId, loadConfig, loadConfigHeaders, loadFunctionsConfigManifest, loadHtmlPages, loadMiddlewareManifest, loadPrerenderManifest, loadRoutesManifest, } from "../adapters/config/util.js";
5
5
  import logger from "../logger.js";
6
+ import { normalizePath } from "../utils/normalize-path.js";
6
7
  import { getCrossPlatformPathRegex } from "../utils/regex.js";
7
8
  /**
8
9
  * @param opts.nextDir - The path to the .next directory
@@ -11,7 +12,7 @@ import { getCrossPlatformPathRegex } from "../utils/regex.js";
11
12
  * @returns
12
13
  */
13
14
  export function openNextEdgePlugins({ nextDir, middlewareInfo, isInCloudflare, }) {
14
- const entryFiles = middlewareInfo?.files.map((file) => path.join(nextDir, file)) ?? [];
15
+ const entryFiles = middlewareInfo?.files.map((file) => normalizePath(path.join(nextDir, file))) ?? [];
15
16
  const routes = middlewareInfo
16
17
  ? [
17
18
  {
@@ -1,3 +1,4 @@
1
+ import type { ReadableStream } from "node:stream/web";
1
2
  interface CachedFetchValue {
2
3
  kind: "FETCH";
3
4
  data: {
@@ -61,7 +62,7 @@ export interface CacheHandlerValue {
61
62
  cacheState?: string;
62
63
  value: IncrementalCacheValue | null;
63
64
  }
64
- export type Extension = "cache" | "fetch";
65
+ export type Extension = "cache" | "fetch" | "composable";
65
66
  type MetaHeaders = {
66
67
  "x-next-cache-tags"?: string;
67
68
  [k: string]: string | string[] | undefined;
@@ -82,11 +83,54 @@ export type TagCacheMetaFile = {
82
83
  N: string;
83
84
  };
84
85
  };
85
- export type IncrementalCacheContext = {
86
- revalidate?: number | false | undefined;
87
- fetchCache?: boolean | undefined;
88
- fetchUrl?: string | undefined;
89
- fetchIdx?: number | undefined;
90
- tags?: string[] | undefined;
86
+ interface SetIncrementalFetchCacheContext {
87
+ fetchCache: true;
88
+ fetchUrl?: string;
89
+ fetchIdx?: number;
90
+ tags?: string[];
91
+ }
92
+ interface SetIncrementalResponseCacheContext {
93
+ fetchCache?: false;
94
+ cacheControl?: {
95
+ revalidate: number | false;
96
+ expire?: number;
97
+ };
98
+ /**
99
+ * True if the route is enabled for PPR.
100
+ */
101
+ isRoutePPREnabled?: boolean;
102
+ /**
103
+ * True if this is a fallback request.
104
+ */
105
+ isFallback?: boolean;
106
+ }
107
+ interface SetIncrementalCacheContext {
108
+ revalidate?: number | false;
109
+ isRoutePPREnabled?: boolean;
110
+ isFallback?: boolean;
111
+ }
112
+ type OldSetIncrementalCacheContext = number | false | undefined;
113
+ export type IncrementalCacheContext = OldSetIncrementalCacheContext | SetIncrementalCacheContext | SetIncrementalFetchCacheContext | SetIncrementalResponseCacheContext;
114
+ export interface ComposableCacheEntry {
115
+ value: ReadableStream<Uint8Array>;
116
+ tags: string[];
117
+ stale: number;
118
+ timestamp: number;
119
+ expire: number;
120
+ revalidate: number;
121
+ }
122
+ export type StoredComposableCacheEntry = Omit<ComposableCacheEntry, "value"> & {
123
+ value: string;
91
124
  };
125
+ export interface ComposableCacheHandler {
126
+ get(cacheKey: string): Promise<ComposableCacheEntry | undefined>;
127
+ set(cacheKey: string, pendingEntry: Promise<ComposableCacheEntry>): Promise<void>;
128
+ refreshTags(): Promise<void>;
129
+ getExpiration(...tags: string[]): Promise<number>;
130
+ expireTags(...tags: string[]): Promise<void>;
131
+ /**
132
+ * This function is only there for older versions and do nothing
133
+ */
134
+ receiveExpiredTags(...tags: string[]): Promise<void>;
135
+ }
92
136
  export {};
@@ -75,6 +75,7 @@ export interface NextConfig {
75
75
  };
76
76
  images: ImageConfig;
77
77
  poweredByHeader?: boolean;
78
+ serverExternalPackages?: string[];
78
79
  }
79
80
  export interface RouteDefinition {
80
81
  page: string;
@@ -1,5 +1,5 @@
1
1
  import type { Readable } from "node:stream";
2
- import type { Meta } from "./cache";
2
+ import type { Extension, Meta, StoredComposableCacheEntry } from "./cache";
3
3
  import type { BaseEventOrResult, BaseOverride, InternalEvent, InternalResult, Origin, ResolvedRoute, StreamCreator, WaitUntil } from "./open-next";
4
4
  export interface QueueMessage {
5
5
  MessageDeduplicationId: string;
@@ -51,12 +51,16 @@ export type WithLastModified<T> = {
51
51
  lastModified?: number;
52
52
  value?: T;
53
53
  };
54
- export type CacheValue<IsFetch extends boolean> = (IsFetch extends true ? CachedFetchValue : CachedFile) & {
54
+ export type CacheEntryType = Extension;
55
+ export type CacheValue<CacheType extends CacheEntryType> = (CacheType extends "fetch" ? CachedFetchValue : CacheType extends "cache" ? CachedFile : StoredComposableCacheEntry) & {
56
+ /**
57
+ * This is available for page cache entry, but only at runtime.
58
+ */
55
59
  revalidate?: number | false;
56
60
  };
57
61
  export type IncrementalCache = {
58
- get<IsFetch extends boolean = false>(key: string, isFetch?: IsFetch): Promise<WithLastModified<CacheValue<IsFetch>> | null>;
59
- set<IsFetch extends boolean = false>(key: string, value: CacheValue<IsFetch>, isFetch?: IsFetch): Promise<void>;
62
+ get<CacheType extends CacheEntryType = "cache">(key: string, cacheType?: CacheType): Promise<WithLastModified<CacheValue<CacheType>> | null>;
63
+ set<CacheType extends CacheEntryType = "cache">(key: string, value: CacheValue<CacheType>, isFetch?: CacheType): Promise<void>;
60
64
  delete(key: string): Promise<void>;
61
65
  name: string;
62
66
  };
@@ -86,6 +90,7 @@ Cons :
86
90
  */
87
91
  export type NextModeTagCache = BaseTagCache & {
88
92
  mode: "nextMode";
93
+ getLastRevalidated(tags: string[]): Promise<number>;
89
94
  hasBeenRevalidated(tags: string[], lastModified?: number): Promise<boolean>;
90
95
  writeTags(tags: string[]): Promise<void>;
91
96
  getPathsByTags?: (tags: string[]) => Promise<string[]>;
@@ -1,3 +1,3 @@
1
1
  import type { CacheValue, WithLastModified } from "../types/overrides";
2
2
  export declare function hasBeenRevalidated(key: string, tags: string[], cacheEntry: WithLastModified<CacheValue<any>>): Promise<boolean>;
3
- export declare function getTagsFromValue(value?: CacheValue<false>): string[];
3
+ export declare function getTagsFromValue(value?: CacheValue<"cache">): string[];
@@ -0,0 +1 @@
1
+ export declare function normalizePath(path: string): string;
@@ -0,0 +1,3 @@
1
+ export function normalizePath(path) {
2
+ return path.replace(/\\/g, "/");
3
+ }
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "access": "public"
4
4
  },
5
5
  "name": "@opennextjs/aws",
6
- "version": "3.5.7",
6
+ "version": "3.6.0",
7
7
  "bin": {
8
8
  "open-next": "./dist/index.js"
9
9
  },