@opennextjs/aws 3.6.5 → 3.7.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 (36) hide show
  1. package/README.md +1 -1
  2. package/dist/adapters/cache.js +4 -4
  3. package/dist/adapters/composable-cache.js +14 -4
  4. package/dist/adapters/edge-adapter.js +5 -1
  5. package/dist/adapters/middleware.js +18 -8
  6. package/dist/build/copyTracedFiles.js +10 -5
  7. package/dist/build/createAssets.js +55 -51
  8. package/dist/build/createServerBundle.js +0 -1
  9. package/dist/build/patch/patches/index.d.ts +0 -1
  10. package/dist/build/patch/patches/index.js +0 -1
  11. package/dist/build/patch/patches/patchNextServer.d.ts +13 -3
  12. package/dist/build/patch/patches/patchNextServer.js +82 -44
  13. package/dist/core/createMainHandler.js +4 -1
  14. package/dist/core/requestHandler.js +11 -2
  15. package/dist/core/resolve.d.ts +5 -0
  16. package/dist/core/resolve.js +11 -0
  17. package/dist/core/routing/cacheInterceptor.js +13 -1
  18. package/dist/core/routing/matcher.d.ts +5 -0
  19. package/dist/core/routing/matcher.js +9 -3
  20. package/dist/core/routing/util.js +2 -0
  21. package/dist/core/routingHandler.d.ts +5 -1
  22. package/dist/core/routingHandler.js +75 -52
  23. package/dist/overrides/assetResolver/dummy.d.ts +8 -0
  24. package/dist/overrides/assetResolver/dummy.js +9 -0
  25. package/dist/overrides/converters/edge.js +7 -1
  26. package/dist/types/global.d.ts +9 -1
  27. package/dist/types/next-types.d.ts +2 -0
  28. package/dist/types/open-next.d.ts +8 -1
  29. package/dist/types/overrides.d.ts +19 -5
  30. package/dist/utils/cache.d.ts +2 -1
  31. package/dist/utils/cache.js +32 -0
  32. package/dist/utils/promise.d.ts +2 -1
  33. package/dist/utils/promise.js +3 -2
  34. package/package.json +1 -1
  35. package/dist/build/patch/patches/dropBabel.d.ts +0 -20
  36. package/dist/build/patch/patches/dropBabel.js +0 -72
package/README.md CHANGED
@@ -23,7 +23,7 @@ OpenNext takes the Next.js build output and converts it into packages that can b
23
23
 
24
24
  ## Features
25
25
 
