Skip to content

Commit f7e154a

Browse files
committed
fixup! test(@angular/build): add library builder scaling benchmark suite
Fix implemented in harness.mts + index.mts: - New prepareBuilderEnv() creates (once per run, idempotently) a tiny symlink directory mapping @angular/build, @angular-devkit/core, @angular-devkit/architect to their dist/ output. - Combined with a NODE_PATH pointing at that shim dir plus each package's own existing node_modules (where ajv-formats, sass, esbuild, etc. are already correctly pnpm-linked), this lets the spawned CLI resolve every bare require(), however deeply nested. - buildOnce() now takes and applies this nodePath, merging with any pre-existing NODE_PATH rather than clobbering it.
1 parent d48bd74 commit f7e154a

2 files changed

Lines changed: 96 additions & 3 deletions

File tree

‎scripts/benchmarks/library-builder/harness.mts‎

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,20 @@ export interface BuildMeasurement {
2626

2727
export interface BuilderPaths {
2828
buildPkg: string;
29+
corePkg: string;
2930
architectPkg: string;
3031
architectCli: string;
3132
}
3233

34+
export interface BuilderEnv extends BuilderPaths {
35+
/** NODE_PATH value to pass to a spawned child so its bare `require()`s resolve into dist/. */
36+
nodePath: string;
37+
}
38+
3339
/** Verifies `@angular/build` and `@angular-devkit/architect` are built before attempting a build. */
3440
export function checkBuilderIsBuilt(repoRoot: string, logger: Console): BuilderPaths | undefined {
3541
const buildPkg = path.join(repoRoot, 'dist', '@angular', 'build');
42+
const corePkg = path.join(repoRoot, 'dist', '@angular-devkit', 'core');
3643
const architectPkg = path.join(repoRoot, 'dist', '@angular-devkit', 'architect');
3744
const architectCli = path.join(architectPkg, 'bin', 'cli.js');
3845

@@ -45,19 +52,89 @@ export function checkBuilderIsBuilt(repoRoot: string, logger: Console): BuilderP
4552
return undefined;
4653
}
4754

48-
return { buildPkg, architectPkg, architectCli };
55+
return { buildPkg, corePkg, architectPkg, architectCli };
56+
}
57+
58+
/** Creates/repairs `shimDir/<name>` as a symlink to `target`, skipping if already correct. */
59+
function linkIntoShim(shimDir: string, name: string, target: string): void {
60+
const linkPath = path.join(shimDir, name);
61+
fs.mkdirSync(path.dirname(linkPath), { recursive: true });
62+
63+
try {
64+
if (fs.readlinkSync(linkPath) === target) {
65+
return;
66+
}
67+
fs.rmSync(linkPath, { force: true });
68+
} catch {
69+
// ENOENT (link doesn't exist yet) -- fall through to create it.
70+
}
71+
72+
fs.symlinkSync(target, linkPath, 'junction');
73+
}
74+
75+
/**
76+
* Prepares the environment a standalone, spawned `architectCli` process needs to resolve its own
77+
* dependencies. `dist/` has no `node_modules` of its own, and a `pnpm install` (`hoist: false`)
78+
* only links `@angular-devkit/core`/`architect`/`@angular/build`'s dependencies into each
79+
* package's own SOURCE directory under `packages/`, not into anything reachable from `dist/`. So
80+
* a bare `require('@angular-devkit/core')` from the built CLI script has nowhere to resolve to.
81+
*
82+
* Fixes this with a small symlink directory mapping each built package's bare specifier to its
83+
* `dist/` output, combined with each package's own `node_modules` (where its actual npm deps --
84+
* ajv-formats, esbuild, sass, etc. -- are already pnpm-linked) into a `NODE_PATH` value: Node
85+
* treats each `NODE_PATH` entry as an extra global resolution root, so this covers arbitrarily
86+
* nested bare requires without having to enumerate individual third-party packages.
87+
*/
88+
export function prepareBuilderEnv(
89+
repoRoot: string,
90+
builder: BuilderPaths,
91+
shimDir: string,
92+
logger: Console,
93+
): BuilderEnv | undefined {
94+
const ownNodeModulesDirs = [
95+
path.join(repoRoot, 'packages', 'angular_devkit', 'core', 'node_modules'),
96+
path.join(repoRoot, 'packages', 'angular_devkit', 'architect', 'node_modules'),
97+
path.join(repoRoot, 'packages', 'angular', 'build', 'node_modules'),
98+
];
99+
100+
const missing = ownNodeModulesDirs.filter((dir) => !fs.existsSync(dir));
101+
if (missing.length > 0) {
102+
logger.error(
103+
'Error: expected node_modules missing for:\n ' +
104+
missing.join('\n ') +
105+
'\nRun "pnpm install" at the repo root first.',
106+
);
107+
108+
return undefined;
109+
}
110+
111+
fs.mkdirSync(shimDir, { recursive: true });
112+
linkIntoShim(shimDir, '@angular/build', builder.buildPkg);
113+
linkIntoShim(shimDir, '@angular-devkit/core', builder.corePkg);
114+
linkIntoShim(shimDir, '@angular-devkit/architect', builder.architectPkg);
115+
116+
const nodePath = [shimDir, ...ownNodeModulesDirs].join(path.delimiter);
117+
118+
return { ...builder, nodePath };
49119
}
50120

