Skip to content

Commit b6f2a11

Browse files
committed
use ContentUpdater plugin
1 parent b2e94bd commit b6f2a11

File tree

5 files changed

+122
-9
lines changed

5 files changed

+122
-9
lines changed

packages/open-next/src/build/createMiddleware.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ export async function createMiddleware(
8181
additionalExternals: config.edgeExternals,
8282
onlyBuildOnce: forceOnlyBuildOnce === true,
8383
name: "middleware",
84-
additionalPlugins: [],
84+
additionalPlugins: () => [],
8585
});
8686

8787
installDependencies(outputPath, config.middleware?.install);
@@ -97,7 +97,7 @@ export async function createMiddleware(
9797
options,
9898
onlyBuildOnce: true,
9999
name: "middleware",
100-
additionalPlugins: [],
100+
additionalPlugins: () => [],
101101
});
102102
}
103103
}

packages/open-next/src/build/createServerBundle.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { FunctionOptions, SplittedFunctionOptions } from "types/open-next";
66
import type { Plugin } from "esbuild";
77
import logger from "../logger.js";
88
import { minifyAll } from "../minimize-js.js";
9+
import { ContentUpdater } from "../plugins/content-updater.js";
910
import { openNextReplacementPlugin } from "../plugins/replacement.js";
1011
import { openNextResolvePlugin } from "../plugins/resolve.js";
1112
import { getCrossPlatformPathRegex } from "../utils/regex.js";
@@ -22,7 +23,7 @@ interface CodeCustomization {
2223
additionalCodePatches: CodePatcher[];
2324
// These plugins are meant to apply during the esbuild bundling process.
2425
// This will only apply to OpenNext code.
25-
additionalPlugins: Plugin[];
26+
additionalPlugins: (contentUpdater: ContentUpdater) => Plugin[];
2627
}
2728

