@opennextjs/aws 3.5.1 → 3.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/adapters/config/util.d.ts +1 -0
  2. package/dist/adapters/config/util.js +5 -2
  3. package/dist/adapters/logger.js +1 -1
  4. package/dist/build/compileCache.js +1 -1
  5. package/dist/build/constant.d.ts +1 -0
  6. package/dist/build/constant.js +1 -0
  7. package/dist/build/copyTracedFiles.d.ts +21 -1
  8. package/dist/build/copyTracedFiles.js +39 -7
  9. package/dist/build/createImageOptimizationBundle.js +1 -1
  10. package/dist/build/createServerBundle.d.ts +9 -1
  11. package/dist/build/createServerBundle.js +28 -8
  12. package/dist/build/edge/createEdgeBundle.d.ts +5 -2
  13. package/dist/build/edge/createEdgeBundle.js +11 -2
  14. package/dist/build/helper.d.ts +3 -2
  15. package/dist/build/helper.js +44 -19
  16. package/dist/build/patch/astCodePatcher.d.ts +58 -0
  17. package/dist/build/patch/astCodePatcher.js +76 -0
  18. package/dist/build/patch/codePatcher.d.ts +46 -0
  19. package/dist/build/patch/codePatcher.js +119 -0
  20. package/dist/build/patch/patchFetchCacheISR.d.ts +5 -0
  21. package/dist/build/patch/patchFetchCacheISR.js +102 -0
  22. package/dist/build/patch/patchFetchCacheWaitUntil.d.ts +3 -0
  23. package/dist/build/patch/patchFetchCacheWaitUntil.js +36 -0
  24. package/dist/core/requestHandler.js +3 -1
  25. package/dist/core/routing/cacheInterceptor.js +6 -1
  26. package/dist/core/routing/matcher.d.ts +1 -1
  27. package/dist/core/routing/matcher.js +9 -1
  28. package/dist/core/routing/middleware.js +6 -0
  29. package/dist/core/routing/queue.d.ts +1 -0
  30. package/dist/core/routing/queue.js +12 -8
  31. package/dist/core/routing/util.js +3 -3
  32. package/dist/logger.d.ts +8 -0
  33. package/dist/logger.js +2 -0
  34. package/dist/overrides/wrappers/express-dev.js +2 -0
  35. package/dist/plugins/content-updater.d.ts +48 -0
  36. package/dist/plugins/content-updater.js +65 -0
  37. package/dist/types/overrides.d.ts +2 -0
  38. package/package.json +4 -2
