From 92cb060fe23e38a490293311888a6f48f27f1a1c Mon Sep 17 00:00:00 2001 From: Victor Berchet Date: Thu, 24 Jul 2025 22:12:11 +0200 Subject: [PATCH] refactor the code patcher --- .changeset/forty-paws-nail.md | 5 + .../open-next/src/build/patch/codePatcher.ts | 95 ++++++-------- .../patches/patchBackgroundRevalidation.ts | 6 +- .../src/build/patch/patches/patchEnvVar.ts | 28 ++-- .../build/patch/patches/patchFetchCacheISR.ts | 50 ++++---- .../patch/patches/patchFetchCacheWaitUntil.ts | 14 +- .../build/patch/patches/patchNextServer.ts | 48 +++---- .../open-next/src/plugins/content-updater.ts | 27 ++-- .../tests/build/patch/codePatcher.test.ts | 121 ++++++++---------- 9 files changed, 168 insertions(+), 226 deletions(-) create mode 100644 .changeset/forty-paws-nail.md diff --git a/.changeset/forty-paws-nail.md b/.changeset/forty-paws-nail.md new file mode 100644 index 000000000..5bdf18b5b --- /dev/null +++ b/.changeset/forty-paws-nail.md @@ -0,0 +1,5 @@ +--- +"@opennextjs/aws": patch +--- + +refactor the CodePatcher diff --git a/packages/open-next/src/build/patch/codePatcher.ts b/packages/open-next/src/build/patch/codePatcher.ts index a36aa6f8b..c258897ad 100644 --- a/packages/open-next/src/build/patch/codePatcher.ts +++ b/packages/open-next/src/build/patch/codePatcher.ts @@ -3,19 +3,18 @@ import logger from "../../logger.js"; import type { getManifests } from "../copyTracedFiles.js"; import * as buildHelper from "../helper.js"; -type Versions = +/** + * Accepted formats: + * - `">=16.0.0"` + * - `"<=16.0.0"` + * - `">=16.0.0 <=17.0.0"` + * + * **Be careful with spaces** + */ +export type Versions = | `>=${number}.${number}.${number} <=${number}.${number}.${number}` | `>=${number}.${number}.${number}` | `<=${number}.${number}.${number}`; -export interface VersionedField { - /** - * The versions of Next.js that this field should be used for - * Should be in the format `">=16.0.0 <=17.0.0"` or `">=16.0.0"` or `"<=17.0.0"` - * **Be careful with spaces** - */ - versions?: Versions; - field: T; -} export type PatchCodeFn = (args: { /** @@ -40,11 +39,13 @@ interface IndividualPatch { pathFilter: RegExp; contentFilter?: RegExp; patchCode: PatchCodeFn; + // Only apply the patch to specific versions of Next.js + versions?: Versions; } export interface CodePatcher { name: string; - patches: IndividualPatch | VersionedField[]; + patches: IndividualPatch[]; } export function parseVersions(versions?: Versions): { @@ -92,48 +93,30 @@ export function parseVersions(versions?: Versions): { }; } -export function extractVersionedField( - fields: VersionedField[], +/** + * Check whether the version is in the range + * + * @param version A semver version + * @param versionRange A version range + * @returns whether the version satisfies the range + */ +export function isVersionInRange( version: string, -): T[] { - const result: T[] = []; + versionRange?: Versions, +): boolean { + const { before, after } = parseVersions(versionRange); - for (const field of fields) { - // No versions specified, the patch always apply - if (!field.versions) { - result.push(field.field); - continue; - } - - const { before, after } = parseVersions(field.versions); + let inRange = true; - // range - if (before && after) { - if ( - buildHelper.compareSemver(version, "<=", before) && - buildHelper.compareSemver(version, ">=", after) - ) { - result.push(field.field); - } - continue; - } - - // before only - if (before) { - if (buildHelper.compareSemver(version, "<=", before)) { - result.push(field.field); - } - continue; - } + if (before) { + inRange &&= buildHelper.compareSemver(version, "<=", before); + } - // after only - if (after) { - if (buildHelper.compareSemver(version, ">=", after)) { - result.push(field.field); - } - } + if (after) { + inRange &&= buildHelper.compareSemver(version, ">=", after); } - return result; + + return inRange; } export async function applyCodePatches( @@ -142,19 +125,17 @@ export async function applyCodePatches( manifests: ReturnType, codePatcher: CodePatcher[], ) { - const nextVersion = buildOptions.nextVersion; logger.time("Applying code patches"); // We first filter against the version // We also flatten the array of patches so that we get both the name and all the necessary patches - const patchesToApply = codePatcher.flatMap(({ name, patches }) => - Array.isArray(patches) - ? extractVersionedField(patches, nextVersion).map((patch) => ({ - name, - patch, - })) - : [{ name, patch: patches }], - ); + const patchesToApply = codePatcher.flatMap(({ name, patches }) => { + return patches + .filter(({ versions }) => + isVersionInRange(buildOptions.nextVersion, versions), + ) + .map((patch) => ({ patch, name })); + }); await Promise.all( tracedFiles.map(async (filePath) => { diff --git a/packages/open-next/src/build/patch/patches/patchBackgroundRevalidation.ts b/packages/open-next/src/build/patch/patches/patchBackgroundRevalidation.ts index 83560252c..72ae2735d 100644 --- a/packages/open-next/src/build/patch/patches/patchBackgroundRevalidation.ts +++ b/packages/open-next/src/build/patch/patches/patchBackgroundRevalidation.ts @@ -21,10 +21,8 @@ export const patchBackgroundRevalidation = { { // TODO: test for earlier versions of Next versions: ">=14.1.0", - field: { - pathFilter: getCrossPlatformPathRegex("server/response-cache/index.js"), - patchCode: createPatchCode(rule), - }, + pathFilter: getCrossPlatformPathRegex("server/response-cache/index.js"), + patchCode: createPatchCode(rule), }, ], } satisfies CodePatcher; diff --git a/packages/open-next/src/build/patch/patches/patchEnvVar.ts b/packages/open-next/src/build/patch/patches/patchEnvVar.ts index 96ee7fb20..c34e9b4d1 100644 --- a/packages/open-next/src/build/patch/patches/patchEnvVar.ts +++ b/packages/open-next/src/build/patch/patches/patchEnvVar.ts @@ -27,32 +27,24 @@ export const patchEnvVars: CodePatcher = { // This patch will set the `NEXT_RUNTIME` env var to "node" to avoid loading unnecessary edge deps at runtime { versions: ">=15.0.0", - field: { - pathFilter: /module\.compiled\.js$/, - contentFilter: /process\.env\.NEXT_RUNTIME/, - patchCode: createPatchCode(envVarRuleCreator("NEXT_RUNTIME", '"node"')), - }, + pathFilter: /module\.compiled\.js$/, + contentFilter: /process\.env\.NEXT_RUNTIME/, + patchCode: createPatchCode(envVarRuleCreator("NEXT_RUNTIME", '"node"')), }, // This patch will set `NODE_ENV` to production to avoid loading unnecessary dev deps at runtime { versions: ">=15.0.0", - field: { - pathFilter: - /(module\.compiled|react\/index|react\/jsx-runtime|react-dom\/index)\.js$/, - contentFilter: /process\.env\.NODE_ENV/, - patchCode: createPatchCode( - envVarRuleCreator("NODE_ENV", '"production"'), - ), - }, + pathFilter: + /(module\.compiled|react\/index|react\/jsx-runtime|react-dom\/index)\.js$/, + contentFilter: /process\.env\.NODE_ENV/, + patchCode: createPatchCode(envVarRuleCreator("NODE_ENV", '"production"')), }, // This patch will set `TURBOPACK` env to false to avoid loading turbopack related deps at runtime { versions: ">=15.0.0", - field: { - pathFilter: /module\.compiled\.js$/, - contentFilter: /process\.env\.TURBOPACK/, - patchCode: createPatchCode(envVarRuleCreator("TURBOPACK", "false")), - }, + pathFilter: /module\.compiled\.js$/, + contentFilter: /process\.env\.TURBOPACK/, + patchCode: createPatchCode(envVarRuleCreator("TURBOPACK", "false")), }, ], }; diff --git a/packages/open-next/src/build/patch/patches/patchFetchCacheISR.ts b/packages/open-next/src/build/patch/patches/patchFetchCacheISR.ts index d117045cb..98f8d5071 100644 --- a/packages/open-next/src/build/patch/patches/patchFetchCacheISR.ts +++ b/packages/open-next/src/build/patch/patches/patchFetchCacheISR.ts @@ -11,7 +11,7 @@ rule: kind: ternary_expression all: - has: {kind: 'null'} - - has: + - has: kind: await_expression has: kind: call_expression @@ -51,7 +51,7 @@ rule: kind: statement_block has: kind: variable_declarator - has: + has: kind: await_expression has: kind: call_expression @@ -84,13 +84,13 @@ rule: pattern: $STORE_OR_CACHE.isOnDemandRevalidate inside: kind: binary_expression - has: + has: kind: member_expression pattern: $STORE_OR_CACHE.isDraftMode inside: kind: if_statement stopBy: end - has: + has: kind: return_statement any: - has: @@ -106,14 +106,12 @@ export const patchFetchCacheForISR: CodePatcher = { patches: [ { versions: ">=14.0.0", - field: { - pathFilter: getCrossPlatformPathRegex( - String.raw`(server/chunks/.*\.js|.*\.runtime\..*\.js|patch-fetch\.js)$`, - { escape: false }, - ), - contentFilter: /\.isOnDemandRevalidate/, - patchCode: createPatchCode(fetchRule, Lang.JavaScript), - }, + pathFilter: getCrossPlatformPathRegex( + String.raw`(server/chunks/.*\.js|.*\.runtime\..*\.js|patch-fetch\.js)$`, + { escape: false }, + ), + contentFilter: /\.isOnDemandRevalidate/, + patchCode: createPatchCode(fetchRule, Lang.JavaScript), }, ], }; @@ -123,14 +121,12 @@ export const patchUnstableCacheForISR: CodePatcher = { patches: [ { versions: ">=14.2.0", - field: { - pathFilter: getCrossPlatformPathRegex( - String.raw`(server/chunks/.*\.js|.*\.runtime\..*\.js|spec-extension/unstable-cache\.js)$`, - { escape: false }, - ), - contentFilter: /\.isOnDemandRevalidate/, - patchCode: createPatchCode(unstable_cacheRule, Lang.JavaScript), - }, + pathFilter: getCrossPlatformPathRegex( + String.raw`(server/chunks/.*\.js|.*\.runtime\..*\.js|spec-extension/unstable-cache\.js)$`, + { escape: false }, + ), + contentFilter: /\.isOnDemandRevalidate/, + patchCode: createPatchCode(unstable_cacheRule, Lang.JavaScript), }, ], }; @@ -140,14 +136,12 @@ export const patchUseCacheForISR: CodePatcher = { patches: [ { versions: ">=15.3.0", - field: { - pathFilter: getCrossPlatformPathRegex( - String.raw`(server/chunks/.*\.js|\.runtime\..*\.js|use-cache/use-cache-wrapper\.js)$`, - { escape: false }, - ), - contentFilter: /\.isOnDemandRevalidate/, - patchCode: createPatchCode(useCacheRule, Lang.JavaScript), - }, + pathFilter: getCrossPlatformPathRegex( + String.raw`(server/chunks/.*\.js|\.runtime\..*\.js|use-cache/use-cache-wrapper\.js)$`, + { escape: false }, + ), + contentFilter: /\.isOnDemandRevalidate/, + patchCode: createPatchCode(useCacheRule, Lang.JavaScript), }, ], }; diff --git a/packages/open-next/src/build/patch/patches/patchFetchCacheWaitUntil.ts b/packages/open-next/src/build/patch/patches/patchFetchCacheWaitUntil.ts index 1266308fa..f1e73ec6b 100644 --- a/packages/open-next/src/build/patch/patches/patchFetchCacheWaitUntil.ts +++ b/packages/open-next/src/build/patch/patches/patchFetchCacheWaitUntil.ts @@ -29,14 +29,12 @@ export const patchFetchCacheSetMissingWaitUntil: CodePatcher = { patches: [ { versions: ">=15.0.0", - field: { - pathFilter: getCrossPlatformPathRegex( - String.raw`(server/chunks/.*\.js|.*\.runtime\..*\.js|patch-fetch\.js)$`, - { escape: false }, - ), - contentFilter: /arrayBuffer\(\)\s*\.then/, - patchCode: createPatchCode(rule), - }, + pathFilter: getCrossPlatformPathRegex( + String.raw`(server/chunks/.*\.js|.*\.runtime\..*\.js|patch-fetch\.js)$`, + { escape: false }, + ), + contentFilter: /arrayBuffer\(\)\s*\.then/, + patchCode: createPatchCode(rule), }, ], }; diff --git a/packages/open-next/src/build/patch/patches/patchNextServer.ts b/packages/open-next/src/build/patch/patches/patchNextServer.ts index 410502550..109ebe4d6 100644 --- a/packages/open-next/src/build/patch/patches/patchNextServer.ts +++ b/packages/open-next/src/build/patch/patches/patchNextServer.ts @@ -81,27 +81,21 @@ const pathFilter = getCrossPlatformPathRegex( const babelPatches = [ // Empty the body of `NextServer#runMiddleware` { - field: { - pathFilter, - contentFilter: /runMiddleware\(/, - patchCode: createPatchCode(createEmptyBodyRule("runMiddleware")), - }, + pathFilter, + contentFilter: /runMiddleware\(/, + patchCode: createPatchCode(createEmptyBodyRule("runMiddleware")), }, // Empty the body of `NextServer#runEdgeFunction` { - field: { - pathFilter, - contentFilter: /runEdgeFunction\(/, - patchCode: createPatchCode(createEmptyBodyRule("runEdgeFunction")), - }, + pathFilter, + contentFilter: /runEdgeFunction\(/, + patchCode: createPatchCode(createEmptyBodyRule("runEdgeFunction")), }, // Drop `error-inspect` that pulls babel { - field: { - pathFilter, - contentFilter: /error-inspect/, - patchCode: createPatchCode(errorInspectRule), - }, + pathFilter, + contentFilter: /error-inspect/, + patchCode: createPatchCode(errorInspectRule), }, ]; @@ -110,30 +104,24 @@ export const patchNextServer: CodePatcher = { patches: [ // Empty the body of `NextServer#imageOptimizer` - unused in OpenNext { - field: { - pathFilter, - contentFilter: /imageOptimizer\(/, - patchCode: createPatchCode(createEmptyBodyRule("imageOptimizer")), - }, + pathFilter, + contentFilter: /imageOptimizer\(/, + patchCode: createPatchCode(createEmptyBodyRule("imageOptimizer")), }, // Disable Next background preloading done at creation of `NextServer` { versions: ">=14.0.0", - field: { - pathFilter, - contentFilter: /this\.nextConfig\.experimental\.preloadEntriesOnStart/, - patchCode: createPatchCode(disablePreloadingRule), - }, + pathFilter, + contentFilter: /this\.nextConfig\.experimental\.preloadEntriesOnStart/, + patchCode: createPatchCode(disablePreloadingRule), }, // Don't match edge functions in `NextServer` { // Next 12 and some version of 13 use the bundled middleware/edge function versions: ">=14.0.0", - field: { - pathFilter, - contentFilter: /getMiddlewareManifest/, - patchCode: createPatchCode(removeMiddlewareManifestRule), - }, + pathFilter, + contentFilter: /getMiddlewareManifest/, + patchCode: createPatchCode(removeMiddlewareManifestRule), }, ...babelPatches, ], diff --git a/packages/open-next/src/plugins/content-updater.ts b/packages/open-next/src/plugins/content-updater.ts index 52da86489..fb0547acf 100644 --- a/packages/open-next/src/plugins/content-updater.ts +++ b/packages/open-next/src/plugins/content-updater.ts @@ -8,10 +8,7 @@ import { readFile } from "node:fs/promises"; import type { OnLoadArgs, OnLoadOptions, Plugin, PluginBuild } from "esbuild"; import type { BuildOptions } from "../build/helper"; -import { - type VersionedField, - extractVersionedField, -} from "../build/patch/codePatcher.js"; +import { type Versions, isVersionInRange } from "../build/patch/codePatcher.js"; export type * from "esbuild"; @@ -31,7 +28,11 @@ export type OnUpdateOptions = OnLoadOptions & { contentFilter: RegExp; }; -export type Updater = OnUpdateOptions & { callback: Callback }; +export type Updater = OnUpdateOptions & { + callback: Callback; + // Restrict the patch to this Next version range + versions?: Versions; +}; export class ContentUpdater { updaters = new Map(); @@ -44,21 +45,19 @@ export class ContentUpdater { * The callbacks are called in order of registration. * * @param name The name of the plugin (must be unique). - * @param updater A versioned field with the callback and `OnUpdateOptions`. + * @param updaters A list of code updaters * @returns A noop ESBuild plugin. */ - updateContent( - name: string, - versionedUpdaters: VersionedField[], - ): Plugin { + updateContent(name: string, updaters: Updater[]): Plugin { if (this.updaters.has(name)) { throw new Error(`Plugin "${name}" already registered`); } - const updaters = extractVersionedField( - versionedUpdaters, - this.buildOptions.nextVersion, + this.updaters.set( + name, + updaters.filter(({ versions }) => + isVersionInRange(this.buildOptions.nextVersion, versions), + ), ); - this.updaters.set(name, updaters); return { name, setup() {}, diff --git a/packages/tests-unit/tests/build/patch/codePatcher.test.ts b/packages/tests-unit/tests/build/patch/codePatcher.test.ts index 88a3726c6..ca3ac91d4 100644 --- a/packages/tests-unit/tests/build/patch/codePatcher.test.ts +++ b/packages/tests-unit/tests/build/patch/codePatcher.test.ts @@ -1,84 +1,71 @@ -import { extractVersionedField } from "@opennextjs/aws/build/patch/codePatcher.js"; - -describe("extractVersionedField", () => { - it("should return the field if the version is between before and after", () => { - const result = extractVersionedField( - [{ versions: ">=15.0.0 <=16.0.0", field: 0 }], - "15.5.0", - ); - - expect(result).toEqual([0]); - }); - - it("should return an empty array if the version is not between before and after", () => { - const result = extractVersionedField( - [{ versions: ">=15.0.0 <=16.0.0", field: 0 }], - "14.0.0", - ); - - expect(result).toEqual([]); - }); - - it("should return the field if the version is equal to before", () => { - const result = extractVersionedField( - [{ versions: "<=15.0.0", field: 0 }], - "15.0.0", - ); - - expect(result).toEqual([0]); +import { + isVersionInRange, + parseVersions, +} from "@opennextjs/aws/build/patch/codePatcher.js"; + +describe("isVersionInRange", () => { + test("before", () => { + expect(isVersionInRange("14.5", "<=16.0.0")).toBe(true); + expect(isVersionInRange("14.5.0", "<=16.0.0")).toBe(true); + expect(isVersionInRange("15", "<=16.0.0")).toBe(true); + expect(isVersionInRange("15.0", "<=16.0.0")).toBe(true); + expect(isVersionInRange("15.0.0", "<=16.0.0")).toBe(true); + expect(isVersionInRange("15.5", "<=16.0.0")).toBe(true); + expect(isVersionInRange("15.5.0", "<=16.0.0")).toBe(true); + expect(isVersionInRange("16", "<=16.0.0")).toBe(true); + expect(isVersionInRange("16.0", "<=16.0.0")).toBe(true); + expect(isVersionInRange("16.0.0", "<=16.0.0")).toBe(true); + expect(isVersionInRange("16.5", "<=16.0.0")).toBe(false); + expect(isVersionInRange("16.5.0", "<=16.0.0")).toBe(false); }); - it("should return the field if the version is greater than after", () => { - const result = extractVersionedField( - [{ versions: ">=16.0.0", field: 0 }], - "16.5.0", - ); - - expect(result).toEqual([0]); + test("after", () => { + expect(isVersionInRange("14.5", ">=15.0.0")).toBe(false); + expect(isVersionInRange("14.5.0", ">=15.0.0")).toBe(false); + expect(isVersionInRange("15", ">=15.0.0")).toBe(true); + expect(isVersionInRange("15.0", ">=15.0.0")).toBe(true); + expect(isVersionInRange("15.0.0", ">=15.0.0")).toBe(true); + expect(isVersionInRange("15.5", ">=15.0.0")).toBe(true); + expect(isVersionInRange("15.5.0", ">=15.0.0")).toBe(true); + expect(isVersionInRange("16", ">=15.0.0")).toBe(true); + expect(isVersionInRange("16.0", ">=15.0.0")).toBe(true); + expect(isVersionInRange("16.0.0", ">=15.0.0")).toBe(true); + expect(isVersionInRange("16.5", ">=15.0.0")).toBe(true); + expect(isVersionInRange("16.5.0", ">=15.0.0")).toBe(true); }); - it("should return the field if the version is less than before", () => { - const result = extractVersionedField( - [{ versions: "<=15.0.0", field: 0 }], - "14.0.0", - ); - - expect(result).toEqual([0]); + test("before and after", () => { + expect(isVersionInRange("14.5", ">=15.0.0 <=16.0.0")).toBe(false); + expect(isVersionInRange("14.5.0", ">=15.0.0 <=16.0.0")).toBe(false); + expect(isVersionInRange("15", ">=15.0.0 <=16.0.0")).toBe(true); + expect(isVersionInRange("15.0", ">=15.0.0 <=16.0.0")).toBe(true); + expect(isVersionInRange("15.0.0", ">=15.0.0 <=16.0.0")).toBe(true); + expect(isVersionInRange("15.5", ">=15.0.0 <=16.0.0")).toBe(true); + expect(isVersionInRange("15.5.0", ">=15.0.0 <=16.0.0")).toBe(true); + expect(isVersionInRange("16", ">=15.0.0 <=16.0.0")).toBe(true); + expect(isVersionInRange("16.0", ">=15.0.0 <=16.0.0")).toBe(true); + expect(isVersionInRange("16.0.0", ">=15.0.0 <=16.0.0")).toBe(true); + expect(isVersionInRange("16.5", ">=15.0.0 <=16.0.0")).toBe(false); + expect(isVersionInRange("16.5.0", ">=15.0.0 <=16.0.0")).toBe(false); }); - it("should return an empty array if version is after before", () => { - const result = extractVersionedField( - [{ versions: "<=15.0.0", field: 0 }], - "15.1.0", - ); - - expect(result).toEqual([]); - }); - - it("should return the field when versions is not specified", () => { - const result = extractVersionedField([{ field: 0 }], "15.1.0"); - - expect(result).toEqual([0]); + test("undefined range", () => { + expect(isVersionInRange("14.5", undefined)).toBe(true); }); +}); +describe("parseVersions", () => { it("should throw an error if a single version range is invalid because of a space before", () => { - expect(() => - extractVersionedField([{ versions: "<= 15.0.0", field: 0 }], "15.0.0"), - ).toThrow("Invalid version range"); + expect(() => parseVersions("<= 15.0.0")).toThrow("Invalid version range"); }); it("should throw an error if a single version range is invalid because of a space inside version", () => { - expect(() => - extractVersionedField([{ versions: ">=16. 0.0", field: 0 }], "15.0.0"), - ).toThrow("Invalid version range"); + expect(() => parseVersions(">=16. 0.0")).toThrow("Invalid version range"); }); it("should throw an error if one of the version range is invalid because of a space before the version", () => { - expect(() => - extractVersionedField( - [{ versions: ">=16.0.0 <= 15.0.0", field: 0 }], - "15.0.0", - ), - ).toThrow("Invalid version range"); + expect(() => parseVersions(">=16.0.0 <= 15.0.0")).toThrow( + "Invalid version range", + ); }); });