2829
export async function createServerBundle(
@@ -207,7 +208,9 @@ async function generateBundle(
207208

208209
const disableRouting = isBefore13413 || config.middleware?.external;
209210

210-
const additionalPlugins = codeCustomization?.additionalPlugins ?? [];
211+
const updater = new ContentUpdater(options);
212+
213+
const additionalPlugins = codeCustomization?.additionalPlugins(updater) ?? [];
211214

212215
const plugins = [
213216
openNextReplacementPlugin({
@@ -235,6 +238,7 @@ async function generateBundle(
235238
overrides,
236239
}),
237240
...additionalPlugins,
241+
updater.plugin,
238242
];
239243

240244
const outfileExt = fnOptions.runtime === "deno" ? "ts" : "mjs";

packages/open-next/src/build/edge/createEdgeBundle.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import type {
1616
import { loadMiddlewareManifest } from "config/util.js";
1717
import type { OriginResolver } from "types/overrides.js";
1818
import logger from "../../logger.js";
19+
import { ContentUpdater } from "../../plugins/content-updater.js";
1920
import { openNextEdgePlugins } from "../../plugins/edge.js";
2021
import { openNextExternalMiddlewarePlugin } from "../../plugins/externalMiddleware.js";
2122
import { openNextReplacementPlugin } from "../../plugins/replacement.js";
@@ -39,7 +40,7 @@ interface BuildEdgeBundleOptions {
3940
additionalExternals?: string[];
4041
onlyBuildOnce?: boolean;
4142
name: string;
42-
additionalPlugins: Plugin[];
43+
additionalPlugins: (contentUpdater: ContentUpdater) => Plugin[];
4344
}
4445

4546
export async function buildEdgeBundle({
@@ -54,14 +55,16 @@ export async function buildEdgeBundle({
5455
additionalExternals,
5556
onlyBuildOnce,
5657
name,
57-
additionalPlugins,
58+
additionalPlugins: additionalPluginsFn,
5859
}: BuildEdgeBundleOptions) {
5960
const isInCloudfare = await isEdgeRuntime(overrides);
6061
function override<T extends keyof Override>(target: T) {
6162
return typeof overrides?.[target] === "string"
6263
? overrides[target]
6364
: undefined;
6465
}
66+
const contentUpdater = new ContentUpdater(options);
67+
const additionalPlugins = additionalPluginsFn(contentUpdater);
6568
await esbuildAsync(
6669
{
6770
entryPoints: [entrypoint],
@@ -101,6 +104,7 @@ export async function buildEdgeBundle({
101104
isInCloudfare,
102105
}),
103106
...additionalPlugins,
107+
contentUpdater.plugin,
104108
],
105109
treeShaking: true,
106110
alias: {
@@ -176,7 +180,7 @@ export async function generateEdgeBundle(
176180
name: string,
177181
options: BuildOptions,
178182
fnOptions: SplittedFunctionOptions,
179-
additionalPlugins: Plugin[] = [],
183+
additionalPlugins: (contentUpdater: ContentUpdater) => Plugin[] = () => [],
180184
) {
181185
logger.info(`Generating edge bundle for: ${name}`);
182186

packages/open-next/src/build/patch/codePatcher.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as fs from "node:fs/promises";
22
import * as buildHelper from "../helper.js";
33

44
// Either before or after should be provided, otherwise just use the field directly
5-
interface VersionedField<T> {
5+
export interface VersionedField<T> {
66
before?:
77
| `${number}`
88
| `${number}.${number}`
@@ -30,7 +30,7 @@ export interface CodePatcher {
3030
patchCode: PatchCodeFn | VersionedField<PatchCodeFn>[];
3131
}
3232

33-
function extractVersionedField<T>(
33+
export function extractVersionedField<T>(
3434
fields: VersionedField<T>[],
3535
version: string,
3636
): T[] {
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/**
2+
* Mostly copied from the cloudflare adapter
3+
* ESBuild stops calling `onLoad` hooks after the first hook returns an updated content.
4+
*
5+
* The updater allows multiple plugins to update the content.
6+
*/
7+
8+
import { readFile } from "node:fs/promises";
9+
10+
import type { OnLoadArgs, OnLoadOptions, Plugin, PluginBuild } from "esbuild";
11+
import type { BuildOptions } from "../build/helper";
12+
import {
13+
type VersionedField,
14+
extractVersionedField,
15+
} from "../build/patch/codePatcher";
16+
17+
/**
18+
* The callbacks returns either an updated content or undefined if the content is unchanged.
19+
*/
20+
export type Callback = (args: {
21+
contents: string;
22+
path: string;
23+
}) => string | undefined | Promise<string | undefined>;
24+
25+
/**
26+
* The callback is called only when `contentFilter` matches the content.
27+
* It can be used as a fast heuristic to prevent an expensive update.
28+
*/
29+
export type OnUpdateOptions = OnLoadOptions & {
30+
contentFilter: RegExp;
31+
};
32+
33+
export type Updater = OnUpdateOptions & { callback: Callback };
34+
35+
export class ContentUpdater {
36+
updaters = new Map<string, Updater[]>();
37+
38+
constructor(private buildOptions: BuildOptions) {}
39+
40+
/**
41+
* Register a callback to update the file content.
42+
*
43+
* The callbacks are called in order of registration.
44+
*
45+
* @param name The name of the plugin (must be unique).
46+
* @param updater A versioned field with the callback and `OnUpdateOptions`.
47+
* @returns A noop ESBuild plugin.
48+
*/
49+
updateContent(
50+
name: string,
51+
versionedUpdaters: VersionedField<Updater>[],
52+
): Plugin {
53+
if (this.updaters.has(name)) {
54+
throw new Error(`Plugin "${name}" already registered`);
55+
}
56+
const updaters = extractVersionedField(
57+
versionedUpdaters,
58+
this.buildOptions.nextVersion,
59+
);
60+
this.updaters.set(name, updaters);
61+
return {
62+
name,
63+
setup() {},
64+
};
65+
}
66+
67+
/**
68+
* Returns an ESBuild plugin applying the registered updates.
69+
*/
70+
get plugin() {
71+
return {
72+
name: "aggregate-on-load",
73+
74+
setup: async (build: PluginBuild) => {
75+
build.onLoad(
76+
{ filter: /\.(js|mjs|cjs|jsx|ts|tsx)$/ },
77+
async (args: OnLoadArgs) => {
78+
let contents = await readFile(args.path, "utf-8");
79+
// biome-ignore lint/correctness/noFlatMapIdentity: <explanation>
80+
const updaters = this.updaters.values().flatMap((u) => u);
81+
for (const {
82+
filter,
83+
namespace,
84+
contentFilter,
85+
callback,
86+
} of updaters) {
87+
if (namespace !== undefined && args.namespace !== namespace) {
88+
continue;
89+
}
90+
if (!args.path.match(filter)) {
91+
continue;
92+
}
93+
if (!contents.match(contentFilter)) {
94+
continue;
95+
}
96+
contents =
97+
(await callback({ contents, path: args.path })) ?? contents;
98+
}
99+
return { contents };
100+
},
101+
);
102+
},
103+
};
104+
}
105+
}

0 commit comments

Comments
 (0)