Skip to content

Commit 21884f2

Browse files
committed
refactor: use rolldown to bundle test files
1 parent 7c72ee1 commit 21884f2

4 files changed

Lines changed: 93 additions & 72 deletions

File tree

src/core/tester/test-bundler-template/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export function generateHtml(
2727
setupCode: string,
2828
testFiles: string[],
2929
): string {
30-
const tests = testFiles.map(f => `<script src="${f}"></script>`).join("\n ");
30+
const tests = testFiles.map(f => `<script src="units/${f}"></script>`).join("\n ");
3131

3232
return htmlRaw.replace("__TEST_FILES__", tests).replace("___SETUP_CODE___", setupCode);
3333
}
Lines changed: 24 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,43 @@
1-
import type { Metafile } from "esbuild";
1+
import type { MetaData } from "./test-bundler.js";
22
import { describe, expect, it } from "vitest";
33
import { findImpactedTests } from "./test-bundler.js";
44

55
describe("findImpactedTests", () => {
6-
const mockMetafileOutputs = {
7-
outputs: {
8-
".scaffold/test/resource/content/units/test1.spec.js": {
9-
entryPoint: "test/test1.spec.ts",
10-
inputs: {
11-
"test/test1.spec.ts": {},
12-
"src/moduleA.ts": {},
13-
"src/moduleB.ts": {},
14-
},
15-
},
16-
".scaffold/test/resource/content/units/test2.spec.js": {
17-
entryPoint: "test/test2.spec.ts",
18-
inputs: {
19-
"test/test2.spec.ts": {},
20-
"src/moduleC.ts": {},
21-
},
22-
},
23-
".scaffold/test/resource/content/units/test3.spec.js": {
24-
entryPoint: "test/test3.spec.ts",
25-
inputs: {
26-
"test/test3.spec.ts": {},
27-
"src/moduleC.ts": {},
28-
},
29-
},
6+
const mockBuildMetadata: MetaData = [
7+
{
8+
fileName: "test1.spec.js",
9+
name: "",
10+
moduleIds: ["src/moduleA.ts", "src/moduleB.ts", "test/test1.spec.ts"],
3011
},
31-
} as unknown as Metafile;
12+
{
13+
fileName: "test2.spec.js",
14+
name: "",
15+
moduleIds: ["test/test2.spec.ts", "src/moduleC.ts"],
16+
},
17+
{
18+
fileName: "test3.spec.js",
19+
name: "",
20+
moduleIds: ["test/test3.spec.ts", "src/moduleC.ts"],
21+
},
22+
];
3223

3324
it("returns affected test file when a test file itself is changed", () => {
34-
const result = findImpactedTests("test/test1.spec.ts", mockMetafileOutputs);
35-
expect(result).toEqual(["units/test1.spec.js"]);
25+
const result = findImpactedTests("test/test1.spec.ts", mockBuildMetadata);
26+
expect(result).toEqual(["test1.spec.js"]);
3627
});
3728

3829
it("returns affected test files when a source file is changed", () => {
39-
const result = findImpactedTests("src/moduleA.ts", mockMetafileOutputs);
40-
expect(result).toEqual(["units/test1.spec.js"]);
30+
const result = findImpactedTests("src/moduleA.ts", mockBuildMetadata);
31+
expect(result).toEqual(["test1.spec.js"]);
4132
});
4233

4334
it("returns multiple affected test files when multiple tests depend on the changed file", () => {
44-
const result = findImpactedTests("src/moduleC.ts", mockMetafileOutputs);
45-
expect(result).toEqual(["units/test2.spec.js", "units/test3.spec.js"]);
35+
const result = findImpactedTests("src/moduleC.ts", mockBuildMetadata);
36+
expect(result).toEqual(["test2.spec.js", "test3.spec.js"]);
4637
});
4738

4839
it("returns an empty array if no test file is affected", () => {
49-
const result = findImpactedTests("src/unrelated.ts", mockMetafileOutputs);
40+
const result = findImpactedTests("src/unrelated.ts", mockBuildMetadata);
5041
expect(result).toEqual([]);
5142
});
5243
});

src/core/tester/test-bundler.ts

Lines changed: 64 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
1-
import type { BuildContext, BuildResult } from "esbuild";
1+
import type { InputOptions, OutputChunk, OutputOptions, RolldownOutput } from "rolldown";
22
import type { Context } from "../../types/index.js";
3-
import { resolve } from "node:path";
4-
import { context } from "esbuild";
3+
import { relative, resolve } from "node:path";
4+
import { cwd } from "node:process";
55
import { copy, outputFile, outputJSON, pathExists } from "fs-extra/esm";
6+
import { rolldown } from "rolldown";
67
import { glob } from "tinyglobby";
7-
import { CACHE_DIR, TESTER_PLUGIN_DIR } from "../../constant.js";
8+
import { CACHE_DIR, TESTER_PLUGIN_DIR, TESTER_PLUGIN_TESTS_DIR } from "../../constant.js";
89
import { saveResource } from "../../utils/file.js";
910
import { logger } from "../../utils/logger.js";
10-
import { toArray } from "../../utils/string.js";
11+
import { normalizePath, toArray } from "../../utils/string.js";
1112
import { generateBootstrap, generateHtml, generateManifest, generateMochaSetup } from "./test-bundler-template/index.js";
1213

1314
export class TestBundler {
14-
private esbuildContext?: BuildContext;
15+
private rolldownOutput?: RolldownOutput;
1516
constructor(
1617
private ctx: Context,
1718
private port: number,
@@ -35,10 +36,11 @@ export class TestBundler {
3536

3637
async regenerate(changedFile: string): Promise<void> {
3738
// re-bundle tests
38-
const esbuildResult = await this.esbuildContext?.rebuild();
39+
await this.bundleTests();
3940

40-
// get affected tests
41-
const tests = findImpactedTests(changedFile, esbuildResult?.metafile);
41+
// get affected tests based on changed file
42+
const metadata = transformRolldownOutputToMetafile(this.rolldownOutput?.output);
43+
const tests = findImpactedTests(changedFile, metadata);
4244

4345
// this.generateTestPage
4446
// mocha setup
@@ -110,21 +112,28 @@ export class TestBundler {
110112

111113
private async bundleTests() {
112114
const testDirs = toArray(this.ctx.test.entries);
113-
// Because esbuild only support `*` and `**`,
114-
// so we need glob ourselves.
115-
// https://esbuild.github.io/api/#glob-style-entry-points
115+
// Find all test files
116116
const entryPoints = (await Promise.all(testDirs.map(dir => glob(`${dir}/**/*.{spec,test}.[jt]s`))))
117117
.flat();
118118

119-
// Bundle all test files, including both JavaScript and TypeScript
120-
this.esbuildContext = await context({
121-
entryPoints,
122-
outdir: `${TESTER_PLUGIN_DIR}/content/units`,
123-
bundle: true,
124-
target: "firefox115",
125-
metafile: true,
126-
});
127-
await this.esbuildContext.rebuild();
119+
// configure rolldown options
120+
const rolldownInputOptions: InputOptions = {
121+
input: entryPoints,
122+
treeshake: false,
123+
preserveEntrySignatures: "allow-extension",
124+
};
125+
126+
const outputOptions: OutputOptions = {
127+
dir: `${TESTER_PLUGIN_DIR}/content/units`,
128+
format: "esm",
129+
sourcemap: true,
130+
codeSplitting: false,
131+
};
132+
133+
const rolldownBuild = await rolldown(rolldownInputOptions);
134+
this.rolldownOutput = await rolldownBuild.write(outputOptions);
135+
136+
await rolldownBuild.close();
128137
}
129138

130139
private async createTestHtml(tests: string[] = []) {
@@ -139,38 +148,55 @@ export class TestBundler {
139148
// html
140149
let testFiles = tests;
141150
if (testFiles.length === 0) {
142-
testFiles = (await glob(`**/*.{spec,test}.js`, { cwd: `${TESTER_PLUGIN_DIR}/content` })).sort();
151+
testFiles = (await glob(`**/*.{spec,test}.js`, { cwd: `${TESTER_PLUGIN_TESTS_DIR}` })).sort();
143152
}
144153
const html = generateHtml(setupCode, testFiles);
145154
await outputFile(`${TESTER_PLUGIN_DIR}/content/index.xhtml`, html);
146155
}
147156
}
148157

158+
interface _MetaData extends Pick<OutputChunk, "fileName" | "name" | "moduleIds"> {}
159+
export type MetaData = _MetaData[];
160+
161+
function transformRolldownOutputToMetafile(output?: RolldownOutput["output"]): MetaData {
162+
if (!output)
163+
return [];
164+
165+
return output
166+
.flat()
167+
.filter(r => r.type === "chunk")
168+
.map(r => ({
169+
fileName: normalizePath(r.fileName),
170+
name: r.name,
171+
moduleIds: r.moduleIds.map(id => relative(cwd(), id)).map(normalizePath),
172+
}));
173+
}
174+
149175
/**
150-
* Determines which test files are impacted by a given changed file based on the esbuild metafile.
176+
* Determines which test files are impacted by a given changed file based on rolldown build output.
151177
*
152178
* This function analyzes the build metadata to find test files that depend on the changed file
153-
* either directly as an entry point or indirectly as an input. It is useful in a watch mode setup
154-
* to selectively rerun only the affected tests.
179+
* either directly as an entry point or indirectly as an input.
155180
*
156181
* @param {string} changedFilePath - The file path of the changed source file.
157-
* @param {BuildResult["metafile"]} buildMetadata - The esbuild metafile containing dependency information.
158-
* @returns {string[]} An array of impacted test file paths that need to be re-executed.
182+
* @param {MetaData} buildMetadata - The transfromed rolldown build outputs.
183+
* @returns {string[]} An array of impacted test file names that need to be re-executed.
159184
*/
160-
export function findImpactedTests(changedFilePath: string, buildMetadata: BuildResult["metafile"]): string[] {
161-
if (!buildMetadata)
162-
return [];
163-
164-
const resolvedChangedFile = resolve(changedFilePath);
185+
export function findImpactedTests(
186+
changedFilePath: string,
187+
buildMetadata: MetaData,
188+
): string[] {
189+
const normalizedChangedFile = resolve(changedFilePath);
165190
const impactedTestFiles = new Set<string>();
166191

167-
for (const [outputFilePath, outputInfo] of Object.entries(buildMetadata.outputs)) {
168-
const testFilePath = outputFilePath.replace(`${TESTER_PLUGIN_DIR}/content/`, "");
169-
// const resolvedEntryPoint = outputInfo.entryPoint ? resolve(outputInfo.entryPoint) : null;
170-
171-
if (Object.keys(outputInfo.inputs).some(inputPath => resolve(inputPath) === resolvedChangedFile)) {
172-
impactedTestFiles.add(testFilePath);
192+
for (const { fileName, moduleIds } of buildMetadata) {
193+
for (const moduleId of moduleIds) {
194+
const normalizedModuleId = resolve(moduleId);
195+
if (normalizedModuleId === normalizedChangedFile) {
196+
impactedTestFiles.add(fileName);
197+
}
173198
}
174199
}
200+
175201
return Array.from(impactedTestFiles);
176202
}

src/utils/string.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,7 @@ export function parseRepoUrl(url?: string): {
5353
const [, owner, repo] = match;
5454
return { owner, repo };
5555
}
56+
57+
export function normalizePath(path: string) {
58+
return path.replace(/\\/g, "/");
59+
}

0 commit comments

Comments
 (0)