@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.
- package/dist/adapters/cache.js +19 -0
- package/dist/build/createImageOptimizationBundle.js +2 -1
- package/dist/build/createServerBundle.js +4 -4
- package/dist/build/edge/createEdgeBundle.js +2 -1
- package/dist/build/validateConfig.js +2 -1
- package/dist/core/createMainHandler.js +2 -1
- package/dist/core/requestHandler.js +9 -2
- package/dist/core/resolve.d.ts +4 -0
- package/dist/core/resolve.js +10 -0
- package/dist/core/routing/middleware.js +3 -1
- package/dist/core/routing/util.d.ts +3 -2
- package/dist/core/routing/util.js +18 -1
- package/dist/overrides/cdnInvalidation/cloudfront.d.ts +9 -0
- package/dist/overrides/cdnInvalidation/cloudfront.js +26 -0
- package/dist/overrides/cdnInvalidation/dummy.d.ts +9 -0
- package/dist/overrides/cdnInvalidation/dummy.js +6 -0
- package/dist/overrides/imageLoader/fs-dev.d.ts +10 -0
- package/dist/overrides/imageLoader/fs-dev.js +14 -0
- package/dist/overrides/incrementalCache/fs-dev.d.ts +3 -0
- package/dist/overrides/incrementalCache/fs-dev.js +30 -0
- package/dist/overrides/incrementalCache/multi-tier-ddb-s3.d.ts +10 -0
- package/dist/overrides/incrementalCache/multi-tier-ddb-s3.js +120 -0
- package/dist/overrides/incrementalCache/s3-lite.d.ts +2 -0
- package/dist/overrides/incrementalCache/s3-lite.js +1 -1
- package/dist/overrides/proxyExternalRequest/node.js +6 -5
- package/dist/overrides/queue/direct.d.ts +3 -0
- package/dist/overrides/queue/direct.js +18 -0
- package/dist/overrides/tagCache/fs-dev.d.ts +3 -0
- package/dist/overrides/tagCache/fs-dev.js +32 -0
- package/dist/overrides/wrappers/cloudflare-edge.js +11 -4
- package/dist/overrides/wrappers/dummy.d.ts +1 -1
- package/dist/overrides/wrappers/dummy.js +6 -2
- package/dist/overrides/wrappers/express-dev.d.ts +7 -0
- package/dist/overrides/wrappers/express-dev.js +43 -0
- package/dist/plugins/edge.js +4 -3
- package/dist/plugins/replacement.d.ts +1 -1
- package/dist/plugins/replacement.js +1 -1
- package/dist/plugins/resolve.d.ts +1 -0
- package/dist/plugins/resolve.js +4 -1
- package/dist/types/global.d.ts +7 -1
- package/dist/types/open-next.d.ts +12 -6
- package/dist/types/overrides.d.ts +10 -1
- package/dist/utils/lru.d.ts +8 -0
- package/dist/utils/lru.js +29 -0
- package/dist/utils/regex.d.ts +18 -0
- package/dist/utils/regex.js +16 -0
- package/package.json +4 -1
package/dist/adapters/cache.js
CHANGED
|
@@ -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: /
|
|
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: /
|
|
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: /
|
|
126
|
+
target: getCrossPlatformPathRegex("core/util.js"),
|
|
126
127
|
deletes: [
|
|
127
128
|
...(disableNextPrebundledReact ? ["requireHooks"] : []),
|
|
128
|
-
...(
|
|
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: /
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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 = {
|
package/dist/core/resolve.d.ts
CHANGED
|
@@ -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 {};
|
package/dist/core/resolve.js
CHANGED
|
@@ -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
|
|
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(
|
|
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(
|
|
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,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,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,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;
|
|
@@ -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
|
|
39
|
+
const resHeaders = _res.headers;
|
|
40
|
+
const nodeReadableStream = resHeaders["content-encoding"] === "br"
|
|
40
41
|
? _res.pipe(require("node:zlib").createBrotliDecompress())
|
|
41
|
-
:
|
|
42
|
+
: resHeaders["content-encoding"] === "gzip"
|
|
42
43
|
? _res.pipe(require("node:zlib").createGunzip())
|
|
43
44
|
: _res;
|
|
44
|
-
const isBase64Encoded = isBinaryContentType(
|
|
45
|
-
!!
|
|
45
|
+
const isBase64Encoded = isBinaryContentType(resHeaders["content-type"]) ||
|
|
46
|
+
!!resHeaders["content-encoding"];
|
|
46
47
|
const result = {
|
|
47
48
|
type: "core",
|
|
48
|
-
headers: filterHeadersForProxy(
|
|
49
|
+
headers: filterHeadersForProxy(resHeaders),
|
|
49
50
|
statusCode: _res.statusCode ?? 200,
|
|
50
51
|
// TODO: check base64 encoding
|
|
51
52
|
isBase64Encoded,
|
|
@@ -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,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
|
|
2
|
-
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,
|
|
25
|
+
for (const [propName, mapping] of Object.entries(cfPropNameMapping)) {
|
|
24
26
|
const propValue = cfProperties?.[propName];
|
|
25
27
|
if (propValue != null) {
|
|
26
|
-
|
|
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);
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
const dummyWrapper = async (
|
|
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:
|
|
9
|
+
supportStreaming: true,
|
|
6
10
|
};
|
|
@@ -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
|
+
};
|
package/dist/plugins/edge.js
CHANGED
|
@@ -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:
|
|
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:
|
|
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: /
|
|
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);
|
|
@@ -6,7 +6,7 @@ const importPattern = /\/\/#import([\s\S]*?)\n\/\/#endImport/gm;
|
|
|
6
6
|
/**
|
|
7
7
|
*
|
|
8
8
|
* openNextPlugin({
|
|
9
|
-
* target: /plugins
|
|
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
|
}
|
package/dist/plugins/resolve.js
CHANGED
|
@@ -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: /
|
|
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) {
|
package/dist/types/global.d.ts
CHANGED
|
@@ -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,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.
|
|
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"
|