Skip to content

Commit f3dcb9d

Browse files
committed
build(@angular/cli): bundle CLI first-party code into ESM chunks with esbuild
Bundle @angular/cli first-party entry points (`lib/cli/index.js` and `lib/init.js`) into ESM chunks using `aspect_rules_esbuild` targeting Node 22 with external packages, code splitting, and bundle sourcemaps disabled. The package `package.json` is now designated as "type": "module", while `bin/package.json` retains CommonJS to allow the `ng` binary bootstrap to validate older Node.js runtimes. A dedicated ES5/CommonJS `bin/version.js` file is stamped by Bazel during release packaging and exposed through a `#version` package subpath import. This enables `bin/ng.js` to perform runtime compatibility checks safely before dynamic ESM import, while allowing bundled chunks to access stamped versions without runtime file I/O. Inlined markdown assets, including MCP resources and command long descriptions, are bundled directly into output chunks via esbuild's text loader, removing the need for runtime filesystem reads and the CommonJS `require.extensions` loader workaround. Ambient `__dirname`, `__filename`, and `createRequire` usages are replaced with `import.meta.dirname` and `import.meta.url`, with `pathToFileURL` used for Windows dynamic imports. A standalone `index.d.ts` declaration file is provided for programmatic package consumers, and unit tests are updated to execute under native Node.js ESM.
1 parent 1860222 commit f3dcb9d

23 files changed

Lines changed: 288 additions & 139 deletions

packages/angular/cli/BUILD.bazel

Lines changed: 64 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
# Use of this source code is governed by an MIT-style license that can be
44
# found in the LICENSE file at https://angular.dev/license
55

6+
load("@aspect_rules_esbuild//esbuild:defs.bzl", "esbuild")
7+
load("@aspect_rules_ts//ts:defs.bzl", "ts_config")
68
load("@npm//:defs.bzl", "npm_link_all_packages")
79
load("//tools:defaults.bzl", "jasmine_test", "npm_package", "ts_project")
810
load("//tools:ng_cli_schema_generator.bzl", "cli_json_schema")
@@ -14,6 +16,24 @@ package(default_visibility = ["//visibility:public"])
1416

1517
npm_link_all_packages()
1618