@@ -2,6 +2,7 @@ import type { FunctionsConfigManifest, MiddlewareManifest, NextConfig, Prerender
2
2
  import type { PublicFiles } from "../../build";
3
3
  export declare function loadConfig(nextDir: string): NextConfig;
4
4
  export declare function loadBuildId(nextDir: string): string;
5
+ export declare function loadPagesManifest(nextDir: string): any;
5
6
  export declare function loadHtmlPages(nextDir: string): string[];
6
7
  export declare function loadPublicAssets(openNextDir: string): PublicFiles;
7
8
  export declare function loadRoutesManifest(nextDir: string): {
@@ -10,10 +10,13 @@ export function loadBuildId(nextDir) {
10
10
  const filePath = path.join(nextDir, "BUILD_ID");
11
11
  return fs.readFileSync(filePath, "utf-8").trim();
12
12
  }
13
- export function loadHtmlPages(nextDir) {
13
+ export function loadPagesManifest(nextDir) {
14
14
  const filePath = path.join(nextDir, "server/pages-manifest.json");
15
15
  const json = fs.readFileSync(filePath, "utf-8");
16
- return Object.entries(JSON.parse(json))
16
+ return JSON.parse(json);
17
+ }
18
+ export function loadHtmlPages(nextDir) {
19
+ return Object.entries(loadPagesManifest(nextDir))
17
20
  .filter(([_, value]) => value.endsWith(".html"))
18
21
  .map(([key]) => key);
19
22
  }
@@ -1,4 +1,4 @@
1
- import { isOpenNextError } from "../utils/error";
1
+ import { isOpenNextError } from "../utils/error.js";
2
2
  export function debug(...args) {
3
3
  if (globalThis.openNextDebug) {
4
4
  console.log(...args);
@@ -11,7 +11,7 @@ export function compileCache(options, format = "cjs") {
11
11
  const { config } = options;
12
12
  const ext = format === "cjs" ? "cjs" : "mjs";
13
13
  const outFile = path.join(options.buildDir, `cache.${ext}`);
14
- const isAfter15 = buildHelper.compareSemver(options.nextVersion, "15.0.0") >= 0;
14
+ const isAfter15 = buildHelper.compareSemver(options.nextVersion, ">=", "15.0.0");
15
15
  buildHelper.esbuildSync({
16
16
  external: ["next", "styled-jsx", "react", "@aws-sdk/*"],
17
17
  entryPoints: [path.join(options.openNextDistDir, "adapters", "cache.js")],
@@ -1 +1,2 @@
1
1
  export declare const MIDDLEWARE_TRACE_FILE = "server/middleware.js.nft.json";
2
+ export declare const INSTRUMENTATION_TRACE_FILE = "server/instrumentation.js.nft.json";
@@ -1,2 +1,3 @@
1
1
  //TODO: Move all other manifest path here as well
2
2
  export const MIDDLEWARE_TRACE_FILE = "server/middleware.js.nft.json";
3
+ export const INSTRUMENTATION_TRACE_FILE = "server/instrumentation.js.nft.json";
@@ -6,5 +6,25 @@ interface CopyTracedFilesOptions {
6
6
  bundledNextServer: boolean;
7
7
  skipServerFiles?: boolean;
8
8
  }
9
- export declare function copyTracedFiles({ buildOutputPath, packagePath, outputDir, routes, bundledNextServer, skipServerFiles, }: CopyTracedFilesOptions): Promise<void>;
9
+ export declare function getManifests(nextDir: string): {
10
+ buildId: string;
11
+ config: import("../types/next-types.js").NextConfig;
12
+ prerenderManifest: import("../types/next-types.js").PrerenderManifest;
13
+ pagesManifest: any;
14
+ appPathsManifest: Record<string, string>;
15
+ middlewareManifest: import("../types/next-types.js").MiddlewareManifest;
16
+ functionsConfigManifest: import("../types/next-types.js").FunctionsConfigManifest;
17
+ };
18
+ export declare function copyTracedFiles({ buildOutputPath, packagePath, outputDir, routes, bundledNextServer, skipServerFiles, }: CopyTracedFilesOptions): Promise<{
19
+ tracedFiles: string[];
20
+ manifests: {
21
+ buildId: string;
22
+ config: import("../types/next-types.js").NextConfig;
23
+ prerenderManifest: import("../types/next-types.js").PrerenderManifest;
24
+ pagesManifest: any;
25
+ appPathsManifest: Record<string, string>;
26
+ middlewareManifest: import("../types/next-types.js").MiddlewareManifest;
27
+ functionsConfigManifest: import("../types/next-types.js").FunctionsConfigManifest;
28
+ };
29
+ }>;
10
30
  export {};
@@ -1,15 +1,38 @@
1
1
  import url from "node:url";
2
2
  import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, readlinkSync, statSync, symlinkSync, writeFileSync, } from "node:fs";
3
3
  import path from "node:path";
4
- import { loadConfig, loadPrerenderManifest } from "../adapters/config/util.js";
4
+ import { loadAppPathsManifest, loadBuildId, loadConfig, loadFunctionsConfigManifest, loadMiddlewareManifest, loadPagesManifest, loadPrerenderManifest, } from "../adapters/config/util.js";
5
+ import { getCrossPlatformPathRegex } from "../utils/regex.js";
5
6
  import logger from "../logger.js";
6
- import { MIDDLEWARE_TRACE_FILE } from "./constant.js";
7
+ import { INSTRUMENTATION_TRACE_FILE, MIDDLEWARE_TRACE_FILE, } from "./constant.js";
7
8
  const __dirname = url.fileURLToPath(new URL(".", import.meta.url));
9
+ //TODO: we need to figure which packages we could safely remove
10
+ const EXCLUDED_PACKAGES = [
11
+ "caniuse-lite",
12
+ "sharp",
13
+ // This seems to be only in Next 15
14
+ // Some of sharp deps are under the @img scope
15
+ "@img",
16
+ ];
17
+ function isExcluded(srcPath) {
18
+ return EXCLUDED_PACKAGES.some((excluded) => srcPath.match(getCrossPlatformPathRegex(`/node_modules/${excluded}/`)));
19
+ }
8
20
  function copyPatchFile(outputDir) {
9
21
  const patchFile = path.join(__dirname, "patch", "patchedAsyncStorage.js");
10
22
  const outputPatchFile = path.join(outputDir, "patchedAsyncStorage.cjs");
11
23
  copyFileSync(patchFile, outputPatchFile);
12
24
  }
25
+ export function getManifests(nextDir) {
26
+ return {
27
+ buildId: loadBuildId(nextDir),
28
+ config: loadConfig(nextDir),
29
+ prerenderManifest: loadPrerenderManifest(nextDir),
30
+ pagesManifest: loadPagesManifest(nextDir),
31
+ appPathsManifest: loadAppPathsManifest(nextDir),
32
+ middlewareManifest: loadMiddlewareManifest(nextDir),
33
+ functionsConfigManifest: loadFunctionsConfigManifest(nextDir),
34
+ };
35
+ }
13
36
  // eslint-disable-next-line sonarjs/cognitive-complexity
14
37
  export async function copyTracedFiles({ buildOutputPath, packagePath, outputDir, routes, bundledNextServer, skipServerFiles, }) {
15
38
  const tsStart = Date.now();
@@ -88,10 +111,18 @@ File ${fullFilePath} does not exist
88
111
  }
89
112
  }
90
113
  };
114
+ // Check for instrumentation trace file
115
+ if (existsSync(path.join(dotNextDir, INSTRUMENTATION_TRACE_FILE))) {
116
+ // We still need to copy the nft.json file so that computeCopyFilesForPage doesn't throw
117
+ copyFileSync(path.join(dotNextDir, INSTRUMENTATION_TRACE_FILE), path.join(standaloneNextDir, INSTRUMENTATION_TRACE_FILE));
118
+ computeCopyFilesForPage("instrumentation");
119
+ logger.debug("Adding instrumentation trace files");
120
+ }
91
121
  if (existsSync(path.join(dotNextDir, MIDDLEWARE_TRACE_FILE))) {
92
122
  // We still need to copy the nft.json file so that computeCopyFilesForPage doesn't throw
93
123
  copyFileSync(path.join(dotNextDir, MIDDLEWARE_TRACE_FILE), path.join(standaloneNextDir, MIDDLEWARE_TRACE_FILE));
94
124
  computeCopyFilesForPage("middleware");
125
+ logger.debug("Adding node middleware trace files");
95
126
  }
96
127
  const hasPageDir = routes.some((route) => route.startsWith("pages/"));
97
128
  const hasAppDir = routes.some((route) => route.startsWith("app/"));
@@ -119,11 +150,8 @@ File ${fullFilePath} does not exist
119
150
  });
120
151
  //Actually copy the files
121
152
  filesToCopy.forEach((to, from) => {
122
- if (
123
- //TODO: we need to figure which packages we could safely remove
124
- from.includes(path.join("node_modules", "caniuse-lite")) ||
125
- // from.includes("jest-worker") || This ones seems necessary for next 12
126
- from.includes(path.join("node_modules", "sharp"))) {
153
+ // We don't want to copy excluded packages (i.e sharp)
154
+ if (isExcluded(from)) {
127
155
  return;
128
156
  }
129
157
  mkdirSync(path.dirname(to), { recursive: true });
@@ -204,4 +232,8 @@ File ${fullFilePath} does not exist
204
232
  .forEach((file) => copyStaticFile(`server/${file}`));
205
233
  }
206
234
  logger.debug("copyTracedFiles:", Date.now() - tsStart, "ms");
235
+ return {
236
+ tracedFiles: Array.from(filesToCopy.values()),
237
+ manifests: getManifests(standaloneNextDir),
238
+ };
207
239
  }
@@ -26,7 +26,7 @@ export async function createImageOptimizationBundle(options) {
26
26
  },
27
27
  }),
28
28
  ];
29
- if (buildHelper.compareSemver(options.nextVersion, "14.1.1") >= 0) {
29
+ if (buildHelper.compareSemver(options.nextVersion, ">=", "14.1.1")) {
30
30
  plugins.push(openNextReplacementPlugin({
31
31
  name: "opennext-14.1.1-image-optimization",
32
32
  target: getCrossPlatformPathRegex("plugins/image-optimization/image-optimization.js"),
@@ -1,2 +1,10 @@
1
+ import type { Plugin } from "esbuild";
2
+ import { ContentUpdater } from "../plugins/content-updater.js";
1
3
  import * as buildHelper from "./helper.js";
2
- export declare function createServerBundle(options: buildHelper.BuildOptions): Promise<void>;
4
+ import { type CodePatcher } from "./patch/codePatcher.js";
5
+ interface CodeCustomization {
6
+ additionalCodePatches?: CodePatcher[];
7
+ additionalPlugins?: (contentUpdater: ContentUpdater) => Plugin[];
8
+ }
9
+ export declare function createServerBundle(options: buildHelper.BuildOptions, codeCustomization?: CodeCustomization): Promise<void>;
10
+ export {};
@@ -2,6 +2,7 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import logger from "../logger.js";
4
4
  import { minifyAll } from "../minimize-js.js";
5
+ import { ContentUpdater } from "../plugins/content-updater.js";
5
6
  import { openNextReplacementPlugin } from "../plugins/replacement.js";
6
7
  import { openNextResolvePlugin } from "../plugins/resolve.js";
7
8
  import { getCrossPlatformPathRegex } from "../utils/regex.js";
@@ -11,7 +12,10 @@ import { copyTracedFiles } from "./copyTracedFiles.js";
11
12
  import { generateEdgeBundle } from "./edge/createEdgeBundle.js";
12
13
  import * as buildHelper from "./helper.js";
13
14
  import { installDependencies } from "./installDeps.js";
14
- export async function createServerBundle(options) {
15
+ import { applyCodePatches } from "./patch/codePatcher.js";
16
+ import { patchFetchCacheForISR, patchUnstableCacheForISR, } from "./patch/patchFetchCacheISR.js";
17
+ import { patchFetchCacheSetMissingWaitUntil } from "./patch/patchFetchCacheWaitUntil.js";
18
+ export async function createServerBundle(options, codeCustomization) {
15
19
  const { config } = options;
16
20
  const foundRoutes = new Set();
17
21
  // Get all functions to build
@@ -29,7 +33,7 @@ export async function createServerBundle(options) {
29
33
  await generateEdgeBundle(name, options, fnOptions);
30
34
  }
31
35
  else {
32
- await generateBundle(name, options, fnOptions);
36
+ await generateBundle(name, options, fnOptions, codeCustomization);
33
37
  }
34
38
  });
35
39
  //TODO: throw an error if not all edge runtime routes has been bundled in a separate function
@@ -67,7 +71,7 @@ export async function createServerBundle(options) {
67
71
  patterns: ["*"],
68
72
  });
69
73
  }
70
- async function generateBundle(name, options, fnOptions) {
74
+ async function generateBundle(name, options, fnOptions, codeCustomization) {
71
75
  const { appPath, appBuildOutputPath, config, outputDir, monorepoRoot } = options;
72
76
  logger.info(`Building server function: ${name}...`);
73
77
  // Create output folder
@@ -100,23 +104,35 @@ async function generateBundle(name, options, fnOptions) {
100
104
  // Copy env files
101
105
  buildHelper.copyEnvFile(appBuildOutputPath, packagePath, outputPath);
102
106
  // Copy all necessary traced files
103
- await copyTracedFiles({
107
+ const { tracedFiles, manifests } = await copyTracedFiles({
104
108
  buildOutputPath: appBuildOutputPath,
105
109
  packagePath,
106
110
  outputDir: outputPath,
107
111
  routes: fnOptions.routes ?? ["app/page.tsx"],
108
112
  bundledNextServer: isBundled,
109
113
  });
114
+ const additionalCodePatches = codeCustomization?.additionalCodePatches ?? [];
115
+ await applyCodePatches(options, tracedFiles, manifests, [
116
+ patchFetchCacheSetMissingWaitUntil,
117
+ patchFetchCacheForISR,
118
+ patchUnstableCacheForISR,
119
+ ...additionalCodePatches,
120
+ ]);
110
121
  // Build Lambda code
111
122
  // note: bundle in OpenNext package b/c the adapter relies on the
112
123
  // "serverless-http" package which is not a dependency in user's
113
124
  // Next.js app.
114
- const disableNextPrebundledReact = buildHelper.compareSemver(options.nextVersion, "13.5.1") >= 0 ||
115
- buildHelper.compareSemver(options.nextVersion, "13.4.1") <= 0;
125
+ const disableNextPrebundledReact = buildHelper.compareSemver(options.nextVersion, ">=", "13.5.1") ||
126
+ buildHelper.compareSemver(options.nextVersion, "<=", "13.4.1");
116
127
  const overrides = fnOptions.override ?? {};
117
- const isBefore13413 = buildHelper.compareSemver(options.nextVersion, "13.4.13") <= 0;
118
- const isAfter141 = buildHelper.compareSemver(options.nextVersion, "14.1") >= 0;
128
+ const isBefore13413 = buildHelper.compareSemver(options.nextVersion, "<=", "13.4.13");
129
+ const isAfter141 = buildHelper.compareSemver(options.nextVersion, ">=", "14.1");
130
+ const isAfter142 = buildHelper.compareSemver(options.nextVersion, ">=", "14.2");
119
131
  const disableRouting = isBefore13413 || config.middleware?.external;
132
+ const updater = new ContentUpdater(options);
133
+ const additionalPlugins = codeCustomization?.additionalPlugins
134
+ ? codeCustomization.additionalPlugins(updater)
135
+ : [];
120
136
  const plugins = [
121
137
  openNextReplacementPlugin({
122
138
  name: `requestHandlerOverride ${name}`,
@@ -124,6 +140,7 @@ async function generateBundle(name, options, fnOptions) {
124
140
  deletes: [
125
141
  ...(disableNextPrebundledReact ? ["applyNextjsPrebundledReact"] : []),
126
142
  ...(disableRouting ? ["withRouting"] : []),
143
+ ...(isAfter142 ? ["patchAsyncStorage"] : []),
127
144
  ],
128
145
  }),
129
146
  openNextReplacementPlugin({
@@ -141,6 +158,9 @@ async function generateBundle(name, options, fnOptions) {
141
158
  fnName: name,
142
159
  overrides,
143
160
  }),
161
+ ...additionalPlugins,
162
+ // The content updater plugin must be the last plugin
163
+ updater.plugin,
144
164
  ];
145
165
  const outfileExt = fnOptions.runtime === "deno" ? "ts" : "mjs";
146
166
  await buildHelper.esbuildAsync({
@@ -1,6 +1,8 @@
1
+ import { type Plugin } from "esbuild";
1
2
  import type { MiddlewareInfo } from "../../types/next-types";
2
3
  import type { IncludedConverter, IncludedOriginResolver, LazyLoadedOverride, OverrideOptions, SplittedFunctionOptions } from "../../types/open-next";
3
4
  import type { OriginResolver } from "../../types/overrides.js";
5
+ import { ContentUpdater } from "../../plugins/content-updater.js";
4
6
  import { type BuildOptions } from "../helper.js";
5
7
  type Override = OverrideOptions & {
6
8
  originResolver?: LazyLoadedOverride<OriginResolver> | IncludedOriginResolver;
@@ -17,7 +19,8 @@ interface BuildEdgeBundleOptions {
17
19
  additionalExternals?: string[];
18
20
  onlyBuildOnce?: boolean;
19
21
  name: string;
22
+ additionalPlugins?: (contentUpdater: ContentUpdater) => Plugin[];
20
23
  }
21
- export declare function buildEdgeBundle({ middlewareInfo, entrypoint, outfile, options, defaultConverter, overrides, additionalInject, includeCache, additionalExternals, onlyBuildOnce, name, }: BuildEdgeBundleOptions): Promise<void>;
22
- export declare function generateEdgeBundle(name: string, options: BuildOptions, fnOptions: SplittedFunctionOptions): Promise<void>;
24
+ export declare function buildEdgeBundle({ middlewareInfo, entrypoint, outfile, options, defaultConverter, overrides, additionalInject, includeCache, additionalExternals, onlyBuildOnce, name, additionalPlugins: additionalPluginsFn, }: BuildEdgeBundleOptions): Promise<void>;
25
+ export declare function generateEdgeBundle(name: string, options: BuildOptions, fnOptions: SplittedFunctionOptions, additionalPlugins?: (contentUpdater: ContentUpdater) => Plugin[]): Promise<void>;
23
26
  export {};
@@ -4,6 +4,7 @@ import path from "node:path";
4
4
  import { build } from "esbuild";
5
5
  import { loadMiddlewareManifest } from "../../adapters/config/util.js";
6
6
  import logger from "../../logger.js";
7
+ import { ContentUpdater } from "../../plugins/content-updater.js";
7
8
  import { openNextEdgePlugins } from "../../plugins/edge.js";
8
9
  import { openNextExternalMiddlewarePlugin } from "../../plugins/externalMiddleware.js";
9
10
  import { openNextReplacementPlugin } from "../../plugins/replacement.js";
@@ -11,13 +12,17 @@ import { openNextResolvePlugin } from "../../plugins/resolve.js";
11
12
  import { getCrossPlatformPathRegex } from "../../utils/regex.js";
12
13
  import { isEdgeRuntime } from "../helper.js";
13
14
  import { copyOpenNextConfig, esbuildAsync } from "../helper.js";
14
- export async function buildEdgeBundle({ middlewareInfo, entrypoint, outfile, options, defaultConverter, overrides, additionalInject, includeCache, additionalExternals, onlyBuildOnce, name, }) {
15
+ export async function buildEdgeBundle({ middlewareInfo, entrypoint, outfile, options, defaultConverter, overrides, additionalInject, includeCache, additionalExternals, onlyBuildOnce, name, additionalPlugins: additionalPluginsFn, }) {
15
16
  const isInCloudfare = await isEdgeRuntime(overrides);
16
17
  function override(target) {
17
18
  return typeof overrides?.[target] === "string"
18
19
  ? overrides[target]
19
20
  : undefined;
20
21
  }
22
+ const contentUpdater = new ContentUpdater(options);
23
+ const additionalPlugins = additionalPluginsFn
24
+ ? additionalPluginsFn(contentUpdater)
25
+ : [];
21
26
  await esbuildAsync({
22
27
  entryPoints: [entrypoint],
23
28
  bundle: true,
@@ -53,6 +58,9 @@ export async function buildEdgeBundle({ middlewareInfo, entrypoint, outfile, opt
53
58
  nextDir: path.join(options.appBuildOutputPath, ".next"),
54
59
  isInCloudfare,
55
60
  }),
61
+ ...additionalPlugins,
62
+ // The content updater plugin must be the last plugin
63
+ contentUpdater.plugin,
56
64
  ],
57
65
  treeShaking: true,
58
66
  alias: {
@@ -117,7 +125,7 @@ Object.defineProperty = function(o, p, a) {
117
125
  });
118
126
  }
119
127
  }
120
- export async function generateEdgeBundle(name, options, fnOptions) {
128
+ export async function generateEdgeBundle(name, options, fnOptions, additionalPlugins = () => []) {
121
129
  logger.info(`Generating edge bundle for: ${name}`);
122
130
  const buildOutputDotNextDir = path.join(options.appBuildOutputPath, ".next");
123
131
  // Create output folder
@@ -153,5 +161,6 @@ export async function generateEdgeBundle(name, options, fnOptions) {
153
161
  overrides: fnOptions.override,
154
162
  additionalExternals: options.config.edgeExternals,
155
163
  name,
164
+ additionalPlugins,
156
165
  });
157
166
  }
@@ -51,15 +51,16 @@ export declare function getHtmlPages(dotNextPath: string): Set<string>;
51
51
  export declare function getBuildId(options: BuildOptions): string;
52
52
  export declare function getOpenNextVersion(): string;
53
53
  export declare function getNextVersion(appPath: string): string;
54
+ export type SemverOp = "=" | ">=" | "<=" | ">" | "<";
54
55
  /**
55
56
  * Compare two semver versions.
56
57
  *
57
58
  * @param v1 - First version. Can be "latest", otherwise it should be a valid semver version in the format of `major.minor.patch`. Usually is the next version from the package.json without canary suffix. If minor or patch are missing, they are considered 0.
58
59
  * @param v2 - Second version. Should not be "latest", it should be a valid semver version in the format of `major.minor.patch`. If minor or patch are missing, they are considered 0.
59
60
  * @example
60
- * compareSemver("1.0.0", "1.0.0") // 0
61
+ * compareSemver("2.0.0", ">=", "1.0.0") === true
61
62
  */
62
- export declare function compareSemver(v1: string, v2: string): number;
63
+ export declare function compareSemver(v1: string, operator: SemverOp, v2: string): boolean;
63
64
  export declare function copyOpenNextConfig(inputDir: string, outputDir: string, isEdge?: boolean): void;
64
65
  export declare function copyEnvFile(appPath: string, packagePath: string, outputPath: string): void;
65
66
  /**
@@ -213,29 +213,54 @@ export function getNextVersion(appPath) {
213
213
  * @param v1 - First version. Can be "latest", otherwise it should be a valid semver version in the format of `major.minor.patch`. Usually is the next version from the package.json without canary suffix. If minor or patch are missing, they are considered 0.
214
214
  * @param v2 - Second version. Should not be "latest", it should be a valid semver version in the format of `major.minor.patch`. If minor or patch are missing, they are considered 0.
215
215
  * @example
216
- * compareSemver("1.0.0", "1.0.0") // 0
216
+ * compareSemver("2.0.0", ">=", "1.0.0") === true
217
217
  */
218
- export function compareSemver(v1, v2) {
219
- if (v1 === "latest")
220
- return 1;
221
- if (/^[^\d]/.test(v1)) {
222
- // biome-ignore lint/style/noParameterAssign:
223
- v1 = v1.substring(1);
218
+ export function compareSemver(v1, operator, v2) {
219
+ // - = 0 when versions are equal
220
+ // - > 0 if v1 > v2
221
+ // - < 0 if v2 > v1
222
+ let versionDiff = 0;
223
+ if (v1 === "latest") {
224
+ versionDiff = 1;
224
225
  }
225
- if (/^[^\d]/.test(v2)) {
226
- // biome-ignore lint/style/noParameterAssign:
227
- v2 = v2.substring(1);
226
+ else {
227
+ if (/^[^\d]/.test(v1)) {
228
+ // biome-ignore lint/style/noParameterAssign:
229
+ v1 = v1.substring(1);
230
+ }
231
+ if (/^[^\d]/.test(v2)) {
232
+ // biome-ignore lint/style/noParameterAssign:
233
+ v2 = v2.substring(1);
234
+ }
235
+ const [major1, minor1 = 0, patch1 = 0] = v1.split(".").map(Number);
236
+ const [major2, minor2 = 0, patch2 = 0] = v2.split(".").map(Number);
237
+ if (Number.isNaN(major1) || Number.isNaN(major2)) {
238
+ throw new Error("The major version is required.");
239
+ }
240
+ if (major1 !== major2) {
241
+ versionDiff = major1 - major2;
242
+ }
243
+ else if (minor1 !== minor2) {
244
+ versionDiff = minor1 - minor2;
245
+ }
246
+ else if (patch1 !== patch2) {
247
+ versionDiff = patch1 - patch2;
248
+ }
228
249
  }
229
- const [major1, minor1 = 0, patch1 = 0] = v1.split(".").map(Number);
230
- const [major2, minor2 = 0, patch2 = 0] = v2.split(".").map(Number);
231
- if (Number.isNaN(major1) || Number.isNaN(major2)) {
232
- throw new Error("The major version is required.");
250
+ switch (operator) {
251
+ case "=":
252
+ return versionDiff === 0;
253
+ case ">=":
254
+ return versionDiff >= 0;
255
+ case "<=":
256
+ return versionDiff <= 0;
257
+ case ">":
258
+ return versionDiff > 0;
259
+ case "<":
260
+ return versionDiff < 0;
261
+ default:
262
+ throw new Error(`Unsupported operator: ${operator}`);
233
263
  }
234
- if (major1 !== major2)
235
- return major1 - major2;
236
- if (minor1 !== minor2)
237
- return minor1 - minor2;
238
- return patch1 - patch2;
239
264
  }
240
265
  export function copyOpenNextConfig(inputDir, outputDir, isEdge = false) {
241
266
  // Copy open-next.config.mjs
@@ -0,0 +1,58 @@
1
+ import { type Edit, Lang, type NapiConfig, type SgNode } from "@ast-grep/napi";
2
+ import type { PatchCodeFn } from "./codePatcher";
3
+ export type * from "@ast-grep/napi";
4
+ /**
5
+ * fix has the same meaning as in yaml rules
6
+ * see https://ast-grep.github.io/guide/rewrite-code.html#using-fix-in-yaml-rule
7
+ */
8
+ export type RuleConfig = NapiConfig & {
9
+ fix?: string;
10
+ };
11
+ /**
12
+ * Returns the `Edit`s and `Match`es for an ast-grep rule in yaml format
13
+ *
14
+ * The rule must have a `fix` to rewrite the matched node.
15
+ *
16
+ * Tip: use https://ast-grep.github.io/playground.html to create rules.
17
+ *
18
+ * @param rule The rule. Either a yaml string or an instance of `RuleConfig`
19
+ * @param root The root node
20
+ * @param once only apply once
21
+ * @returns A list of edits and a list of matches.
22
+ */
23
+ export declare function applyRule(rule: string | RuleConfig, root: SgNode, { once }?: {
24
+ once?: boolean | undefined;
25
+ }): {
26
+ edits: Edit[];
27
+ matches: SgNode[];
28
+ };
29
+ /**
30
+ * Parse a file and obtain its root.
31
+ *
32
+ * @param path The file path
33
+ * @param lang The language to parse. Defaults to TypeScript.
34
+ * @returns The root for the file.
35
+ */
36
+ export declare function parseFile(path: string, lang?: Lang): SgNode;
37
+ /**
38
+ * Patches the code from by applying the rule.
39
+ *
40
+ * This function is mainly for on off edits and tests,
41
+ * use `getRuleEdits` to apply multiple rules.
42
+ *
43
+ * @param code The source code
44
+ * @param rule The astgrep rule (yaml or NapiConfig)
45
+ * @param lang The language used by the source code
46
+ * @param lang Whether to apply the rule only once
47
+ * @returns The patched code
48
+ */
49
+ export declare function patchCode(code: string, rule: string | RuleConfig, { lang, once }?: {
50
+ lang?: Lang | undefined;
51
+ once?: boolean | undefined;
52
+ }): string;
53
+ /**
54
+ * @param rule
55
+ * @param lang
56
+ * @returns a callback applying the the rule.
57
+ */
58
+ export declare function createPatchCode(rule: string | RuleConfig, lang?: Lang): PatchCodeFn;
@@ -0,0 +1,76 @@
1
+ // Mostly copied from the cloudflare adapter
2
+ import { readFileSync } from "node:fs";
3
+ import { Lang, parse, } from "@ast-grep/napi";
4
+ import yaml from "yaml";
5
+ /**
6
+ * Returns the `Edit`s and `Match`es for an ast-grep rule in yaml format
7
+ *
8
+ * The rule must have a `fix` to rewrite the matched node.
9
+ *
10
+ * Tip: use https://ast-grep.github.io/playground.html to create rules.
11
+ *
12
+ * @param rule The rule. Either a yaml string or an instance of `RuleConfig`
13
+ * @param root The root node
14
+ * @param once only apply once
15
+ * @returns A list of edits and a list of matches.
16
+ */
17
+ export function applyRule(rule, root, { once = false } = {}) {
18
+ const ruleConfig = typeof rule === "string" ? yaml.parse(rule) : rule;
19
+ if (ruleConfig.transform) {
20
+ throw new Error("transform is not supported");
21
+ }
22
+ if (!ruleConfig.fix) {
23
+ throw new Error("no fix to apply");
24
+ }
25
+ const fix = ruleConfig.fix;
26
+ const matches = once
27
+ ? [root.find(ruleConfig)].filter((m) => m !== null)
28
+ : root.findAll(ruleConfig);
29
+ const edits = [];
30
+ matches.forEach((match) => {
31
+ edits.push(match.replace(
32
+ // Replace known placeholders by their value
33
+ fix
34
+ .replace(/\$\$\$([A-Z0-9_]+)/g, (_m, name) => match
35
+ .getMultipleMatches(name)
36
+ .map((n) => n.text())
37
+ .join(""))
38
+ .replace(/\$([A-Z0-9_]+)/g, (m, name) => match.getMatch(name)?.text() ?? m)));
39
+ });
40
+ return { edits, matches };
41
+ }
42
+ /**
43
+ * Parse a file and obtain its root.
44
+ *
45
+ * @param path The file path
46
+ * @param lang The language to parse. Defaults to TypeScript.
47
+ * @returns The root for the file.
48
+ */
49
+ export function parseFile(path, lang = Lang.TypeScript) {
50
+ return parse(lang, readFileSync(path, { encoding: "utf-8" })).root();
51
+ }
52
+ /**
53
+ * Patches the code from by applying the rule.
54
+ *
55
+ * This function is mainly for on off edits and tests,
56
+ * use `getRuleEdits` to apply multiple rules.
57
+ *
58
+ * @param code The source code
59
+ * @param rule The astgrep rule (yaml or NapiConfig)
60
+ * @param lang The language used by the source code
61
+ * @param lang Whether to apply the rule only once
62
+ * @returns The patched code
63
+ */
64
+ export function patchCode(code, rule, { lang = Lang.TypeScript, once = false } = {}) {
65
+ const node = parse(lang, code).root();
66
+ const { edits } = applyRule(rule, node, { once });
67
+ return node.commitEdits(edits);
68
+ }
69
+ /**
70
+ * @param rule
71
+ * @param lang
72
+ * @returns a callback applying the the rule.
73
+ */
74
+ export function createPatchCode(rule, lang = Lang.TypeScript) {
75
+ return async ({ code }) => patchCode(code, rule, { lang });
76
+ }
@@ -0,0 +1,46 @@
1
+ import type { getManifests } from "../copyTracedFiles.js";
2
+ import * as buildHelper from "../helper.js";
3
+ type Versions = `>=${number}.${number}.${number} <=${number}.${number}.${number}` | `>=${number}.${number}.${number}` | `<=${number}.${number}.${number}`;
4
+ export interface VersionedField<T> {
5
+ /**
6
+ * The versions of Next.js that this field should be used for
7
+ * Should be in the format `">=16.0.0 <=17.0.0"` or `">=16.0.0"` or `"<=17.0.0"`
8
+ * **Be careful with spaces**
9
+ */
10
+ versions?: Versions;
11
+ field: T;
12
+ }
13
+ export type PatchCodeFn = (args: {
14
+ /**
15
+ * The code of the file that needs to be patched
16
+ */
17
+ code: string;
18
+ /**
19
+ * The final path of the file that needs to be patched
20
+ */
21
+ filePath: string;
22
+ /**
23
+ * All files that are traced and will be included in the bundle
24
+ */
25
+ tracedFiles: string[];
26
+ /**
27
+ * Next.js manifests that are used by Next at runtime
28
+ */
29
+ manifests: ReturnType<typeof getManifests>;
30
+ }) => Promise<string>;
31
+ interface IndividualPatch {
32
+ pathFilter: RegExp;
33
+ contentFilter?: RegExp;
34
+ patchCode: PatchCodeFn;
35
+ }
36
+ export interface CodePatcher {
37
+ name: string;
38
+ patches: IndividualPatch | VersionedField<IndividualPatch>[];
39
+ }
40
+ export declare function parseVersions(versions?: Versions): {
41
+ before?: string;
42
+ after?: string;
43
+ };
44
+ export declare function extractVersionedField<T>(fields: VersionedField<T>[], version: string): T[];
45
+ export declare function applyCodePatches(buildOptions: buildHelper.BuildOptions, tracedFiles: string[], manifests: ReturnType<typeof getManifests>, codePatcher: CodePatcher[]): Promise<void>;
46
+ export {};
@@ -0,0 +1,119 @@
1
+ import * as fs from "node:fs/promises";
2
+ import logger from "../../logger.js";
3
+ import * as buildHelper from "../helper.js";
4
+ export function parseVersions(versions) {
5
+ if (!versions) {
6
+ return {};
7
+ }
8
+ // We need to use regex to extract the versions
9
+ const versionRegex = /([<>]=)(\d+\.\d+\.\d+)/g;
10
+ const matches = Array.from(versions.matchAll(versionRegex));
11
+ if (matches.length === 0) {
12
+ throw new Error("Invalid version range, no matches found");
13
+ }
14
+ if (matches.length > 2) {
15
+ throw new Error("Invalid version range, too many matches found");
16
+ }
17
+ let after;
18
+ let before;
19
+ for (const match of matches) {
20
+ const [_, operator, version] = match;
21
+ if (operator === "<=") {
22
+ before = version;
23
+ }
24
+ else {
25
+ after = version;
26
+ }
27
+ }
28
+ // Before returning we reconstruct the version string and compare it to the original
29
+ // If they don't match we throw an error
30
+ // We have to do this because template literal types here seems to allow for extra spaces
31
+ // that could easily break the version comparison and allow some patch to be applied on incorrect versions
32
+ // This might even go unnoticed
33
+ const reconstructedVersion = `${after ? `>=${after}` : ""}${before ? `${after ? " " : ""}<=${before}` : ""}`;
34
+ if (reconstructedVersion !== versions) {
35
+ throw new Error("Invalid version range, the reconstructed version does not match the original");
36
+ }
37
+ return {
38
+ before,
39
+ after,
40
+ };
41
+ }
42
+ export function extractVersionedField(fields, version) {
43
+ const result = [];
44
+ for (const field of fields) {
45
+ // No versions specified, the patch always apply
46
+ if (!field.versions) {
47
+ result.push(field.field);
48
+ continue;
49
+ }
50
+ const { before, after } = parseVersions(field.versions);
51
+ // range
52
+ if (before && after) {
53
+ if (buildHelper.compareSemver(version, "<=", before) &&
54
+ buildHelper.compareSemver(version, ">=", after)) {
55
+ result.push(field.field);
56
+ }
57
+ continue;
58
+ }
59
+ // before only
60
+ if (before) {
61
+ if (buildHelper.compareSemver(version, "<=", before)) {
62
+ result.push(field.field);
63
+ }
64
+ continue;
65
+ }
66
+ // after only
67
+ if (after) {
68
+ if (buildHelper.compareSemver(version, ">=", after)) {
69
+ result.push(field.field);
70
+ }
71
+ }
72
+ }
73
+ return result;
74
+ }
75
+ export async function applyCodePatches(buildOptions, tracedFiles, manifests, codePatcher) {
76
+ const nextVersion = buildOptions.nextVersion;
77
+ logger.time("Applying code patches");
78
+ // We first filter against the version
79
+ // We also flatten the array of patches so that we get both the name and all the necessary patches
80
+ const patchesToApply = codePatcher.flatMap(({ name, patches }) => Array.isArray(patches)
81
+ ? extractVersionedField(patches, nextVersion).map((patch) => ({
82
+ name,
83
+ patch,
84
+ }))
85
+ : [{ name, patch: patches }]);
86
+ await Promise.all(tracedFiles.map(async (filePath) => {
87
+ // We check the filename against the filter to see if we should apply the patch
88
+ const patchMatchingPath = patchesToApply.filter(({ patch }) => {
89
+ return filePath.match(patch.pathFilter);
90
+ });
91
+ if (patchMatchingPath.length === 0) {
92
+ return;
93
+ }
94
+ const content = await fs.readFile(filePath, "utf-8");
95
+ // We filter a last time against the content this time
96
+ const patchToApply = patchMatchingPath.filter(({ patch }) => {
97
+ if (!patch.contentFilter) {
98
+ return true;
99
+ }
100
+ return content.match(patch.contentFilter);
101
+ });
102
+ if (patchToApply.length === 0) {
103
+ return;
104
+ }
105
+ // We apply the patches
106
+ let patchedContent = content;
107
+ for (const { patch, name } of patchToApply) {
108
+ logger.debug(`Applying code patch: ${name} to ${filePath}`);
109
+ patchedContent = await patch.patchCode({
110
+ code: patchedContent,
111
+ filePath,
112
+ tracedFiles,
113
+ manifests,
114
+ });
115
+ }
116
+ await fs.writeFile(filePath, patchedContent);
117
+ }));
118
+ logger.timeEnd("Applying code patches");
119
+ }
@@ -0,0 +1,5 @@
1
+ import type { CodePatcher } from "./codePatcher";
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
+ 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 patchFetchCacheForISR: CodePatcher;
5
+ export declare const patchUnstableCacheForISR: CodePatcher;
@@ -0,0 +1,102 @@
1
+ import { Lang } from "@ast-grep/napi";
2
+ import { getCrossPlatformPathRegex } from "../../utils/regex.js";
3
+ import { createPatchCode } from "./astCodePatcher.js";
4
+ export const fetchRule = `
5
+ rule:
6
+ kind: member_expression
7
+ pattern: $WORK_STORE.isOnDemandRevalidate
8
+ inside:
9
+ kind: ternary_expression
10
+ all:
11
+ - has: {kind: 'null'}
12
+ - has:
13
+ kind: await_expression
14
+ has:
15
+ kind: call_expression
16
+ all:
17
+ - has:
18
+ kind: member_expression
19
+ has:
20
+ kind: property_identifier
21
+ field: property
22
+ regex: get
23
+ - has:
24
+ kind: arguments
25
+ has:
26
+ kind: object
27
+ has:
28
+ kind: pair
29
+ all:
30
+ - has:
31
+ kind: property_identifier
32
+ field: key
33
+ regex: softTags
34
+ inside:
35
+ kind: variable_declarator
36
+
37
+ fix:
38
+ ($WORK_STORE.isOnDemandRevalidate && !globalThis.__openNextAls?.getStore()?.isISRRevalidation)
39
+ `;
40
+ export const unstable_cacheRule = `
41
+ rule:
42
+ kind: member_expression
43
+ pattern: $STORE_OR_CACHE.isOnDemandRevalidate
44
+ inside:
45
+ kind: if_statement
46
+ stopBy: end
47
+ has:
48
+ kind: statement_block
49
+ has:
50
+ kind: variable_declarator
51
+ has:
52
+ kind: await_expression
53
+ has:
54
+ kind: call_expression
55
+ all:
56
+ - has:
57
+ kind: member_expression
58
+ has:
59
+ kind: property_identifier
60
+ field: property
61
+ regex: get
62
+ - has:
63
+ kind: arguments
64
+ has:
65
+ kind: object
66
+ has:
67
+ kind: pair
68
+ all:
69
+ - has:
70
+ kind: property_identifier
71
+ field: key
72
+ regex: softTags
73
+ stopBy: end
74
+ fix:
75
+ ($STORE_OR_CACHE.isOnDemandRevalidate && !globalThis.__openNextAls?.getStore()?.isISRRevalidation)
76
+ `;
77
+ export const patchFetchCacheForISR = {
78
+ name: "patch-fetch-cache-for-isr",
79
+ patches: [
80
+ {
81
+ versions: ">=14.0.0",
82
+ field: {
83
+ pathFilter: getCrossPlatformPathRegex(String.raw `(server/chunks/.*\.js|.*\.runtime\..*\.js|patch-fetch\.js)$`, { escape: false }),
84
+ contentFilter: /\.isOnDemandRevalidate/,
85
+ patchCode: createPatchCode(fetchRule, Lang.JavaScript),
86
+ },
87
+ },
88
+ ],
89
+ };
90
+ export const patchUnstableCacheForISR = {
91
+ name: "patch-unstable-cache-for-isr",
92
+ patches: [
93
+ {
94
+ versions: ">=14.2.0",
95
+ field: {
96
+ pathFilter: getCrossPlatformPathRegex(String.raw `(server/chunks/.*\.js|.*\.runtime\..*\.js|spec-extension/unstable-cache\.js)$`, { escape: false }),
97
+ contentFilter: /\.isOnDemandRevalidate/,
98
+ patchCode: createPatchCode(unstable_cacheRule, Lang.JavaScript),
99
+ },
100
+ },
101
+ ],
102
+ };
@@ -0,0 +1,3 @@
1
+ import type { CodePatcher } from "./codePatcher";
2
+ export declare const rule = "\nrule:\n kind: call_expression\n pattern: $PROMISE\n all:\n - has: { pattern: $_.arrayBuffer().then, stopBy: end }\n - has: { pattern: \"Buffer.from\", stopBy: end }\n - any:\n - inside:\n kind: sequence_expression\n inside:\n kind: return_statement\n - inside:\n kind: expression_statement\n precedes:\n kind: return_statement\n - has: { pattern: $_.FETCH, stopBy: end }\n\nfix: |\n globalThis.__openNextAls?.getStore()?.pendingPromiseRunner.add($PROMISE)\n";
3
+ export declare const patchFetchCacheSetMissingWaitUntil: CodePatcher;
@@ -0,0 +1,36 @@
1
+ import { getCrossPlatformPathRegex } from "../../utils/regex.js";
2
+ import { createPatchCode } from "./astCodePatcher.js";
3
+ export const rule = `
4
+ rule:
5
+ kind: call_expression
6
+ pattern: $PROMISE
7
+ all:
8
+ - has: { pattern: $_.arrayBuffer().then, stopBy: end }
9
+ - has: { pattern: "Buffer.from", stopBy: end }
10
+ - any:
11
+ - inside:
12
+ kind: sequence_expression
13
+ inside:
14
+ kind: return_statement
15
+ - inside:
16
+ kind: expression_statement
17
+ precedes:
18
+ kind: return_statement
19
+ - has: { pattern: $_.FETCH, stopBy: end }
20
+
21
+ fix: |
22
+ globalThis.__openNextAls?.getStore()?.pendingPromiseRunner.add($PROMISE)
23
+ `;
24
+ export const patchFetchCacheSetMissingWaitUntil = {
25
+ name: "patch-fetch-cache-set-missing-wait-until",
26
+ patches: [
27
+ {
28
+ versions: ">=15.0.0",
29
+ field: {
30
+ pathFilter: getCrossPlatformPathRegex(String.raw `(server/chunks/.*\.js|.*\.runtime\..*\.js|patch-fetch\.js)$`, { escape: false }),
31
+ contentFilter: /arrayBuffer\(\)\s*\.then/,
32
+ patchCode: createPatchCode(rule),
33
+ },
34
+ },
35
+ ],
36
+ };
@@ -9,7 +9,9 @@ import routingHandler, { INTERNAL_HEADER_INITIAL_URL, INTERNAL_HEADER_RESOLVED_R
9
9
  import { requestHandler, setNextjsPrebundledReact } from "./util";
10
10
  // This is used to identify requests in the cache
11
11
  globalThis.__openNextAls = new AsyncLocalStorage();
12
+ //#override patchAsyncStorage
12
13
  patchAsyncStorage();
14
+ //#endOverride
13
15
  export async function openNextHandler(internalEvent, options) {
14
16
  const initialHeaders = internalEvent.headers;
15
17
  // We run everything in the async local storage context so that it is available in the middleware as well as in NextServer
@@ -94,7 +96,7 @@ export async function openNextHandler(internalEvent, options) {
94
96
  resolvedRoutes: [],
95
97
  origin: false,
96
98
  initialURL: internalEvent.url,
97
- }, headers, options.streamCreator);
99
+ }, routingResult.headers, options.streamCreator);
98
100
  response.statusCode = routingResult.statusCode;
99
101
  response.flushHeaders();
100
102
  const [bodyToConsume, bodyToReturn] = routingResult.body.tee();
@@ -48,7 +48,12 @@ async function computeCacheControl(path, body, host, revalidate, lastModified) {
48
48
  url = `${NextConfig.basePath}${url}`;
49
49
  }
50
50
  await globalThis.queue.send({
51
- MessageBody: { host, url },
51
+ MessageBody: {
52
+ host,
53
+ url,
54
+ eTag: etag,
55
+ lastModified: lastModified ?? Date.now(),
56
+ },
52
57
  MessageDeduplicationId: hash(`${path}-${lastModified}-${etag}`),
53
58
  MessageGroupId: generateMessageGroupId(path),
54
59
  });
@@ -3,12 +3,12 @@ import type { InternalEvent, InternalResult } from "../../types/open-next";
3
3
  export declare function getNextConfigHeaders(event: InternalEvent, configHeaders?: Header[] | undefined): Record<string, string | undefined>;
4
4
  export declare function handleRewrites<T extends RewriteDefinition>(event: InternalEvent, rewrites: T[]): {
5
5
  internalEvent: {
6
+ query: Record<string, string | string[]>;
6
7
  rawPath: string;
7
8
  url: string;
8
9
  method: string;
9
10
  body?: Buffer;
10
11
  headers: Record<string, string>;
11
- query: Record<string, string | string[]>;
12
12
  cookies: Record<string, string>;
13
13
  remoteAddress: string;
14
14
  type: "core";
@@ -145,10 +145,17 @@ export function handleRewrites(event, rewrites) {
145
145
  rewrittenHost = unescapeRegex(toDestinationHost(params));
146
146
  rewrittenQuery = unescapeRegex(toDestinationQuery(params));
147
147
  }
148
+ // We need to strip the locale from the path if it's a local api route
149
+ if (NextConfig.i18n && !isExternalRewrite) {
150
+ const strippedPathLocale = rewrittenPath.replace(new RegExp(`^/(${NextConfig.i18n.locales.join("|")})`), "");
151
+ if (strippedPathLocale.startsWith("/api/")) {
152
+ rewrittenPath = strippedPathLocale;
153
+ }
154
+ }
148
155
  rewrittenUrl = isExternalRewrite
149
156
  ? `${protocol}//${rewrittenHost}${rewrittenPath}`
150
157
  : new URL(rewrittenPath, event.url).href;
151
- // Should we merge the query params or use only the ones from the rewrite?
158
+ // We merge query params from the source and the destination
152
159
  finalQuery = {
153
160
  ...query,
154
161
  ...convertFromQueryString(rewrittenQuery),
@@ -159,6 +166,7 @@ export function handleRewrites(event, rewrites) {
159
166
  return {
160
167
  internalEvent: {
161
168
  ...event,
169
+ query: finalQuery,
162
170
  rawPath: new URL(rewrittenUrl).pathname,
163
171
  url: rewrittenUrl,
164
172
  },
@@ -73,6 +73,12 @@ export async function handleMiddleware(internalEvent, middlewareLoader = default
73
73
  const reqHeaders = {};
74
74
  const resHeaders = {};
75
75
  responseHeaders.delete("x-middleware-override-headers");
76
+ /* Next will set the header `x-middleware-set-cookie` when you `set-cookie` in the middleware.
77
+ * We can delete it here since it will be set in `set-cookie` aswell. Next removes this header in the response themselves.
78
+ * `x-middleware-next` is set when you invoke `NextResponse.next()`. We can delete it here aswell.
79
+ */
80
+ responseHeaders.delete("x-middleware-set-cookie");
81
+ responseHeaders.delete("x-middleware-next");
76
82
  const xMiddlewareKey = "x-middleware-request-";
77
83
  responseHeaders.forEach((value, key) => {
78
84
  if (key.startsWith(xMiddlewareKey)) {
@@ -1 +1,2 @@
1
+ export declare function generateShardId(rawPath: string, maxConcurrency: number, prefix: string): string;
1
2
  export declare function generateMessageGroupId(rawPath: string): string;
@@ -1,3 +1,14 @@
1
+ export function generateShardId(rawPath, maxConcurrency, prefix) {
2
+ let a = cyrb128(rawPath);
3
+ // We use mulberry32 to generate a random int between 0 and MAX_REVALIDATE_CONCURRENCY
4
+ let t = (a += 0x6d2b79f5);
5
+ t = Math.imul(t ^ (t >>> 15), t | 1);
6
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
7
+ const randomFloat = ((t ^ (t >>> 14)) >>> 0) / 4294967296;
8
+ // This will generate a random int between 0 and maxConcurrency
9
+ const randomInt = Math.floor(randomFloat * maxConcurrency);
10
+ return `${prefix}-${randomInt}`;
11
+ }
1
12
  // Since we're using a FIFO queue, every messageGroupId is treated sequentially
2
13
  // This could cause a backlog of messages in the queue if there is too much page to
3
14
  // revalidate at once. To avoid this, we generate a random messageGroupId for each
@@ -6,17 +17,10 @@
6
17
  // will always have the same messageGroupId.
7
18
  // https://stackoverflow.com/questions/521295/seeding-the-random-number-generator-in-javascript#answer-47593316
8
19
  export function generateMessageGroupId(rawPath) {
9
- let a = cyrb128(rawPath);
10
- // We use mulberry32 to generate a random int between 0 and MAX_REVALIDATE_CONCURRENCY
11
- let t = (a += 0x6d2b79f5);
12
- t = Math.imul(t ^ (t >>> 15), t | 1);
13
- t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
14
- const randomFloat = ((t ^ (t >>> 14)) >>> 0) / 4294967296;
15
20
  // This will generate a random int between 0 and MAX_REVALIDATE_CONCURRENCY
16
21
  // This means that we could have 1000 revalidate request at the same time
17
22
  const maxConcurrency = Number.parseInt(process.env.MAX_REVALIDATE_CONCURRENCY ?? "10");
18
- const randomInt = Math.floor(randomFloat * maxConcurrency);
19
- return `revalidate-${randomInt}`;
23
+ return generateShardId(rawPath, maxConcurrency, "revalidate");
20
24
  }
21
25
  // Used to generate a hash int from a string
22
26
  function cyrb128(str) {
@@ -268,10 +268,10 @@ export async function revalidateIfRequired(host, rawPath, headers, req) {
268
268
  const lastModified = globalThis.__openNextAls.getStore()?.lastModified ?? 0;
269
269
  // For some weird cases, lastModified is not set, haven't been able to figure out yet why
270
270
  // For those cases we add the etag to the deduplication id, it might help
271
- const etag = headers.etag ?? headers.ETag ?? "";
271
+ const eTag = `${headers.etag ?? headers.ETag ?? ""}`;
272
272
  await globalThis.queue.send({
273
- MessageBody: { host, url: revalidateUrl },
274
- MessageDeduplicationId: hash(`${rawPath}-${lastModified}-${etag}`),
273
+ MessageBody: { host, url: revalidateUrl, eTag, lastModified },
274
+ MessageDeduplicationId: hash(`${rawPath}-${lastModified}-${eTag}`),
275
275
  MessageGroupId: generateMessageGroupId(rawPath),
276
276
  });
277
277
  }
package/dist/logger.d.ts CHANGED
@@ -8,5 +8,13 @@ declare const _default: {
8
8
  };
9
9
  warn: (...args: any[]) => void;
10
10
  error: (...args: any[]) => void;
11
+ time: {
12
+ (label?: string): void;
13
+ (label?: string): void;
14
+ };
15
+ timeEnd: {
16
+ (label?: string): void;
17
+ (label?: string): void;
18
+ };
11
19
  };
12
20
  export default _default;
package/dist/logger.js CHANGED
@@ -10,4 +10,6 @@ export default {
10
10
  info: console.log,
11
11
  warn: (...args) => console.warn(chalk.yellow("WARN"), ...args),
12
12
  error: (...args) => console.error(chalk.red("ERROR"), ...args),
13
+ time: console.time,
14
+ timeEnd: console.timeEnd,
13
15
  };
@@ -19,7 +19,9 @@ const wrapper = async (handler, converter) => {
19
19
  const internalEvent = await converter.convertFrom(req);
20
20
  const streamCreator = {
21
21
  writeHeaders: (prelude) => {
22
+ res.setHeader("Set-Cookie", prelude.cookies);
22
23
  res.writeHead(prelude.statusCode, prelude.headers);
24
+ res.flushHeaders();
23
25
  return res;
24
26
  },
25
27
  onFinish: () => { },
@@ -0,0 +1,48 @@
1
+ /**
2
+ * ESBuild stops calling `onLoad` hooks after the first hook returns an updated content.
3
+ *
4
+ * The updater allows multiple plugins to update the content.
5
+ */
6
+ import type { OnLoadOptions, Plugin, PluginBuild } from "esbuild";
7
+ import type { BuildOptions } from "../build/helper";
8
+ import { type VersionedField } from "../build/patch/codePatcher.js";
9
+ export type * from "esbuild";
10
+ /**
11
+ * The callbacks returns either an updated content or undefined if the content is unchanged.
12
+ */
13
+ export type Callback = (args: {
14
+ contents: string;
15
+ path: string;
16
+ }) => string | undefined | Promise<string | undefined>;
17
+ /**
18
+ * The callback is called only when `contentFilter` matches the content.
19
+ * It can be used as a fast heuristic to prevent an expensive update.
20
+ */
21
+ export type OnUpdateOptions = OnLoadOptions & {
22
+ contentFilter: RegExp;
23
+ };
24
+ export type Updater = OnUpdateOptions & {
25
+ callback: Callback;
26
+ };
27
+ export declare class ContentUpdater {
28
+ private buildOptions;
29
+ updaters: Map<string, Updater[]>;
30
+ constructor(buildOptions: BuildOptions);
31
+ /**
32
+ * Register a callback to update the file content.
33
+ *
34
+ * The callbacks are called in order of registration.
35
+ *
36
+ * @param name The name of the plugin (must be unique).
37
+ * @param updater A versioned field with the callback and `OnUpdateOptions`.
38
+ * @returns A noop ESBuild plugin.
39
+ */
40
+ updateContent(name: string, versionedUpdaters: VersionedField<Updater>[]): Plugin;
41
+ /**
42
+ * Returns an ESBuild plugin applying the registered updates.
43
+ */
44
+ get plugin(): {
45
+ name: string;
46
+ setup: (build: PluginBuild) => Promise<void>;
47
+ };
48
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * ESBuild stops calling `onLoad` hooks after the first hook returns an updated content.
3
+ *
4
+ * The updater allows multiple plugins to update the content.
5
+ */
6
+ import { readFile } from "node:fs/promises";
7
+ import { extractVersionedField, } from "../build/patch/codePatcher.js";
8
+ export class ContentUpdater {
9
+ buildOptions;
10
+ updaters = new Map();
11
+ constructor(buildOptions) {
12
+ this.buildOptions = buildOptions;
13
+ }
14
+ /**
15
+ * Register a callback to update the file content.
16
+ *
17
+ * The callbacks are called in order of registration.
18
+ *
19
+ * @param name The name of the plugin (must be unique).
20
+ * @param updater A versioned field with the callback and `OnUpdateOptions`.
21
+ * @returns A noop ESBuild plugin.
22
+ */
23
+ updateContent(name, versionedUpdaters) {
24
+ if (this.updaters.has(name)) {
25
+ throw new Error(`Plugin "${name}" already registered`);
26
+ }
27
+ const updaters = extractVersionedField(versionedUpdaters, this.buildOptions.nextVersion);
28
+ this.updaters.set(name, updaters);
29
+ return {
30
+ name,
31
+ setup() { },
32
+ };
33
+ }
34
+ /**
35
+ * Returns an ESBuild plugin applying the registered updates.
36
+ */
37
+ get plugin() {
38
+ return {
39
+ name: "aggregate-on-load",
40
+ setup: async (build) => {
41
+ build.onLoad({ filter: /\.(js|mjs|cjs|jsx|ts|tsx)$/ }, async (args) => {
42
+ const updaters = Array.from(this.updaters.values()).flat();
43
+ if (updaters.length === 0) {
44
+ return;
45
+ }
46
+ let contents = await readFile(args.path, "utf-8");
47
+ for (const { filter, namespace, contentFilter, callback, } of updaters) {
48
+ if (namespace !== undefined && args.namespace !== namespace) {
49
+ continue;
50
+ }
51
+ if (!args.path.match(filter)) {
52
+ continue;
53
+ }
54
+ if (!contents.match(contentFilter)) {
55
+ continue;
56
+ }
57
+ contents =
58
+ (await callback({ contents, path: args.path })) ?? contents;
59
+ }
60
+ return { contents };
61
+ });
62
+ },
63
+ };
64
+ }
65
+ }
@@ -6,6 +6,8 @@ export interface QueueMessage {
6
6
  MessageBody: {
7
7
  host: string;
8
8
  url: string;
9
+ lastModified: number;
10
+ eTag: string;
9
11
  };
10
12
  MessageGroupId: string;
11
13
  }
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "access": "public"
4
4
  },
5
5
  "name": "@opennextjs/aws",
6
- "version": "3.5.1",
6
+ "version": "3.5.3",
7
7
  "bin": {
8
8
  "open-next": "./dist/index.js"
9
9
  },
@@ -33,6 +33,7 @@
33
33
  "README.md"
34
34
  ],
35
35
  "dependencies": {
36
+ "@ast-grep/napi": "^0.35.0",
36
37
  "@aws-sdk/client-cloudfront": "3.398.0",
37
38
  "@aws-sdk/client-dynamodb": "^3.398.0",
38
39
  "@aws-sdk/client-lambda": "^3.398.0",
@@ -46,7 +47,8 @@
46
47
  "esbuild": "0.19.2",
47
48
  "express": "5.0.1",
48
49
  "path-to-regexp": "^6.3.0",
49
- "urlpattern-polyfill": "^10.0.0"
50
+ "urlpattern-polyfill": "^10.0.0",
51
+ "yaml": "^2.7.0"
50
52
  },
51
53
  "devDependencies": {
52
54
  "@types/aws-lambda": "^8.10.109",