Skip to content

Commit a0f45f9

Browse files
committed
refactor(@angular/build): optimize test entrypoint name generation
The `getTestEntrypoints` function previously used a series of chained string replacement operations to generate a bundle name from a test file's path. For each file, this created multiple intermediate strings that were immediately discarded. This commit refactors the name generation logic into a single-pass function. The new implementation iterates over the file's relative path once to construct the final dash-cased name, significantly reducing the number of temporary string allocations and the resulting garbage collector pressure. This provides a performance improvement, particularly in large projects with thousands of test files.
1 parent 67e42a4 commit a0f45f9

File tree

1 file changed

+46
-10
lines changed

1 file changed

+46
-10
lines changed

packages/angular/build/src/builders/unit-test/test-discovery.ts

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -85,19 +85,11 @@ export function getTestEntrypoints(
8585
{ projectSourceRoot, workspaceRoot, removeTestExtension }: TestEntrypointsOptions,
8686
): Map<string, string> {
8787
const seen = new Set<string>();
88+
const roots = [projectSourceRoot, workspaceRoot];
8889

8990
return new Map(
9091
Array.from(testFiles, (testFile) => {
91-
const relativePath = removeRoots(testFile, [projectSourceRoot, workspaceRoot])
92-
// Strip leading dots and path separators.
93-
.replace(/^[./\\]+/, '')
94-
// Replace any path separators with dashes.
95-
.replace(/[/\\]/g, '-');
96-
let fileName = basename(relativePath, extname(relativePath));
97-
if (removeTestExtension) {
98-
fileName = fileName.replace(/\.(spec|test)$/, '');
99-
}
100-
92+
const fileName = generateNameFromPath(testFile, roots, !!removeTestExtension);
10193
const baseName = `spec-${fileName}`;
10294
let uniqueName = baseName;
10395
let suffix = 2;
@@ -112,6 +104,50 @@ export function getTestEntrypoints(
112104
);
113105
}
114106

107+
/**
108+
* Generates a unique, dash-delimited name from a file path.
109+
* This is used to create a consistent and readable bundle name for a given test file.
110+
* @param testFile The absolute path to the test file.
111+
* @param roots An array of root paths to remove from the beginning of the test file path.
112+
* @param removeTestExtension Whether to remove the `.spec` or `.test` extension from the result.
113+
* @returns A dash-cased name derived from the relative path of the test file.
114+
*/
115+
function generateNameFromPath(
116+
testFile: string,
117+
roots: string[],
118+
removeTestExtension: boolean,
119+
): string {
120+
const relativePath = removeRoots(testFile, roots);
121+
122+
let startIndex = 0;
123+
// Skip leading dots and slashes
124+
while (startIndex < relativePath.length && /^[./\\]$/.test(relativePath[startIndex])) {
125+
startIndex++;
126+
}
127+
128+
let endIndex = relativePath.length;
129+
if (removeTestExtension) {
130+
const match = relativePath.match(/\.(spec|test)\.[^.]+$/);
131+
if (match?.index) {
132+
endIndex = match.index;
133+
}
134+
} else {
135+
const extIndex = relativePath.lastIndexOf('.');
136+
if (extIndex > startIndex) {
137+
endIndex = extIndex;
138+
}
139+
}
140+
141+
// Build the final string in a single pass
142+
let result = '';
143+
for (let i = startIndex; i < endIndex; i++) {
144+
const char = relativePath[i];
145+
result += char === '/' || char === '\\' ? '-' : char;
146+
}
147+
148+
return result;
149+
}
150+
115151
const removeLeadingSlash = (pattern: string): string => {
116152
if (pattern.charAt(0) === '/') {
117153
return pattern.substring(1);

0 commit comments

Comments
 (0)