19+
ts_config(
20+
name = "tsconfig-build",
21+
src = "tsconfig-build.json",
22+
deps = [
23+
"//:build-tsconfig",
24+
],
25+
)
26+
27+
ts_config(
28+
name = "tsconfig-test",
29+
src = "tsconfig-test.json",
30+
deps = [
31+
":tsconfig-build",
32+
"//:node_modules/@types/jasmine",
33+
"//:node_modules/@types/node",
34+
],
35+
)
36+
1737
genrule(
1838
name = "angular_best_practices",
1939
srcs = [
@@ -25,19 +45,19 @@ genrule(
2545
""",
2646
)
2747

28-
RUNTIME_ASSETS = glob(
48+
PACKAGE_ASSETS = glob(
2949
include = [
3050
"bin/**/*",
31-
"src/**/*.md",
3251
],
3352
exclude = [
3453
"lib/config/workspace-schema.json",
3554
],
3655
) + [
3756
"//packages/angular/cli:lib/config/schema.json",
38-
":angular_best_practices",
3957
]
4058

59+
RUNTIME_ASSETS = PACKAGE_ASSETS + glob(["src/**/*.md"]) + [":angular_best_practices"]
60+
4161
ts_project(
4262
name = "angular-cli",
4363
srcs = glob(
@@ -54,6 +74,7 @@ ts_project(
5474
"//packages/angular/cli:lib/config/workspace-schema.ts",
5575
],
5676
data = RUNTIME_ASSETS,
77+
tsconfig = ":tsconfig-build",
5778
deps = [
5879
":node_modules/@angular-devkit/architect",
5980
":node_modules/@angular-devkit/core",
@@ -77,6 +98,30 @@ ts_project(
7798
],
7899
)
79100

101+
esbuild(
102+
name = "bundled_cli",
103+
srcs = [
104+
":angular-cli",
105+
":angular_best_practices",
106+
] + glob(["src/**/*.md"]),
107+
config = {
108+
"packages": "external",
109+
"loader": {
110+
".md": "text",
111+
},
112+
},
113+
entry_points = [
114+
"lib/cli/index.js",
115+
"lib/init.js",
116+
],
117+
format = "esm",
118+
output_dir = True,
119+
platform = "node",
120+
sourcemap = False,
121+
splitting = True,
122+
target = "node22",
123+
)
124+
80125
CLI_SCHEMA_DATA = [
81126
"//packages/angular/build:schemas",
82127
"//packages/angular_devkit/build_angular:schemas",
@@ -109,6 +154,7 @@ ts_project(
109154
"node_modules/**",
110155
],
111156
),
157+
tsconfig = ":tsconfig-test",
112158
deps = [
113159
":angular-cli",
114160
":node_modules/@angular-devkit/core",
@@ -124,7 +170,14 @@ ts_project(
124170

125171
jasmine_test(
126172
name = "test",
127-
data = [":angular-cli_test_lib"],
173+
data = [
174+
"package.json",
175+
"test-esm-loader.mjs",
176+
":angular-cli_test_lib",
177+
],
178+
node_options = [
179+
"--import=./test-esm-loader.mjs",
180+
],
128181
)
129182

130183
genrule(
@@ -144,14 +197,17 @@ npm_package(
144197
"//packages/angular_devkit/schematics:package.json",
145198
"//packages/schematics/angular:package.json",
146199
],
200+
replace_prefixes = {
201+
"bundled_cli/": "lib/",
202+
},
147203
stamp_files = [
148-
"src/utilities/version.js",
149-
"src/utilities/node-version.js",
204+
"bin/version.js",
150205
],
151206
tags = ["release-package"],
152-
deps = RUNTIME_ASSETS + [
207+
deps = PACKAGE_ASSETS + [
153208
":README.md",
154-
":angular-cli",
209+
":bundled_cli",
210+
":index.d.ts",
155211
":license",
156212
],
157213
)

packages/angular/cli/bin/ng.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
'use strict';
1313

1414
const path = require('path');
15-
const nodeUtils = require('../src/utilities/node-version');
15+
const nodeUtils = require('./version');
1616

1717
// Error if the external CLI appears to be used inside a google3 context.
1818
if (process.cwd().split(path.sep).includes('google3')) {
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
'use strict';
2+
/**
3+
* @license
4+
* Copyright Google LLC All Rights Reserved.
5+
*
6+
* Use of this source code is governed by an MIT-style license that can be
7+
* found in the LICENSE file at https://angular.dev/license
8+
*/
9+
10+
/**
11+
* The supported Node.js version for the Angular CLI.
12+
*/
13+
var SUPPORTED_NODE_VERSIONS = '0.0.0-ENGINES-NODE';
14+
15+
/**
16+
* The version of the Angular CLI.
17+
*/
18+
var VERSION = '0.0.0-PLACEHOLDER';
19+
20+
/**
21+
* The supported Node.js versions.
22+
*/
23+
var supportedNodeVersions = SUPPORTED_NODE_VERSIONS.replace(/[\^~<>=]/g, '')
24+
.split('||')
25+
.map(function (v) {
26+
return v.trim();
27+
});
28+
29+
/**
30+
* Checks if the current Node.js version is supported.
31+
* @returns `true` if the current Node.js version is supported, `false` otherwise.
32+
*/
33+
function isNodeVersionSupported() {
34+
if (SUPPORTED_NODE_VERSIONS.charAt(0) === '0') {
35+
return true;
36+
}
37+
38+
var parts = process.versions.node.split('.', 3).map(Number);
39+
var processMajor = parts[0];
40+
var processMinor = parts[1];
41+
var processPatch = parts[2];
42+
43+
for (var i = 0; i < supportedNodeVersions.length; i++) {
44+
var vParts = supportedNodeVersions[i].split('.', 3).map(Number);
45+
var major = vParts[0];
46+
var minor = vParts[1];
47+
var patch = vParts[2];
48+
if (
49+
(major === processMajor && processMinor === minor && processPatch >= patch) ||
50+
(major === processMajor && processMinor > minor)
51+
) {
52+
return true;
53+
}
54+
}
55+
56+
return false;
57+
}
58+
59+
module.exports = {
60+
VERSION: VERSION,
61+
SUPPORTED_NODE_VERSIONS: SUPPORTED_NODE_VERSIONS,
62+
supportedNodeVersions: supportedNodeVersions,
63+
isNodeVersionSupported: isNodeVersionSupported,
64+
};

packages/angular/cli/index.d.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
export declare class Version {
10+
readonly full: string;
11+
readonly major: string;
12+
readonly minor: string;
13+
readonly patch: string;
14+
constructor(full: string);
15+
}
16+
17+
export declare const VERSION: Version;
18+
19+
export default function (options: { cliArgs: string[] }): Promise<number>;

packages/angular/cli/lib/init.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import { readFile } from 'node:fs/promises';
1010
import { createRequire } from 'node:module';
1111
import * as path from 'node:path';
12+
import { pathToFileURL } from 'node:url';
1213
import { SemVer, major } from 'semver';
1314
import { disableVersionCheck } from '../src/utilities/environment-options';
1415
import { VERSION } from '../src/utilities/version';
@@ -71,7 +72,7 @@ let forceExit = false;
7172
// version of ng-cli you have installed in a local package.json
7273
const cwdRequire = createRequire(process.cwd() + '/');
7374
const projectLocalCli = cwdRequire.resolve('@angular/cli');
74-
cli = await import(projectLocalCli);
75+
cli = await import(pathToFileURL(projectLocalCli).href);
7576

7677
const globalVersion = new SemVer(VERSION.full);
7778

@@ -150,7 +151,11 @@ let forceExit = false;
150151
cli = await import('./cli');
151152
}
152153

153-
if ('default' in cli) {
154+
// Support both ESM and CommonJS local CLI packages. When importing older CommonJS
155+
// packages with an `__esModule` default export, Node.js wraps the exports in an ESM
156+
// namespace requiring the default export to be unwrapped multiple times.
157+
let depth = 0;
158+
while (typeof cli === 'object' && cli !== null && 'default' in cli && depth++ < 3) {
154159
cli = cli['default'];
155160
}
156161

packages/angular/cli/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
"version": "0.0.0-PLACEHOLDER",
44
"description": "CLI tool for Angular",
55
"main": "lib/cli/index.js",
6+
"typings": "index.d.ts",
7+
"type": "module",
68
"bin": {
79
"ng": "bin/ng.js"
810
},
@@ -11,6 +13,9 @@
1113
"angular-cli",
1214
"Angular CLI"
1315
],
16+
"imports": {
17+
"#version": "./bin/version.js"
18+
},
1419
"dependencies": {
1520
"@angular-devkit/architect": "workspace:0.0.0-EXPERIMENTAL-PLACEHOLDER",
1621
"@angular-devkit/core": "workspace:0.0.0-PLACEHOLDER",

packages/angular/cli/src/command-builder/command-module.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import { AngularWorkspace } from '../utilities/config';
1818
import { memoize } from '../utilities/memoize';
1919
import { CommandContext, CommandScope, Options, OtherOptions } from './definitions';
2020
import { Option, addSchemaOptionsToCommand } from './utilities/json-schema';
21-
import '../utilities/markdown-loader';
2221

2322
export { CommandScope };
2423
export type { CommandContext, Options, OtherOptions };

packages/angular/cli/src/command-builder/definitions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import { logging } from '@angular-devkit/core';
10-
import type { Argv, CamelCaseKey } from 'yargs';
10+
import type { Argv, CamelCaseKey } from 'yargs' with { 'resolution-mode': 'require' };
1111
import type { PackageManager } from '../package-managers/package-manager';
1212
import { AngularWorkspace } from '../utilities/config';
1313

packages/angular/cli/src/command-builder/schematics-command-module.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -420,10 +420,10 @@ export abstract class SchematicsCommandModule
420420
return workspace
421421
? // Workspace
422422
collectionName === DEFAULT_SCHEMATICS_COLLECTION
423-
? // Favor __dirname for @schematics/angular to use the build-in version
424-
[__dirname, process.cwd(), root]
425-
: [process.cwd(), root, __dirname]
423+
? // Favor import.meta.dirname for @schematics/angular to use the build-in version
424+
[import.meta.dirname, process.cwd(), root]
425+
: [process.cwd(), root, import.meta.dirname]
426426
: // Global
427-
[__dirname, process.cwd()];
427+
[import.meta.dirname, process.cwd()];
428428
}
429429
}

packages/angular/cli/src/command-builder/utilities/json-schema_spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88

99
import { JsonObject, schema } from '@angular-devkit/core';
10+
import type { Argv } from 'yargs';
1011
import yargs from 'yargs';
1112

1213
import { Option, addSchemaOptionsToCommand, parseJsonSchemaToOptions } from './json-schema';
@@ -20,7 +21,7 @@ describe('parseJsonSchemaToOptions', () => {
2021
return localYargs.parseAsync(args);
2122
};
2223

23-
let localYargs: yargs.Argv<unknown>;
24+
let localYargs: Argv<unknown>;
2425
let options: Option[];
2526

2627
beforeAll(async () => {

0 commit comments

Comments
 (0)