26
- OpenNext aims to support all Next.js 14 features. Some features are work in progress. If you are running into any problems make sure to check the [docs](https://opennext.js.org/aws) first before you open a [new issue](/issues/new) or visit our [Discord](https://discord.gg/opennextjs) to let us know!
26
+ OpenNext aims to support all Next.js 15 features. Some features are work in progress. If you are running into any problems make sure to check the [docs](https://opennext.js.org/aws) first before you open a [new issue](/issues/new) or visit our [Discord](https://discord.gg/opennextjs) to let us know!
27
27
 
28
28
  - [x] App & Pages Router
29
29
  - [x] API routes
@@ -1,4 +1,4 @@
1
- import { getTagsFromValue, hasBeenRevalidated } from "../utils/cache";
1
+ import { getTagsFromValue, hasBeenRevalidated, writeTags } from "../utils/cache";
2
2
  import { isBinaryContentType } from "../utils/binary";
3
3
  import { debug, error, warn } from "./logger";
4
4
  function isFetchCache(options) {
@@ -246,7 +246,7 @@ export default class Cache {
246
246
  try {
247
247
  if (globalThis.tagCache.mode === "nextMode") {
248
248
  const paths = (await globalThis.tagCache.getPathsByTags?.(_tags)) ?? [];
249
- await globalThis.tagCache.writeTags(_tags);
249
+ await writeTags(_tags);
250
250
  if (paths.length > 0) {
251
251
  // TODO: we should introduce a new method in cdnInvalidationHandler to invalidate paths by tags for cdn that supports it
252
252
  // It also means that we'll need to provide the tags used in every request to the wrapper or converter.
@@ -291,7 +291,7 @@ export default class Cache {
291
291
  }
292
292
  }
293
293
  // Update all keys with the given tag with revalidatedAt set to now
294
- await globalThis.tagCache.writeTags(toInsert);
294
+ await writeTags(toInsert);
295
295
  // We can now invalidate all paths in the CDN
296
296
  // This only applies to `revalidateTag`, not to `res.revalidate()`
297
297
  const uniquePaths = Array.from(new Set(toInsert
@@ -340,7 +340,7 @@ export default class Cache {
340
340
  const storedTags = await globalThis.tagCache.getByPath(key);
341
341
  const tagsToWrite = derivedTags.filter((tag) => !storedTags.includes(tag));
342
342
  if (tagsToWrite.length > 0) {
343
- await globalThis.tagCache.writeTags(tagsToWrite.map((tag) => ({
343
+ await writeTags(tagsToWrite.map((tag) => ({
344
344
  path: key,
345
345
  tag: tag,
346
346
  // In case the tags are not there we just need to create them
@@ -1,8 +1,15 @@
1
+ import { writeTags } from "../utils/cache";
1
2
  import { fromReadableStream, toReadableStream } from "../utils/stream";
2
3
  import { debug } from "./logger";
4
+ const pendingWritePromiseMap = new Map();
3
5
  export default {
4
6
  async get(cacheKey) {
5
7
  try {
8
+ // We first check if we have a pending write for this cache key
9
+ // If we do, we return the pending promise instead of fetching the cache
10
+ if (pendingWritePromiseMap.has(cacheKey)) {
11
+ return pendingWritePromiseMap.get(cacheKey);
12
+ }
6
13
  const result = await globalThis.incrementalCache.get(cacheKey, "composable");
7
14
  if (!result?.value?.value) {
8
15
  return undefined;
@@ -32,7 +39,10 @@ export default {
32
39
  }
33
40
  },
34
41
  async set(cacheKey, pendingEntry) {
35
- const entry = await pendingEntry;
42
+ pendingWritePromiseMap.set(cacheKey, pendingEntry);
43
+ const entry = await pendingEntry.finally(() => {
44
+ pendingWritePromiseMap.delete(cacheKey);
45
+ });
36
46
  const valueToStore = await fromReadableStream(entry.value);
37
47
  await globalThis.incrementalCache.set(cacheKey, {
38
48
  ...entry,
@@ -42,7 +52,7 @@ export default {
42
52
  const storedTags = await globalThis.tagCache.getByPath(cacheKey);
43
53
  const tagsToWrite = entry.tags.filter((tag) => !storedTags.includes(tag));
44
54
  if (tagsToWrite.length > 0) {
45
- await globalThis.tagCache.writeTags(tagsToWrite.map((tag) => ({ tag, path: cacheKey })));
55
+ await writeTags(tagsToWrite.map((tag) => ({ tag, path: cacheKey })));
46
56
  }
47
57
  }
48
58
  },
@@ -60,7 +70,7 @@ export default {
60
70
  },
61
71
  async expireTags(...tags) {
62
72
  if (globalThis.tagCache.mode === "nextMode") {
63
- return globalThis.tagCache.writeTags(tags);
73
+ return writeTags(tags);
64
74
  }
65
75
  const tagCache = globalThis.tagCache;
66
76
  const revalidatedAt = Date.now();
@@ -79,7 +89,7 @@ export default {
79
89
  for (const entry of pathsToUpdate.flat()) {
80
90
  setToWrite.add(entry);
81
91
  }
82
- await globalThis.tagCache.writeTags(Array.from(setToWrite));
92
+ await writeTags(Array.from(setToWrite));
83
93
  },
84
94
  // This one is necessary for older versions of next
85
95
  async receiveExpiredTags(...tags) {
@@ -4,11 +4,15 @@ import { emptyReadableStream } from "../utils/stream";
4
4
  import { NextConfig } from "../adapters/config";
5
5
  import { createGenericHandler } from "../core/createGenericHandler";
6
6
  import { convertBodyToReadableStream } from "../core/routing/util";
7
+ import { INTERNAL_EVENT_REQUEST_ID } from "../core/routingHandler";
7
8
  globalThis.__openNextAls = new AsyncLocalStorage();
8
9
  const defaultHandler = async (internalEvent, options) => {
9
10
  globalThis.isEdgeRuntime = true;
11
+ const requestId = globalThis.openNextConfig.middleware?.external
12
+ ? internalEvent.headers[INTERNAL_EVENT_REQUEST_ID]
13
+ : Math.random().toString(36);
10
14
  // We run everything in the async local storage context so that it is available in edge runtime functions
11
- return runWithOpenNextRequestContext({ isISRRevalidation: false, waitUntil: options?.waitUntil }, async () => {
15
+ return runWithOpenNextRequestContext({ isISRRevalidation: false, waitUntil: options?.waitUntil, requestId }, async () => {
12
16
  // @ts-expect-error - This is bundled
13
17
  const handler = await import("./middleware.mjs");
14
18
  const response = await handler.default({
@@ -1,25 +1,29 @@
1
1
  import { runWithOpenNextRequestContext } from "../utils/promise";
2
2
  import { debug, error } from "../adapters/logger";
3
3
  import { createGenericHandler } from "../core/createGenericHandler";
4
- import { resolveIncrementalCache, resolveOriginResolver, resolveProxyRequest, resolveQueue, resolveTagCache, } from "../core/resolve";
4
+ import { resolveAssetResolver, resolveIncrementalCache, resolveOriginResolver, resolveProxyRequest, resolveQueue, resolveTagCache, } from "../core/resolve";
5
5
  import { constructNextUrl } from "../core/routing/util";
6
- import routingHandler, { INTERNAL_HEADER_INITIAL_URL, INTERNAL_HEADER_RESOLVED_ROUTES, } from "../core/routingHandler";
6
+ import routingHandler, { INTERNAL_EVENT_REQUEST_ID, INTERNAL_HEADER_INITIAL_URL, INTERNAL_HEADER_RESOLVED_ROUTES, } from "../core/routingHandler";
7
7
  globalThis.internalFetch = fetch;
8
8
  globalThis.__openNextAls = new AsyncLocalStorage();
9
9
  const defaultHandler = async (internalEvent, options) => {
10
- const originResolver = await resolveOriginResolver(globalThis.openNextConfig.middleware?.originResolver);
11
- const externalRequestProxy = await resolveProxyRequest(globalThis.openNextConfig.middleware?.override?.proxyExternalRequest);
10
+ const config = globalThis.openNextConfig.middleware;
11
+ const originResolver = await resolveOriginResolver(config?.originResolver);
12
+ const externalRequestProxy = await resolveProxyRequest(config?.override?.proxyExternalRequest);
13
+ const assetResolver = await resolveAssetResolver(config?.assetResolver);
12
14
  //#override includeCacheInMiddleware
13
- globalThis.tagCache = await resolveTagCache(globalThis.openNextConfig.middleware?.override?.tagCache);
14
- globalThis.queue = await resolveQueue(globalThis.openNextConfig.middleware?.override?.queue);
15
- globalThis.incrementalCache = await resolveIncrementalCache(globalThis.openNextConfig.middleware?.override?.incrementalCache);
15
+ globalThis.tagCache = await resolveTagCache(config?.override?.tagCache);
16
+ globalThis.queue = await resolveQueue(config?.override?.queue);
17
+ globalThis.incrementalCache = await resolveIncrementalCache(config?.override?.incrementalCache);
16
18
  //#endOverride
19
+ const requestId = Math.random().toString(36);
17
20
  // We run everything in the async local storage context so that it is available in the external middleware
18
21
  return runWithOpenNextRequestContext({
19
22
  isISRRevalidation: internalEvent.headers["x-isr"] === "1",
20
23
  waitUntil: options?.waitUntil,
24
+ requestId,
21
25
  }, async () => {
22
- const result = await routingHandler(internalEvent);
26
+ const result = await routingHandler(internalEvent, { assetResolver });
23
27
  if ("internalEvent" in result) {
24
28
  debug("Middleware intercepted event", internalEvent);
25
29
  if (!result.isExternalRewrite) {
@@ -32,6 +36,7 @@ const defaultHandler = async (internalEvent, options) => {
32
36
  ...result.internalEvent.headers,
33
37
  [INTERNAL_HEADER_INITIAL_URL]: internalEvent.url,
34
38
  [INTERNAL_HEADER_RESOLVED_ROUTES]: JSON.stringify(result.resolvedRoutes),
39
+ [INTERNAL_EVENT_REQUEST_ID]: requestId,
35
40
  },
36
41
  },
37
42
  isExternalRewrite: result.isExternalRewrite,
@@ -50,6 +55,10 @@ const defaultHandler = async (internalEvent, options) => {
50
55
  type: "middleware",
51
56
  internalEvent: {
52
57
  ...result.internalEvent,
58
+ headers: {
59
+ ...result.internalEvent.headers,
60
+ [INTERNAL_EVENT_REQUEST_ID]: requestId,
61
+ },
53
62
  rawPath: "/500",
54
63
  url: constructNextUrl(result.internalEvent.url, "/500"),
55
64
  method: "GET",
@@ -63,6 +72,7 @@ const defaultHandler = async (internalEvent, options) => {
63
72
  };
64
73
  }
65
74
  }
75
+ result.headers[INTERNAL_EVENT_REQUEST_ID] = requestId;
66
76
  debug("Middleware response", result);
67
77
  return result;
68
78
  });
@@ -1,5 +1,5 @@
1
1
  import url from "node:url";
2
- import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, readlinkSync, statSync, symlinkSync, writeFileSync, } from "node:fs";
2
+ import { copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, readlinkSync, statSync, symlinkSync, writeFileSync, } from "node:fs";
3
3
  import path from "node:path";
4
4
  import { loadAppPathsManifest, loadBuildId, loadConfig, loadFunctionsConfigManifest, loadMiddlewareManifest, loadPagesManifest, loadPrerenderManifest, } from "../adapters/config/util.js";
5
5
  import { getCrossPlatformPathRegex } from "../utils/regex.js";
@@ -223,15 +223,15 @@ File ${serverPath} does not exist
223
223
  copyFileSync(path.join(standaloneNextDir, filePath), path.join(outputNextDir, filePath));
224
224
  }
225
225
  };
226
+ const manifests = getManifests(standaloneNextDir);
227
+ const { config, prerenderManifest, pagesManifest } = manifests;
226
228
  // Get all the static files - Should be only for pages dir
227
229
  // Ideally we would filter only those that might get accessed in this specific functions
228
230
  // Maybe even move this to s3 directly
229
231
  if (hasPageDir) {
230
232
  // First we get truly static files - i.e. pages without getStaticProps
231
- const staticFiles = Object.values(loadPagesManifest(standaloneNextDir));
233
+ const staticFiles = Object.values(pagesManifest);
232
234
  // Then we need to get all fallback: true dynamic routes html
233
- const prerenderManifest = loadPrerenderManifest(standaloneNextDir);
234
- const config = loadConfig(standaloneNextDir);
235
235
  const locales = config.i18n?.locales;
236
236
  Object.values(prerenderManifest.dynamicRoutes).forEach((route) => {
237
237
  if (typeof route.fallback === "string") {
@@ -249,10 +249,15 @@ File ${serverPath} does not exist
249
249
  .filter((file) => file.endsWith(".html"))
250
250
  .forEach((file) => copyStaticFile(`server/${file}`));
251
251
  }
252
+ // Copy .next/static/css from standalone to output dir
253
+ // needed for optimizeCss feature to work
254
+ if (config.experimental.optimizeCss) {
255
+ cpSync(path.join(standaloneNextDir, "static", "css"), path.join(outputNextDir, "static", "css"), { recursive: true });
256
+ }
252
257
  logger.debug("copyTracedFiles:", Date.now() - tsStart, "ms");
253
258
  return {
254
259
  tracedFiles,
255
260
  nodePackages,
256
- manifests: getManifests(standaloneNextDir),
261
+ manifests,
257
262
  };
258
263
  }
@@ -62,44 +62,46 @@ export function createCacheAssets(options) {
62
62
  const packagePath = buildHelper.getPackagePath(options);
63
63
  const buildId = buildHelper.getBuildId(options);
64
64
  let useTagCache = false;
65
- // Copy pages to cache folder
66
65
  const dotNextPath = path.join(appBuildOutputPath, ".next/standalone", packagePath);
67
- const outputPath = path.join(outputDir, "cache", buildId);
68
- [".next/server/pages", ".next/server/app"]
66
+ const outputCachePath = path.join(outputDir, "cache", buildId);
67
+ fs.mkdirSync(outputCachePath, { recursive: true });
68
+ const sourceDirs = [".next/server/pages", ".next/server/app"]
69
69
  .map((dir) => path.join(dotNextPath, dir))
70
- .filter(fs.existsSync)
71
- .forEach((dir) => fs.cpSync(dir, outputPath, { recursive: true }));
72
- // Remove non-cache files
70
+ .filter(fs.existsSync);
73
71
  const htmlPages = buildHelper.getHtmlPages(dotNextPath);
74
- buildHelper.removeFiles(outputPath, ({ relativePath }) => relativePath.endsWith(".js") ||
72
+ const isFileSkipped = (relativePath) => relativePath.endsWith(".js") ||
75
73
  relativePath.endsWith(".js.nft.json") ||
76
- (relativePath.endsWith(".html") && htmlPages.has(relativePath)));
74
+ (relativePath.endsWith(".html") && htmlPages.has(relativePath));
77
75
  // Merge cache files into a single file
78
76
  const cacheFilesPath = {};
79
- buildHelper.traverseFiles(outputPath, () => true, ({ absolutePath }) => {
80
- const ext = path.extname(absolutePath);
81
- switch (ext) {
82
- case ".meta":
83
- case ".html":
84
- case ".json":
85
- case ".body":
86
- case ".rsc": {
87
- const newFilePath = absolutePath
88
- .substring(0, absolutePath.length - ext.length)
89
- .replace(/\.prefetch$/, "")
90
- .concat(".cache");
91
- cacheFilesPath[newFilePath] = {
92
- [ext.slice(1)]: absolutePath,
93
- ...cacheFilesPath[newFilePath],
94
- };
95
- break;
77
+ // Process each source directory
78
+ sourceDirs.forEach((sourceDir) => {
79
+ buildHelper.traverseFiles(sourceDir, ({ relativePath }) => !isFileSkipped(relativePath), ({ absolutePath, relativePath }) => {
80
+ const ext = path.extname(absolutePath);
81
+ switch (ext) {
82
+ case ".meta":
83
+ case ".html":
84
+ case ".json":
85
+ case ".body":
86
+ case ".rsc": {
87
+ const newFilePath = path
88
+ .join(outputCachePath, relativePath)
89
+ .substring(0, path.join(outputCachePath, relativePath).length - ext.length)
90
+ .replace(/\.prefetch$/, "")
91
+ .concat(".cache");
92
+ cacheFilesPath[newFilePath] = {
93
+ [ext.slice(1)]: absolutePath,
94
+ ...cacheFilesPath[newFilePath],
95
+ };
96
+ break;
97
+ }
98
+ case ".map":
99
+ break;
100
+ default:
101
+ logger.warn(`Unknown file extension: ${ext}`);
102
+ break;
96
103
  }
97
- case ".map":
98
- break;
99
- default:
100
- logger.warn(`Unknown file extension: ${ext}`);
101
- break;
102
- }
104
+ });
103
105
  });
104
106
  // Generate cache file
105
107
  Object.entries(cacheFilesPath).forEach(([cacheFilePath, files]) => {
@@ -122,6 +124,8 @@ export function createCacheAssets(options) {
122
124
  : "utf8")
123
125
  : undefined,
124
126
  };
127
+ // Ensure directory exists before writing
128
+ fs.mkdirSync(path.dirname(cacheFilePath), { recursive: true });
125
129
  fs.writeFileSync(cacheFilePath, JSON.stringify(cacheFileContent));
126
130
  });
127
131
  // We need to traverse the cache to find every .meta file
@@ -149,36 +153,36 @@ export function createCacheAssets(options) {
149
153
  if (!options.config.dangerous?.disableTagCache) {
150
154
  // Compute dynamodb cache data
151
155
  // Traverse files inside cache to find all meta files and cache tags associated with them
152
- buildHelper.traverseFiles(outputPath, ({ absolutePath }) => absolutePath.endsWith(".meta"), ({ absolutePath, relativePath }) => {
153
- const fileContent = fs.readFileSync(absolutePath, "utf8");
154
- const fileData = JSON.parse(fileContent);
155
- if (fileData.headers?.["x-next-cache-tags"]) {
156
- fileData.headers["x-next-cache-tags"]
157
- .split(",")
158
- .forEach((tag) => {
159
- // TODO: We should split the tag using getDerivedTags from next.js or maybe use an in house implementation
160
- metaFiles.push({
161
- tag: { S: path.posix.join(buildId, tag.trim()) },
162
- path: {
163
- S: path.posix.join(buildId, relativePath.replace(".meta", "")),
164
- },
165
- // We don't care about the revalidation time here, we just need to make sure it's there
166
- revalidatedAt: { N: "1" },
156
+ sourceDirs.forEach((sourceDir) => {
157
+ buildHelper.traverseFiles(sourceDir, ({ absolutePath, relativePath }) => absolutePath.endsWith(".meta") && !isFileSkipped(relativePath), ({ absolutePath, relativePath }) => {
158
+ const fileContent = fs.readFileSync(absolutePath, "utf8");
159
+ const fileData = JSON.parse(fileContent);
160
+ if (fileData.headers?.["x-next-cache-tags"]) {
161
+ fileData.headers["x-next-cache-tags"]
162
+ .split(",")
163
+ .forEach((tag) => {
164
+ // TODO: We should split the tag using getDerivedTags from next.js or maybe use an in house implementation
165
+ metaFiles.push({
166
+ tag: { S: path.posix.join(buildId, tag.trim()) },
167
+ path: {
168
+ S: path.posix.join(buildId, relativePath.replace(".meta", "")),
169
+ },
170
+ // We don't care about the revalidation time here, we just need to make sure it's there
171
+ revalidatedAt: { N: "1" },
172
+ });
167
173
  });
168
- });
169
- }
174
+ }
175
+ });
170
176
  });
171
177
  if (metaFiles.length > 0) {
172
178
  useTagCache = true;
173
179
  const providerPath = path.join(outputDir, "dynamodb-provider");
174
- //Copy open-next.config.mjs into the bundle
180
+ // Copy open-next.config.mjs into the bundle
175
181
  fs.mkdirSync(providerPath, { recursive: true });
176
182
  buildHelper.copyOpenNextConfig(options.buildDir, providerPath);
177
183
  // TODO: check if metafiles doesn't contain duplicates
178
184
  fs.writeFileSync(path.join(providerPath, "dynamodb-cache.json"), JSON.stringify(metaFiles));
179
185
  }
180
186
  }
181
- // We need to remove files later because we need the metafiles for dynamodb tags cache
182
- buildHelper.removeFiles(outputPath, ({ relativePath }) => !relativePath.endsWith(".cache"));
183
187
  return { useTagCache, metaFiles };
184
188
  }
@@ -126,7 +126,6 @@ async function generateBundle(name, options, fnOptions, codeCustomization) {
126
126
  patches.patchEnvVars,
127
127
  patches.patchBackgroundRevalidation,
128
128
  patches.patchUseCacheForISR,
129
- patches.patchDropBabel,
130
129
  ...additionalCodePatches,
131
130
  ]);
132
131
  // Build Lambda code
@@ -3,4 +3,3 @@ export { patchNextServer } from "./patchNextServer.js";
3
3
  export { patchFetchCacheForISR, patchUnstableCacheForISR, patchUseCacheForISR, } from "./patchFetchCacheISR.js";
4
4
  export { patchFetchCacheSetMissingWaitUntil } from "./patchFetchCacheWaitUntil.js";
5
5
  export { patchBackgroundRevalidation } from "./patchBackgroundRevalidation.js";
6
- export { patchDropBabel } from "./dropBabel.js";
@@ -3,4 +3,3 @@ export { patchNextServer } from "./patchNextServer.js";
3
3
  export { patchFetchCacheForISR, patchUnstableCacheForISR, patchUseCacheForISR, } from "./patchFetchCacheISR.js";
4
4
  export { patchFetchCacheSetMissingWaitUntil } from "./patchFetchCacheWaitUntil.js";
5
5
  export { patchBackgroundRevalidation } from "./patchBackgroundRevalidation.js";
6
- export { patchDropBabel } from "./dropBabel.js";
@@ -1,5 +1,15 @@
1
1
  import type { CodePatcher } from "../codePatcher.js";
2
- export declare const minimalRule = "\nrule:\n kind: member_expression\n pattern: process.env.NEXT_MINIMAL\n any:\n - inside:\n kind: parenthesized_expression\n stopBy: end\n inside:\n kind: if_statement\n any:\n - inside:\n kind: statement_block\n inside:\n kind: method_definition\n any:\n - has: {kind: property_identifier, field: name, regex: runEdgeFunction}\n - has: {kind: property_identifier, field: name, regex: runMiddleware}\n - has: {kind: property_identifier, field: name, regex: imageOptimizer}\n - has:\n kind: statement_block\n has:\n kind: expression_statement\n pattern: res.statusCode = 400;\nfix:\n 'true'\n";
3
- 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";
4
- 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";
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
+ 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
+ /**
5
+ * Swaps the body for a throwing implementation
6
+ *
7
+ * @param methodName The name of the method
8
+ * @returns A rule to replace the body with a `throw`
9
+ */
10
+ export declare function createEmptyBodyRule(methodName: string): string;
11
+ /**
12
+ * Drops `require("./node-environment-extensions/error-inspect");`
13
+ */
14
+ export declare const errorInspectRule = "\nrule:\n pattern: require(\"./node-environment-extensions/error-inspect\");\nfix: |-\n // Removed by OpenNext\n // require(\"./node-environment-extensions/error-inspect\");\n";
5
15
  export declare const patchNextServer: CodePatcher;
@@ -1,51 +1,24 @@
1
+ import { getCrossPlatformPathRegex } from "../../../utils/regex.js";
1
2
  import { createPatchCode } from "../astCodePatcher.js";
2
- // This rule will replace the `NEXT_MINIMAL` env variable with true in multiple places to avoid executing unwanted path (i.e. next middleware, edge functions and image optimization)
3
- export const minimalRule = `
4
- rule:
5
- kind: member_expression
6
- pattern: process.env.NEXT_MINIMAL
7
- any:
8
- - inside:
9
- kind: parenthesized_expression
10
- stopBy: end
11
- inside:
12
- kind: if_statement
13
- any:
14
- - inside:
15
- kind: statement_block
16
- inside:
17
- kind: method_definition
18
- any:
19
- - has: {kind: property_identifier, field: name, regex: runEdgeFunction}
20
- - has: {kind: property_identifier, field: name, regex: runMiddleware}
21
- - has: {kind: property_identifier, field: name, regex: imageOptimizer}
22
- - has:
23
- kind: statement_block
24
- has:
25
- kind: expression_statement
26
- pattern: res.statusCode = 400;
27
- fix:
28
- 'true'
29
- `;
30
- // This rule will disable the background preloading of route done by NextServer by default during the creation of NextServer
3
+ // Disable the background preloading of route done by NextServer by default during the creation of NextServer
31
4
  export const disablePreloadingRule = `
32
5
  rule:
33
6
  kind: statement_block
34
7
  inside:
35
8
  kind: if_statement
36
- any:
37
- - has:
9
+ any:
10
+ - has:
38
11
  kind: member_expression
39
12
  pattern: this.nextConfig.experimental.preloadEntriesOnStart
40
13
  stopBy: end
41
- - has:
14
+ - has:
42
15
  kind: binary_expression
43
16
  pattern: appDocumentPreloading === true
44
17
  stopBy: end
45
18
  fix:
46
19
  '{}'
47
20
  `;
48
- // This rule is mostly for splitted edge functions so that we don't try to match them on the other non edge functions
21
+ // Mostly for splitted edge functions so that we don't try to match them on the other non edge functions
49
22
  export const removeMiddlewareManifestRule = `
50
23
  rule:
51
24
  kind: statement_block
@@ -53,39 +26,104 @@ rule:
53
26
  kind: method_definition
54
27
  has:
55
28
  kind: property_identifier
56
- regex: getMiddlewareManifest
29
+ regex: ^getMiddlewareManifest$
57
30
  fix:
58
31
  '{return null;}'
59
32
  `;
33
+ /**
34
+ * Swaps the body for a throwing implementation
35
+ *
36
+ * @param methodName The name of the method
37
+ * @returns A rule to replace the body with a `throw`
38
+ */
39
+ export function createEmptyBodyRule(methodName) {
40
+ return `
41
+ rule:
42
+ pattern:
43
+ selector: method_definition
44
+ context: "class { async ${methodName}($$$PARAMS) { $$$_ } }"
45
+ fix: |-
46
+ async ${methodName}($$$PARAMS) {
47
+ throw new Error("${methodName} should not be called with OpenNext");
48
+ }
49
+ `;
50
+ }
51
+ /**
52
+ * Drops `require("./node-environment-extensions/error-inspect");`
53
+ */
54
+ export const errorInspectRule = `
55
+ rule:
56
+ pattern: require("./node-environment-extensions/error-inspect");
57
+ fix: |-
58
+ // Removed by OpenNext
59
+ // require("./node-environment-extensions/error-inspect");
60
+ `;
61
+ const pathFilter = getCrossPlatformPathRegex(String.raw `/next/dist/server/next-server\.js$`, {
62
+ escape: false,
63
+ });
64
+ /**
65
+ * Patches to avoid pulling babel (~4MB).
66
+ *
67
+ * Details:
68
+ * - empty `NextServer#runMiddleware` and `NextServer#runEdgeFunction` that are not used
69
+ * - drop `next/dist/server/node-environment-extensions/error-inspect.js`
70
+ */
71
+ const babelPatches = [
72
+ // Empty the body of `NextServer#runMiddleware`
73
+ {
74
+ field: {
75
+ pathFilter,
76
+ contentFilter: /runMiddleware\(/,
77
+ patchCode: createPatchCode(createEmptyBodyRule("runMiddleware")),
78
+ },
79
+ },
80
+ // Empty the body of `NextServer#runEdgeFunction`
81
+ {
82
+ field: {
83
+ pathFilter,
84
+ contentFilter: /runEdgeFunction\(/,
85
+ patchCode: createPatchCode(createEmptyBodyRule("runEdgeFunction")),
86
+ },
87
+ },
88
+ // Drop `error-inspect` that pulls babel
89
+ {
90
+ field: {
91
+ pathFilter,
92
+ contentFilter: /error-inspect/,
93
+ patchCode: createPatchCode(errorInspectRule),
94
+ },
95
+ },
96
+ ];
60
97
  export const patchNextServer = {
61
98
  name: "patch-next-server",
62
99
  patches: [
63
- // Skip executing next middleware, edge functions and image optimization inside NextServer
100
+ // Empty the body of `NextServer#imageOptimizer` - unused in OpenNext
64
101
  {
65
- versions: ">=15.0.0",
66
102
  field: {
67
- pathFilter: /next-server\.(js)$/,
68
- contentFilter: /process\.env\.NEXT_MINIMAL/,
69
- patchCode: createPatchCode(minimalRule),
103
+ pathFilter,
104
+ contentFilter: /imageOptimizer\(/,
105
+ patchCode: createPatchCode(createEmptyBodyRule("imageOptimizer")),
70
106
  },
71
107
  },
72
- // Disable Next background preloading done at creation of `NetxServer`
108
+ // Disable Next background preloading done at creation of `NextServer`
73
109
  {
74
- versions: ">=15.0.0",
110
+ versions: ">=14.0.0",
75
111
  field: {
76
- pathFilter: /next-server\.(js)$/,
112
+ pathFilter,
77
113
  contentFilter: /this\.nextConfig\.experimental\.preloadEntriesOnStart/,
78
114
  patchCode: createPatchCode(disablePreloadingRule),
79
115
  },
80
116
  },
81
117
  // Don't match edge functions in `NextServer`
82
118
  {
83
- versions: ">=15.0.0",
119
+ // Next 12 and some version of 13 use the bundled middleware/edge function
120
+ versions: ">=14.0.0",
84
121
  field: {
85
- pathFilter: /next-server\.(js)$/,
122
+ pathFilter,
86
123
  contentFilter: /getMiddlewareManifest/,
87
124
  patchCode: createPatchCode(removeMiddlewareManifestRule),
88
125
  },
89
126
  },
127
+ ...babelPatches,
90
128
  ],
91
129
  };
@@ -1,7 +1,7 @@
1
1
  import { debug } from "../adapters/logger";
2
2
  import { generateUniqueId } from "../adapters/util";
3
3
  import { openNextHandler } from "./requestHandler";
4
- import { resolveCdnInvalidation, resolveConverter, resolveIncrementalCache, resolveProxyRequest, resolveQueue, resolveTagCache, resolveWrapper, } from "./resolve";
4
+ import { resolveAssetResolver, resolveCdnInvalidation, resolveConverter, resolveIncrementalCache, resolveProxyRequest, resolveQueue, resolveTagCache, resolveWrapper, } from "./resolve";
5
5
  export async function createMainHandler() {
6
6
  // @ts-expect-error `./open-next.config.mjs` exists only in the build output
7
7
  const config = await import("./open-next.config.mjs").then((m) => m.default);
@@ -16,6 +16,9 @@ export async function createMainHandler() {
16
16
  globalThis.queue = await resolveQueue(thisFunction.override?.queue);
17
17
  globalThis.incrementalCache = await resolveIncrementalCache(thisFunction.override?.incrementalCache);
18
18
  globalThis.tagCache = await resolveTagCache(thisFunction.override?.tagCache);
19
+ if (config.middleware?.external !== true) {
20
+ globalThis.assetResolver = await resolveAssetResolver(globalThis.openNextConfig.middleware?.assetResolver);
21
+ }
19
22
  globalThis.proxyExternalRequest = await resolveProxyRequest(thisFunction.override?.proxyExternalRequest);
20
23
  globalThis.cdnInvalidationHandler = await resolveCdnInvalidation(thisFunction.override?.cdnInvalidation);
21
24
  // From the config, we create the converter
@@ -5,7 +5,7 @@ import { NextConfig } from "../adapters/config/index";
5
5
  import { debug, error } from "../adapters/logger";
6
6
  import { patchAsyncStorage } from "./patchAsyncStorage";
7
7
  import { constructNextUrl, convertRes, convertToQuery, createServerResponse, } from "./routing/util";
8
- import routingHandler, { INTERNAL_HEADER_INITIAL_URL, INTERNAL_HEADER_RESOLVED_ROUTES, MIDDLEWARE_HEADER_PREFIX, MIDDLEWARE_HEADER_PREFIX_LEN, } from "./routingHandler";
8
+ import routingHandler, { INTERNAL_EVENT_REQUEST_ID, INTERNAL_HEADER_INITIAL_URL, INTERNAL_HEADER_RESOLVED_ROUTES, MIDDLEWARE_HEADER_PREFIX, MIDDLEWARE_HEADER_PREFIX_LEN, } from "./routingHandler";
9
9
  import { requestHandler, setNextjsPrebundledReact } from "./util";
10
10
  // This is used to identify requests in the cache
11
11
  globalThis.__openNextAls = new AsyncLocalStorage();
@@ -14,10 +14,17 @@ patchAsyncStorage();
14
14
  //#endOverride
15
15
  export async function openNextHandler(internalEvent, options) {
16
16
  const initialHeaders = internalEvent.headers;
17
+ // We only use the requestId header if we are using an external middleware
18
+ // This is to ensure that no one can spoof the requestId
19
+ // When using an external middleware, we always assume that headers cannot be spoofed
20
+ const requestId = globalThis.openNextConfig.middleware?.external
21
+ ? internalEvent.headers[INTERNAL_EVENT_REQUEST_ID]
22
+ : Math.random().toString(36);
17
23
  // We run everything in the async local storage context so that it is available in the middleware as well as in NextServer
18
24
  return runWithOpenNextRequestContext({
19
25
  isISRRevalidation: initialHeaders["x-isr"] === "1",
20
26
  waitUntil: options?.waitUntil,
27
+ requestId,
21
28
  }, async () => {
22
29
  await globalThis.__next_route_preloader("waitUntil");
23
30
  if (initialHeaders["x-forwarded-host"]) {
@@ -40,7 +47,9 @@ export async function openNextHandler(internalEvent, options) {
40
47
  ...internalHeaders,
41
48
  };
42
49
  //#override withRouting
43
- routingResult = await routingHandler(internalEvent);
50
+ routingResult = await routingHandler(internalEvent, {
51
+ assetResolver: globalThis.assetResolver,
52
+ });
44
53
  //#endOverride
45
54
  const headers = "type" in routingResult
46
55
  ? routingResult.headers