51121
/** Spawns one cold `lib:build` run against `projectDir` and times it. */
52122
export function buildOnce(
53123
projectDir: string,
54124
architectCli: string,
125+
nodePath: string,
55126
): { durationMs: number; status: 'ok' | 'fail' } {
127+
const existingNodePath = process.env['NODE_PATH'];
128+
const mergedNodePath = existingNodePath
129+
? `${nodePath}${path.delimiter}${existingNodePath}`
130+
: nodePath;
131+
56132
const start = performance.now();
57133
const result = spawnSync(process.execPath, [architectCli, 'lib:build'], {
58134
cwd: projectDir,
59135
stdio: 'pipe',
60136
maxBuffer: Infinity,
137+
env: { ...process.env, NODE_PATH: mergedNodePath },
61138
});
62139
const durationMs = performance.now() - start;
63140
const status = result.status === 0 ? 'ok' : 'fail';

‎scripts/benchmarks/library-builder/index.mts‎

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,13 @@ import {
2020
type StyleFlavor,
2121
generateFixture,
2222
} from './fixtures.mts';
23-
import { type BuildMeasurement, buildOnce, checkBuilderIsBuilt, median } from './harness.mts';
23+
import {
24+
type BuildMeasurement,
25+
buildOnce,
26+
checkBuilderIsBuilt,
27+
median,
28+
prepareBuilderEnv,
29+
} from './harness.mts';
2430
import { formatCsvReport, formatJsonReport } from './reporters.mts';
2531

2632
export interface LibraryBuilderBenchmarkOptions {
@@ -35,6 +41,7 @@ export interface LibraryBuilderBenchmarkOptions {
3541

3642
const repoRoot = fileURLToPath(new URL('../../../', import.meta.url));
3743
const scratchRoot = path.join(repoRoot, 'dist', '.library-benchmark-fixtures');
44+
const nodeModulesShimDir = path.join(repoRoot, 'dist', '.library-benchmark-node-modules-shim');
3845

3946
/**
4047
* Default entry-point counts for the `flat` layout, chosen to resolve a U-shaped scaling curve.
@@ -66,6 +73,11 @@ export async function runLibraryBuilderBenchmarks(
6673
return { measurements: [], exitCode: 1 };
6774
}
6875

76+
const builderEnv = prepareBuilderEnv(repoRoot, builder, nodeModulesShimDir, console);
77+
if (!builderEnv) {
78+
return { measurements: [], exitCode: 1 };
79+
}
80+
6981
const layout = options.layout ?? 'flat';
7082
const iterations = options.iterations ?? 3;
7183
const sizes =
@@ -104,7 +116,11 @@ export async function runLibraryBuilderBenchmarks(
104116

105117
const durations: number[] = [];
106118
for (let i = 1; i <= iterations; i++) {
107-
const { durationMs, status } = buildOnce(projectDir, builder.architectCli);
119+
const { durationMs, status } = buildOnce(
120+
projectDir,
121+
builder.architectCli,
122+
builderEnv.nodePath,
123+
);
108124
if (!options.json) {
109125
// eslint-disable-next-line no-console
110126
console.log(

0 commit comments

Comments
 (0)