From 127e211d588e4a184b7202b43023a15ec064cc48 Mon Sep 17 00:00:00 2001 From: Dorseuil Nicolas Date: Fri, 14 Feb 2025 14:52:23 +0100 Subject: [PATCH 01/10] poc for code patching --- .../open-next/src/build/copyTracedFiles.ts | 6 + .../open-next/src/build/createServerBundle.ts | 25 +++- .../open-next/src/build/patch/codePatcher.ts | 114 ++++++++++++++++++ 3 files changed, 141 insertions(+), 4 deletions(-) create mode 100644 packages/open-next/src/build/patch/codePatcher.ts diff --git a/packages/open-next/src/build/copyTracedFiles.ts b/packages/open-next/src/build/copyTracedFiles.ts index cbacc6938..417c3943c 100644 --- a/packages/open-next/src/build/copyTracedFiles.ts +++ b/packages/open-next/src/build/copyTracedFiles.ts @@ -323,4 +323,10 @@ File ${fullFilePath} does not exist } logger.debug("copyTracedFiles:", Date.now() - tsStart, "ms"); + + return { + tracedFiles: filesToCopy.values(), + // TODO: actually return this + manifests: {}, + }; } diff --git a/packages/open-next/src/build/createServerBundle.ts b/packages/open-next/src/build/createServerBundle.ts index 6b4a3747d..65007541a 100644 --- a/packages/open-next/src/build/createServerBundle.ts +++ b/packages/open-next/src/build/createServerBundle.ts @@ -14,8 +14,12 @@ import { copyTracedFiles } from "./copyTracedFiles.js"; import { generateEdgeBundle } from "./edge/createEdgeBundle.js"; import * as buildHelper from "./helper.js"; import { installDependencies } from "./installDeps.js"; +import { applyCodePatches, type CodePatcher } from "./patch/codePatcher.js"; -export async function createServerBundle(options: buildHelper.BuildOptions) { +export async function createServerBundle( + options: buildHelper.BuildOptions, + additionalCodePatches: CodePatcher[] = [], +) { const { config } = options; const foundRoutes = new Set(); // Get all functions to build @@ -36,7 +40,7 @@ export async function createServerBundle(options: buildHelper.BuildOptions) { if (fnOptions.runtime === "edge") { await generateEdgeBundle(name, options, fnOptions); } else { - await generateBundle(name, options, fnOptions); + await generateBundle(name, options, fnOptions, additionalCodePatches); } }); @@ -101,6 +105,7 @@ async function generateBundle( name: string, options: buildHelper.BuildOptions, fnOptions: SplittedFunctionOptions, + additionalCodePatches: CodePatcher[] = [], ) { const { appPath, appBuildOutputPath, config, outputDir, monorepoRoot } = options; @@ -152,8 +157,8 @@ async function generateBundle( // Copy env files buildHelper.copyEnvFile(appBuildOutputPath, packagePath, outputPath); - // Copy all necessary traced files - await copyTracedFiles({ + // Copy all necessary traced files{ + const { tracedFiles, manifests } = await copyTracedFiles({ buildOutputPath: appBuildOutputPath, packagePath, outputDir: outputPath, @@ -161,6 +166,18 @@ async function generateBundle( bundledNextServer: isBundled, }); + await applyCodePatches(options, Array.from(tracedFiles), manifests, [ + // TODO: create real code patchers here + { + name: "fakePatchChunks", + filter: /chunks\/\d*\.js/, + patchCode: async ({ code }) => { + return `console.log("patched chunk");\n${code}`; + }, + }, + ...additionalCodePatches, + ]); + // Build Lambda code // note: bundle in OpenNext package b/c the adapter relies on the // "serverless-http" package which is not a dependency in user's diff --git a/packages/open-next/src/build/patch/codePatcher.ts b/packages/open-next/src/build/patch/codePatcher.ts new file mode 100644 index 000000000..de355fabe --- /dev/null +++ b/packages/open-next/src/build/patch/codePatcher.ts @@ -0,0 +1,114 @@ +import * as buildHelper from "../helper.js"; +import * as fs from "node:fs/promises"; + +// Either before or after should be provided, otherwise just use the field directly +interface VersionedField { + before?: `${number}.${number}.${number}`; + after?: `${number}.${number}.${number}`; + field: T; +} + +//TODO: create a version of this that would use ast-grep +type PatchCodeFn = (args: { + // The content of the file that needs to be patched + code: string; + // The final path of the file that needs to be patched + filePath: string; + // All js files that will be included in this specific server function + tracedFiles: string[]; + // All next.js manifest that are present at runtime - Key relative to `.next` folder + manifests: Record; +}) => Promise; + +export interface CodePatcher { + name: string; + filter: RegExp | VersionedField[]; + contentFilter?: RegExp | VersionedField[]; + patchCode: PatchCodeFn | VersionedField[]; +} + +function extractVersionedField( + fields: VersionedField[], + version: string, +): T[] { + const result: T[] = []; + for (const field of fields) { + if ( + field.before && + field.after && + buildHelper.compareSemver(version, field.before) >= 0 && + buildHelper.compareSemver(version, field.after) < 0 + ) { + result.push(field.field); + } else if ( + field.before && + buildHelper.compareSemver(version, field.before) >= 0 + ) { + result.push(field.field); + } else if ( + field.after && + buildHelper.compareSemver(version, field.after) < 0 + ) { + result.push(field.field); + } + } + return result; +} + +export async function applyCodePatches( + buildOptions: buildHelper.BuildOptions, + tracedFiles: string[], + manifests: Record, + codePatcher: CodePatcher[], +) { + await Promise.all( + tracedFiles.map(async (filePath) => { + const nextVersion = buildOptions.nextVersion; + // We check the filename against the filter to see if we should apply the patch + const patchToPotentiallyApply = codePatcher.filter((patch) => { + const filters = Array.isArray(patch.filter) + ? extractVersionedField(patch.filter, nextVersion) + : [patch.filter]; + return filters.some((filter) => filePath.match(filter)); + }); + if (patchToPotentiallyApply.length === 0) { + return; + } + const content = await fs.readFile(filePath, "utf-8"); + // We filter a last time against the content this time + const patchToApply = patchToPotentiallyApply.filter((patch) => { + if (!patch.contentFilter) { + return true; + } + const contentFilters = Array.isArray(patch.contentFilter) + ? extractVersionedField(patch.contentFilter, nextVersion) + : [patch.contentFilter]; + return contentFilters.some((filter) => + // If there is no filter, we just return true to apply the patch + filter ? content.match(filter) : true, + ); + }); + if (patchToApply.length === 0) { + return; + } + + // We apply the patches + + patchToApply.forEach(async (patch) => { + const patchCodeFns = Array.isArray(patch.patchCode) + ? extractVersionedField(patch.patchCode, nextVersion) + : [patch.patchCode]; + let patchedContent = content; + for (const patchCodeFn of patchCodeFns) { + patchedContent = await patchCodeFn({ + code: patchedContent, + filePath, + tracedFiles, + manifests, + }); + } + await fs.writeFile(filePath, patchedContent); + }); + }), + ); +} From dea2f3474f7a46aecdaa41720ec9bd8459c47fd3 Mon Sep 17 00:00:00 2001 From: Dorseuil Nicolas Date: Mon, 17 Feb 2025 12:14:29 +0100 Subject: [PATCH 02/10] review fix --- .../open-next/src/build/createServerBundle.ts | 24 ++++++++++++++---- .../src/build/edge/createEdgeBundle.ts | 7 +++++- .../open-next/src/build/patch/codePatcher.ts | 25 +++++++++++-------- 3 files changed, 39 insertions(+), 17 deletions(-) diff --git a/packages/open-next/src/build/createServerBundle.ts b/packages/open-next/src/build/createServerBundle.ts index 65007541a..382b3a4ae 100644 --- a/packages/open-next/src/build/createServerBundle.ts +++ b/packages/open-next/src/build/createServerBundle.ts @@ -3,6 +3,7 @@ import path from "node:path"; import type { FunctionOptions, SplittedFunctionOptions } from "types/open-next"; +import type { Plugin } from "esbuild"; import logger from "../logger.js"; import { minifyAll } from "../minimize-js.js"; import { openNextReplacementPlugin } from "../plugins/replacement.js"; @@ -14,11 +15,19 @@ import { copyTracedFiles } from "./copyTracedFiles.js"; import { generateEdgeBundle } from "./edge/createEdgeBundle.js"; import * as buildHelper from "./helper.js"; import { installDependencies } from "./installDeps.js"; -import { applyCodePatches, type CodePatcher } from "./patch/codePatcher.js"; +import { type CodePatcher, applyCodePatches } from "./patch/codePatcher.js"; + +interface CodeCustomization { + // These patches are meant to apply on user and next generated code + additionalCodePatches: CodePatcher[]; + // These plugins are meant to apply during the esbuild bundling process. + // This will only apply to OpenNext code. + additionalPlugins: Plugin[]; +} export async function createServerBundle( options: buildHelper.BuildOptions, - additionalCodePatches: CodePatcher[] = [], + codeCustomization?: CodeCustomization, ) { const { config } = options; const foundRoutes = new Set(); @@ -40,7 +49,7 @@ export async function createServerBundle( if (fnOptions.runtime === "edge") { await generateEdgeBundle(name, options, fnOptions); } else { - await generateBundle(name, options, fnOptions, additionalCodePatches); + await generateBundle(name, options, fnOptions, codeCustomization); } }); @@ -105,7 +114,7 @@ async function generateBundle( name: string, options: buildHelper.BuildOptions, fnOptions: SplittedFunctionOptions, - additionalCodePatches: CodePatcher[] = [], + codeCustomization?: CodeCustomization, ) { const { appPath, appBuildOutputPath, config, outputDir, monorepoRoot } = options; @@ -166,11 +175,13 @@ async function generateBundle( bundledNextServer: isBundled, }); + const additionalCodePatches = codeCustomization?.additionalCodePatches ?? []; + await applyCodePatches(options, Array.from(tracedFiles), manifests, [ // TODO: create real code patchers here { name: "fakePatchChunks", - filter: /chunks\/\d*\.js/, + pathFilter: /chunks\/\d+\.js/, patchCode: async ({ code }) => { return `console.log("patched chunk");\n${code}`; }, @@ -196,6 +207,8 @@ async function generateBundle( const disableRouting = isBefore13413 || config.middleware?.external; + const additionalPlugins = codeCustomization?.additionalPlugins ?? []; + const plugins = [ openNextReplacementPlugin({ name: `requestHandlerOverride ${name}`, @@ -221,6 +234,7 @@ async function generateBundle( fnName: name, overrides, }), + ...additionalPlugins, ]; const outfileExt = fnOptions.runtime === "deno" ? "ts" : "mjs"; diff --git a/packages/open-next/src/build/edge/createEdgeBundle.ts b/packages/open-next/src/build/edge/createEdgeBundle.ts index 84d5c46d7..b5b6b5eec 100644 --- a/packages/open-next/src/build/edge/createEdgeBundle.ts +++ b/packages/open-next/src/build/edge/createEdgeBundle.ts @@ -2,7 +2,7 @@ import { mkdirSync } from "node:fs"; import fs from "node:fs"; import path from "node:path"; -import { build } from "esbuild"; +import { type Plugin, build } from "esbuild"; import type { MiddlewareInfo } from "types/next-types"; import type { IncludedConverter, @@ -39,6 +39,7 @@ interface BuildEdgeBundleOptions { additionalExternals?: string[]; onlyBuildOnce?: boolean; name: string; + additionalPlugins: Plugin[]; } export async function buildEdgeBundle({ @@ -53,6 +54,7 @@ export async function buildEdgeBundle({ additionalExternals, onlyBuildOnce, name, + additionalPlugins, }: BuildEdgeBundleOptions) { const isInCloudfare = await isEdgeRuntime(overrides); function override(target: T) { @@ -98,6 +100,7 @@ export async function buildEdgeBundle({ nextDir: path.join(options.appBuildOutputPath, ".next"), isInCloudfare, }), + ...additionalPlugins, ], treeShaking: true, alias: { @@ -173,6 +176,7 @@ export async function generateEdgeBundle( name: string, options: BuildOptions, fnOptions: SplittedFunctionOptions, + additionalPlugins: Plugin[] = [], ) { logger.info(`Generating edge bundle for: ${name}`); @@ -226,5 +230,6 @@ export async function generateEdgeBundle( overrides: fnOptions.override, additionalExternals: options.config.edgeExternals, name, + additionalPlugins, }); } diff --git a/packages/open-next/src/build/patch/codePatcher.ts b/packages/open-next/src/build/patch/codePatcher.ts index de355fabe..45a8add71 100644 --- a/packages/open-next/src/build/patch/codePatcher.ts +++ b/packages/open-next/src/build/patch/codePatcher.ts @@ -1,10 +1,13 @@ -import * as buildHelper from "../helper.js"; import * as fs from "node:fs/promises"; +import * as buildHelper from "../helper.js"; // Either before or after should be provided, otherwise just use the field directly interface VersionedField { - before?: `${number}.${number}.${number}`; - after?: `${number}.${number}.${number}`; + before?: + | `${number}` + | `${number}.${number}` + | `${number}.${number}.${number}`; + after?: `${number}` | `${number}.${number}` | `${number}.${number}.${number}`; field: T; } @@ -22,7 +25,7 @@ type PatchCodeFn = (args: { export interface CodePatcher { name: string; - filter: RegExp | VersionedField[]; + pathFilter: RegExp | VersionedField[]; contentFilter?: RegExp | VersionedField[]; patchCode: PatchCodeFn | VersionedField[]; } @@ -61,22 +64,22 @@ export async function applyCodePatches( manifests: Record, codePatcher: CodePatcher[], ) { + const nextVersion = buildOptions.nextVersion; await Promise.all( tracedFiles.map(async (filePath) => { - const nextVersion = buildOptions.nextVersion; // We check the filename against the filter to see if we should apply the patch - const patchToPotentiallyApply = codePatcher.filter((patch) => { - const filters = Array.isArray(patch.filter) - ? extractVersionedField(patch.filter, nextVersion) - : [patch.filter]; + const patchMatchingPath = codePatcher.filter((patch) => { + const filters = Array.isArray(patch.pathFilter) + ? extractVersionedField(patch.pathFilter, nextVersion) + : [patch.pathFilter]; return filters.some((filter) => filePath.match(filter)); }); - if (patchToPotentiallyApply.length === 0) { + if (patchMatchingPath.length === 0) { return; } const content = await fs.readFile(filePath, "utf-8"); // We filter a last time against the content this time - const patchToApply = patchToPotentiallyApply.filter((patch) => { + const patchToApply = patchMatchingPath.filter((patch) => { if (!patch.contentFilter) { return true; } From f43255dccdf824df83fc71c4a6cafedd62dcae00 Mon Sep 17 00:00:00 2001 From: Dorseuil Nicolas Date: Mon, 17 Feb 2025 12:18:43 +0100 Subject: [PATCH 03/10] fix build --- packages/open-next/src/build/createMiddleware.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/open-next/src/build/createMiddleware.ts b/packages/open-next/src/build/createMiddleware.ts index 914acce12..677bacddb 100644 --- a/packages/open-next/src/build/createMiddleware.ts +++ b/packages/open-next/src/build/createMiddleware.ts @@ -81,6 +81,7 @@ export async function createMiddleware( additionalExternals: config.edgeExternals, onlyBuildOnce: forceOnlyBuildOnce === true, name: "middleware", + additionalPlugins: [], }); installDependencies(outputPath, config.middleware?.install); @@ -96,6 +97,7 @@ export async function createMiddleware( options, onlyBuildOnce: true, name: "middleware", + additionalPlugins: [], }); } } From d95fb6e186770ae82a7cbe0c33c8bcb06248626c Mon Sep 17 00:00:00 2001 From: Dorseuil Nicolas Date: Wed, 26 Feb 2025 12:33:13 +0100 Subject: [PATCH 04/10] use ContentUpdater plugin --- .../open-next/src/build/createMiddleware.ts | 4 +- .../open-next/src/build/createServerBundle.ts | 8 +- .../src/build/edge/createEdgeBundle.ts | 10 +- .../open-next/src/build/patch/codePatcher.ts | 4 +- .../open-next/src/plugins/content-updater.ts | 105 ++++++++++++++++++ 5 files changed, 122 insertions(+), 9 deletions(-) create mode 100644 packages/open-next/src/plugins/content-updater.ts diff --git a/packages/open-next/src/build/createMiddleware.ts b/packages/open-next/src/build/createMiddleware.ts index 677bacddb..3ce60e239 100644 --- a/packages/open-next/src/build/createMiddleware.ts +++ b/packages/open-next/src/build/createMiddleware.ts @@ -81,7 +81,7 @@ export async function createMiddleware( additionalExternals: config.edgeExternals, onlyBuildOnce: forceOnlyBuildOnce === true, name: "middleware", - additionalPlugins: [], + additionalPlugins: () => [], }); installDependencies(outputPath, config.middleware?.install); @@ -97,7 +97,7 @@ export async function createMiddleware( options, onlyBuildOnce: true, name: "middleware", - additionalPlugins: [], + additionalPlugins: () => [], }); } } diff --git a/packages/open-next/src/build/createServerBundle.ts b/packages/open-next/src/build/createServerBundle.ts index 382b3a4ae..30b6dee35 100644 --- a/packages/open-next/src/build/createServerBundle.ts +++ b/packages/open-next/src/build/createServerBundle.ts @@ -6,6 +6,7 @@ import type { FunctionOptions, SplittedFunctionOptions } from "types/open-next"; import type { Plugin } from "esbuild"; import logger from "../logger.js"; import { minifyAll } from "../minimize-js.js"; +import { ContentUpdater } from "../plugins/content-updater.js"; import { openNextReplacementPlugin } from "../plugins/replacement.js"; import { openNextResolvePlugin } from "../plugins/resolve.js"; import { getCrossPlatformPathRegex } from "../utils/regex.js"; @@ -22,7 +23,7 @@ interface CodeCustomization { additionalCodePatches: CodePatcher[]; // These plugins are meant to apply during the esbuild bundling process. // This will only apply to OpenNext code. - additionalPlugins: Plugin[]; + additionalPlugins: (contentUpdater: ContentUpdater) => Plugin[]; } export async function createServerBundle( @@ -207,7 +208,9 @@ async function generateBundle( const disableRouting = isBefore13413 || config.middleware?.external; - const additionalPlugins = codeCustomization?.additionalPlugins ?? []; + const updater = new ContentUpdater(options); + + const additionalPlugins = codeCustomization?.additionalPlugins(updater) ?? []; const plugins = [ openNextReplacementPlugin({ @@ -235,6 +238,7 @@ async function generateBundle( overrides, }), ...additionalPlugins, + updater.plugin, ]; const outfileExt = fnOptions.runtime === "deno" ? "ts" : "mjs"; diff --git a/packages/open-next/src/build/edge/createEdgeBundle.ts b/packages/open-next/src/build/edge/createEdgeBundle.ts index b5b6b5eec..145518750 100644 --- a/packages/open-next/src/build/edge/createEdgeBundle.ts +++ b/packages/open-next/src/build/edge/createEdgeBundle.ts @@ -16,6 +16,7 @@ import type { import { loadMiddlewareManifest } from "config/util.js"; import type { OriginResolver } from "types/overrides.js"; import logger from "../../logger.js"; +import { ContentUpdater } from "../../plugins/content-updater.js"; import { openNextEdgePlugins } from "../../plugins/edge.js"; import { openNextExternalMiddlewarePlugin } from "../../plugins/externalMiddleware.js"; import { openNextReplacementPlugin } from "../../plugins/replacement.js"; @@ -39,7 +40,7 @@ interface BuildEdgeBundleOptions { additionalExternals?: string[]; onlyBuildOnce?: boolean; name: string; - additionalPlugins: Plugin[]; + additionalPlugins: (contentUpdater: ContentUpdater) => Plugin[]; } export async function buildEdgeBundle({ @@ -54,7 +55,7 @@ export async function buildEdgeBundle({ additionalExternals, onlyBuildOnce, name, - additionalPlugins, + additionalPlugins: additionalPluginsFn, }: BuildEdgeBundleOptions) { const isInCloudfare = await isEdgeRuntime(overrides); function override(target: T) { @@ -62,6 +63,8 @@ export async function buildEdgeBundle({ ? overrides[target] : undefined; } + const contentUpdater = new ContentUpdater(options); + const additionalPlugins = additionalPluginsFn(contentUpdater); await esbuildAsync( { entryPoints: [entrypoint], @@ -101,6 +104,7 @@ export async function buildEdgeBundle({ isInCloudfare, }), ...additionalPlugins, + contentUpdater.plugin, ], treeShaking: true, alias: { @@ -176,7 +180,7 @@ export async function generateEdgeBundle( name: string, options: BuildOptions, fnOptions: SplittedFunctionOptions, - additionalPlugins: Plugin[] = [], + additionalPlugins: (contentUpdater: ContentUpdater) => Plugin[] = () => [], ) { logger.info(`Generating edge bundle for: ${name}`); diff --git a/packages/open-next/src/build/patch/codePatcher.ts b/packages/open-next/src/build/patch/codePatcher.ts index 45a8add71..355b538fd 100644 --- a/packages/open-next/src/build/patch/codePatcher.ts +++ b/packages/open-next/src/build/patch/codePatcher.ts @@ -2,7 +2,7 @@ import * as fs from "node:fs/promises"; import * as buildHelper from "../helper.js"; // Either before or after should be provided, otherwise just use the field directly -interface VersionedField { +export interface VersionedField { before?: | `${number}` | `${number}.${number}` @@ -30,7 +30,7 @@ export interface CodePatcher { patchCode: PatchCodeFn | VersionedField[]; } -function extractVersionedField( +export function extractVersionedField( fields: VersionedField[], version: string, ): T[] { diff --git a/packages/open-next/src/plugins/content-updater.ts b/packages/open-next/src/plugins/content-updater.ts new file mode 100644 index 000000000..d54503253 --- /dev/null +++ b/packages/open-next/src/plugins/content-updater.ts @@ -0,0 +1,105 @@ +/** + * Mostly copied from the cloudflare adapter + * ESBuild stops calling `onLoad` hooks after the first hook returns an updated content. + * + * The updater allows multiple plugins to update the content. + */ + +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"; + +/** + * The callbacks returns either an updated content or undefined if the content is unchanged. + */ +export type Callback = (args: { + contents: string; + path: string; +}) => string | undefined | Promise; + +/** + * The callback is called only when `contentFilter` matches the content. + * It can be used as a fast heuristic to prevent an expensive update. + */ +export type OnUpdateOptions = OnLoadOptions & { + contentFilter: RegExp; +}; + +export type Updater = OnUpdateOptions & { callback: Callback }; + +export class ContentUpdater { + updaters = new Map(); + + constructor(private buildOptions: BuildOptions) {} + + /** + * Register a callback to update the file content. + * + * 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`. + * @returns A noop ESBuild plugin. + */ + updateContent( + name: string, + versionedUpdaters: VersionedField[], + ): 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); + return { + name, + setup() {}, + }; + } + + /** + * Returns an ESBuild plugin applying the registered updates. + */ + get plugin() { + return { + name: "aggregate-on-load", + + setup: async (build: PluginBuild) => { + build.onLoad( + { filter: /\.(js|mjs|cjs|jsx|ts|tsx)$/ }, + async (args: OnLoadArgs) => { + let contents = await readFile(args.path, "utf-8"); + // biome-ignore lint/correctness/noFlatMapIdentity: + const updaters = this.updaters.values().flatMap((u) => u); + for (const { + filter, + namespace, + contentFilter, + callback, + } of updaters) { + if (namespace !== undefined && args.namespace !== namespace) { + continue; + } + if (!args.path.match(filter)) { + continue; + } + if (!contents.match(contentFilter)) { + continue; + } + contents = + (await callback({ contents, path: args.path })) ?? contents; + } + return { contents }; + }, + ); + }, + }; + } +} From a5f403ec3b8185523ee7ce96c63da1b88093389b Mon Sep 17 00:00:00 2001 From: Dorseuil Nicolas Date: Wed, 26 Feb 2025 15:23:57 +0100 Subject: [PATCH 05/10] added ast-grep code patcher --- packages/open-next/package.json | 1 + .../src/build/patch/astCodePatcher.ts | 114 ++++++++++++++++++ .../open-next/src/build/patch/codePatcher.ts | 3 +- pnpm-lock.yaml | 100 +++++++++++++++ 4 files changed, 216 insertions(+), 2 deletions(-) create mode 100644 packages/open-next/src/build/patch/astCodePatcher.ts diff --git a/packages/open-next/package.json b/packages/open-next/package.json index 224fa4f23..77b00ae98 100644 --- a/packages/open-next/package.json +++ b/packages/open-next/package.json @@ -37,6 +37,7 @@ "README.md" ], "dependencies": { + "@ast-grep/napi": "^0.35.0", "@aws-sdk/client-cloudfront": "3.398.0", "@aws-sdk/client-dynamodb": "^3.398.0", "@aws-sdk/client-lambda": "^3.398.0", diff --git a/packages/open-next/src/build/patch/astCodePatcher.ts b/packages/open-next/src/build/patch/astCodePatcher.ts new file mode 100644 index 000000000..779ea5d19 --- /dev/null +++ b/packages/open-next/src/build/patch/astCodePatcher.ts @@ -0,0 +1,114 @@ +// Mostly copied from the cloudflare adapter +import { readFileSync } from "node:fs"; + +import { + type Edit, + Lang, + type NapiConfig, + type SgNode, + parse, +} from "@ast-grep/napi"; +import yaml from "yaml"; +import type { PatchCodeFn } from "./codePatcher"; + +/** + * fix has the same meaning as in yaml rules + * see https://ast-grep.github.io/guide/rewrite-code.html#using-fix-in-yaml-rule + */ +export type RuleConfig = NapiConfig & { fix?: string }; + +/** + * Returns the `Edit`s and `Match`es for an ast-grep rule in yaml format + * + * The rule must have a `fix` to rewrite the matched node. + * + * Tip: use https://ast-grep.github.io/playground.html to create rules. + * + * @param rule The rule. Either a yaml string or an instance of `RuleConfig` + * @param root The root node + * @param once only apply once + * @returns A list of edits and a list of matches. + */ +export function applyRule( + rule: string | RuleConfig, + root: SgNode, + { once = false } = {}, +) { + const ruleConfig: RuleConfig = + typeof rule === "string" ? yaml.parse(rule) : rule; + if (ruleConfig.transform) { + throw new Error("transform is not supported"); + } + if (!ruleConfig.fix) { + throw new Error("no fix to apply"); + } + + const fix = ruleConfig.fix; + + const matches = once + ? [root.find(ruleConfig)].filter((m) => m !== null) + : root.findAll(ruleConfig); + + const edits: Edit[] = []; + + matches.forEach((match) => { + edits.push( + match.replace( + // Replace known placeholders by their value + fix + .replace(/\$\$\$([A-Z0-9_]+)/g, (_m, name) => + match + .getMultipleMatches(name) + .map((n) => n.text()) + .join(""), + ) + .replace( + /\$([A-Z0-9_]+)/g, + (m, name) => match.getMatch(name)?.text() ?? m, + ), + ), + ); + }); + + return { edits, matches }; +} + +/** + * Parse a file and obtain its root. + * + * @param path The file path + * @param lang The language to parse. Defaults to TypeScript. + * @returns The root for the file. + */ +export function parseFile(path: string, lang = Lang.TypeScript) { + return parse(lang, readFileSync(path, { encoding: "utf-8" })).root(); +} + +/** + * Patches the code from by applying the rule. + * + * This function is mainly for on off edits and tests, + * use `getRuleEdits` to apply multiple rules. + * + * @param code The source code + * @param rule The astgrep rule (yaml or NapiConfig) + * @param lang The language used by the source code + * @param lang Whether to apply the rule only once + * @returns The patched code + */ +export function patchCode( + code: string, + rule: string | RuleConfig, + { lang = Lang.TypeScript, once = false } = {}, +): string { + const node = parse(lang, code).root(); + const { edits } = applyRule(rule, node, { once }); + return node.commitEdits(edits); +} + +export function createPatchCode( + rule: string | RuleConfig, + lang = Lang.TypeScript, +): PatchCodeFn { + return async ({ code }) => patchCode(code, rule, { lang }); +} diff --git a/packages/open-next/src/build/patch/codePatcher.ts b/packages/open-next/src/build/patch/codePatcher.ts index 355b538fd..4879c125a 100644 --- a/packages/open-next/src/build/patch/codePatcher.ts +++ b/packages/open-next/src/build/patch/codePatcher.ts @@ -11,8 +11,7 @@ export interface VersionedField { field: T; } -//TODO: create a version of this that would use ast-grep -type PatchCodeFn = (args: { +export type PatchCodeFn = (args: { // The content of the file that needs to be patched code: string; // The final path of the file that needs to be patched diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a53debc16..8b64d6809 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -204,6 +204,9 @@ importers: packages/open-next: dependencies: + '@ast-grep/napi': + specifier: ^0.35.0 + version: 0.35.0 '@aws-sdk/client-cloudfront': specifier: 3.398.0 version: 3.398.0(aws-crt@1.23.0) @@ -323,6 +326,64 @@ packages: resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} + '@ast-grep/napi-darwin-arm64@0.35.0': + resolution: {integrity: sha512-T+MN4Oinc+sXjXCIHzfxDDWY7r2pKgPxM6zVeVlkMTrJV2mJtyKYBIS+CABhRM6kflps2T2I6l4DGaKV/8Ym9w==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@ast-grep/napi-darwin-x64@0.35.0': + resolution: {integrity: sha512-pEYiN6JI1HY2uWhMYJ9+3yIMyVYKuYdFzeD+dL7odA3qzK0o9N9AM3/NOt4ynU2EhufaWCJr0P5NoQ636qN6MQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@ast-grep/napi-linux-arm64-gnu@0.35.0': + resolution: {integrity: sha512-NBuzQngABGKz7lhG08IQb+7nPqUx81Ol37xmS3ZhVSdSgM0mtp93rCbgFTkJcAFE8IMfCHQSg7G4g0Iotz4ABQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@ast-grep/napi-linux-arm64-musl@0.35.0': + resolution: {integrity: sha512-1EcvHPwyWpCL/96LuItBYGfeI5FaMTRvL+dHbO/hL5q1npqbb5qn+ppJwtNOjTPz8tayvgggxVk9T4C2O7taYA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@ast-grep/napi-linux-x64-gnu@0.35.0': + resolution: {integrity: sha512-FDzNdlqmQnsiWXhnLxusw5AOfEcEM+5xtmrnAf3SBRFr86JyWD9qsynnFYC2pnP9hlMfifNH2TTmMpyGJW49Xw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@ast-grep/napi-linux-x64-musl@0.35.0': + resolution: {integrity: sha512-wlmndjfBafT8u5p4DBnoRQyoCSGNuVSz7rT3TqhvlHcPzUouRWMn95epU9B1LNLyjXvr9xHeRjSktyCN28w57Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@ast-grep/napi-win32-arm64-msvc@0.35.0': + resolution: {integrity: sha512-gkhJeYc4rrZLX2icLxalPikTLMR57DuIYLwLr9g+StHYXIsGHrbfrE6Nnbdd8Izfs34ArFCrcwdaMrGlvOPSeg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@ast-grep/napi-win32-ia32-msvc@0.35.0': + resolution: {integrity: sha512-OdUuRa3chHCZ65y+qALfkUjz0W0Eg21YZ9TyPquV5why07M6HAK38mmYGzLxFH6294SvRQhs+FA/rAfbKeH0jA==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@ast-grep/napi-win32-x64-msvc@0.35.0': + resolution: {integrity: sha512-pcQRUHqbroTN1oQ56V982a7IZTUUySQYWa2KEyksiifHGuBuitlzcyzFGjT96ThcqD9XW0UVJMvpoF2Qjh006Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@ast-grep/napi@0.35.0': + resolution: {integrity: sha512-3ucaaSxV6fxXoqHrE/rxAvP1THnDdY5jNzGlnvx+JvnY9C/dSRKc0jlRMRz59N3El572+/yNRUUpAV1T9aBJug==} + engines: {node: '>= 10'} + '@aws-cdk/asset-awscli-v1@2.2.208': resolution: {integrity: sha512-r4CuHZaiBioU6waWhCNdEL4MO1+rfbcYVS/Ndz1XNGB5cxIRZwAS0Si6qD2D6nsgpPojiruFl67T1t5M9Va8kQ==} @@ -5497,6 +5558,45 @@ snapshots: '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 + '@ast-grep/napi-darwin-arm64@0.35.0': + optional: true + + '@ast-grep/napi-darwin-x64@0.35.0': + optional: true + + '@ast-grep/napi-linux-arm64-gnu@0.35.0': + optional: true + + '@ast-grep/napi-linux-arm64-musl@0.35.0': + optional: true + + '@ast-grep/napi-linux-x64-gnu@0.35.0': + optional: true + + '@ast-grep/napi-linux-x64-musl@0.35.0': + optional: true + + '@ast-grep/napi-win32-arm64-msvc@0.35.0': + optional: true + + '@ast-grep/napi-win32-ia32-msvc@0.35.0': + optional: true + + '@ast-grep/napi-win32-x64-msvc@0.35.0': + optional: true + + '@ast-grep/napi@0.35.0': + optionalDependencies: + '@ast-grep/napi-darwin-arm64': 0.35.0 + '@ast-grep/napi-darwin-x64': 0.35.0 + '@ast-grep/napi-linux-arm64-gnu': 0.35.0 + '@ast-grep/napi-linux-arm64-musl': 0.35.0 + '@ast-grep/napi-linux-x64-gnu': 0.35.0 + '@ast-grep/napi-linux-x64-musl': 0.35.0 + '@ast-grep/napi-win32-arm64-msvc': 0.35.0 + '@ast-grep/napi-win32-ia32-msvc': 0.35.0 + '@ast-grep/napi-win32-x64-msvc': 0.35.0 + '@aws-cdk/asset-awscli-v1@2.2.208': {} '@aws-cdk/asset-kubectl-v20@2.1.3': {} From 3865adc8bdd6ecc0dd2eac6eaffb1eeaf81851ee Mon Sep 17 00:00:00 2001 From: Dorseuil Nicolas Date: Mon, 3 Mar 2025 14:13:18 +0100 Subject: [PATCH 06/10] added some manifests --- .../open-next/src/adapters/config/util.ts | 8 ++++-- .../open-next/src/build/copyTracedFiles.ts | 26 ++++++++++++++++--- .../open-next/src/build/createServerBundle.ts | 3 ++- .../open-next/src/plugins/content-updater.ts | 8 +++--- 4 files changed, 36 insertions(+), 9 deletions(-) diff --git a/packages/open-next/src/adapters/config/util.ts b/packages/open-next/src/adapters/config/util.ts index fb64ae1ca..147932598 100644 --- a/packages/open-next/src/adapters/config/util.ts +++ b/packages/open-next/src/adapters/config/util.ts @@ -21,10 +21,14 @@ export function loadBuildId(nextDir: string) { return fs.readFileSync(filePath, "utf-8").trim(); } -export function loadHtmlPages(nextDir: string) { +export function loadPagesManifest(nextDir: string) { const filePath = path.join(nextDir, "server/pages-manifest.json"); const json = fs.readFileSync(filePath, "utf-8"); - return Object.entries(JSON.parse(json)) + return JSON.parse(json); +} + +export function loadHtmlPages(nextDir: string) { + return Object.entries(loadPagesManifest(nextDir)) .filter(([_, value]) => (value as string).endsWith(".html")) .map(([key]) => key); } diff --git a/packages/open-next/src/build/copyTracedFiles.ts b/packages/open-next/src/build/copyTracedFiles.ts index 417c3943c..3c6a9c773 100644 --- a/packages/open-next/src/build/copyTracedFiles.ts +++ b/packages/open-next/src/build/copyTracedFiles.ts @@ -13,7 +13,15 @@ import { } from "node:fs"; import path from "node:path"; -import { loadConfig, loadPrerenderManifest } from "config/util.js"; +import { + loadAppPathsManifest, + loadBuildId, + loadConfig, + loadFunctionsConfigManifest, + loadMiddlewareManifest, + loadPagesManifest, + loadPrerenderManifest, +} from "config/util.js"; import { getCrossPlatformPathRegex } from "utils/regex.js"; import logger from "../logger.js"; import { MIDDLEWARE_TRACE_FILE } from "./constant.js"; @@ -50,6 +58,19 @@ interface CopyTracedFilesOptions { skipServerFiles?: boolean; } +// TODO: add all the necessary manifests here +function getManifests(nextDir: string) { + return { + buildId: loadBuildId(nextDir), + config: loadConfig(nextDir), + prerenderManifest: loadPrerenderManifest(nextDir), + pagesManifest: loadPagesManifest(nextDir), + appPathsManifest: loadAppPathsManifest(nextDir), + middlewareManifest: loadMiddlewareManifest(nextDir), + functionsConfigManifest: loadFunctionsConfigManifest(nextDir), + }; +} + // eslint-disable-next-line sonarjs/cognitive-complexity export async function copyTracedFiles({ buildOutputPath, @@ -326,7 +347,6 @@ File ${fullFilePath} does not exist return { tracedFiles: filesToCopy.values(), - // TODO: actually return this - manifests: {}, + manifests: getManifests(standaloneNextDir), }; } diff --git a/packages/open-next/src/build/createServerBundle.ts b/packages/open-next/src/build/createServerBundle.ts index 30b6dee35..d2331922b 100644 --- a/packages/open-next/src/build/createServerBundle.ts +++ b/packages/open-next/src/build/createServerBundle.ts @@ -183,7 +183,8 @@ async function generateBundle( { name: "fakePatchChunks", pathFilter: /chunks\/\d+\.js/, - patchCode: async ({ code }) => { + patchCode: async ({ code, manifests }) => { + console.log(manifests); return `console.log("patched chunk");\n${code}`; }, }, diff --git a/packages/open-next/src/plugins/content-updater.ts b/packages/open-next/src/plugins/content-updater.ts index d54503253..f34cf0367 100644 --- a/packages/open-next/src/plugins/content-updater.ts +++ b/packages/open-next/src/plugins/content-updater.ts @@ -12,7 +12,7 @@ import type { BuildOptions } from "../build/helper"; import { type VersionedField, extractVersionedField, -} from "../build/patch/codePatcher"; +} from "../build/patch/codePatcher.js"; /** * The callbacks returns either an updated content or undefined if the content is unchanged. @@ -75,9 +75,11 @@ export class ContentUpdater { build.onLoad( { filter: /\.(js|mjs|cjs|jsx|ts|tsx)$/ }, async (args: OnLoadArgs) => { + const updaters = Array.from(this.updaters.values()).flat(); + if (updaters.length === 0) { + return; + } let contents = await readFile(args.path, "utf-8"); - // biome-ignore lint/correctness/noFlatMapIdentity: - const updaters = this.updaters.values().flatMap((u) => u); for (const { filter, namespace, From e0fa5a6d2acd4ee3486abb4a4eed8b958f18a691 Mon Sep 17 00:00:00 2001 From: Dorseuil Nicolas Date: Mon, 3 Mar 2025 15:07:10 +0100 Subject: [PATCH 07/10] fix and add logs --- packages/open-next/package.json | 3 +- .../open-next/src/build/patch/codePatcher.ts | 16 +++++-- .../tests/build/patch/codePatcher.test.ts | 48 +++++++++++++++++++ pnpm-lock.yaml | 10 ++++ 4 files changed, 72 insertions(+), 5 deletions(-) create mode 100644 packages/tests-unit/tests/build/patch/codePatcher.test.ts diff --git a/packages/open-next/package.json b/packages/open-next/package.json index 77b00ae98..013382ace 100644 --- a/packages/open-next/package.json +++ b/packages/open-next/package.json @@ -51,7 +51,8 @@ "esbuild": "0.19.2", "express": "5.0.1", "path-to-regexp": "^6.3.0", - "urlpattern-polyfill": "^10.0.0" + "urlpattern-polyfill": "^10.0.0", + "yaml": "^2.7.0" }, "devDependencies": { "@types/aws-lambda": "^8.10.109", diff --git a/packages/open-next/src/build/patch/codePatcher.ts b/packages/open-next/src/build/patch/codePatcher.ts index 4879c125a..4c65d1fbe 100644 --- a/packages/open-next/src/build/patch/codePatcher.ts +++ b/packages/open-next/src/build/patch/codePatcher.ts @@ -1,12 +1,15 @@ import * as fs from "node:fs/promises"; import * as buildHelper from "../helper.js"; +import logger from "../../logger.js"; // Either before or after should be provided, otherwise just use the field directly export interface VersionedField { + // The version before which the field should be used before?: | `${number}` | `${number}.${number}` | `${number}.${number}.${number}`; + // The version after which the field should be used after?: `${number}` | `${number}.${number}` | `${number}.${number}.${number}`; field: T; } @@ -38,18 +41,18 @@ export function extractVersionedField( if ( field.before && field.after && - buildHelper.compareSemver(version, field.before) >= 0 && - buildHelper.compareSemver(version, field.after) < 0 + buildHelper.compareSemver(version, field.before) <= 0 && + buildHelper.compareSemver(version, field.after) > 0 ) { result.push(field.field); } else if ( field.before && - buildHelper.compareSemver(version, field.before) >= 0 + buildHelper.compareSemver(version, field.before) <= 0 ) { result.push(field.field); } else if ( field.after && - buildHelper.compareSemver(version, field.after) < 0 + buildHelper.compareSemver(version, field.after) > 0 ) { result.push(field.field); } @@ -64,6 +67,7 @@ export async function applyCodePatches( codePatcher: CodePatcher[], ) { const nextVersion = buildOptions.nextVersion; + console.time("Applying code patches"); await Promise.all( tracedFiles.map(async (filePath) => { // We check the filename against the filter to see if we should apply the patch @@ -101,6 +105,9 @@ export async function applyCodePatches( ? extractVersionedField(patch.patchCode, nextVersion) : [patch.patchCode]; let patchedContent = content; + logger.debug( + `Applying ${patchCodeFns.length} patches to ${filePath} for ${patch.name}`, + ); for (const patchCodeFn of patchCodeFns) { patchedContent = await patchCodeFn({ code: patchedContent, @@ -113,4 +120,5 @@ export async function applyCodePatches( }); }), ); + console.timeEnd("Applying code patches"); } diff --git a/packages/tests-unit/tests/build/patch/codePatcher.test.ts b/packages/tests-unit/tests/build/patch/codePatcher.test.ts new file mode 100644 index 000000000..acc6dfe7c --- /dev/null +++ b/packages/tests-unit/tests/build/patch/codePatcher.test.ts @@ -0,0 +1,48 @@ +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( + [{ before: "16.0.0", after: "15.0.0", field: 0 }], + "15.5.0", + ); + + expect(result).toEqual([0]); + }); + + it("should return the field if the version is equal to before", () => { + const result = extractVersionedField( + [{ before: "15.0.0", after: "16.0.0", field: 0 }], + "15.0.0", + ); + + expect(result).toEqual([0]); + }); + + it("should return the field if the version is greater than after", () => { + const result = extractVersionedField( + [{ after: "16.0.0", field: 0 }], + "16.5.0", + ); + + expect(result).toEqual([0]); + }); + + it("should return the field if the version is less than before", () => { + const result = extractVersionedField( + [{ before: "15.0.0", field: 0 }], + "14.0.0", + ); + + expect(result).toEqual([0]); + }); + + it("should return an empty array if version is after before", () => { + const result = extractVersionedField( + [{ before: "15.0.0", field: 0 }], + "15.1.0", + ); + + expect(result).toEqual([]); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8b64d6809..4327bfa4f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -249,6 +249,9 @@ importers: urlpattern-polyfill: specifier: ^10.0.0 version: 10.0.0 + yaml: + specifier: ^2.7.0 + version: 2.7.0 devDependencies: '@types/aws-lambda': specifier: ^8.10.109 @@ -5510,6 +5513,11 @@ packages: engines: {node: '>= 14'} hasBin: true + yaml@2.7.0: + resolution: {integrity: sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==} + engines: {node: '>= 14'} + hasBin: true + yargs-parser@20.2.9: resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} engines: {node: '>=10'} @@ -12300,6 +12308,8 @@ snapshots: yaml@2.6.0: {} + yaml@2.7.0: {} + yargs-parser@20.2.9: {} yargs-parser@21.1.1: {} From 94bbf0a0b31b803ca34646bf84739ebeb5356691 Mon Sep 17 00:00:00 2001 From: Dorseuil Nicolas Date: Mon, 3 Mar 2025 15:09:28 +0100 Subject: [PATCH 08/10] add patch for fetch cache wait until --- .../open-next/src/build/createServerBundle.ts | 10 +---- .../build/patch/patchFetchCacheWaitUntil.ts | 40 +++++++++++++++++++ 2 files changed, 42 insertions(+), 8 deletions(-) create mode 100644 packages/open-next/src/build/patch/patchFetchCacheWaitUntil.ts diff --git a/packages/open-next/src/build/createServerBundle.ts b/packages/open-next/src/build/createServerBundle.ts index d2331922b..73599214b 100644 --- a/packages/open-next/src/build/createServerBundle.ts +++ b/packages/open-next/src/build/createServerBundle.ts @@ -17,6 +17,7 @@ import { generateEdgeBundle } from "./edge/createEdgeBundle.js"; import * as buildHelper from "./helper.js"; import { installDependencies } from "./installDeps.js"; import { type CodePatcher, applyCodePatches } from "./patch/codePatcher.js"; +import { patchFetchCacheSetMissingWaitUntil } from "./patch/patchFetchCacheWaitUntil.js"; interface CodeCustomization { // These patches are meant to apply on user and next generated code @@ -180,14 +181,7 @@ async function generateBundle( await applyCodePatches(options, Array.from(tracedFiles), manifests, [ // TODO: create real code patchers here - { - name: "fakePatchChunks", - pathFilter: /chunks\/\d+\.js/, - patchCode: async ({ code, manifests }) => { - console.log(manifests); - return `console.log("patched chunk");\n${code}`; - }, - }, + patchFetchCacheSetMissingWaitUntil, ...additionalCodePatches, ]); diff --git a/packages/open-next/src/build/patch/patchFetchCacheWaitUntil.ts b/packages/open-next/src/build/patch/patchFetchCacheWaitUntil.ts new file mode 100644 index 000000000..7692fe4b2 --- /dev/null +++ b/packages/open-next/src/build/patch/patchFetchCacheWaitUntil.ts @@ -0,0 +1,40 @@ +import { getCrossPlatformPathRegex } from "utils/regex.js"; +import type { CodePatcher } from "./codePatcher"; +import { createPatchCode } from "./astCodePatcher.js"; + +export const rule = ` +rule: + kind: call_expression + pattern: $PROMISE + all: + - has: { pattern: $_.arrayBuffer().then, stopBy: end } + - has: { pattern: "Buffer.from", stopBy: end } + - any: + - inside: + kind: sequence_expression + inside: + kind: return_statement + - inside: + kind: expression_statement + precedes: + kind: return_statement + - has: { pattern: $_.FETCH, stopBy: end } + +fix: | + globalThis.__openNextAls?.getStore()?.waitUntil?.($PROMISE) +`; + +export const patchFetchCacheSetMissingWaitUntil: CodePatcher = { + name: "patch-fetch-cache-set-missing-wait-until", + pathFilter: [ + { + after: "15.0.0", + field: getCrossPlatformPathRegex( + String.raw`(server/chunks/.*\.js|.*\.runtime\..*\.js|patch-fetch\.js)$`, + { escape: false }, + ), + }, + ], + contentFilter: /arrayBuffer\(\)\s*\.then/, + patchCode: createPatchCode(rule), +}; From 99e60e7f0c63831f30cb1d503ee1eda8f976c568 Mon Sep 17 00:00:00 2001 From: Dorseuil Nicolas Date: Mon, 3 Mar 2025 15:10:07 +0100 Subject: [PATCH 09/10] lint --- packages/open-next/src/build/patch/codePatcher.ts | 2 +- packages/open-next/src/build/patch/patchFetchCacheWaitUntil.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/open-next/src/build/patch/codePatcher.ts b/packages/open-next/src/build/patch/codePatcher.ts index 4c65d1fbe..7a412c457 100644 --- a/packages/open-next/src/build/patch/codePatcher.ts +++ b/packages/open-next/src/build/patch/codePatcher.ts @@ -1,6 +1,6 @@ import * as fs from "node:fs/promises"; -import * as buildHelper from "../helper.js"; import logger from "../../logger.js"; +import * as buildHelper from "../helper.js"; // Either before or after should be provided, otherwise just use the field directly export interface VersionedField { diff --git a/packages/open-next/src/build/patch/patchFetchCacheWaitUntil.ts b/packages/open-next/src/build/patch/patchFetchCacheWaitUntil.ts index 7692fe4b2..a77d58ccd 100644 --- a/packages/open-next/src/build/patch/patchFetchCacheWaitUntil.ts +++ b/packages/open-next/src/build/patch/patchFetchCacheWaitUntil.ts @@ -1,6 +1,6 @@ import { getCrossPlatformPathRegex } from "utils/regex.js"; -import type { CodePatcher } from "./codePatcher"; import { createPatchCode } from "./astCodePatcher.js"; +import type { CodePatcher } from "./codePatcher"; export const rule = ` rule: From 4b903588d904e02b014843aded28ed13b84b30ad Mon Sep 17 00:00:00 2001 From: Dorseuil Nicolas Date: Mon, 3 Mar 2025 15:48:37 +0100 Subject: [PATCH 10/10] fix for missing waitUntil in aws --- packages/open-next/src/build/patch/patchFetchCacheWaitUntil.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/open-next/src/build/patch/patchFetchCacheWaitUntil.ts b/packages/open-next/src/build/patch/patchFetchCacheWaitUntil.ts index a77d58ccd..4e13f5ac2 100644 --- a/packages/open-next/src/build/patch/patchFetchCacheWaitUntil.ts +++ b/packages/open-next/src/build/patch/patchFetchCacheWaitUntil.ts @@ -21,7 +21,7 @@ rule: - has: { pattern: $_.FETCH, stopBy: end } fix: | - globalThis.__openNextAls?.getStore()?.waitUntil?.($PROMISE) + globalThis.__openNextAls?.getStore()?.pendingPromiseRunner.add($PROMISE) `; export const patchFetchCacheSetMissingWaitUntil: CodePatcher = {