@opennextjs/aws 3.3.1 → 3.4.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 (47) hide show
  1. package/dist/adapters/cache.js +19 -0
  2. package/dist/build/createImageOptimizationBundle.js +2 -1
  3. package/dist/build/createServerBundle.js +4 -4
  4. package/dist/build/edge/createEdgeBundle.js +2 -1
  5. package/dist/build/validateConfig.js +2 -1
  6. package/dist/core/createMainHandler.js +2 -1
  7. package/dist/core/requestHandler.js +9 -2
  8. package/dist/core/resolve.d.ts +4 -0
  9. package/dist/core/resolve.js +10 -0
  10. package/dist/core/routing/middleware.js +3 -1
  11. package/dist/core/routing/util.d.ts +3 -2
  12. package/dist/core/routing/util.js +18 -1
  13. package/dist/overrides/cdnInvalidation/cloudfront.d.ts +9 -0
  14. package/dist/overrides/cdnInvalidation/cloudfront.js +26 -0
  15. package/dist/overrides/cdnInvalidation/dummy.d.ts +9 -0
  16. package/dist/overrides/cdnInvalidation/dummy.js +6 -0
  17. package/dist/overrides/imageLoader/fs-dev.d.ts +10 -0
  18. package/dist/overrides/imageLoader/fs-dev.js +14 -0
  19. package/dist/overrides/incrementalCache/fs-dev.d.ts +3 -0
  20. package/dist/overrides/incrementalCache/fs-dev.js +30 -0
  21. package/dist/overrides/incrementalCache/multi-tier-ddb-s3.d.ts +10 -0
  22. package/dist/overrides/incrementalCache/multi-tier-ddb-s3.js +120 -0
  23. package/dist/overrides/incrementalCache/s3-lite.d.ts +2 -0
  24. package/dist/overrides/incrementalCache/s3-lite.js +1 -1
  25. package/dist/overrides/proxyExternalRequest/node.js +6 -5
  26. package/dist/overrides/queue/direct.d.ts +3 -0
  27. package/dist/overrides/queue/direct.js +18 -0
  28. package/dist/overrides/tagCache/fs-dev.d.ts +3 -0
  29. package/dist/overrides/tagCache/fs-dev.js +32 -0
  30. package/dist/overrides/wrappers/cloudflare-edge.js +11 -4
  31. package/dist/overrides/wrappers/dummy.d.ts +1 -1
  32. package/dist/overrides/wrappers/dummy.js +6 -2
  33. package/dist/overrides/wrappers/express-dev.d.ts +7 -0
  34. package/dist/overrides/wrappers/express-dev.js +43 -0
  35. package/dist/plugins/edge.js +4 -3
  36. package/dist/plugins/replacement.d.ts +1 -1
  37. package/dist/plugins/replacement.js +1 -1
  38. package/dist/plugins/resolve.d.ts +1 -0
  39. package/dist/plugins/resolve.js +4 -1
  40. package/dist/types/global.d.ts +7 -1
  41. package/dist/types/open-next.d.ts +12 -6
  42. package/dist/types/overrides.d.ts +10 -1
  43. package/dist/utils/lru.d.ts +8 -0
  44. package/dist/utils/lru.js +29 -0
  45. package/dist/utils/regex.d.ts +18 -0
  46. package/dist/utils/regex.js +16 -0
  47. package/package.json +4 -1
@@ -275,6 +275,25 @@ export default class Cache {
275
275
  }
276
276
  // Update all keys with the given tag with revalidatedAt set to now
277
277
  await globalThis.tagCache.writeTags(toInsert);
278
+ // We can now invalidate all paths in the CDN
279
+ // This only applies to `revalidateTag`, not to `res.revalidate()`
280
+ const uniquePaths = Array.from(new Set(toInsert
281
+ // We need to filter fetch cache key as they are not in the CDN
282
+ .filter((t) => t.tag.startsWith("_N_T_/"))
283
+ .map((t) => `/${t.path}`)));
284
+ if (uniquePaths.length > 0) {
285
+ await globalThis.cdnInvalidationHandler.invalidatePaths(uniquePaths.map((path) => ({
286
+ initialPath: path,
287
+ rawPath: path,
288
+ resolvedRoutes: [
289
+ {
290
+ route: path,
291
+ // TODO: ideally here we should check if it's an app router page or route
292
+ type: "app",
293
+ },
294
+ ],
295
+ })));
296
+ }
278
297
  }
279
298
  }
