Skip to content

Commit 4e9a367

Browse files
committed
refactor(core): enhance resolve plugin to support dynamic override resolution and improve path handling
1 parent 48a2a55 commit 4e9a367

2 files changed

Lines changed: 249 additions & 186 deletions

File tree

Lines changed: 170 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { mkdir, rm, writeFile } from "node:fs/promises";
1+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
22
import { tmpdir } from "node:os";
3-
import { join } from "node:path";
3+
import { dirname, join } from "node:path";
44

5-
import type { PluginBuild } from "esbuild";
6-
import { afterEach, beforeEach, describe, expect, test } from "vitest";
5+
import { build } from "esbuild";
6+
import { afterAll, beforeAll, describe, expect, test } from "vitest";
77

88
import { openNextResolvePlugin } from "./resolve.js";
99

@@ -62,73 +62,138 @@ export async function resolveCdnInvalidation(cdnInvalidation) {
6262
}
6363
`.trim();
6464

65-
type OnLoadCallback = (args: { path: string }) => Promise<{ contents: string }>;
65+
// The default overrides imported by the fixture above, and the alternatives the
66+
// tests redirect to.
67+
const OVERRIDE_MODULES = [
68+
"overrides/converters/node.js",
69+
"overrides/converters/edge.js",
70+
"overrides/wrappers/node.js",
71+
"overrides/wrappers/cloudflare-edge.js",
72+
"overrides/tagCache/fs-dev-nextMode.js",
73+
"overrides/queue/direct.js",
74+
"overrides/incrementalCache/fs-dev.js",
75+
"overrides/imageLoader/fs-dev.js",
76+
"overrides/originResolver/pattern-env.js",
77+
"overrides/warmer/dummy.js",
78+
"overrides/proxyExternalRequest/node.js",
79+
"overrides/cdnInvalidation/dummy.js",
80+
];
6681

67-
function createStubBuild() {
68-
let capturedCb: OnLoadCallback | undefined;
69-
const stub = {
70-
onLoad: (_opts: { filter: RegExp }, cb: OnLoadCallback) => {
71-
capturedCb = cb;
72-
},
73-
} as unknown as PluginBuild;
74-
return { stub, getCallback: () => capturedCb! };
82+
// Packages resolved through node_modules for the full-path override cases.
83+
const CORE_PKG_MODULES = [
84+
"overrides/converters/edge.js",
85+
"overrides/converters/dummy.js",
86+
"overrides/imageLoader/dummy.js",
87+
"overrides/originResolver/dummy.js",
88+
"overrides/proxyExternalRequest/fetch.js",
89+
];
90+
const AWS_PKG_MODULES = [
91+
"overrides/wrappers/aws-lambda.js",
92+
"overrides/wrappers/aws-lambda-streaming.js",
93+
"overrides/converters/aws-apigw-v2.js",
94+
"overrides/tagCache/dynamodb.js",
95+
"overrides/queue/sqs.js",
96+
"overrides/incrementalCache/s3.js",
97+
"overrides/warmer/aws-lambda.js",
98+
"overrides/cdnInvalidation/cloudfront.js",
99+
];
100+
101+
let root: string;
102+
103+
/** Writes a module exporting a marker identifying it by its path in the fixture. */
104+
async function writeModule(relPath: string) {
105+
const fullPath = join(root, relPath);
106+
await mkdir(dirname(fullPath), { recursive: true });
107+
await writeFile(fullPath, `export default "MARKER:${relPath}";`, "utf-8");
75108
}
76109

77-
describe("openNextResolvePlugin", () => {
78-
let fixturePath: string;
79-
let fixtureDir: string;
80-
81-
beforeEach(async () => {
82-
fixtureDir = join(tmpdir(), `resolve-test-${Date.now()}`, "core");
83-
await mkdir(fixtureDir, { recursive: true });
84-
fixturePath = join(fixtureDir, "resolve.js");
85-
await writeFile(fixturePath, FIXTURE_CONTENT, "utf-8");
110+
/** Marker bundled for an override living next to the fixture `resolve.js`. */
111+
function local(relPath: string) {
112+
return `MARKER:overrides/${relPath}`;
113+
}
114+
115+
/** Marker bundled for an override coming from a package in `node_modules`. */
116+
function pkg(name: string, relPath: string) {
117+
return `MARKER:node_modules/${name}/overrides/${relPath}`;
118+
}
119+
120+
/** Bundles the fixture with the plugin and returns the generated code. */
121+
async function bundleWithPlugin(opts: Parameters<typeof openNextResolvePlugin>[0], entry = "entry.js") {
122+
const result = await build({
123+
entryPoints: [join(root, entry)],
124+
absWorkingDir: root,
125+
bundle: true,
126+
write: false,
127+
format: "esm",
128+
platform: "node",
129+
outfile: join(root, "out.js"),
130+
plugins: [openNextResolvePlugin(opts)],
86131
});
132+
return result.outputFiles[0].text;
133+
}
134+
135+
describe("openNextResolvePlugin", () => {
136+
beforeAll(async () => {
137+
root = await mkdtemp(join(tmpdir(), "resolve-test-"));
138+
139+
await mkdir(join(root, "core"), { recursive: true });
140+
await writeFile(join(root, "core", "resolve.js"), FIXTURE_CONTENT, "utf-8");
141+
await writeFile(join(root, "entry.js"), `export * from "./core/resolve.js";`, "utf-8");
87142

88-
afterEach(async () => {
89-
// Clean up the temp directory (go up one level from "core")
90-
await rm(join(fixtureDir, ".."), { recursive: true, force: true });
143+
for (const mod of OVERRIDE_MODULES) {
144+
await writeModule(mod);
145+
}
146+
for (const [name, modules] of [
147+
["@opennextjs/core", CORE_PKG_MODULES],
148+
["@opennextjs/aws", AWS_PKG_MODULES],
149+
] as const) {
150+
await mkdir(join(root, "node_modules", name), { recursive: true });
151+
await writeFile(
152+
join(root, "node_modules", name, "package.json"),
153+
JSON.stringify({ name, type: "module" }),
154+
"utf-8"
155+
);
156+
for (const mod of modules) {
157+
await writeModule(join("node_modules", name, mod));
158+
}
159+
}
91160
});
92161

93-
async function runPlugin(opts: Parameters<typeof openNextResolvePlugin>[0]) {
94-
const plugin = openNextResolvePlugin(opts);
95-
const { stub, getCallback } = createStubBuild();
96-
plugin.setup(stub);
97-
const cb = getCallback();
98-
return cb({ path: fixturePath });
99-
}
162+
afterAll(async () => {
163+
await rm(root, { recursive: true, force: true });
164+
});
100165

101166
test("A - full-path default verbatim: core full path default replaces anchor", async () => {
102-
const result = await runPlugin({
167+
const contents = await bundleWithPlugin({
103168
overrides: {},
104169
defaultOverrides: { converter: "@opennextjs/core/overrides/converters/edge.js" },
105170
fnName: "test",
106171
});
107-
expect(result.contents).toContain("overrides/converters/edge.js");
108-
expect(result.contents).not.toContain('"../overrides/converters/node.js"');
172+
expect(contents).toContain(pkg("@opennextjs/core", "converters/edge.js"));
173+
expect(contents).not.toContain(local("converters/node.js"));
109174
});
110175

111176
test("B - cross-package user full aws path wins over core default", async () => {
112-
const result = await runPlugin({
177+
const contents = await bundleWithPlugin({
113178
overrides: { converter: "@opennextjs/aws/overrides/converters/aws-apigw-v2.js" },
114179
defaultOverrides: { converter: "@opennextjs/core/overrides/converters/edge.js" },
115180
fnName: "test",
116181
});
117-
expect(result.contents).toContain("overrides/converters/aws-apigw-v2.js");
118-
expect(result.contents).not.toContain("overrides/converters/edge.js");
182+
expect(contents).toContain(pkg("@opennextjs/aws", "converters/aws-apigw-v2.js"));
183+
expect(contents).not.toContain(pkg("@opennextjs/core", "converters/edge.js"));
119184
});
120185

121186
test("C - no-op anchor stays: no override no default keeps relative core path", async () => {
122-
const result = await runPlugin({
187+
const contents = await bundleWithPlugin({
123188
overrides: {},
124189
defaultOverrides: {},
125190
fnName: "test",
126191
});
127-
expect(result.contents).toContain("../overrides/converters/node.js");
192+
expect(contents).toContain(local("converters/node.js"));
128193
});
129194

130195
test("D - 10-key mixed aws+core full paths all rewritten", async () => {
131-
const result = await runPlugin({
196+
const contents = await bundleWithPlugin({
132197
overrides: {},
133198
defaultOverrides: {
134199
wrapper: "@opennextjs/aws/overrides/wrappers/aws-lambda.js",
@@ -144,42 +209,46 @@ describe("openNextResolvePlugin", () => {
144209
},
145210
fnName: "test",
146211
});
147-
expect(result.contents).toContain("overrides/wrappers/aws-lambda.js");
148-
expect(result.contents).toContain("overrides/converters/edge.js");
149-
expect(result.contents).toContain("overrides/tagCache/dynamodb.js");
150-
expect(result.contents).toContain("overrides/queue/sqs.js");
151-
expect(result.contents).toContain("overrides/incrementalCache/s3.js");
152-
expect(result.contents).toContain("overrides/imageLoader/dummy.js");
153-
expect(result.contents).toContain("overrides/originResolver/dummy.js");
154-
expect(result.contents).toContain("overrides/warmer/aws-lambda.js");
155-
expect(result.contents).toContain("overrides/proxyExternalRequest/fetch.js");
156-
expect(result.contents).toContain("overrides/cdnInvalidation/cloudfront.js");
212+
expect(contents).toContain(pkg("@opennextjs/aws", "wrappers/aws-lambda.js"));
213+
expect(contents).toContain(pkg("@opennextjs/core", "converters/edge.js"));
214+
expect(contents).toContain(pkg("@opennextjs/aws", "tagCache/dynamodb.js"));
215+
expect(contents).toContain(pkg("@opennextjs/aws", "queue/sqs.js"));
216+
expect(contents).toContain(pkg("@opennextjs/aws", "incrementalCache/s3.js"));
217+
expect(contents).toContain(pkg("@opennextjs/core", "imageLoader/dummy.js"));
218+
expect(contents).toContain(pkg("@opennextjs/core", "originResolver/dummy.js"));
219+
expect(contents).toContain(pkg("@opennextjs/aws", "warmer/aws-lambda.js"));
220+
expect(contents).toContain(pkg("@opennextjs/core", "proxyExternalRequest/fetch.js"));
221+
expect(contents).toContain(pkg("@opennextjs/aws", "cdnInvalidation/cloudfront.js"));
222+
// None of the defaults are bundled anymore
223+
expect(contents).not.toContain(local("wrappers/node.js"));
224+
expect(contents).not.toContain(local("tagCache/fs-dev-nextMode.js"));
225+
expect(contents).not.toContain(local("incrementalCache/fs-dev.js"));
157226
});
158227

159228
test("E - deprecated cloudflare bare name becomes legacy relative core path", async () => {
160-
const result = await runPlugin({
229+
const contents = await bundleWithPlugin({
161230
overrides: { wrapper: "cloudflare" },
162231
defaultOverrides: {},
163232
fnName: "test",
164233
});
165-
expect(result.contents).toContain("../overrides/wrappers/cloudflare-edge.js");
166-
expect(result.contents).not.toContain("cloudflare.js");
234+
expect(contents).toContain(local("wrappers/cloudflare-edge.js"));
235+
expect(contents).not.toContain(local("wrappers/node.js"));
167236
});
168237

169238
test("F - function override becomes full dummy core path", async () => {
170239
// oxlint-disable-next-line @typescript-eslint/no-explicit-any - testing function override
171240
const fnOverride = (() => ({})) as any;
172-
const result = await runPlugin({
241+
const contents = await bundleWithPlugin({
173242
overrides: { converter: fnOverride },
174243
defaultOverrides: { converter: "@opennextjs/core/overrides/converters/edge.js" },
175244
fnName: "test",
176245
});
177-
expect(result.contents).toContain("@opennextjs/core/overrides/converters/dummy.js");
178-
expect(result.contents).not.toContain("@opennextjs/core/overrides/converters/edge.js");
246+
expect(contents).toContain(pkg("@opennextjs/core", "converters/dummy.js"));
247+
expect(contents).not.toContain(pkg("@opennextjs/core", "converters/edge.js"));
179248
});
180249

181250
test("G - AWS server defaults produce aws full paths", async () => {
182-
const result = await runPlugin({
251+
const contents = await bundleWithPlugin({
183252
overrides: {},
184253
defaultOverrides: {
185254
wrapper: "@opennextjs/aws/overrides/wrappers/aws-lambda-streaming.js",
@@ -190,42 +259,67 @@ describe("openNextResolvePlugin", () => {
190259
},
191260
fnName: "server",
192261
});
193-
expect(result.contents).toContain("overrides/wrappers/aws-lambda-streaming.js");
194-
expect(result.contents).toContain("overrides/converters/aws-apigw-v2.js");
195-
expect(result.contents).toContain("overrides/incrementalCache/s3.js");
196-
expect(result.contents).toContain("overrides/tagCache/dynamodb.js");
197-
expect(result.contents).toContain("overrides/queue/sqs.js");
262+
expect(contents).toContain(pkg("@opennextjs/aws", "wrappers/aws-lambda-streaming.js"));
263+
expect(contents).toContain(pkg("@opennextjs/aws", "converters/aws-apigw-v2.js"));
264+
expect(contents).toContain(pkg("@opennextjs/aws", "incrementalCache/s3.js"));
265+
expect(contents).toContain(pkg("@opennextjs/aws", "tagCache/dynamodb.js"));
266+
expect(contents).toContain(pkg("@opennextjs/aws", "queue/sqs.js"));
267+
// Keys without an override keep their default
268+
expect(contents).toContain(local("imageLoader/fs-dev.js"));
198269
});
199270

200271
test("H - bare-name user override becomes legacy relative core path", async () => {
201-
const result = await runPlugin({
272+
const contents = await bundleWithPlugin({
202273
overrides: { converter: "edge" },
203274
defaultOverrides: {},
204275
fnName: "test",
205276
});
206-
expect(result.contents).toContain("../overrides/converters/edge.js");
207-
expect(result.contents).not.toContain("@opennextjs/core/overrides/converters/node.js");
277+
expect(contents).toContain(local("converters/edge.js"));
278+
expect(contents).not.toContain(local("converters/node.js"));
208279
});
209280

210-
test("I - resolvable package specifier is converted to relative filesystem path", async () => {
211-
const rootDir = join(fixtureDir, "..");
212-
const pkgDir = join(rootDir, "node_modules", "@test-pkg", "wrapper");
213-
await mkdir(pkgDir, { recursive: true });
281+
test("I - resolvable package specifier is resolved through node_modules", async () => {
282+
await mkdir(join(root, "node_modules", "@test-pkg", "wrapper"), { recursive: true });
214283
await writeFile(
215-
join(pkgDir, "package.json"),
284+
join(root, "node_modules", "@test-pkg", "wrapper", "package.json"),
216285
JSON.stringify({ name: "@test-pkg/wrapper", main: "index.js" }),
217286
"utf-8"
218287
);
219-
await writeFile(join(pkgDir, "index.js"), "module.exports = {};", "utf-8");
288+
await writeModule(join("node_modules", "@test-pkg", "wrapper", "index.js"));
220289

221-
const result = await runPlugin({
290+
const contents = await bundleWithPlugin({
222291
overrides: { wrapper: "@test-pkg/wrapper" },
223292
defaultOverrides: {},
224293
fnName: "test",
225294
});
226295

227-
expect(result.contents).not.toContain('"@test-pkg/wrapper"');
228-
expect(result.contents).toContain("node_modules/@test-pkg/wrapper/index.js");
229-
expect(result.contents).toMatch(/"\.\/.*node_modules\/@test-pkg\/wrapper\/index\.js"/);
296+
expect(contents).toContain("MARKER:node_modules/@test-pkg/wrapper/index.js");
297+
expect(contents).not.toContain(local("wrappers/node.js"));
298+
});
299+
300+
test("J - overrides of other modules are left alone", async () => {
301+
await writeFile(
302+
join(root, "core", "other.js"),
303+
`export const load = () => import("../overrides/converters/node.js");`,
304+
"utf-8"
305+
);
306+
await writeFile(
307+
join(root, "entry-other.js"),
308+
`export * from "./core/resolve.js";\nexport * from "./core/other.js";`,
309+
"utf-8"
310+
);
311+
312+
const contents = await bundleWithPlugin(
313+
{
314+
overrides: { converter: "edge" },
315+
defaultOverrides: {},
316+
fnName: "test",
317+
},
318+
"entry-other.js"
319+
);
320+
321+
// `resolve.js` gets the override, `other.js` keeps importing the default
322+
expect(contents).toContain(local("converters/edge.js"));
323+
expect(contents).toContain(local("converters/node.js"));
230324
});
231325
});

0 commit comments

Comments
 (0)