@opennextjs/aws 3.5.6 → 3.5.8

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.
@@ -233,8 +233,11 @@ export default class Cache {
233
233
  if (config?.disableTagCache || config?.disableIncrementalCache) {
234
234
  return;
235
235
  }
236
+ const _tags = Array.isArray(tags) ? tags : [tags];
237
+ if (_tags.length === 0) {
238
+ return;
239
+ }
236
240
  try {
237
- const _tags = Array.isArray(tags) ? tags : [tags];
238
241
  if (globalThis.tagCache.mode === "nextMode") {
239
242
  const paths = (await globalThis.tagCache.getPathsByTags?.(_tags)) ?? [];
240
243
  await globalThis.tagCache.writeTags(_tags);
@@ -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");
@@ -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
  }
@@ -1,6 +1,16 @@
1
1
  import type { TagCacheMetaFile } from "../types/cache.js";
2
2
  import * as buildHelper from "./helper.js";
3
- export declare function createStaticAssets(options: buildHelper.BuildOptions): void;
3
+ /**
4
+ * Copy the static assets to the output folder
5
+ *
6
+ * WARNING: `useBasePath` should be set to `false` when the output file is used.
7
+ *
8
+ * @param options OpenNext build options
9
+ * @param useBasePath whether to copy files into the to Next.js configured basePath
10
+ */
11
+ export declare function createStaticAssets(options: buildHelper.BuildOptions, { useBasePath }?: {
12
+ useBasePath?: boolean | undefined;
13
+ }): void;
4
14
  /**
5
15
  * Create the cache assets.
6
16
  *
@@ -1,16 +1,31 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { loadConfig } from "../adapters/config/util.js";
3
4
  import logger from "../logger.js";
4
5
  import { isBinaryContentType } from "../utils/binary.js";
5
6
  import * as buildHelper from "./helper.js";
6
- export function createStaticAssets(options) {
7
+ /**
8
+ * Copy the static assets to the output folder
9
+ *
10
+ * WARNING: `useBasePath` should be set to `false` when the output file is used.
11
+ *
12
+ * @param options OpenNext build options
13
+ * @param useBasePath whether to copy files into the to Next.js configured basePath
14
+ */
15
+ export function createStaticAssets(options, { useBasePath = false } = {}) {
7
16
  logger.info("Bundling static assets...");
8
17
  const { appBuildOutputPath, appPublicPath, outputDir, appPath } = options;
18
+ const NextConfig = loadConfig(path.join(appBuildOutputPath, ".next"));
19
+ const basePath = useBasePath ? (NextConfig.basePath ?? "") : "";
9
20
  // Create output folder
10
- const outputPath = path.join(outputDir, "assets");
21
+ const outputPath = path.join(outputDir, "assets", basePath);
11
22
  fs.mkdirSync(outputPath, { recursive: true });
12
23
  /**
13
- * Next.js outputs assets into multiple files. Copy into the same directory.
24
+ * Next.js outputs assets into multiple files.
25
+ *
26
+ * Copy into the same directory:
27
+ * - `.open-next/assets` when `useBasePath` is `false`
28
+ * - `.open-next/assets/basePath` when `useBasePath` is `true`
14
29
  *
15
30
  * Copy over:
16
31
  * - .next/BUILD_ID => BUILD_ID
@@ -14,7 +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";
17
+ import { patchBackgroundRevalidation, patchEnvVars, patchFetchCacheForISR, patchFetchCacheSetMissingWaitUntil, patchNextServer, patchUnstableCacheForISR, } from "./patch/patches/index.js";
18
18
  export async function createServerBundle(options, codeCustomization) {
19
19
  const { config } = options;
20
20
  const foundRoutes = new Set();
@@ -121,6 +121,7 @@ async function generateBundle(name, options, fnOptions, codeCustomization) {
121
121
  patchUnstableCacheForISR,
122
122
  patchNextServer,
123
123
  patchEnvVars,
124
+ patchBackgroundRevalidation,
124
125
  ...additionalCodePatches,
125
126
  ]);
126
127
  // Build Lambda code
@@ -146,6 +147,7 @@ async function generateBundle(name, options, fnOptions, codeCustomization) {
146
147
  ...(disableNextPrebundledReact ? ["applyNextjsPrebundledReact"] : []),
147
148
  ...(disableRouting ? ["withRouting"] : []),
148
149
  ...(isAfter142 ? ["patchAsyncStorage"] : []),
150
+ ...(isAfter141 ? ["appendPrefetch"] : []),
149
151
  ],
150
152
  }),
151
153
  openNextReplacementPlugin({
@@ -2,3 +2,4 @@ export { envVarRuleCreator, patchEnvVars } from "./patchEnvVar.js";
2
2
  export { patchNextServer } from "./patchNextServer.js";
3
3
  export { patchFetchCacheForISR, patchUnstableCacheForISR, } from "./patchFetchCacheISR.js";
4
4
  export { patchFetchCacheSetMissingWaitUntil } from "./patchFetchCacheWaitUntil.js";
5
+ export { patchBackgroundRevalidation } from "./patchBackgroundRevalidation.js";
@@ -2,3 +2,4 @@ export { envVarRuleCreator, patchEnvVars } from "./patchEnvVar.js";
2
2
  export { patchNextServer } from "./patchNextServer.js";
3
3
  export { patchFetchCacheForISR, patchUnstableCacheForISR, } from "./patchFetchCacheISR.js";
4
4
  export { patchFetchCacheSetMissingWaitUntil } from "./patchFetchCacheWaitUntil.js";
5
+ export { patchBackgroundRevalidation } from "./patchBackgroundRevalidation.js";
@@ -0,0 +1,11 @@
1
+ export declare const rule = "\nrule:\n kind: binary_expression\n all:\n - has:\n kind: unary_expression\n regex: \"!cachedResponse.isStale\"\n - has:\n kind: member_expression\n regex: \"context.isPrefetch\"\nfix:\n 'true'";
2
+ export declare const patchBackgroundRevalidation: {
3
+ name: string;
4
+ patches: {
5
+ versions: ">=14.1.0";
6
+ field: {
7
+ pathFilter: RegExp;
8
+ patchCode: import("../codePatcher.js").PatchCodeFn;
9
+ };
10
+ }[];
11
+ };
@@ -0,0 +1,27 @@
1
+ import { getCrossPlatformPathRegex } from "../../../utils/regex.js";
2
+ import { createPatchCode } from "../astCodePatcher.js";
3
+ export const rule = `
4
+ rule:
5
+ kind: binary_expression
6
+ all:
7
+ - has:
8
+ kind: unary_expression
9
+ regex: "!cachedResponse.isStale"
10
+ - has:
11
+ kind: member_expression
12
+ regex: "context.isPrefetch"
13
+ fix:
14
+ 'true'`;
15
+ export const patchBackgroundRevalidation = {
16
+ name: "patchBackgroundRevalidation",
17
+ patches: [
18
+ {
19
+ // TODO: test for earlier versions of Next
20
+ versions: ">=14.1.0",
21
+ field: {
22
+ pathFilter: getCrossPlatformPathRegex("server/response-cache/index.js"),
23
+ patchCode: createPatchCode(rule),
24
+ },
25
+ },
26
+ ],
27
+ };
@@ -115,7 +115,12 @@ export async function openNextHandler(internalEvent, options) {
115
115
  // 1. We could just let the revalidation go as normal, but due to race conditions the revalidation will be unreliable
116
116
  // 2. We could alter the lastModified time of our cache to make next believe that the cache is fresh, but this could cause issues with stale data since the cdn will cache the stale data as if it was fresh
117
117
  // 3. OUR CHOICE: We could pass a purpose prefetch header to the serverless function to make next believe that the request is a prefetch request and not trigger revalidation (This could potentially break in the future if next changes the behavior of prefetch requests)
118
- headers: { ...headers, purpose: "prefetch" },
118
+ headers: {
119
+ ...headers,
120
+ //#override appendPrefetch
121
+ purpose: "prefetch",
122
+ //#endOverride
123
+ },
119
124
  body: preprocessedEvent.body,
120
125
  remoteAddress: preprocessedEvent.remoteAddress,
121
126
  };
@@ -71,13 +71,15 @@ export async function handleMiddleware(internalEvent, initialSearch, middlewareL
71
71
  const responseHeaders = result.headers;
72
72
  const reqHeaders = {};
73
73
  const resHeaders = {};
74
- responseHeaders.delete("x-middleware-override-headers");
75
- /* Next will set the header `x-middleware-set-cookie` when you `set-cookie` in the middleware.
76
- * We can delete it here since it will be set in `set-cookie` aswell. Next removes this header in the response themselves.
77
- * `x-middleware-next` is set when you invoke `NextResponse.next()`. We can delete it here aswell.
78
- */
79
- responseHeaders.delete("x-middleware-set-cookie");
80
- responseHeaders.delete("x-middleware-next");
74
+ // These are internal headers used by Next.js, we don't want to expose them to the client
75
+ const filteredHeaders = [
76
+ "x-middleware-override-headers",
77
+ "x-middleware-set-cookie",
78
+ "x-middleware-next",
79
+ "x-middleware-rewrite",
80
+ // We need to drop `content-encoding` because it will be decoded
81
+ "content-encoding",
82
+ ];
81
83
  const xMiddlewareKey = "x-middleware-request-";
82
84
  responseHeaders.forEach((value, key) => {
83
85
  if (key.startsWith(xMiddlewareKey)) {
@@ -85,6 +87,8 @@ export async function handleMiddleware(internalEvent, initialSearch, middlewareL
85
87
  reqHeaders[k] = value;
86
88
  }
87
89
  else {
90
+ if (filteredHeaders.includes(key.toLowerCase()))
91
+ return;
88
92
  if (key.toLowerCase() === "set-cookie") {
89
93
  resHeaders[key] = resHeaders[key]
90
94
  ? [...resHeaders[key], value]
@@ -95,20 +99,6 @@ export async function handleMiddleware(internalEvent, initialSearch, middlewareL
95
99
  }
96
100
  }
97
101
  });
98
- // If the middleware returned a Redirect, we set the `Location` header with
99
- // the redirected url and end the response.
100
- if (statusCode >= 300 && statusCode < 400) {
101
- resHeaders.location =
102
- responseHeaders.get("location") ?? resHeaders.location;
103
- // res.setHeader("Location", location);
104
- return {
105
- body: emptyReadableStream(),
106
- type: internalEvent.type,
107
- statusCode: statusCode,
108
- headers: resHeaders,
109
- isBase64Encoded: false,
110
- };
111
- }
112
102
  // If the middleware returned a Rewrite, set the `url` to the pathname of the rewrite
113
103
  // NOTE: the header was added to `req` from above
114
104
  const rewriteUrl = responseHeaders.get("x-middleware-rewrite");
@@ -134,11 +124,11 @@ export async function handleMiddleware(internalEvent, initialSearch, middlewareL
134
124
  });
135
125
  }
136
126
  }
137
- // If the middleware returned a `NextResponse`, pipe the body to res. This will return
138
- // the body immediately to the client.
139
- if (result.body) {
127
+ // If the middleware wants to directly return a response (i.e. not using `NextResponse.next()` or `NextResponse.rewrite()`)
128
+ // we return the response directly
129
+ if (!rewriteUrl && !responseHeaders.get("x-middleware-next")) {
140
130
  // transfer response body to res
141
- const body = result.body;
131
+ const body = result.body ?? emptyReadableStream();
142
132
  return {
143
133
  type: internalEvent.type,
144
134
  statusCode: statusCode,
@@ -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;
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "access": "public"
4
4
  },
5
5
  "name": "@opennextjs/aws",
6
- "version": "3.5.6",
6
+ "version": "3.5.8",
7
7
  "bin": {
8
8
  "open-next": "./dist/index.js"
9
9
  },