280
299
  catch (e) {
@@ -4,6 +4,7 @@ import path from "node:path";
4
4
  import logger from "../logger.js";
5
5
  import { openNextReplacementPlugin } from "../plugins/replacement.js";
6
6
  import { openNextResolvePlugin } from "../plugins/resolve.js";
7
+ import { getCrossPlatformPathRegex } from "../utils/regex.js";
7
8
  import * as buildHelper from "./helper.js";
8
9
  import { installDependencies } from "./installDeps.js";
9
10
  const require = createRequire(import.meta.url);
@@ -28,7 +29,7 @@ export async function createImageOptimizationBundle(options) {
28
29
  if (buildHelper.compareSemver(options.nextVersion, "14.1.1") >= 0) {
29
30
  plugins.push(openNextReplacementPlugin({
30
31
  name: "opennext-14.1.1-image-optimization",
31
- target: /plugins(\/|\\)image-optimization(\/|\\)image-optimization\.js/g,
32
+ target: getCrossPlatformPathRegex("plugins/image-optimization/image-optimization.js"),
32
33
  replacements: [
33
34
  require.resolve("../adapters/plugins/image-optimization/image-optimization.replacement.js"),
34
35
  ],
@@ -4,6 +4,7 @@ import logger from "../logger.js";
4
4
  import { minifyAll } from "../minimize-js.js";
5
5
  import { openNextReplacementPlugin } from "../plugins/replacement.js";
6
6
  import { openNextResolvePlugin } from "../plugins/resolve.js";
7
+ import { getCrossPlatformPathRegex } from "../utils/regex.js";
7
8
  import { bundleNextServer } from "./bundleNextServer.js";
8
9
  import { compileCache } from "./compileCache.js";
9
10
  import { copyTracedFiles } from "./copyTracedFiles.js";
@@ -114,7 +115,7 @@ async function generateBundle(name, options, fnOptions) {
114
115
  const plugins = [
115
116
  openNextReplacementPlugin({
116
117
  name: `requestHandlerOverride ${name}`,
117
- target: /core(\/|\\)requestHandler\.js/g,
118
+ target: getCrossPlatformPathRegex("core/requestHandler.js"),
118
119
  deletes: [
119
120
  ...(disableNextPrebundledReact ? ["applyNextjsPrebundledReact"] : []),
120
121
  ...(disableRouting ? ["withRouting"] : []),
@@ -122,11 +123,10 @@ async function generateBundle(name, options, fnOptions) {
122
123
  }),
123
124
  openNextReplacementPlugin({
124
125
  name: `utilOverride ${name}`,
125
- target: /core(\/|\\)util\.js/g,
126
+ target: getCrossPlatformPathRegex("core/util.js"),
126
127
  deletes: [
127
128
  ...(disableNextPrebundledReact ? ["requireHooks"] : []),
128
- ...(disableRouting ? ["trustHostHeader"] : []),
129
- ...(!isBefore13413 ? ["requestHandlerHost"] : []),
129
+ ...(isBefore13413 ? ["trustHostHeader"] : ["requestHandlerHost"]),
130
130
  ...(isAfter141
131
131
  ? ["experimentalIncrementalCacheHandler"]
132
132
  : ["stableIncrementalCache"]),
@@ -6,6 +6,7 @@ import logger from "../../logger.js";
6
6
  import { openNextEdgePlugins } from "../../plugins/edge.js";
7
7
  import { openNextReplacementPlugin } from "../../plugins/replacement.js";
8
8
  import { openNextResolvePlugin } from "../../plugins/resolve.js";
9
+ import { getCrossPlatformPathRegex } from "../../utils/regex.js";
9
10
  import { isEdgeRuntime } from "../helper.js";
10
11
  import { copyOpenNextConfig, esbuildAsync } from "../helper.js";
11
12
  export async function buildEdgeBundle({ middlewareInfo, entrypoint, outfile, options, defaultConverter, overrides, additionalInject, includeCache, additionalExternals, onlyBuildOnce, name, }) {
@@ -41,7 +42,7 @@ export async function buildEdgeBundle({ middlewareInfo, entrypoint, outfile, opt
41
42
  }),
42
43
  openNextReplacementPlugin({
43
44
  name: "externalMiddlewareOverrides",
44
- target: /adapters(\/|\\)middleware\.js/g,
45
+ target: getCrossPlatformPathRegex("adapters/middleware.js"),
45
46
  deletes: includeCache ? [] : ["includeCacheInMiddleware"],
46
47
  }),
47
48
  openNextEdgePlugins({
@@ -11,7 +11,8 @@ const compatibilityMatrix = {
11
11
  "cloudflare-edge": ["edge"],
12
12
  "cloudflare-node": ["edge"],
13
13
  node: ["node"],
14
- dummy: [],
14
+ "express-dev": ["node"],
15
+ dummy: ["dummy"],
15
16
  };
16
17
  function validateFunctionOptions(fnOptions) {
17
18
  if (fnOptions.runtime === "edge" && fnOptions.experimentalBundledNextServer) {
@@ -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 { resolveConverter, resolveIncrementalCache, resolveProxyRequest, resolveQueue, resolveTagCache, resolveWrapper, } from "./resolve";
4
+ import { 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);
@@ -15,6 +15,7 @@ export async function createMainHandler() {
15
15
  globalThis.incrementalCache = await resolveIncrementalCache(thisFunction.override?.incrementalCache);
16
16
  globalThis.tagCache = await resolveTagCache(thisFunction.override?.tagCache);
17
17
  globalThis.proxyExternalRequest = await resolveProxyRequest(thisFunction.override?.proxyExternalRequest);
18
+ globalThis.cdnInvalidationHandler = await resolveCdnInvalidation(thisFunction.override?.cdnInvalidation);
18
19
  globalThis.lastModified = {};
19
20
  // From the config, we create the converter
20
21
  const converter = await resolveConverter(thisFunction.override?.converter);
@@ -82,7 +82,14 @@ export async function openNextHandler(internalEvent, responseStreaming) {
82
82
  if ("type" in routingResult) {
83
83
  // response is used only in the streaming case
84
84
  if (responseStreaming) {
85
- const response = createServerResponse(internalEvent, headers, responseStreaming);
85
+ const response = createServerResponse({
86
+ internalEvent,
87
+ isExternalRewrite: false,
88
+ isISR: false,
89
+ resolvedRoutes: [],
90
+ origin: false,
91
+ initialPath: internalEvent.rawPath,
92
+ }, headers, responseStreaming);
86
93
  response.statusCode = routingResult.statusCode;
87
94
  response.flushHeaders();
88
95
  const [bodyToConsume, bodyToReturn] = routingResult.body.tee();
@@ -117,7 +124,7 @@ export async function openNextHandler(internalEvent, responseStreaming) {
117
124
  store.mergeHeadersPriority = mergeHeadersPriority;
118
125
  }
119
126
  const req = new IncomingMessage(reqProps);
120
- const res = createServerResponse(preprocessedEvent, overwrittenResponseHeaders, responseStreaming);
127
+ const res = createServerResponse(routingResult, overwrittenResponseHeaders, responseStreaming);
121
128
  await processRequest(req, res, preprocessedEvent);
122
129
  const { statusCode, headers: responseHeaders, isBase64Encoded, body, } = convertRes(res);
123
130
  const internalResult = {
@@ -43,4 +43,8 @@ export declare function resolveWarmerInvoke(warmer: RemoveUndefined<OpenNextConf
43
43
  * @__PURE__
44
44
  */
45
45
  export declare function resolveProxyRequest(proxyRequest: OverrideOptions["proxyExternalRequest"]): Promise<import("../types/overrides").ProxyExternalRequest>;
46
+ /**
47
+ * @__PURE__
48
+ */
49
+ export declare function resolveCdnInvalidation(cdnInvalidation: OverrideOptions["cdnInvalidation"]): Promise<import("../types/overrides").CDNInvalidationHandler>;
46
50
  export {};
@@ -98,3 +98,13 @@ export async function resolveProxyRequest(proxyRequest) {
98
98
  const m_1 = await import("../overrides/proxyExternalRequest/node.js");
99
99
  return m_1.default;
100
100
  }
101
+ /**
102
+ * @__PURE__
103
+ */
104
+ export async function resolveCdnInvalidation(cdnInvalidation) {
105
+ if (typeof cdnInvalidation === "function") {
106
+ return cdnInvalidation();
107
+ }
108
+ const m_1 = await import("../overrides/cdnInvalidation/dummy.js");
109
+ return m_1.default;
110
+ }
@@ -44,7 +44,9 @@ export async function handleMiddleware(internalEvent, middlewareLoader = default
44
44
  const result = await middleware.default({
45
45
  // `geo` is pre Next 15.
46
46
  geo: {
47
- city: headers["x-open-next-city"],
47
+ // The city name is percent-encoded.
48
+ // See https://github.com/vercel/vercel/blob/4cb6143/packages/functions/src/headers.ts#L94C19-L94C37
49
+ city: decodeURIComponent(headers["x-open-next-city"]),
48
50
  country: headers["x-open-next-country"],
49
51
  region: headers["x-open-next-region"],
50
52
  latitude: headers["x-open-next-latitude"],
@@ -2,7 +2,7 @@ import type { OutgoingHttpHeaders } from "node:http";
2
2
  import type { IncomingMessage } from "../../http/index.js";
3
3
  import { OpenNextNodeResponse } from "../../http/openNextResponse.js";
4
4
  import type { MiddlewareManifest } from "../../types/next-types";
5
- import type { InternalEvent, InternalResult, StreamCreator } from "../../types/open-next.js";
5
+ import type { InternalEvent, InternalResult, RoutingResult, StreamCreator } from "../../types/open-next.js";
6
6
  /**
7
7
  *
8
8
  * @__PURE__
@@ -90,4 +90,5 @@ export declare function fixISRHeaders(headers: OutgoingHttpHeaders): void;
90
90
  * @returns
91
91
  * @__PURE__
92
92
  */
93
- export declare function createServerResponse(internalEvent: InternalEvent, headers: Record<string, string | string[] | undefined>, responseStream?: StreamCreator): OpenNextNodeResponse;
93
+ export declare function createServerResponse(routingResult: RoutingResult, headers: Record<string, string | string[] | undefined>, responseStream?: StreamCreator): OpenNextNodeResponse;
94
+ export declare function invalidateCDNOnRequest(params: RoutingResult, headers: OutgoingHttpHeaders): Promise<void>;
@@ -352,7 +352,8 @@ export function fixISRHeaders(headers) {
352
352
  * @returns
353
353
  * @__PURE__
354
354
  */
355
- export function createServerResponse(internalEvent, headers, responseStream) {
355
+ export function createServerResponse(routingResult, headers, responseStream) {
356
+ const internalEvent = routingResult.internalEvent;
356
357
  return new OpenNextNodeResponse((_headers) => {
357
358
  fixCacheHeaderForHtmlPages(internalEvent, _headers);
358
359
  fixSWRCacheHeader(_headers);
@@ -360,5 +361,21 @@ export function createServerResponse(internalEvent, headers, responseStream) {
360
361
  fixISRHeaders(_headers);
361
362
  }, async (_headers) => {
362
363
  await revalidateIfRequired(internalEvent.headers.host, internalEvent.rawPath, _headers);
364
+ await invalidateCDNOnRequest(routingResult, _headers);
363
365
  }, responseStream, headers);
364
366
  }
367
+ // This function is used only for `res.revalidate()`
368
+ export async function invalidateCDNOnRequest(params, headers) {
369
+ const { internalEvent, initialPath, resolvedRoutes } = params;
370
+ const isIsrRevalidation = internalEvent.headers["x-isr"] === "1";
371
+ if (!isIsrRevalidation &&
372
+ headers[CommonHeaders.NEXT_CACHE] === "REVALIDATED") {
373
+ await globalThis.cdnInvalidationHandler.invalidatePaths([
374
+ {
375
+ initialPath,
376
+ rawPath: internalEvent.rawPath,
377
+ resolvedRoutes,
378
+ },
379
+ ]);
380
+ }
381
+ }
@@ -0,0 +1,9 @@
1
+ declare const _default: {
2
+ name: string;
3
+ invalidatePaths: (paths: {
4
+ initialPath: string;
5
+ rawPath: string;
6
+ resolvedRoutes: import("../../types/open-next").ResolvedRoute[];
7
+ }[]) => Promise<void>;
8
+ };
9
+ export default _default;
@@ -0,0 +1,26 @@
1
+ import { CloudFrontClient, CreateInvalidationCommand, } from "@aws-sdk/client-cloudfront";
2
+ const cloudfront = new CloudFrontClient({});
3
+ export default {
4
+ name: "cloudfront",
5
+ invalidatePaths: async (paths) => {
6
+ const constructedPaths = paths.flatMap(({ initialPath, resolvedRoutes }) => {
7
+ const isAppRouter = resolvedRoutes.some((route) => route.type === "app");
8
+ // revalidateTag doesn't have any leading slash, remove it just to be sure
9
+ const path = initialPath.replace(/^\//, "");
10
+ return isAppRouter
11
+ ? [`/${path}`, `/${path}?_rsc=*`]
12
+ : [
13
+ `/${path}`,
14
+ `/_next/data/${process.env.NEXT_BUILD_ID}${path === "/" ? "/index" : `/${path}`}.json*`,
15
+ ];
16
+ });
17
+ await cloudfront.send(new CreateInvalidationCommand({
18
+ DistributionId: process.env.CLOUDFRONT_DISTRIBUTION_ID,
19
+ InvalidationBatch: {
20
+ // Do we need to limit the number of paths? Or batch them into multiple commands?
21
+ Paths: { Quantity: constructedPaths.length, Items: constructedPaths },
22
+ CallerReference: `${Date.now()}`,
23
+ },
24
+ }));
25
+ },
26
+ };
@@ -0,0 +1,9 @@
1
+ declare const _default: {
2
+ name: string;
3
+ invalidatePaths: (_: {
4
+ initialPath: string;
5
+ rawPath: string;
6
+ resolvedRoutes: import("../../types/open-next").ResolvedRoute[];
7
+ }[]) => Promise<void>;
8
+ };
9
+ export default _default;
@@ -0,0 +1,6 @@
1
+ export default {
2
+ name: "dummy",
3
+ invalidatePaths: (_) => {
4
+ return Promise.resolve();
5
+ },
6
+ };
@@ -0,0 +1,10 @@
1
+ import fs from "node:fs";
2
+ declare const _default: {
3
+ name: string;
4
+ load: (url: string) => Promise<{
5
+ body: fs.ReadStream;
6
+ contentType: string;
7
+ cacheControl: string;
8
+ }>;
9
+ };
10
+ export default _default;
@@ -0,0 +1,14 @@
1
+ import fs from "node:fs";
2
+ export default {
3
+ name: "fs-dev",
4
+ load: async (url) => {
5
+ const basePath = "../../assets";
6
+ const body = fs.createReadStream(`${basePath}/${url}`);
7
+ const contentType = url.endsWith(".png") ? "image/png" : "image/jpeg";
8
+ return {
9
+ body,
10
+ contentType,
11
+ cacheControl: "public, max-age=31536000, immutable",
12
+ };
13
+ },
14
+ };
@@ -0,0 +1,3 @@
1
+ import type { IncrementalCache } from "../../types/overrides.js";
2
+ declare const cache: IncrementalCache;
3
+ export default cache;
@@ -0,0 +1,30 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ const buildId = process.env.NEXT_BUILD_ID;
4
+ const basePath = path.resolve(process.cwd(), `../../cache/${buildId}`);
5
+ const getCacheKey = (key) => {
6
+ return path.join(basePath, `${key}.cache`);
7
+ };
8
+ const cache = {
9
+ name: "fs-dev",
10
+ get: async (key) => {
11
+ const fileData = await fs.readFile(getCacheKey(key), "utf-8");
12
+ const data = JSON.parse(fileData);
13
+ const { mtime } = await fs.stat(getCacheKey(key));
14
+ return {
15
+ value: data,
16
+ lastModified: mtime.getTime(),
17
+ };
18
+ },
19
+ set: async (key, value, isFetch) => {
20
+ const data = JSON.stringify(value);
21
+ const cacheKey = getCacheKey(key);
22
+ // We need to create the directory before writing the file
23
+ await fs.mkdir(path.dirname(cacheKey), { recursive: true });
24
+ await fs.writeFile(cacheKey, data);
25
+ },
26
+ delete: async (key) => {
27
+ await fs.rm(getCacheKey(key));
28
+ },
29
+ };
30
+ export default cache;
@@ -0,0 +1,10 @@
1
+ import type { IncrementalCache } from "../../types/overrides";
2
+ /**
3
+ * This cache implementation uses a multi-tier cache with a local cache, a DynamoDB metadata cache and an S3 cache.
4
+ * It uses the same DynamoDB table as the default tag cache and the same S3 bucket as the default incremental cache.
5
+ * It will first check the local cache.
6
+ * If the local cache is expired, it will check the DynamoDB metadata cache to see if the local cache is still valid.
7
+ * Lastly it will check the S3 cache.
8
+ */
9
+ declare const multiTierCache: IncrementalCache;
10
+ export default multiTierCache;
@@ -0,0 +1,120 @@
1
+ import { customFetchClient } from "../../utils/fetch";
2
+ import { LRUCache } from "../../utils/lru";
3
+ import { debug } from "../../adapters/logger";
4
+ import S3Cache, { getAwsClient } from "./s3-lite";
5
+ // TTL for the local cache in milliseconds
6
+ const localCacheTTL = process.env.OPEN_NEXT_LOCAL_CACHE_TTL_MS
7
+ ? Number.parseInt(process.env.OPEN_NEXT_LOCAL_CACHE_TTL_MS, 10)
8
+ : 0;
9
+ // Maximum size of the local cache in nb of entries
10
+ const maxCacheSize = process.env.OPEN_NEXT_LOCAL_CACHE_SIZE
11
+ ? Number.parseInt(process.env.OPEN_NEXT_LOCAL_CACHE_SIZE, 10)
12
+ : 1000;
13
+ const localCache = new LRUCache(maxCacheSize);
14
+ const awsFetch = (body, type = "get") => {
15
+ const { CACHE_BUCKET_REGION } = process.env;
16
+ const client = getAwsClient();
17
+ return customFetchClient(client)(`https://dynamodb.${CACHE_BUCKET_REGION}.amazonaws.com`, {
18
+ method: "POST",
19
+ headers: {
20
+ "Content-Type": "application/x-amz-json-1.0",
21
+ "X-Amz-Target": `DynamoDB_20120810.${type === "get" ? "GetItem" : "PutItem"}`,
22
+ },
23
+ body,
24
+ });
25
+ };
26
+ const buildDynamoKey = (key) => {
27
+ const { NEXT_BUILD_ID } = process.env;
28
+ return `__meta_${NEXT_BUILD_ID}_${key}`;
29
+ };
30
+ /**
31
+ * This cache implementation uses a multi-tier cache with a local cache, a DynamoDB metadata cache and an S3 cache.
32
+ * It uses the same DynamoDB table as the default tag cache and the same S3 bucket as the default incremental cache.
33
+ * It will first check the local cache.
34
+ * If the local cache is expired, it will check the DynamoDB metadata cache to see if the local cache is still valid.
35
+ * Lastly it will check the S3 cache.
36
+ */
37
+ const multiTierCache = {
38
+ name: "multi-tier-ddb-s3",
39
+ async get(key, isFetch) {
40
+ // First we check the local cache
41
+ const localCacheEntry = localCache.get(key);
42
+ if (localCacheEntry) {
43
+ if (Date.now() - localCacheEntry.lastModified < localCacheTTL) {
44
+ debug("Using local cache without checking ddb");
45
+ return localCacheEntry;
46
+ }
47
+ try {
48
+ // Here we'll check ddb metadata to see if the local cache is still valid
49
+ const { CACHE_DYNAMO_TABLE } = process.env;
50
+ const result = await awsFetch(JSON.stringify({
51
+ TableName: CACHE_DYNAMO_TABLE,
52
+ Key: {
53
+ path: { S: buildDynamoKey(key) },
54
+ tag: { S: buildDynamoKey(key) },
55
+ },
56
+ }));
57
+ if (result.status === 200) {
58
+ const data = await result.json();
59
+ const hasBeenDeleted = data.Item?.deleted?.BOOL;
60
+ if (hasBeenDeleted) {
61
+ localCache.delete(key);
62
+ return { value: undefined, lastModified: 0 };
63
+ }
64
+ // If the metadata is older than the local cache, we can use the local cache
65
+ // If it's not found we assume that no write has been done yet and we can use the local cache
66
+ const lastModified = data.Item?.revalidatedAt?.N
67
+ ? Number.parseInt(data.Item.revalidatedAt.N, 10)
68
+ : 0;
69
+ if (lastModified <= localCacheEntry.lastModified) {
70
+ debug("Using local cache after checking ddb");
71
+ return localCacheEntry;
72
+ }
73
+ }
74
+ }
75
+ catch (e) {
76
+ debug("Failed to get metadata from ddb", e);
77
+ }
78
+ }
79
+ const result = await S3Cache.get(key, isFetch);
80
+ if (result.value) {
81
+ localCache.set(key, {
82
+ value: result.value,
83
+ lastModified: result.lastModified ?? Date.now(),
84
+ });
85
+ }
86
+ return result;
87
+ },
88
+ // Both for set and delete we choose to do the write to S3 first and then to DynamoDB
89
+ // Which means that if it fails in DynamoDB, instance that don't have local cache will work as expected.
90
+ // But instance that have local cache will have a stale cache until the next working set or delete.
91
+ async set(key, value, isFetch) {
92
+ const revalidatedAt = Date.now();
93
+ await S3Cache.set(key, value, isFetch);
94
+ await awsFetch(JSON.stringify({
95
+ TableName: process.env.CACHE_DYNAMO_TABLE,
96
+ Item: {
97
+ tag: { S: buildDynamoKey(key) },
98
+ path: { S: buildDynamoKey(key) },
99
+ revalidatedAt: { N: String(revalidatedAt) },
100
+ },
101
+ }), "set");
102
+ localCache.set(key, {
103
+ value,
104
+ lastModified: revalidatedAt,
105
+ });
106
+ },
107
+ async delete(key) {
108
+ await S3Cache.delete(key);
109
+ await awsFetch(JSON.stringify({
110
+ TableName: process.env.CACHE_DYNAMO_TABLE,
111
+ Item: {
112
+ tag: { S: buildDynamoKey(key) },
113
+ path: { S: buildDynamoKey(key) },
114
+ deleted: { BOOL: true },
115
+ },
116
+ }), "set");
117
+ localCache.delete(key);
118
+ },
119
+ };
120
+ export default multiTierCache;
@@ -1,3 +1,5 @@
1
+ import { AwsClient } from "aws4fetch";
1
2
  import type { IncrementalCache } from "../../types/overrides";
3
+ export declare const getAwsClient: () => AwsClient;
2
4
  declare const incrementalCache: IncrementalCache;
3
5
  export default incrementalCache;
@@ -5,7 +5,7 @@ import { IgnorableError, RecoverableError } from "../../utils/error";
5
5
  import { customFetchClient } from "../../utils/fetch";
6
6
  import { parseNumberFromEnv } from "../../adapters/util";
7
7
  let awsClient = null;
8
- const getAwsClient = () => {
8
+ export const getAwsClient = () => {
9
9
  const { CACHE_BUCKET_REGION } = process.env;
10
10
  if (awsClient) {
11
11
  return awsClient;
@@ -36,16 +36,17 @@ const nodeProxy = {
36
36
  method,
37
37
  rejectUnauthorized: false,
38
38
  }, (_res) => {
39
- const nodeReadableStream = _res.headers["content-encoding"] === "br"
39
+ const resHeaders = _res.headers;
40
+ const nodeReadableStream = resHeaders["content-encoding"] === "br"
40
41
  ? _res.pipe(require("node:zlib").createBrotliDecompress())
41
- : _res.headers["content-encoding"] === "gzip"
42
+ : resHeaders["content-encoding"] === "gzip"
42
43
  ? _res.pipe(require("node:zlib").createGunzip())
43
44
  : _res;
44
- const isBase64Encoded = isBinaryContentType(headers["content-type"]) ||
45
- !!headers["content-encoding"];
45
+ const isBase64Encoded = isBinaryContentType(resHeaders["content-type"]) ||
46
+ !!resHeaders["content-encoding"];
46
47
  const result = {
47
48
  type: "core",
48
- headers: filterHeadersForProxy(_res.headers),
49
+ headers: filterHeadersForProxy(resHeaders),
49
50
  statusCode: _res.statusCode ?? 200,
50
51
  // TODO: check base64 encoding
51
52
  isBase64Encoded,
@@ -0,0 +1,3 @@
1
+ import type { Queue } from "../../types/overrides.js";
2
+ declare const queue: Queue;
3
+ export default queue;
@@ -0,0 +1,18 @@
1
+ const queue = {
2
+ name: "dev-queue",
3
+ send: async (message) => {
4
+ const prerenderManifest = (await import("../../adapters/config/index.js"))
5
+ .PrerenderManifest;
6
+ const { host, url } = message.MessageBody;
7
+ const protocol = host.includes("localhost") ? "http" : "https";
8
+ const revalidateId = prerenderManifest.preview.previewModeId;
9
+ await globalThis.internalFetch(`${protocol}://${host}${url}`, {
10
+ method: "HEAD",
11
+ headers: {
12
+ "x-prerender-revalidate": revalidateId,
13
+ "x-isr": "1",
14
+ },
15
+ });
16
+ },
17
+ };
18
+ export default queue;
@@ -0,0 +1,3 @@
1
+ import type { TagCache } from "../../types/overrides";
2
+ declare const tagCache: TagCache;
3
+ export default tagCache;
@@ -0,0 +1,32 @@
1
+ import fs from "node:fs";
2
+ const tagFile = "../../dynamodb-provider/dynamodb-cache.json";
3
+ const tagContent = fs.readFileSync(tagFile, "utf-8");
4
+ let tags = JSON.parse(tagContent);
5
+ const tagCache = {
6
+ name: "fs-dev",
7
+ getByPath: async (path) => {
8
+ return tags
9
+ .filter((tagPathMapping) => tagPathMapping.path.S === path)
10
+ .map((tag) => tag.tag.S);
11
+ },
12
+ getByTag: async (tag) => {
13
+ return tags
14
+ .filter((tagPathMapping) => tagPathMapping.tag.S === tag)
15
+ .map((tag) => tag.path.S);
16
+ },
17
+ getLastModified: async (path, lastModified) => {
18
+ const revalidatedTags = tags.filter((tagPathMapping) => tagPathMapping.path.S === path &&
19
+ Number.parseInt(tagPathMapping.revalidatedAt.N) > (lastModified ?? 0));
20
+ return revalidatedTags.length > 0 ? -1 : (lastModified ?? Date.now());
21
+ },
22
+ writeTags: async (newTags) => {
23
+ const newTagsSet = new Set(newTags.map(({ tag, path }) => `${tag}-${path}`));
24
+ const unchangedTags = tags.filter(({ tag, path }) => !newTagsSet.has(`${tag.S}-${path.S}`));
25
+ tags = unchangedTags.concat(newTags.map((tag) => ({
26
+ tag: { S: tag.tag },
27
+ path: { S: tag.path },
28
+ revalidatedAt: { N: String(tag.revalidatedAt) },
29
+ })));
30
+ },
31
+ };
32
+ export default tagCache;
@@ -1,5 +1,7 @@
1
- const cfPropNameToHeaderName = {
2
- city: "x-open-next-city",
1
+ const cfPropNameMapping = {
2
+ // The city name is percent-encoded.
3
+ // See https://github.com/vercel/vercel/blob/4cb6143/packages/functions/src/headers.ts#L94C19-L94C37
4
+ city: [encodeURIComponent, "x-open-next-city"],
3
5
  country: "x-open-next-country",
4
6
  regionCode: "x-open-next-region",
5
7
  latitude: "x-open-next-latitude",
@@ -20,10 +22,15 @@ const handler = async (handler, converter) => async (request, env, ctx) => {
20
22
  // See https://developers.cloudflare.com/workers/runtime-apis/request
21
23
  // Note: This code could be moved to a cloudflare specific converter when one is created.
22
24
  const cfProperties = request.cf;
23
- for (const [propName, headerName] of Object.entries(cfPropNameToHeaderName)) {
25
+ for (const [propName, mapping] of Object.entries(cfPropNameMapping)) {
24
26
  const propValue = cfProperties?.[propName];
25
27
  if (propValue != null) {
26
- internalEvent.headers[headerName] = propValue;
28
+ const [encode, headerName] = Array.isArray(mapping)
29
+ ? mapping
30
+ : [null, mapping];
31
+ internalEvent.headers[headerName] = encode
32
+ ? encode(propValue)
33
+ : propValue;
27
34
  }
28
35
  }
29
36
  const response = await handler(internalEvent);
@@ -2,6 +2,6 @@ import type { WrapperHandler } from "../../types/overrides";
2
2
  declare const _default: {
3
3
  name: string;
4
4
  wrapper: WrapperHandler;
5
- supportStreaming: false;
5
+ supportStreaming: true;
6
6
  };
7
7
  export default _default;
@@ -1,6 +1,10 @@
1
- const dummyWrapper = async () => async () => undefined;
1
+ const dummyWrapper = async (handler, converter) => {
2
+ return async (event, responseStream) => {
3
+ return await handler(event, responseStream);
4
+ };
5
+ };
2
6
  export default {
3
7
  name: "dummy",
4
8
  wrapper: dummyWrapper,
5
- supportStreaming: false,
9
+ supportStreaming: true,
6
10
  };
@@ -0,0 +1,7 @@
1
+ import type { WrapperHandler } from "../../types/overrides.js";
2
+ declare const _default: {
3
+ wrapper: WrapperHandler;
4
+ name: string;
5
+ supportStreaming: boolean;
6
+ };
7
+ export default _default;
@@ -0,0 +1,43 @@
1
+ import express from "express";
2
+ const wrapper = async (handler, converter) => {
3
+ const app = express();
4
+ // To serve static assets
5
+ app.use(express.static("../../assets"));
6
+ const imageHandlerPath = "../../image-optimization-function/index.mjs";
7
+ const imageHandler = await import(imageHandlerPath).then((m) => m.handler);
8
+ app.all("/_next/image", async (req, res) => {
9
+ const internalEvent = await converter.convertFrom(req);
10
+ const _res = {
11
+ writeHeaders: (prelude) => {
12
+ res.writeHead(prelude.statusCode, prelude.headers);
13
+ return res;
14
+ },
15
+ };
16
+ await imageHandler(internalEvent, _res);
17
+ });
18
+ app.all("*paths", async (req, res) => {
19
+ const internalEvent = await converter.convertFrom(req);
20
+ const _res = {
21
+ writeHeaders: (prelude) => {
22
+ res.writeHead(prelude.statusCode, prelude.headers);
23
+ return res;
24
+ },
25
+ onFinish: () => { },
26
+ };
27
+ await handler(internalEvent, _res);
28
+ });
29
+ const server = app.listen(Number.parseInt(process.env.PORT ?? "3000", 10), () => {
30
+ console.log(`Server running on port ${process.env.PORT ?? 3000}`);
31
+ });
32
+ app.on("error", (err) => {
33
+ console.error("error", err);
34
+ });
35
+ return () => {
36
+ server.close();
37
+ };
38
+ };
39
+ export default {
40
+ wrapper,
41
+ name: "expresss-dev",
42
+ supportStreaming: true,
43
+ };
@@ -3,6 +3,7 @@ import path from "node:path";
3
3
  import chalk from "chalk";
4
4
  import { loadAppPathRoutesManifest, loadAppPathsManifest, loadAppPathsManifestKeys, loadBuildId, loadConfig, loadConfigHeaders, loadHtmlPages, loadMiddlewareManifest, loadPrerenderManifest, loadRoutesManifest, } from "../adapters/config/util.js";
5
5
  import logger from "../logger.js";
6
+ import { getCrossPlatformPathRegex } from "../utils/regex.js";
6
7
  /**
7
8
  * @param opts.nextDir - The path to the .next directory
8
9
  * @param opts.edgeFunctionHandlerPath - The path to the edgeFunctionHandler.js file that we'll use to bundle the routing
@@ -28,7 +29,7 @@ export function openNextEdgePlugins({ nextDir, edgeFunctionHandlerPath, middlewa
28
29
  logger.debug(chalk.blue("OpenNext Edge plugin"));
29
30
  if (edgeFunctionHandlerPath) {
30
31
  // If we bundle the routing, we need to resolve the middleware
31
- build.onResolve({ filter: /\.\/middleware.mjs/g }, () => {
32
+ build.onResolve({ filter: getCrossPlatformPathRegex("./middleware.mjs") }, () => {
32
33
  return {
33
34
  path: edgeFunctionHandlerPath,
34
35
  };
@@ -55,7 +56,7 @@ export function openNextEdgePlugins({ nextDir, edgeFunctionHandlerPath, middlewa
55
56
  loader: "js",
56
57
  }));
57
58
  // We inject the entry files into the edgeFunctionHandler
58
- build.onLoad({ filter: /\/edgeFunctionHandler.js/g }, async (args) => {
59
+ build.onLoad({ filter: getCrossPlatformPathRegex("/edgeFunctionHandler.js") }, async (args) => {
59
60
  let contents = readFileSync(args.path, "utf-8");
60
61
  contents = `
61
62
  globalThis._ENTRIES = {};
@@ -117,7 +118,7 @@ ${contents}
117
118
  contents,
118
119
  };
119
120
  });
120
- build.onLoad({ filter: /adapters\/config\/index/g }, async () => {
121
+ build.onLoad({ filter: getCrossPlatformPathRegex("adapters/config/index") }, async () => {
121
122
  const NextConfig = loadConfig(nextDir);
122
123
  const BuildId = loadBuildId(nextDir);
123
124
  const HtmlPages = loadHtmlPages(nextDir);
@@ -8,7 +8,7 @@ export interface IPluginSettings {
8
8
  /**
9
9
  *
10
10
  * openNextPlugin({
11
- * target: /plugins\/default\.js/g,
11
+ * target: /plugins(\/|\\)default\.js/g,
12
12
  * replacements: [require.resolve("./plugins/default.js")],
13
13
  * deletes: ["id1"],
14
14
  * })
@@ -6,7 +6,7 @@ const importPattern = /\/\/#import([\s\S]*?)\n\/\/#endImport/gm;
6
6
  /**
7
7
  *
8
8
  * openNextPlugin({
9
- * target: /plugins\/default\.js/g,
9
+ * target: /plugins(\/|\\)default\.js/g,
10
10
  * replacements: [require.resolve("./plugins/default.js")],
11
11
  * deletes: ["id1"],
12
12
  * })
@@ -12,6 +12,7 @@ export interface IPluginSettings {
12
12
  originResolver?: LazyLoadedOverride<OriginResolver> | IncludedOriginResolver;
13
13
  warmer?: LazyLoadedOverride<Warmer> | IncludedWarmer;
14
14
  proxyExternalRequest?: OverrideOptions["proxyExternalRequest"];
15
+ cdnInvalidation?: OverrideOptions["cdnInvalidation"];
15
16
  };
16
17
  fnName?: string;
17
18
  }
@@ -1,6 +1,7 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import chalk from "chalk";
3
3
  import logger from "../logger.js";
4
+ import { getCrossPlatformPathRegex } from "../utils/regex.js";
4
5
  function getOverrideOrDummy(override) {
5
6
  if (typeof override === "string") {
6
7
  return override;
@@ -19,6 +20,7 @@ const nameToFolder = {
19
20
  originResolver: "originResolver",
20
21
  warmer: "warmer",
21
22
  proxyExternalRequest: "proxyExternalRequest",
23
+ cdnInvalidation: "cdnInvalidation",
22
24
  };
23
25
  const defaultOverrides = {
24
26
  wrapper: "aws-lambda",
@@ -30,6 +32,7 @@ const defaultOverrides = {
30
32
  originResolver: "pattern-env",
31
33
  warmer: "aws-lambda",
32
34
  proxyExternalRequest: "node",
35
+ cdnInvalidation: "dummy",
33
36
  };
34
37
  /**
35
38
  * @param opts.overrides - The name of the overrides to use
@@ -40,7 +43,7 @@ export function openNextResolvePlugin({ overrides, fnName, }) {
40
43
  name: "opennext-resolve",
41
44
  setup(build) {
42
45
  logger.debug(chalk.blue("OpenNext Resolve plugin"), fnName ? `for ${fnName}` : "");
43
- build.onLoad({ filter: /core(\/|\\)resolve\.js/g }, async (args) => {
46
+ build.onLoad({ filter: getCrossPlatformPathRegex("core/resolve.js") }, async (args) => {
44
47
  let contents = readFileSync(args.path, "utf-8");
45
48
  const overridesEntries = Object.entries(overrides ?? {});
46
49
  for (let [overrideName, overrideValue] of overridesEntries) {
@@ -1,6 +1,6 @@
1
1
  import type { AsyncLocalStorage } from "node:async_hooks";
2
2
  import type { OutgoingHttpHeaders } from "node:http";
3
- import type { IncrementalCache, ProxyExternalRequest, Queue, TagCache } from "./overrides";
3
+ import type { CDNInvalidationHandler, IncrementalCache, ProxyExternalRequest, Queue, TagCache } from "./overrides";
4
4
  import type { DetachedPromiseRunner } from "../utils/promise";
5
5
  import type { OpenNextConfig } from "./open-next";
6
6
  export interface RequestData {
@@ -184,5 +184,11 @@ declare global {
184
184
  * Defined in `createMainHandler`.
185
185
  */
186
186
  var proxyExternalRequest: ProxyExternalRequest;
187
+ /**
188
+ * The function that will be called when the CDN needs invalidating (either from `revalidateTag` or from `res.revalidate`)
189
+ * Available in main functions
190
+ * Defined in `createMainHandler`
191
+ */
192
+ var cdnInvalidationHandler: CDNInvalidationHandler;
187
193
  }
188
194
  export {};
@@ -1,7 +1,7 @@
1
1
  import type { ReadableStream } from "node:stream/web";
2
2
  import type { Writable } from "node:stream";
3
3
  import type { WarmerEvent, WarmerResponse } from "../adapters/warmer-function";
4
- import type { Converter, ImageLoader, IncrementalCache, OriginResolver, ProxyExternalRequest, Queue, TagCache, Warmer, Wrapper } from "./overrides";
4
+ import type { CDNInvalidationHandler, Converter, ImageLoader, IncrementalCache, OriginResolver, ProxyExternalRequest, Queue, TagCache, Warmer, Wrapper } from "./overrides";
5
5
  export type BaseEventOrResult<T extends string = string> = {
6
6
  type: T;
7
7
  };
@@ -66,7 +66,7 @@ export interface Origin {
66
66
  port?: number;
67
67
  customHeaders?: Record<string, string>;
68
68
  }
69
- export type IncludedWrapper = "aws-lambda" | "aws-lambda-streaming" | "node" | "cloudflare" | "cloudflare-edge" | "cloudflare-node" | "dummy";
69
+ export type IncludedWrapper = "aws-lambda" | "aws-lambda-streaming" | "node" | "cloudflare" | "cloudflare-edge" | "cloudflare-node" | "express-dev" | "dummy";
70
70
  export type IncludedConverter = "aws-apigw-v2" | "aws-apigw-v1" | "aws-cloudfront" | "edge" | "node" | "sqs-revalidate" | "dummy";
71
71
  export type RouteType = "route" | "page" | "app";
72
72
  export interface ResolvedRoute {
@@ -83,13 +83,14 @@ export interface RoutingResult {
83
83
  }
84
84
  export interface MiddlewareResult extends RoutingResult, BaseEventOrResult<"middleware"> {
85
85
  }
86
- export type IncludedQueue = "sqs" | "sqs-lite" | "dummy";
87
- export type IncludedIncrementalCache = "s3" | "s3-lite" | "dummy";
88
- export type IncludedTagCache = "dynamodb" | "dynamodb-lite" | "dummy";
89
- export type IncludedImageLoader = "s3" | "host" | "dummy";
86
+ export type IncludedQueue = "sqs" | "sqs-lite" | "direct" | "dummy";
87
+ export type IncludedIncrementalCache = "s3" | "s3-lite" | "multi-tier-ddb-s3" | "fs-dev" | "dummy";
88
+ export type IncludedTagCache = "dynamodb" | "dynamodb-lite" | "fs-dev" | "dummy";
89
+ export type IncludedImageLoader = "s3" | "host" | "fs-dev" | "dummy";
90
90
  export type IncludedOriginResolver = "pattern-env" | "dummy";
91
91
  export type IncludedWarmer = "aws-lambda" | "dummy";
92
92
  export type IncludedProxyExternalRequest = "node" | "fetch" | "dummy";
93
+ export type IncludedCDNInvalidationHandler = "cloudfront" | "dummy";
93
94
  export interface DefaultOverrideOptions<E extends BaseEventOrResult = InternalEvent, R extends BaseEventOrResult = InternalResult> {
94
95
  /**
95
96
  * This is the main entrypoint of your app.
@@ -129,6 +130,11 @@ export interface OverrideOptions extends DefaultOverrideOptions {
129
130
  * @default "node"
130
131
  */
131
132
  proxyExternalRequest?: IncludedProxyExternalRequest | LazyLoadedOverride<ProxyExternalRequest>;
133
+ /**
134
+ * Add possibility to override the default cdn invalidation for On Demand Revalidation
135
+ * @default "dummy"
136
+ */
137
+ cdnInvalidation?: IncludedCDNInvalidationHandler | LazyLoadedOverride<CDNInvalidationHandler>;
132
138
  }
133
139
  export interface InstallOptions {
134
140
  /**
@@ -1,6 +1,6 @@
1
1
  import type { Readable } from "node:stream";
2
2
  import type { Meta } from "./cache";
3
- import type { BaseEventOrResult, BaseOverride, InternalEvent, InternalResult, Origin, StreamCreator } from "./open-next";
3
+ import type { BaseEventOrResult, BaseOverride, InternalEvent, InternalResult, Origin, ResolvedRoute, StreamCreator } from "./open-next";
4
4
  export interface QueueMessage {
5
5
  MessageDeduplicationId: string;
6
6
  MessageBody: {
@@ -84,3 +84,12 @@ export type OriginResolver = BaseOverride & {
84
84
  export type ProxyExternalRequest = BaseOverride & {
85
85
  proxy: (event: InternalEvent) => Promise<InternalResult>;
86
86
  };
87
+ type CDNPath = {
88
+ initialPath: string;
89
+ rawPath: string;
90
+ resolvedRoutes: ResolvedRoute[];
91
+ };
92
+ export type CDNInvalidationHandler = BaseOverride & {
93
+ invalidatePaths: (paths: CDNPath[]) => Promise<void>;
94
+ };
95
+ export {};
@@ -0,0 +1,8 @@
1
+ export declare class LRUCache<T> {
2
+ private maxSize;
3
+ private cache;
4
+ constructor(maxSize: number);
5
+ get(key: string): T | undefined;
6
+ set(key: string, value: any): void;
7
+ delete(key: string): void;
8
+ }
@@ -0,0 +1,29 @@
1
+ export class LRUCache {
2
+ maxSize;
3
+ cache = new Map();
4
+ constructor(maxSize) {
5
+ this.maxSize = maxSize;
6
+ }
7
+ get(key) {
8
+ const result = this.cache.get(key);
9
+ // We could have used .has to allow for nullish value to be stored but we don't need that right now
10
+ if (result) {
11
+ // By removing and setting the key again we ensure it's the most recently used
12
+ this.cache.delete(key);
13
+ this.cache.set(key, result);
14
+ }
15
+ return result;
16
+ }
17
+ set(key, value) {
18
+ if (this.cache.size >= this.maxSize) {
19
+ const firstKey = this.cache.keys().next().value;
20
+ if (firstKey !== undefined) {
21
+ this.cache.delete(firstKey);
22
+ }
23
+ }
24
+ this.cache.set(key, value);
25
+ }
26
+ delete(key) {
27
+ this.cache.delete(key);
28
+ }
29
+ }
@@ -0,0 +1,18 @@
1
+ type Options = {
2
+ escape?: boolean;
3
+ flags?: string;
4
+ };
5
+ /**
6
+ * Constructs a regular expression for a path that supports separators for multiple platforms
7
+ * - Uses posix separators (`/`) as the input that should be made cross-platform.
8
+ * - Special characters are escaped by default but can be controlled through opts.escape.
9
+ * - Posix separators are always escaped.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * getCrossPlatformPathRegex("./middleware.mjs")
14
+ * getCrossPlatformPathRegex(String.raw`\./middleware\.(mjs|cjs)`, { escape: false })
15
+ * ```
16
+ */
17
+ export declare function getCrossPlatformPathRegex(regex: string, { escape: shouldEscape, flags }?: Options): RegExp;
18
+ export {};
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Constructs a regular expression for a path that supports separators for multiple platforms
3
+ * - Uses posix separators (`/`) as the input that should be made cross-platform.
4
+ * - Special characters are escaped by default but can be controlled through opts.escape.
5
+ * - Posix separators are always escaped.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * getCrossPlatformPathRegex("./middleware.mjs")
10
+ * getCrossPlatformPathRegex(String.raw`\./middleware\.(mjs|cjs)`, { escape: false })
11
+ * ```
12
+ */
13
+ export function getCrossPlatformPathRegex(regex, { escape: shouldEscape = true, flags = "g" } = {}) {
14
+ const newExpr = (shouldEscape ? regex.replace(/([[\]().*+?^$|{}\\])/g, "\\$1") : regex).replaceAll("/", String.raw `(?:\/|\\)`);
15
+ return new RegExp(newExpr, flags);
16
+ }
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "access": "public"
4
4
  },
5
5
  "name": "@opennextjs/aws",
6
- "version": "3.3.1",
6
+ "version": "3.4.0",
7
7
  "bin": {
8
8
  "open-next": "./dist/index.js"
9
9
  },
@@ -30,6 +30,7 @@
30
30
  "README.md"
31
31
  ],
32
32
  "dependencies": {
33
+ "@aws-sdk/client-cloudfront": "3.398.0",
33
34
  "@aws-sdk/client-dynamodb": "^3.398.0",
34
35
  "@aws-sdk/client-lambda": "^3.398.0",
35
36
  "@aws-sdk/client-s3": "^3.398.0",
@@ -41,12 +42,14 @@
41
42
  "aws4fetch": "^1.0.18",
42
43
  "chalk": "^5.3.0",
43
44
  "esbuild": "0.19.2",
45
+ "express": "5.0.1",
44
46
  "path-to-regexp": "^6.3.0",
45
47
  "promise.series": "^0.2.0",
46
48
  "urlpattern-polyfill": "^10.0.0"
47
49
  },
48
50
  "devDependencies": {
49
51
  "@types/aws-lambda": "^8.10.109",
52
+ "@types/express": "5.0.0",
50
53
  "@types/node": "20.17.6",
51
54
  "tsc-alias": "^1.8.8",
52
55
  "typescript": "5.6.3"