diff --git a/.changeset/fuzzy-jeans-draw.md b/.changeset/fuzzy-jeans-draw.md new file mode 100644 index 000000000..3312d39fe --- /dev/null +++ b/.changeset/fuzzy-jeans-draw.md @@ -0,0 +1,5 @@ +--- +"@opennextjs/aws": minor +--- + +Refactor overrides diff --git a/examples/app-router/app/image-optimization/page.tsx b/examples/app-router/app/image-optimization/page.tsx index baba473bc..05e171396 100644 --- a/examples/app-router/app/image-optimization/page.tsx +++ b/examples/app-router/app/image-optimization/page.tsx @@ -4,7 +4,7 @@ export default function ImageOptimization() { return (
Open Next architecture { - const openNextParams = globalThis.openNextConfig.middleware; - if (typeof openNextParams?.originResolver === "function") { - return openNextParams.originResolver(); - } - - return Promise.resolve({ - name: "env", - resolve: async (_path: string) => { - try { - const origin = JSON.parse( - process.env.OPEN_NEXT_ORIGIN ?? "{}", - ) as Record; - for (const [key, value] of Object.entries( - globalThis.openNextConfig.functions ?? {}, - ).filter(([key]) => key !== "default")) { - if ( - value.patterns.some((pattern) => { - // Convert cloudfront pattern to regex - return new RegExp( - // transform glob pattern to regex - "/" + - pattern - .replace(/\*\*/g, "(.*)") - .replace(/\*/g, "([^/]*)") - .replace(/\//g, "\\/") - .replace(/\?/g, "."), - ).test(_path); - }) - ) { - debug("Using origin", key, value.patterns); - return origin[key]; - } - } - if (_path.startsWith("/_next/image") && origin["imageOptimizer"]) { - debug("Using origin", "imageOptimizer", _path); - return origin["imageOptimizer"]; - } - if (origin["default"]) { - debug("Using default origin", origin["default"], _path); - return origin["default"]; - } - return false as const; - } catch (e) { - error("Error while resolving origin", e); - return false as const; - } - }, - }); -}; - globalThis.internalFetch = fetch; const defaultHandler = async (internalEvent: InternalEvent) => { - const originResolver = await resolveOriginResolver(); + const originResolver = await resolveOriginResolver( + globalThis.openNextConfig.middleware?.originResolver, + ); //#override includeCacheInMiddleware globalThis.tagCache = await resolveTagCache( diff --git a/packages/open-next/src/adapters/warmer-function.ts b/packages/open-next/src/adapters/warmer-function.ts index 00addca4b..7d6cf067a 100644 --- a/packages/open-next/src/adapters/warmer-function.ts +++ b/packages/open-next/src/adapters/warmer-function.ts @@ -1,7 +1,5 @@ -import { Warmer } from "types/open-next.js"; - import { createGenericHandler } from "../core/createGenericHandler.js"; -import { debug, error } from "./logger.js"; +import { resolveWarmerInvoke } from "../core/resolve.js"; import { generateUniqueId } from "./util.js"; export interface WarmerEvent { @@ -17,88 +15,6 @@ export interface WarmerResponse { serverId: string; } -const resolveWarmerInvoke = async () => { - const openNextParams = globalThis.openNextConfig.warmer!; - if (typeof openNextParams?.invokeFunction === "function") { - return await openNextParams.invokeFunction(); - } else { - return Promise.resolve({ - name: "aws-invoke", - invoke: async (warmerId: string) => { - const { InvokeCommand, LambdaClient } = await import( - "@aws-sdk/client-lambda" - ); - const lambda = new LambdaClient({}); - const warmParams = JSON.parse(process.env.WARM_PARAMS!) as { - concurrency: number; - function: string; - }[]; - - for (const warmParam of warmParams) { - const { concurrency: CONCURRENCY, function: FUNCTION_NAME } = - warmParam; - debug({ - event: "warmer invoked", - functionName: FUNCTION_NAME, - concurrency: CONCURRENCY, - warmerId, - }); - const ret = await Promise.all( - Array.from({ length: CONCURRENCY }, (_v, i) => i).map((i) => { - try { - return lambda.send( - new InvokeCommand({ - FunctionName: FUNCTION_NAME, - InvocationType: "RequestResponse", - Payload: Buffer.from( - JSON.stringify({ - type: "warmer", - warmerId, - index: i, - concurrency: CONCURRENCY, - delay: 75, - } satisfies WarmerEvent), - ), - }), - ); - } catch (e) { - error(`failed to warm up #${i}`, e); - // ignore error - } - }), - ); - - // Print status - - const warmedServerIds = ret - .map((r, i) => { - if (r?.StatusCode !== 200 || !r?.Payload) { - error(`failed to warm up #${i}:`, r?.Payload?.toString()); - return; - } - const payload = JSON.parse( - Buffer.from(r.Payload).toString(), - ) as WarmerResponse; - return { - statusCode: r.StatusCode, - payload, - type: "warmer" as const, - }; - }) - .filter((r): r is Exclude => !!r); - - debug({ - event: "warmer result", - sent: CONCURRENCY, - success: warmedServerIds.length, - uniqueServersWarmed: [...new Set(warmedServerIds)].length, - }); - } - }, - }); - } -}; - export const handler = await createGenericHandler({ handler: defaultHandler, type: "warmer", @@ -107,7 +23,9 @@ export const handler = await createGenericHandler({ async function defaultHandler() { const warmerId = `warmer-${generateUniqueId()}`; - const invokeFn = await resolveWarmerInvoke(); + const invokeFn = await resolveWarmerInvoke( + globalThis.openNextConfig.warmer?.invokeFunction, + ); await invokeFn.invoke(warmerId); diff --git a/packages/open-next/src/core/createMainHandler.ts b/packages/open-next/src/core/createMainHandler.ts index deb5ecda9..e9a1f378d 100644 --- a/packages/open-next/src/core/createMainHandler.ts +++ b/packages/open-next/src/core/createMainHandler.ts @@ -5,8 +5,8 @@ import { DetachedPromiseRunner } from "utils/promise"; import { debug } from "../adapters/logger"; import { generateUniqueId } from "../adapters/util"; -import type { IncrementalCache } from "../cache/incremental/types"; -import type { Queue } from "../queue/types"; +import type { IncrementalCache } from "../overrides/incrementalCache/types"; +import type { Queue } from "../overrides/queue/types"; import { openNextHandler } from "./requestHandler.js"; import { resolveConverter, diff --git a/packages/open-next/src/core/resolve.ts b/packages/open-next/src/core/resolve.ts index aac5a8a54..28c73c4a0 100644 --- a/packages/open-next/src/core/resolve.ts +++ b/packages/open-next/src/core/resolve.ts @@ -6,11 +6,13 @@ import { InternalEvent, InternalResult, LazyLoadedOverride, + OriginResolver, OverrideOptions, + Warmer, Wrapper, } from "types/open-next.js"; -import { TagCache } from "../cache/tag/types.js"; +import { TagCache } from "../overrides/tagCache/types.js"; export async function resolveConverter< E extends BaseEventOrResult = InternalEvent, @@ -21,7 +23,7 @@ export async function resolveConverter< if (typeof converter === "function") { return converter(); } else { - const m_1 = await import(`../converters/aws-apigw-v2.js`); + const m_1 = await import(`../overrides/converters/aws-apigw-v2.js`); // @ts-expect-error return m_1.default; } @@ -35,7 +37,7 @@ export async function resolveWrapper< return wrapper(); } else { // This will be replaced by the bundler - const m_1 = await import("../wrappers/aws-lambda.js"); + const m_1 = await import("../overrides/wrappers/aws-lambda.js"); // @ts-expect-error return m_1.default; } @@ -54,7 +56,7 @@ export async function resolveTagCache( return tagCache(); } else { // This will be replaced by the bundler - const m_1 = await import("../cache/tag/dynamodb.js"); + const m_1 = await import("../overrides/tagCache/dynamodb.js"); return m_1.default; } } @@ -69,7 +71,7 @@ export async function resolveQueue(queue: OverrideOptions["queue"]) { if (typeof queue === "function") { return queue(); } else { - const m_1 = await import("../queue/sqs.js"); + const m_1 = await import("../overrides/queue/sqs.js"); return m_1.default; } } @@ -86,7 +88,7 @@ export async function resolveIncrementalCache( if (typeof incrementalCache === "function") { return incrementalCache(); } else { - const m_1 = await import("../cache/incremental/s3.js"); + const m_1 = await import("../overrides/incrementalCache/s3.js"); return m_1.default; } } @@ -106,3 +108,32 @@ export async function resolveImageLoader( return m_1.default; } } + +/** + * @returns + * @__PURE__ + */ +export async function resolveOriginResolver( + originResolver?: LazyLoadedOverride | string, +) { + if (typeof originResolver === "function") { + return originResolver(); + } else { + const m_1 = await import("../overrides/originResolver/pattern-env.js"); + return m_1.default; + } +} + +/** + * @__PURE__ + */ +export async function resolveWarmerInvoke( + warmer?: LazyLoadedOverride | "aws-lambda", +) { + if (typeof warmer === "function") { + return warmer(); + } else { + const m_1 = await import("../overrides/warmer/aws-lambda.js"); + return m_1.default; + } +} diff --git a/packages/open-next/src/core/routing/cacheInterceptor.ts b/packages/open-next/src/core/routing/cacheInterceptor.ts index a3f83f0cc..29848753c 100644 --- a/packages/open-next/src/core/routing/cacheInterceptor.ts +++ b/packages/open-next/src/core/routing/cacheInterceptor.ts @@ -5,7 +5,7 @@ import { InternalEvent, InternalResult } from "types/open-next"; import { emptyReadableStream, toReadableStream } from "utils/stream"; import { debug } from "../../adapters/logger"; -import { CacheValue } from "../../cache/incremental/types"; +import { CacheValue } from "../../overrides/incrementalCache/types"; import { localizePath } from "./i18n"; import { generateMessageGroupId } from "./util"; diff --git a/packages/open-next/src/converters/aws-apigw-v1.ts b/packages/open-next/src/overrides/converters/aws-apigw-v1.ts similarity index 96% rename from packages/open-next/src/converters/aws-apigw-v1.ts rename to packages/open-next/src/overrides/converters/aws-apigw-v1.ts index c5e1fcfa6..4dbc1e2ec 100644 --- a/packages/open-next/src/converters/aws-apigw-v1.ts +++ b/packages/open-next/src/overrides/converters/aws-apigw-v1.ts @@ -1,8 +1,8 @@ -import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda"; +import type { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda"; import type { Converter, InternalEvent, InternalResult } from "types/open-next"; import { fromReadableStream } from "utils/stream"; -import { debug } from "../adapters/logger"; +import { debug } from "../../adapters/logger"; import { removeUndefinedFromQuery } from "./utils"; function normalizeAPIGatewayProxyEventHeaders( diff --git a/packages/open-next/src/converters/aws-apigw-v2.ts b/packages/open-next/src/overrides/converters/aws-apigw-v2.ts similarity index 94% rename from packages/open-next/src/converters/aws-apigw-v2.ts rename to packages/open-next/src/overrides/converters/aws-apigw-v2.ts index 8bc443cc7..e9eefe794 100644 --- a/packages/open-next/src/converters/aws-apigw-v2.ts +++ b/packages/open-next/src/overrides/converters/aws-apigw-v2.ts @@ -1,10 +1,13 @@ -import { APIGatewayProxyEventV2, APIGatewayProxyResultV2 } from "aws-lambda"; +import type { + APIGatewayProxyEventV2, + APIGatewayProxyResultV2, +} from "aws-lambda"; import { parseCookies } from "http/util"; import type { Converter, InternalEvent, InternalResult } from "types/open-next"; import { fromReadableStream } from "utils/stream"; -import { debug } from "../adapters/logger"; -import { convertToQuery } from "../core/routing/util"; +import { debug } from "../../adapters/logger"; +import { convertToQuery } from "../../core/routing/util"; import { removeUndefinedFromQuery } from "./utils"; // Not sure which one is reallly needed as this is not documented anywhere but server actions redirect are not working without this, it causes a 500 error from cloudfront itself with a 'x-amzErrortype: InternalFailure' header diff --git a/packages/open-next/src/converters/aws-cloudfront.ts b/packages/open-next/src/overrides/converters/aws-cloudfront.ts similarity index 96% rename from packages/open-next/src/converters/aws-cloudfront.ts rename to packages/open-next/src/overrides/converters/aws-cloudfront.ts index c00f89d94..08d490a60 100644 --- a/packages/open-next/src/converters/aws-cloudfront.ts +++ b/packages/open-next/src/overrides/converters/aws-cloudfront.ts @@ -1,24 +1,25 @@ -import { +import type { OutgoingHttpHeader } from "node:http"; + +import type { CloudFrontCustomOrigin, CloudFrontHeaders, CloudFrontRequest, CloudFrontRequestEvent, CloudFrontRequestResult, } from "aws-lambda"; -import { OutgoingHttpHeader } from "http"; import { parseCookies } from "http/util"; import type { Converter, InternalEvent, InternalResult } from "types/open-next"; import { fromReadableStream } from "utils/stream"; -import { debug } from "../adapters/logger"; +import { debug } from "../../adapters/logger"; import { convertRes, convertToQuery, convertToQueryString, createServerResponse, proxyRequest, -} from "../core/routing/util"; -import { MiddlewareOutputEvent } from "../core/routingHandler"; +} from "../../core/routing/util"; +import type { MiddlewareOutputEvent } from "../../core/routingHandler"; const CloudFrontBlacklistedHeaders = [ // Disallowed headers, see: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/edge-function-restrictions-all.html#function-restrictions-disallowed-headers diff --git a/packages/open-next/src/converters/dummy.ts b/packages/open-next/src/overrides/converters/dummy.ts similarity index 89% rename from packages/open-next/src/converters/dummy.ts rename to packages/open-next/src/overrides/converters/dummy.ts index 917b50e13..dda76c670 100644 --- a/packages/open-next/src/converters/dummy.ts +++ b/packages/open-next/src/overrides/converters/dummy.ts @@ -1,4 +1,4 @@ -import { Converter } from "types/open-next"; +import type { Converter } from "types/open-next"; type DummyEventOrResult = { type: "dummy"; diff --git a/packages/open-next/src/converters/edge.ts b/packages/open-next/src/overrides/converters/edge.ts similarity index 95% rename from packages/open-next/src/converters/edge.ts rename to packages/open-next/src/overrides/converters/edge.ts index 7341c7486..140f621e2 100644 --- a/packages/open-next/src/converters/edge.ts +++ b/packages/open-next/src/overrides/converters/edge.ts @@ -1,9 +1,9 @@ import { Buffer } from "node:buffer"; import { parseCookies } from "http/util"; -import { Converter, InternalEvent, InternalResult } from "types/open-next"; +import type { Converter, InternalEvent, InternalResult } from "types/open-next"; -import { MiddlewareOutputEvent } from "../core/routingHandler"; +import { MiddlewareOutputEvent } from "../../core/routingHandler"; const converter: Converter< InternalEvent, diff --git a/packages/open-next/src/converters/node.ts b/packages/open-next/src/overrides/converters/node.ts similarity index 97% rename from packages/open-next/src/converters/node.ts rename to packages/open-next/src/overrides/converters/node.ts index ec3e28641..50251d0f2 100644 --- a/packages/open-next/src/converters/node.ts +++ b/packages/open-next/src/overrides/converters/node.ts @@ -1,4 +1,4 @@ -import { IncomingMessage } from "http"; +import type { IncomingMessage } from "http"; import { parseCookies } from "http/util"; import type { Converter, InternalResult } from "types/open-next"; diff --git a/packages/open-next/src/converters/sqs-revalidate.ts b/packages/open-next/src/overrides/converters/sqs-revalidate.ts similarity index 79% rename from packages/open-next/src/converters/sqs-revalidate.ts rename to packages/open-next/src/overrides/converters/sqs-revalidate.ts index 0d18d93da..c8c29c02b 100644 --- a/packages/open-next/src/converters/sqs-revalidate.ts +++ b/packages/open-next/src/overrides/converters/sqs-revalidate.ts @@ -1,7 +1,7 @@ -import { SQSEvent } from "aws-lambda"; -import { Converter } from "types/open-next"; +import type { SQSEvent } from "aws-lambda"; +import type { Converter } from "types/open-next"; -import { RevalidateEvent } from "../adapters/revalidate"; +import type { RevalidateEvent } from "../../adapters/revalidate"; const converter: Converter = { convertFrom(event: SQSEvent) { diff --git a/packages/open-next/src/converters/utils.ts b/packages/open-next/src/overrides/converters/utils.ts similarity index 100% rename from packages/open-next/src/converters/utils.ts rename to packages/open-next/src/overrides/converters/utils.ts diff --git a/packages/open-next/src/overrides/imageLoader/dummy.ts b/packages/open-next/src/overrides/imageLoader/dummy.ts new file mode 100644 index 000000000..aa013a3ae --- /dev/null +++ b/packages/open-next/src/overrides/imageLoader/dummy.ts @@ -0,0 +1,11 @@ +import { ImageLoader } from "types/open-next"; +import { FatalError } from "utils/error"; + +const dummyLoader: ImageLoader = { + name: "dummy", + load: async (_: string) => { + throw new FatalError("Dummy loader is not implemented"); + }, +}; + +export default dummyLoader; diff --git a/packages/open-next/src/overrides/incrementalCache/dummy.ts b/packages/open-next/src/overrides/incrementalCache/dummy.ts new file mode 100644 index 000000000..51580a775 --- /dev/null +++ b/packages/open-next/src/overrides/incrementalCache/dummy.ts @@ -0,0 +1,16 @@ +import { IncrementalCache } from "./types"; + +const dummyIncrementalCache: IncrementalCache = { + name: "dummy", + get: async () => { + throw new Error("Dummy cache is not implemented"); + }, + set: async () => { + throw new Error("Dummy cache is not implemented"); + }, + delete: async () => { + throw new Error("Dummy cache is not implemented"); + }, +}; + +export default dummyIncrementalCache; diff --git a/packages/open-next/src/cache/incremental/s3-lite.ts b/packages/open-next/src/overrides/incrementalCache/s3-lite.ts similarity index 98% rename from packages/open-next/src/cache/incremental/s3-lite.ts rename to packages/open-next/src/overrides/incrementalCache/s3-lite.ts index 37fda1be4..25d24ba61 100644 --- a/packages/open-next/src/cache/incremental/s3-lite.ts +++ b/packages/open-next/src/overrides/incrementalCache/s3-lite.ts @@ -1,11 +1,11 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import { AwsClient } from "aws4fetch"; import path from "path"; +import { Extension } from "types/cache"; import { IgnorableError, RecoverableError } from "utils/error"; import { customFetchClient } from "utils/fetch"; import { parseNumberFromEnv } from "../../adapters/util"; -import { Extension } from "../next-types"; import { IncrementalCache } from "./types"; let awsClient: AwsClient | null = null; diff --git a/packages/open-next/src/cache/incremental/s3.ts b/packages/open-next/src/overrides/incrementalCache/s3.ts similarity index 97% rename from packages/open-next/src/cache/incremental/s3.ts rename to packages/open-next/src/overrides/incrementalCache/s3.ts index 2e277ba19..cc1cdb3e1 100644 --- a/packages/open-next/src/cache/incremental/s3.ts +++ b/packages/open-next/src/overrides/incrementalCache/s3.ts @@ -6,10 +6,10 @@ import { S3ClientConfig, } from "@aws-sdk/client-s3"; import path from "path"; +import { Extension } from "types/cache"; import { awsLogger } from "../../adapters/logger"; import { parseNumberFromEnv } from "../../adapters/util"; -import { Extension } from "../next-types"; import { IncrementalCache } from "./types"; const { diff --git a/packages/open-next/src/cache/incremental/types.ts b/packages/open-next/src/overrides/incrementalCache/types.ts similarity index 96% rename from packages/open-next/src/cache/incremental/types.ts rename to packages/open-next/src/overrides/incrementalCache/types.ts index 81030eed1..71948665c 100644 --- a/packages/open-next/src/cache/incremental/types.ts +++ b/packages/open-next/src/overrides/incrementalCache/types.ts @@ -1,4 +1,4 @@ -import { Meta } from "../next-types"; +import { Meta } from "types/cache"; export type S3CachedFile = | { diff --git a/packages/open-next/src/overrides/originResolver/dummy.ts b/packages/open-next/src/overrides/originResolver/dummy.ts new file mode 100644 index 000000000..89aef325e --- /dev/null +++ b/packages/open-next/src/overrides/originResolver/dummy.ts @@ -0,0 +1,10 @@ +import { OriginResolver } from "types/open-next"; + +const dummyOriginResolver: OriginResolver = { + name: "dummy", + resolve: async (_path: string) => { + return false as const; + }, +}; + +export default dummyOriginResolver; diff --git a/packages/open-next/src/overrides/originResolver/pattern-env.ts b/packages/open-next/src/overrides/originResolver/pattern-env.ts new file mode 100644 index 000000000..73b142158 --- /dev/null +++ b/packages/open-next/src/overrides/originResolver/pattern-env.ts @@ -0,0 +1,50 @@ +import { Origin, OriginResolver } from "types/open-next"; + +import { debug, error } from "../../adapters/logger"; + +const envLoader: OriginResolver = { + name: "env", + resolve: async (_path: string) => { + try { + const origin = JSON.parse(process.env.OPEN_NEXT_ORIGIN ?? "{}") as Record< + string, + Origin + >; + for (const [key, value] of Object.entries( + globalThis.openNextConfig.functions ?? {}, + ).filter(([key]) => key !== "default")) { + if ( + value.patterns.some((pattern) => { + // Convert cloudfront pattern to regex + return new RegExp( + // transform glob pattern to regex + "/" + + pattern + .replace(/\*\*/g, "(.*)") + .replace(/\*/g, "([^/]*)") + .replace(/\//g, "\\/") + .replace(/\?/g, "."), + ).test(_path); + }) + ) { + debug("Using origin", key, value.patterns); + return origin[key]; + } + } + if (_path.startsWith("/_next/image") && origin["imageOptimizer"]) { + debug("Using origin", "imageOptimizer", _path); + return origin["imageOptimizer"]; + } + if (origin["default"]) { + debug("Using default origin", origin["default"], _path); + return origin["default"]; + } + return false as const; + } catch (e) { + error("Error while resolving origin", e); + return false as const; + } + }, +}; + +export default envLoader; diff --git a/packages/open-next/src/overrides/queue/dummy.ts b/packages/open-next/src/overrides/queue/dummy.ts new file mode 100644 index 000000000..a1c2e579c --- /dev/null +++ b/packages/open-next/src/overrides/queue/dummy.ts @@ -0,0 +1,10 @@ +import { Queue } from "./types"; + +const dummyQueue: Queue = { + name: "dummy", + send: async () => { + throw new Error("Dummy queue is not implemented"); + }, +}; + +export default dummyQueue; diff --git a/packages/open-next/src/queue/sqs-lite.ts b/packages/open-next/src/overrides/queue/sqs-lite.ts similarity index 94% rename from packages/open-next/src/queue/sqs-lite.ts rename to packages/open-next/src/overrides/queue/sqs-lite.ts index dfb97581b..813a71a6f 100644 --- a/packages/open-next/src/queue/sqs-lite.ts +++ b/packages/open-next/src/overrides/queue/sqs-lite.ts @@ -2,8 +2,8 @@ import { AwsClient } from "aws4fetch"; import { RecoverableError } from "utils/error"; import { customFetchClient } from "utils/fetch"; -import { error } from "../adapters/logger"; -import { Queue } from "./types"; +import { error } from "../../adapters/logger"; +import type { Queue } from "./types"; let awsClient: AwsClient | null = null; diff --git a/packages/open-next/src/queue/sqs.ts b/packages/open-next/src/overrides/queue/sqs.ts similarity index 87% rename from packages/open-next/src/queue/sqs.ts rename to packages/open-next/src/overrides/queue/sqs.ts index cbc2bdfec..43cc320bf 100644 --- a/packages/open-next/src/queue/sqs.ts +++ b/packages/open-next/src/overrides/queue/sqs.ts @@ -1,7 +1,7 @@ import { SendMessageCommand, SQSClient } from "@aws-sdk/client-sqs"; -import { awsLogger } from "../adapters/logger"; -import { Queue } from "./types"; +import { awsLogger } from "../../adapters/logger"; +import type { Queue } from "./types"; // Expected environment variables const { REVALIDATION_QUEUE_REGION, REVALIDATION_QUEUE_URL } = process.env; diff --git a/packages/open-next/src/queue/types.ts b/packages/open-next/src/overrides/queue/types.ts similarity index 100% rename from packages/open-next/src/queue/types.ts rename to packages/open-next/src/overrides/queue/types.ts diff --git a/packages/open-next/src/cache/tag/constants.ts b/packages/open-next/src/overrides/tagCache/constants.ts similarity index 100% rename from packages/open-next/src/cache/tag/constants.ts rename to packages/open-next/src/overrides/tagCache/constants.ts diff --git a/packages/open-next/src/overrides/tagCache/dummy.ts b/packages/open-next/src/overrides/tagCache/dummy.ts new file mode 100644 index 000000000..0b4b52db2 --- /dev/null +++ b/packages/open-next/src/overrides/tagCache/dummy.ts @@ -0,0 +1,20 @@ +import { TagCache } from "./types"; + +// We don't want to throw error on this one because we might use it when we don't need tag cache +const dummyTagCache: TagCache = { + name: "dummy", + getByPath: async () => { + return []; + }, + getByTag: async () => { + return []; + }, + getLastModified: async (_: string, lastModified) => { + return lastModified ?? Date.now(); + }, + writeTags: async () => { + return; + }, +}; + +export default dummyTagCache; diff --git a/packages/open-next/src/cache/tag/dynamodb-lite.ts b/packages/open-next/src/overrides/tagCache/dynamodb-lite.ts similarity index 99% rename from packages/open-next/src/cache/tag/dynamodb-lite.ts rename to packages/open-next/src/overrides/tagCache/dynamodb-lite.ts index 8dcef36a6..b0c1543e2 100644 --- a/packages/open-next/src/cache/tag/dynamodb-lite.ts +++ b/packages/open-next/src/overrides/tagCache/dynamodb-lite.ts @@ -10,7 +10,7 @@ import { getDynamoBatchWriteCommandConcurrency, MAX_DYNAMO_BATCH_WRITE_ITEM_COUNT, } from "./constants"; -import { TagCache } from "./types"; +import type { TagCache } from "./types"; let awsClient: AwsClient | null = null; diff --git a/packages/open-next/src/cache/tag/dynamodb.ts b/packages/open-next/src/overrides/tagCache/dynamodb.ts similarity index 99% rename from packages/open-next/src/cache/tag/dynamodb.ts rename to packages/open-next/src/overrides/tagCache/dynamodb.ts index 454b9800b..8d9ec89c1 100644 --- a/packages/open-next/src/cache/tag/dynamodb.ts +++ b/packages/open-next/src/overrides/tagCache/dynamodb.ts @@ -12,7 +12,7 @@ import { getDynamoBatchWriteCommandConcurrency, MAX_DYNAMO_BATCH_WRITE_ITEM_COUNT, } from "./constants"; -import { TagCache } from "./types"; +import type { TagCache } from "./types"; const { CACHE_BUCKET_REGION, CACHE_DYNAMO_TABLE, NEXT_BUILD_ID } = process.env; diff --git a/packages/open-next/src/cache/tag/types.ts b/packages/open-next/src/overrides/tagCache/types.ts similarity index 100% rename from packages/open-next/src/cache/tag/types.ts rename to packages/open-next/src/overrides/tagCache/types.ts diff --git a/packages/open-next/src/overrides/warmer/aws-lambda.ts b/packages/open-next/src/overrides/warmer/aws-lambda.ts new file mode 100644 index 000000000..d7da9bea7 --- /dev/null +++ b/packages/open-next/src/overrides/warmer/aws-lambda.ts @@ -0,0 +1,83 @@ +import { Warmer } from "types/open-next"; + +import { debug, error } from "../../adapters/logger"; +import type { + WarmerEvent, + WarmerResponse, +} from "../../adapters/warmer-function"; + +const lambdaWarmerInvoke: Warmer = { + name: "aws-invoke", + invoke: async (warmerId: string) => { + const { InvokeCommand, LambdaClient } = await import( + "@aws-sdk/client-lambda" + ); + const lambda = new LambdaClient({}); + const warmParams = JSON.parse(process.env.WARM_PARAMS!) as { + concurrency: number; + function: string; + }[]; + + for (const warmParam of warmParams) { + const { concurrency: CONCURRENCY, function: FUNCTION_NAME } = warmParam; + debug({ + event: "warmer invoked", + functionName: FUNCTION_NAME, + concurrency: CONCURRENCY, + warmerId, + }); + const ret = await Promise.all( + Array.from({ length: CONCURRENCY }, (_v, i) => i).map((i) => { + try { + return lambda.send( + new InvokeCommand({ + FunctionName: FUNCTION_NAME, + InvocationType: "RequestResponse", + Payload: Buffer.from( + JSON.stringify({ + type: "warmer", + warmerId, + index: i, + concurrency: CONCURRENCY, + delay: 75, + } satisfies WarmerEvent), + ), + }), + ); + } catch (e) { + error(`failed to warm up #${i}`, e); + // ignore error + } + }), + ); + + // Print status + + const warmedServerIds = ret + .map((r, i) => { + if (r?.StatusCode !== 200 || !r?.Payload) { + error(`failed to warm up #${i}:`, r?.Payload?.toString()); + return; + } + const payload = JSON.parse( + Buffer.from(r.Payload).toString(), + ) as WarmerResponse; + return { + statusCode: r.StatusCode, + payload, + type: "warmer" as const, + }; + }) + .filter((r): r is Exclude => !!r); + + debug({ + event: "warmer result", + sent: CONCURRENCY, + success: warmedServerIds.length, + uniqueServersWarmed: [...new Set(warmedServerIds)].length, + }); + } + }, +}; + +export default lambdaWarmerInvoke; diff --git a/packages/open-next/src/overrides/warmer/dummy.ts b/packages/open-next/src/overrides/warmer/dummy.ts new file mode 100644 index 000000000..01112f5b1 --- /dev/null +++ b/packages/open-next/src/overrides/warmer/dummy.ts @@ -0,0 +1,10 @@ +import type { Warmer } from "types/open-next"; + +const dummyWarmer: Warmer = { + name: "dummy", + invoke: async (_: string) => { + throw new Error("Dummy warmer is not implemented"); + }, +}; + +export default dummyWarmer; diff --git a/packages/open-next/src/wrappers/aws-lambda-streaming.ts b/packages/open-next/src/overrides/wrappers/aws-lambda-streaming.ts similarity index 90% rename from packages/open-next/src/wrappers/aws-lambda-streaming.ts rename to packages/open-next/src/overrides/wrappers/aws-lambda-streaming.ts index d953eb2bd..8a6e57f25 100644 --- a/packages/open-next/src/wrappers/aws-lambda-streaming.ts +++ b/packages/open-next/src/overrides/wrappers/aws-lambda-streaming.ts @@ -1,12 +1,15 @@ -import { Readable, Writable } from "node:stream"; +import { Readable, type Writable } from "node:stream"; import zlib from "node:zlib"; -import { APIGatewayProxyEventV2 } from "aws-lambda"; -import { StreamCreator } from "http/index.js"; -import { WrapperHandler } from "types/open-next"; +import type { APIGatewayProxyEventV2 } from "aws-lambda"; +import type { StreamCreator } from "http/index.js"; +import type { WrapperHandler } from "types/open-next"; -import { debug, error } from "../adapters/logger"; -import { WarmerEvent, WarmerResponse } from "../adapters/warmer-function"; +import { debug, error } from "../../adapters/logger"; +import type { + WarmerEvent, + WarmerResponse, +} from "../../adapters/warmer-function"; type AwsLambdaEvent = APIGatewayProxyEventV2 | WarmerEvent; diff --git a/packages/open-next/src/wrappers/aws-lambda.ts b/packages/open-next/src/overrides/wrappers/aws-lambda.ts similarity index 93% rename from packages/open-next/src/wrappers/aws-lambda.ts rename to packages/open-next/src/overrides/wrappers/aws-lambda.ts index e4fa8afc8..833951b3b 100644 --- a/packages/open-next/src/wrappers/aws-lambda.ts +++ b/packages/open-next/src/overrides/wrappers/aws-lambda.ts @@ -8,10 +8,13 @@ import type { CloudFrontRequestEvent, CloudFrontRequestResult, } from "aws-lambda"; -import { StreamCreator } from "http/openNextResponse"; +import type { StreamCreator } from "http/openNextResponse"; import type { WrapperHandler } from "types/open-next"; -import { WarmerEvent, WarmerResponse } from "../adapters/warmer-function"; +import type { + WarmerEvent, + WarmerResponse, +} from "../../adapters/warmer-function"; type AwsLambdaEvent = | APIGatewayProxyEventV2 diff --git a/packages/open-next/src/wrappers/cloudflare.ts b/packages/open-next/src/overrides/wrappers/cloudflare.ts similarity index 92% rename from packages/open-next/src/wrappers/cloudflare.ts rename to packages/open-next/src/overrides/wrappers/cloudflare.ts index bfeea939e..55a217246 100644 --- a/packages/open-next/src/wrappers/cloudflare.ts +++ b/packages/open-next/src/overrides/wrappers/cloudflare.ts @@ -4,7 +4,7 @@ import type { WrapperHandler, } from "types/open-next"; -import { MiddlewareOutputEvent } from "../core/routingHandler"; +import type { MiddlewareOutputEvent } from "../../core/routingHandler"; const handler: WrapperHandler< InternalEvent, diff --git a/packages/open-next/src/overrides/wrappers/dummy.ts b/packages/open-next/src/overrides/wrappers/dummy.ts new file mode 100644 index 000000000..0f66667fa --- /dev/null +++ b/packages/open-next/src/overrides/wrappers/dummy.ts @@ -0,0 +1,9 @@ +import { WrapperHandler } from "types/open-next"; + +const dummyWrapper: WrapperHandler = async () => async () => undefined; + +export default { + name: "dummy", + handler: dummyWrapper, + supportStreaming: false, +}; diff --git a/packages/open-next/src/wrappers/node.ts b/packages/open-next/src/overrides/wrappers/node.ts similarity index 93% rename from packages/open-next/src/wrappers/node.ts rename to packages/open-next/src/overrides/wrappers/node.ts index 978cd0aef..bc1b69c5c 100644 --- a/packages/open-next/src/wrappers/node.ts +++ b/packages/open-next/src/overrides/wrappers/node.ts @@ -1,9 +1,9 @@ import { createServer } from "node:http"; -import { StreamCreator } from "http/index.js"; +import type { StreamCreator } from "http/index.js"; import type { WrapperHandler } from "types/open-next"; -import { debug, error } from "../adapters/logger"; +import { debug, error } from "../../adapters/logger"; const wrapper: WrapperHandler = async (handler, converter) => { const server = createServer(async (req, res) => { diff --git a/packages/open-next/src/plugins/resolve.ts b/packages/open-next/src/plugins/resolve.ts index 48eb2f3a0..de28f8c63 100644 --- a/packages/open-next/src/plugins/resolve.ts +++ b/packages/open-next/src/plugins/resolve.ts @@ -5,8 +5,12 @@ import type { DefaultOverrideOptions, ImageLoader, IncludedImageLoader, + IncludedOriginResolver, + IncludedWarmer, LazyLoadedOverride, + OriginResolver, OverrideOptions, + Warmer, } from "types/open-next"; import logger from "../logger.js"; @@ -19,19 +23,47 @@ export interface IPluginSettings { queue?: OverrideOptions["queue"]; incrementalCache?: OverrideOptions["incrementalCache"]; imageLoader?: LazyLoadedOverride | IncludedImageLoader; + originResolver?: + | LazyLoadedOverride + | IncludedOriginResolver; + warmer?: LazyLoadedOverride | IncludedWarmer; }; fnName?: string; } -function getOverrideOrDefault< - Override extends string | LazyLoadedOverride, ->(override: Override, defaultOverride: string) { +function getOverrideOrDummy>( + override: Override, +) { if (typeof override === "string") { return override; } - return defaultOverride; + // We can return dummy here because if it's not a string, it's a LazyLoadedOverride + return "dummy"; } +// This could be useful in the future to map overrides to nested folders +const nameToFolder = { + wrapper: "wrappers", + converter: "converters", + tagCache: "tagCache", + queue: "queue", + incrementalCache: "incrementalCache", + imageLoader: "imageLoader", + originResolver: "originResolver", + warmer: "warmer", +}; + +const defaultOverrides = { + wrapper: "aws-lambda", + converter: "aws-apigw-v2", + tagCache: "dynamodb", + queue: "sqs", + incrementalCache: "s3", + imageLoader: "s3", + originResolver: "pattern-env", + warmer: "aws-lambda", +}; + /** * @param opts.overrides - The name of the overrides to use * @returns @@ -46,56 +78,18 @@ export function openNextResolvePlugin({ logger.debug(`OpenNext Resolve plugin for ${fnName}`); build.onLoad({ filter: /core(\/|\\)resolve\.js/g }, async (args) => { let contents = readFileSync(args.path, "utf-8"); - //TODO: refactor this. Every override should be at the same place so we can generate this dynamically - if (overrides?.wrapper) { - contents = contents.replace( - "../wrappers/aws-lambda.js", - `../wrappers/${getOverrideOrDefault( - overrides.wrapper, - "aws-lambda", - )}.js`, - ); - } - if (overrides?.converter) { - contents = contents.replace( - "../converters/aws-apigw-v2.js", - `../converters/${getOverrideOrDefault( - overrides.converter, - "dummy", - )}.js`, - ); - } - if (overrides?.tagCache) { - contents = contents.replace( - "../cache/tag/dynamodb.js", - `../cache/tag/${getOverrideOrDefault( - overrides.tagCache, - "dynamodb-lite", - )}.js`, - ); - } - if (overrides?.queue) { - contents = contents.replace( - "../queue/sqs.js", - `../queue/${getOverrideOrDefault(overrides.queue, "sqs-lite")}.js`, - ); - } - if (overrides?.incrementalCache) { - contents = contents.replace( - "../cache/incremental/s3.js", - `../cache/incremental/${getOverrideOrDefault( - overrides.incrementalCache, - "s3-lite", - )}.js`, - ); - } - if (overrides?.imageLoader) { + const overridesEntries = Object.entries(overrides ?? {}); + for (const [overrideName, overrideValue] of overridesEntries) { + if (!overrideValue) { + continue; + } + const folder = + nameToFolder[overrideName as keyof typeof nameToFolder]; + const defaultOverride = + defaultOverrides[overrideName as keyof typeof defaultOverrides]; contents = contents.replace( - "../overrides/imageLoader/s3.js", - `../overrides/imageLoader/${getOverrideOrDefault( - overrides.imageLoader, - "s3", - )}.js`, + `../overrides/${folder}/${defaultOverride}.js`, + `../overrides/${folder}/${getOverrideOrDummy(overrideValue)}.js`, ); } return { diff --git a/packages/open-next/src/cache/next-types.ts b/packages/open-next/src/types/cache.ts similarity index 100% rename from packages/open-next/src/cache/next-types.ts rename to packages/open-next/src/types/cache.ts diff --git a/packages/open-next/src/types/open-next.ts b/packages/open-next/src/types/open-next.ts index 5efb27de5..dbc8f9239 100644 --- a/packages/open-next/src/types/open-next.ts +++ b/packages/open-next/src/types/open-next.ts @@ -4,9 +4,9 @@ import type { ReadableStream } from "node:stream/web"; import type { StreamCreator } from "http/index.js"; import type { WarmerEvent, WarmerResponse } from "../adapters/warmer-function"; -import type { IncrementalCache } from "../cache/incremental/types"; -import type { TagCache } from "../cache/tag/types"; -import type { Queue } from "../queue/types"; +import type { IncrementalCache } from "../overrides/incrementalCache/types"; +import type { Queue } from "../overrides/queue/types"; +import type { TagCache } from "../overrides/tagCache/types"; export type BaseEventOrResult = { type: T; @@ -139,6 +139,10 @@ export type IncludedTagCache = "dynamodb" | "dynamodb-lite"; export type IncludedImageLoader = "s3" | "host"; +export type IncludedOriginResolver = "pattern-env"; + +export type IncludedWarmer = "aws-lambda"; + export interface DefaultOverrideOptions< E extends BaseEventOrResult = InternalEvent, R extends BaseEventOrResult = InternalResult, @@ -284,7 +288,9 @@ export interface OpenNextConfig { * OPEN_NEXT_ORIGIN should be a json stringified object with the key of the splitted function as key and the origin as value * @default "pattern-env" */ - originResolver?: "pattern-env" | LazyLoadedOverride; + originResolver?: + | IncludedOriginResolver + | LazyLoadedOverride; }; /** @@ -294,7 +300,7 @@ export interface OpenNextConfig { * @default undefined */ warmer?: DefaultFunctionOptions & { - invokeFunction: "aws-lambda" | LazyLoadedOverride; + invokeFunction: IncludedWarmer | LazyLoadedOverride; }; /** diff --git a/packages/tests-unit/tests/converters/aws-apigw-v1.test.ts b/packages/tests-unit/tests/converters/aws-apigw-v1.test.ts index 03234bb3f..343ef72ed 100644 --- a/packages/tests-unit/tests/converters/aws-apigw-v1.test.ts +++ b/packages/tests-unit/tests/converters/aws-apigw-v1.test.ts @@ -1,5 +1,5 @@ /* eslint-disable sonarjs/no-duplicate-string */ -import converter from "@opennextjs/aws/converters/aws-apigw-v1.js"; +import converter from "@opennextjs/aws/overrides/converters/aws-apigw-v1.js"; import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda"; import { Readable } from "stream"; diff --git a/packages/tests-unit/tests/converters/aws-apigw-v2.test.ts b/packages/tests-unit/tests/converters/aws-apigw-v2.test.ts index ab9e4f505..420e41e87 100644 --- a/packages/tests-unit/tests/converters/aws-apigw-v2.test.ts +++ b/packages/tests-unit/tests/converters/aws-apigw-v2.test.ts @@ -1,4 +1,4 @@ -import converter from "@opennextjs/aws/converters/aws-apigw-v2.js"; +import converter from "@opennextjs/aws/overrides/converters/aws-apigw-v2.js"; import { APIGatewayProxyEventV2 } from "aws-lambda"; import { Readable } from "stream"; import { vi } from "vitest"; diff --git a/packages/tests-unit/tests/converters/aws-cloudfront.test.ts b/packages/tests-unit/tests/converters/aws-cloudfront.test.ts index bed3ed744..81749a558 100644 --- a/packages/tests-unit/tests/converters/aws-cloudfront.test.ts +++ b/packages/tests-unit/tests/converters/aws-cloudfront.test.ts @@ -1,4 +1,4 @@ -import converter from "@opennextjs/aws/converters/aws-cloudfront.js"; +import converter from "@opennextjs/aws/overrides/converters/aws-cloudfront.js"; import { CloudFrontRequestEvent, CloudFrontRequestResult } from "aws-lambda"; import { Readable } from "stream"; import { vi } from "vitest"; diff --git a/packages/tests-unit/tests/converters/node.test.ts b/packages/tests-unit/tests/converters/node.test.ts index da0c07757..d77fd708f 100644 --- a/packages/tests-unit/tests/converters/node.test.ts +++ b/packages/tests-unit/tests/converters/node.test.ts @@ -1,5 +1,5 @@ -import converter from "@opennextjs/aws/converters/node.js"; import { IncomingMessage } from "@opennextjs/aws/http/request.js"; +import converter from "@opennextjs/aws/overrides/converters/node.js"; describe("convertFrom", () => { it("should convert GET request", async () => { diff --git a/packages/tests-unit/tests/converters/utils.test.ts b/packages/tests-unit/tests/converters/utils.test.ts index 796fc6d31..9ca7b71bb 100644 --- a/packages/tests-unit/tests/converters/utils.test.ts +++ b/packages/tests-unit/tests/converters/utils.test.ts @@ -1,4 +1,4 @@ -import { removeUndefinedFromQuery } from "@opennextjs/aws/converters/utils.js"; +import { removeUndefinedFromQuery } from "@opennextjs/aws/overrides/converters/utils.js"; describe("removeUndefinedFromQuery", () => { it("should remove undefined from query", () => {