@@ -26,13 +26,20 @@ export interface BuildMeasurement {
2626
2727export 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. */
3440export 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. */
52122export 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' ;
0 commit comments