From 0f8d88b4e9cbfed840d248231ad59d7853ebe62f Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Wed, 16 Sep 2026 08:54:34 -0400 Subject: [PATCH] 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. --- packages/angular/cli/BUILD.bazel | 72 ++++++++++++++++--- packages/angular/cli/bin/ng.js | 2 +- packages/angular/cli/bin/version.js | 64 +++++++++++++++++ packages/angular/cli/index.d.ts | 19 +++++ packages/angular/cli/lib/init.ts | 9 ++- packages/angular/cli/package.json | 5 ++ .../cli/src/command-builder/command-module.ts | 1 - .../cli/src/command-builder/definitions.ts | 2 +- .../schematics-command-module.ts | 8 +-- .../utilities/json-schema_spec.ts | 3 +- .../utilities/schematic-engine-host.ts | 4 +- .../commands/mcp/resources/instructions.ts | 11 ++- .../cli/src/commands/mcp/tools/ai-tutor.ts | 31 ++++---- .../src/commands/mcp/tools/best-practices.ts | 16 +---- .../angular/cli/src/commands/update/cli.ts | 4 +- packages/angular/cli/src/typings.d.ts | 9 ++- packages/angular/cli/src/utilities/config.ts | 13 +++- .../cli/src/utilities/markdown-loader.ts | 35 --------- .../angular/cli/src/utilities/node-version.ts | 40 +---------- packages/angular/cli/src/utilities/version.ts | 4 +- packages/angular/cli/test-esm-loader.mjs | 48 +++++++++++++ packages/angular/cli/tsconfig-build.json | 9 +++ packages/angular/cli/tsconfig-test.json | 6 ++ 23 files changed, 276 insertions(+), 139 deletions(-) create mode 100644 packages/angular/cli/bin/version.js create mode 100644 packages/angular/cli/index.d.ts delete mode 100644 packages/angular/cli/src/utilities/markdown-loader.ts create mode 100644 packages/angular/cli/test-esm-loader.mjs create mode 100644 packages/angular/cli/tsconfig-build.json create mode 100644 packages/angular/cli/tsconfig-test.json diff --git a/packages/angular/cli/BUILD.bazel b/packages/angular/cli/BUILD.bazel index 1eb48f2fc6f2..2fbad284fd31 100644 --- a/packages/angular/cli/BUILD.bazel +++ b/packages/angular/cli/BUILD.bazel @@ -3,6 +3,8 @@ # Use of this source code is governed by an MIT-style license that can be # found in the LICENSE file at https://angular.dev/license +load("@aspect_rules_esbuild//esbuild:defs.bzl", "esbuild") +load("@aspect_rules_ts//ts:defs.bzl", "ts_config") load("@npm//:defs.bzl", "npm_link_all_packages") load("//tools:defaults.bzl", "jasmine_test", "npm_package", "ts_project") load("//tools:ng_cli_schema_generator.bzl", "cli_json_schema") @@ -14,6 +16,24 @@ package(default_visibility = ["//visibility:public"]) npm_link_all_packages() +ts_config( + name = "tsconfig-build", + src = "tsconfig-build.json", + deps = [ + "//:build-tsconfig", + ], +) + +ts_config( + name = "tsconfig-test", + src = "tsconfig-test.json", + deps = [ + ":tsconfig-build", + "//:node_modules/@types/jasmine", + "//:node_modules/@types/node", + ], +) + genrule( name = "angular_best_practices", srcs = [ @@ -25,19 +45,19 @@ genrule( """, ) -RUNTIME_ASSETS = glob( +PACKAGE_ASSETS = glob( include = [ "bin/**/*", - "src/**/*.md", ], exclude = [ "lib/config/workspace-schema.json", ], ) + [ "//packages/angular/cli:lib/config/schema.json", - ":angular_best_practices", ] +RUNTIME_ASSETS = PACKAGE_ASSETS + glob(["src/**/*.md"]) + [":angular_best_practices"] + ts_project( name = "angular-cli", srcs = glob( @@ -54,6 +74,7 @@ ts_project( "//packages/angular/cli:lib/config/workspace-schema.ts", ], data = RUNTIME_ASSETS, + tsconfig = ":tsconfig-build", deps = [ ":node_modules/@angular-devkit/architect", ":node_modules/@angular-devkit/core", @@ -77,6 +98,30 @@ ts_project( ], ) +esbuild( + name = "bundled_cli", + srcs = [ + ":angular-cli", + ":angular_best_practices", + ] + glob(["src/**/*.md"]), + config = { + "packages": "external", + "loader": { + ".md": "text", + }, + }, + entry_points = [ + "lib/cli/index.js", + "lib/init.js", + ], + format = "esm", + output_dir = True, + platform = "node", + sourcemap = False, + splitting = True, + target = "node22", +) + CLI_SCHEMA_DATA = [ "//packages/angular/build:schemas", "//packages/angular_devkit/build_angular:schemas", @@ -109,6 +154,7 @@ ts_project( "node_modules/**", ], ), + tsconfig = ":tsconfig-test", deps = [ ":angular-cli", ":node_modules/@angular-devkit/core", @@ -124,7 +170,14 @@ ts_project( jasmine_test( name = "test", - data = [":angular-cli_test_lib"], + data = [ + "package.json", + "test-esm-loader.mjs", + ":angular-cli_test_lib", + ], + node_options = [ + "--import=./test-esm-loader.mjs", + ], ) genrule( @@ -144,14 +197,17 @@ npm_package( "//packages/angular_devkit/schematics:package.json", "//packages/schematics/angular:package.json", ], + replace_prefixes = { + "bundled_cli/": "lib/", + }, stamp_files = [ - "src/utilities/version.js", - "src/utilities/node-version.js", + "bin/version.js", ], tags = ["release-package"], - deps = RUNTIME_ASSETS + [ + deps = PACKAGE_ASSETS + [ ":README.md", - ":angular-cli", + ":bundled_cli", + ":index.d.ts", ":license", ], ) diff --git a/packages/angular/cli/bin/ng.js b/packages/angular/cli/bin/ng.js index ac01a0935fc0..a95f51b8def4 100755 --- a/packages/angular/cli/bin/ng.js +++ b/packages/angular/cli/bin/ng.js @@ -12,7 +12,7 @@ 'use strict'; const path = require('path'); -const nodeUtils = require('../src/utilities/node-version'); +const nodeUtils = require('./version'); // Error if the external CLI appears to be used inside a google3 context. if (process.cwd().split(path.sep).includes('google3')) { diff --git a/packages/angular/cli/bin/version.js b/packages/angular/cli/bin/version.js new file mode 100644 index 000000000000..38edd4e299c7 --- /dev/null +++ b/packages/angular/cli/bin/version.js @@ -0,0 +1,64 @@ +'use strict'; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +/** + * The supported Node.js version for the Angular CLI. + */ +var SUPPORTED_NODE_VERSIONS = '0.0.0-ENGINES-NODE'; + +/** + * The version of the Angular CLI. + */ +var VERSION = '0.0.0-PLACEHOLDER'; + +/** + * The supported Node.js versions. + */ +var supportedNodeVersions = SUPPORTED_NODE_VERSIONS.replace(/[\^~<>=]/g, '') + .split('||') + .map(function (v) { + return v.trim(); + }); + +/** + * Checks if the current Node.js version is supported. + * @returns `true` if the current Node.js version is supported, `false` otherwise. + */ +function isNodeVersionSupported() { + if (SUPPORTED_NODE_VERSIONS.charAt(0) === '0') { + return true; + } + + var parts = process.versions.node.split('.', 3).map(Number); + var processMajor = parts[0]; + var processMinor = parts[1]; + var processPatch = parts[2]; + + for (var i = 0; i < supportedNodeVersions.length; i++) { + var vParts = supportedNodeVersions[i].split('.', 3).map(Number); + var major = vParts[0]; + var minor = vParts[1]; + var patch = vParts[2]; + if ( + (major === processMajor && processMinor === minor && processPatch >= patch) || + (major === processMajor && processMinor > minor) + ) { + return true; + } + } + + return false; +} + +module.exports = { + VERSION: VERSION, + SUPPORTED_NODE_VERSIONS: SUPPORTED_NODE_VERSIONS, + supportedNodeVersions: supportedNodeVersions, + isNodeVersionSupported: isNodeVersionSupported, +}; diff --git a/packages/angular/cli/index.d.ts b/packages/angular/cli/index.d.ts new file mode 100644 index 000000000000..2a73c4bbc059 --- /dev/null +++ b/packages/angular/cli/index.d.ts @@ -0,0 +1,19 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +export declare class Version { + readonly full: string; + readonly major: string; + readonly minor: string; + readonly patch: string; + constructor(full: string); +} + +export declare const VERSION: Version; + +export default function (options: { cliArgs: string[] }): Promise; diff --git a/packages/angular/cli/lib/init.ts b/packages/angular/cli/lib/init.ts index 97d410890955..a503d9a87f70 100644 --- a/packages/angular/cli/lib/init.ts +++ b/packages/angular/cli/lib/init.ts @@ -9,6 +9,7 @@ import { readFile } from 'node:fs/promises'; import { createRequire } from 'node:module'; import * as path from 'node:path'; +import { pathToFileURL } from 'node:url'; import { SemVer, major } from 'semver'; import { disableVersionCheck } from '../src/utilities/environment-options'; import { VERSION } from '../src/utilities/version'; @@ -71,7 +72,7 @@ let forceExit = false; // version of ng-cli you have installed in a local package.json const cwdRequire = createRequire(process.cwd() + '/'); const projectLocalCli = cwdRequire.resolve('@angular/cli'); - cli = await import(projectLocalCli); + cli = await import(pathToFileURL(projectLocalCli).href); const globalVersion = new SemVer(VERSION.full); @@ -150,7 +151,11 @@ let forceExit = false; cli = await import('./cli'); } - if ('default' in cli) { + // Support both ESM and CommonJS local CLI packages. When importing older CommonJS + // packages with an `__esModule` default export, Node.js wraps the exports in an ESM + // namespace requiring the default export to be unwrapped multiple times. + let depth = 0; + while (typeof cli === 'object' && cli !== null && 'default' in cli && depth++ < 3) { cli = cli['default']; } diff --git a/packages/angular/cli/package.json b/packages/angular/cli/package.json index 2504ea0da35b..bae0b97abe97 100644 --- a/packages/angular/cli/package.json +++ b/packages/angular/cli/package.json @@ -3,6 +3,8 @@ "version": "0.0.0-PLACEHOLDER", "description": "CLI tool for Angular", "main": "lib/cli/index.js", + "typings": "index.d.ts", + "type": "module", "bin": { "ng": "bin/ng.js" }, @@ -11,6 +13,9 @@ "angular-cli", "Angular CLI" ], + "imports": { + "#version": "./bin/version.js" + }, "dependencies": { "@angular-devkit/architect": "workspace:0.0.0-EXPERIMENTAL-PLACEHOLDER", "@angular-devkit/core": "workspace:0.0.0-PLACEHOLDER", diff --git a/packages/angular/cli/src/command-builder/command-module.ts b/packages/angular/cli/src/command-builder/command-module.ts index 35e0d4f8201d..ec92bc452d5b 100644 --- a/packages/angular/cli/src/command-builder/command-module.ts +++ b/packages/angular/cli/src/command-builder/command-module.ts @@ -18,7 +18,6 @@ import { AngularWorkspace } from '../utilities/config'; import { memoize } from '../utilities/memoize'; import { CommandContext, CommandScope, Options, OtherOptions } from './definitions'; import { Option, addSchemaOptionsToCommand } from './utilities/json-schema'; -import '../utilities/markdown-loader'; export { CommandScope }; export type { CommandContext, Options, OtherOptions }; diff --git a/packages/angular/cli/src/command-builder/definitions.ts b/packages/angular/cli/src/command-builder/definitions.ts index d552b432b685..27c6dc8a6ae9 100644 --- a/packages/angular/cli/src/command-builder/definitions.ts +++ b/packages/angular/cli/src/command-builder/definitions.ts @@ -7,7 +7,7 @@ */ import { logging } from '@angular-devkit/core'; -import type { Argv, CamelCaseKey } from 'yargs'; +import type { Argv, CamelCaseKey } from 'yargs' with { 'resolution-mode': 'require' }; import type { PackageManager } from '../package-managers/package-manager'; import { AngularWorkspace } from '../utilities/config'; diff --git a/packages/angular/cli/src/command-builder/schematics-command-module.ts b/packages/angular/cli/src/command-builder/schematics-command-module.ts index c74b44101e93..e2561fd1c273 100644 --- a/packages/angular/cli/src/command-builder/schematics-command-module.ts +++ b/packages/angular/cli/src/command-builder/schematics-command-module.ts @@ -420,10 +420,10 @@ export abstract class SchematicsCommandModule return workspace ? // Workspace collectionName === DEFAULT_SCHEMATICS_COLLECTION - ? // Favor __dirname for @schematics/angular to use the build-in version - [__dirname, process.cwd(), root] - : [process.cwd(), root, __dirname] + ? // Favor import.meta.dirname for @schematics/angular to use the build-in version + [import.meta.dirname, process.cwd(), root] + : [process.cwd(), root, import.meta.dirname] : // Global - [__dirname, process.cwd()]; + [import.meta.dirname, process.cwd()]; } } diff --git a/packages/angular/cli/src/command-builder/utilities/json-schema_spec.ts b/packages/angular/cli/src/command-builder/utilities/json-schema_spec.ts index 11228e4adca0..496a2094b62c 100644 --- a/packages/angular/cli/src/command-builder/utilities/json-schema_spec.ts +++ b/packages/angular/cli/src/command-builder/utilities/json-schema_spec.ts @@ -7,6 +7,7 @@ */ import { JsonObject, schema } from '@angular-devkit/core'; +import type { Argv } from 'yargs'; import yargs from 'yargs'; import { Option, addSchemaOptionsToCommand, parseJsonSchemaToOptions } from './json-schema'; @@ -20,7 +21,7 @@ describe('parseJsonSchemaToOptions', () => { return localYargs.parseAsync(args); }; - let localYargs: yargs.Argv; + let localYargs: Argv; let options: Option[]; beforeAll(async () => { diff --git a/packages/angular/cli/src/command-builder/utilities/schematic-engine-host.ts b/packages/angular/cli/src/command-builder/utilities/schematic-engine-host.ts index 25b723c467a2..067d4cd00460 100644 --- a/packages/angular/cli/src/command-builder/utilities/schematic-engine-host.ts +++ b/packages/angular/cli/src/command-builder/utilities/schematic-engine-host.ts @@ -73,7 +73,7 @@ export class SchematicEngineHost extends NodeModulesEngineHost { // Mimic behavior of ExportStringRef class used in default behavior const fullPath = path[0] === '.' ? resolve(parentPath ?? process.cwd(), path) : path; - const referenceRequire = createRequire(__filename); + const referenceRequire = createRequire(import.meta.url); const schematicFile = referenceRequire.resolve(fullPath, { paths: [parentPath] }); if (shouldWrapSchematic(schematicFile, collectionDescription?.encapsulation)) { @@ -139,7 +139,7 @@ function wrap( moduleCache: Map, exportName?: string, ): () => unknown { - const hostRequire = createRequire(__filename); + const hostRequire = createRequire(import.meta.url); const schematicRequire = createRequire(schematicFile); const customRequire = function (id: string) { diff --git a/packages/angular/cli/src/commands/mcp/resources/instructions.ts b/packages/angular/cli/src/commands/mcp/resources/instructions.ts index 90902d60b389..636de5471a1c 100644 --- a/packages/angular/cli/src/commands/mcp/resources/instructions.ts +++ b/packages/angular/cli/src/commands/mcp/resources/instructions.ts @@ -7,8 +7,7 @@ */ import type { McpServer } from '@modelcontextprotocol/server'; -import { readFile } from 'node:fs/promises'; -import { join } from 'node:path'; +import bestPracticesText from './best-practices.md'; export function registerInstructionsResource(server: McpServer): void { server.registerResource( @@ -23,10 +22,8 @@ export function registerInstructionsResource(server: McpServer): void { ' typed forms, modern control flow syntax, and other current conventions.', mimeType: 'text/markdown', }, - async () => { - const text = await readFile(join(__dirname, 'best-practices.md'), 'utf-8'); - - return { contents: [{ uri: 'instructions://best-practices', text }] }; - }, + async () => ({ + contents: [{ uri: 'instructions://best-practices', text: bestPracticesText }], + }), ); } diff --git a/packages/angular/cli/src/commands/mcp/tools/ai-tutor.ts b/packages/angular/cli/src/commands/mcp/tools/ai-tutor.ts index 590d0940c747..c72f0fe42a12 100644 --- a/packages/angular/cli/src/commands/mcp/tools/ai-tutor.ts +++ b/packages/angular/cli/src/commands/mcp/tools/ai-tutor.ts @@ -6,8 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import { readFile } from 'node:fs/promises'; -import { join } from 'node:path'; +import aiTutorText from '../resources/ai-tutor.md'; import { declareTool } from './tool-registry'; export const AI_TUTOR_TOOL = declareTool({ @@ -37,23 +36,17 @@ with a new core identity and knowledge base. isReadOnly: true, isLocalOnly: true, factory: () => { - let aiTutorText: string; - - return async () => { - aiTutorText ??= await readFile(join(__dirname, '../resources/ai-tutor.md'), 'utf-8'); - - return { - content: [ - { - type: 'text', - text: aiTutorText, - annotations: { - audience: ['assistant'], - priority: 1.0, - }, + return async () => ({ + content: [ + { + type: 'text', + text: aiTutorText, + annotations: { + audience: ['assistant'], + priority: 1.0, }, - ], - }; - }; + }, + ], + }); }, }); diff --git a/packages/angular/cli/src/commands/mcp/tools/best-practices.ts b/packages/angular/cli/src/commands/mcp/tools/best-practices.ts index 0b40c5c0c207..d8a8d1251604 100644 --- a/packages/angular/cli/src/commands/mcp/tools/best-practices.ts +++ b/packages/angular/cli/src/commands/mcp/tools/best-practices.ts @@ -17,9 +17,10 @@ import { readFile, stat } from 'node:fs/promises'; import { createRequire } from 'node:module'; -import { dirname, isAbsolute, join, relative, resolve } from 'node:path'; +import { dirname, isAbsolute, relative, resolve } from 'node:path'; import { z } from 'zod'; import { VERSION } from '../../../utilities/version'; +import bundledBestPractices from '../resources/best-practices.md'; import { isAllowedWorkspacePath } from '../workspace-utils'; import { type McpToolContext, declareTool } from './tool-registry'; @@ -59,15 +60,6 @@ that must be followed for any task involving the creation, analysis, or modifica factory: createBestPracticesHandler, }); -/** - * Retrieves the content of the generic best practices guide that is bundled with the CLI. - * This serves as a fallback when a version-specific guide cannot be found. - * @returns A promise that resolves to the string content of the bundled markdown file. - */ -async function getBundledBestPractices(): Promise { - return readFile(join(__dirname, '../resources/best-practices.md'), 'utf-8'); -} - /** * Attempts to find and read a version-specific best practices guide from the user's installed * version of `@angular/core`. It looks for a custom `angular` metadata property in the @@ -200,8 +192,6 @@ async function getVersionSpecificBestPractices( * @returns An async function that serves as the tool's executor. */ function createBestPracticesHandler({ logger, server }: McpToolContext) { - let bundledBestPractices: Promise; - return async (input: BestPracticesInput) => { let content: string | undefined; let source: string | undefined; @@ -221,7 +211,7 @@ function createBestPracticesHandler({ logger, server }: McpToolContext) { // If the version-specific guide was not found for any reason, fall back to the bundled version. if (content === undefined) { - content = await (bundledBestPractices ??= getBundledBestPractices()); + content = bundledBestPractices; source = `bundled (CLI v${VERSION.full})`; } diff --git a/packages/angular/cli/src/commands/update/cli.ts b/packages/angular/cli/src/commands/update/cli.ts index cbe703552c9a..fd0eeca166a3 100644 --- a/packages/angular/cli/src/commands/update/cli.ts +++ b/packages/angular/cli/src/commands/update/cli.ts @@ -60,7 +60,7 @@ class CommandError extends Error {} export default class UpdateCommandModule extends CommandModule { override scope = CommandScope.In; protected override shouldReportAnalytics = false; - private readonly resolvePaths = [__dirname, this.context.root]; + private readonly resolvePaths = [import.meta.dirname, this.context.root]; command = 'update [packages..]'; describe = 'Updates your workspace and its dependencies. See https://update.angular.dev/.'; @@ -237,7 +237,7 @@ export default class UpdateCommandModule extends CommandModule favor @schematics/update from this package + // import.meta.dirname -> favor @schematics/update from this package // Otherwise, use packages from the active workspace (migrations) resolvePaths: this.resolvePaths, schemaValidation: true, diff --git a/packages/angular/cli/src/typings.d.ts b/packages/angular/cli/src/typings.d.ts index 12e7de03c204..948d7b1d2280 100644 --- a/packages/angular/cli/src/typings.d.ts +++ b/packages/angular/cli/src/typings.d.ts @@ -6,7 +6,14 @@ * found in the LICENSE file at https://angular.dev/license */ -declare module '*/long-description.md' { +declare module '*.md' { const content: string; export default content; } + +declare module '#version' { + export const VERSION: string; + export const SUPPORTED_NODE_VERSIONS: string; + export const supportedNodeVersions: string[]; + export function isNodeVersionSupported(): boolean; +} diff --git a/packages/angular/cli/src/utilities/config.ts b/packages/angular/cli/src/utilities/config.ts index 7b9188bb3d89..6aa762aa264f 100644 --- a/packages/angular/cli/src/utilities/config.ts +++ b/packages/angular/cli/src/utilities/config.ts @@ -11,7 +11,7 @@ import { existsSync, promises as fs } from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; import { PackageManager } from '../../lib/config/workspace-schema'; -import { findUp, findUpSync } from './find-up'; +import { findUp } from './find-up'; import { JSONFile, readAndParseJson } from './json-file'; function isJsonObject(value: json.JsonValue | undefined): value is json.JsonObject { @@ -47,7 +47,14 @@ function createWorkspaceHost(): workspaces.WorkspaceHost { }; } -export const workspaceSchemaPath = path.join(__dirname, '../../lib/config/schema.json'); +const currentDirectory = import.meta.dirname; + +// In bundled ESM output, files are located in `lib/` directly adjacent to `config/schema.json` +// whereas in unbundled development/tests, `config.js` is in `src/utilities/`. +const bundledSchemaPath = path.join(currentDirectory, 'config/schema.json'); +export const workspaceSchemaPath = existsSync(bundledSchemaPath) + ? bundledSchemaPath + : path.join(currentDirectory, '../../lib/config/schema.json'); const configNames = ['angular.json', '.angular.json']; const globalFileName = '.angular-config.json'; @@ -76,7 +83,7 @@ async function projectFilePath(projectPath?: string): Promise { return ( (projectPath && (await findUp(configNames, projectPath))) || (await findUp(configNames, process.cwd())) || - (await findUp(configNames, __dirname)) + (await findUp(configNames, currentDirectory)) ); } diff --git a/packages/angular/cli/src/utilities/markdown-loader.ts b/packages/angular/cli/src/utilities/markdown-loader.ts deleted file mode 100644 index 3a70a5eaae1c..000000000000 --- a/packages/angular/cli/src/utilities/markdown-loader.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.dev/license - */ - -import { readFileSync } from 'node:fs'; - -const LONG_DESCRIPTION_REGEXP = /[/\\]long-description\.md$/; - -function isCommandLongDescription(filePath: string | undefined): boolean { - return !!filePath && LONG_DESCRIPTION_REGEXP.test(filePath); -} - -// Register markdown extension hook for CommonJS execution -if (typeof require !== 'undefined' && require.extensions) { - const originalMdExtension = require.extensions['.md']; - require.extensions['.md'] = (module, filename) => { - if (isCommandLongDescription(filename)) { - module.exports = readFileSync(filename, 'utf8'); - - return; - } - - if (originalMdExtension) { - originalMdExtension(module, filename); - } else { - const err = new Error(`Cannot find module '${filename}'`); - (err as NodeJS.ErrnoException).code = 'MODULE_NOT_FOUND'; - throw err; - } - }; -} diff --git a/packages/angular/cli/src/utilities/node-version.ts b/packages/angular/cli/src/utilities/node-version.ts index 7bed934428c8..18f06b960369 100644 --- a/packages/angular/cli/src/utilities/node-version.ts +++ b/packages/angular/cli/src/utilities/node-version.ts @@ -11,45 +11,9 @@ * @important This file must not import any other modules. */ -/** - * The supported Node.js version for the Angular CLI. - */ - -const SUPPORTED_NODE_VERSIONS = '0.0.0-ENGINES-NODE'; - -/** - * The supported Node.js versions. - */ -export const supportedNodeVersions = SUPPORTED_NODE_VERSIONS.replace(/[\^~<>=]/g, '') - .split('||') - .map((v) => v.trim()); - -/** - * Checks if the current Node.js version is supported. - * @returns `true` if the current Node.js version is supported, `false` otherwise. - */ -export function isNodeVersionSupported(): boolean { - if (SUPPORTED_NODE_VERSIONS.charAt(0) === '0') { - // Unlike `pkg_npm`, `ts_library` which is used to run unit tests does not support substitutions. - return true; - } - - const [processMajor, processMinor, processPatch] = process.versions.node - .split('.', 3) - .map((part) => Number(part)); +import { SUPPORTED_NODE_VERSIONS, supportedNodeVersions } from '#version'; - for (const version of supportedNodeVersions) { - const [major, minor, patch] = version.split('.', 3).map((part) => Number(part)); - if ( - (major === processMajor && processMinor === minor && processPatch >= patch) || - (major === processMajor && processMinor > minor) - ) { - return true; - } - } - - return false; -} +export { SUPPORTED_NODE_VERSIONS, supportedNodeVersions, isNodeVersionSupported } from '#version'; /** * Checks if the current Node.js version is the minimum supported version. diff --git a/packages/angular/cli/src/utilities/version.ts b/packages/angular/cli/src/utilities/version.ts index d16feb2d4b15..2218a39b3e78 100644 --- a/packages/angular/cli/src/utilities/version.ts +++ b/packages/angular/cli/src/utilities/version.ts @@ -6,6 +6,8 @@ * found in the LICENSE file at https://angular.dev/license */ +import { VERSION as versionString } from '#version'; + // Same structure as used in framework packages class Version { readonly major: string; @@ -20,4 +22,4 @@ class Version { } } -export const VERSION = new Version('0.0.0-PLACEHOLDER'); +export const VERSION = new Version(versionString); diff --git a/packages/angular/cli/test-esm-loader.mjs b/packages/angular/cli/test-esm-loader.mjs new file mode 100644 index 000000000000..200010ad19d4 --- /dev/null +++ b/packages/angular/cli/test-esm-loader.mjs @@ -0,0 +1,48 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { readFile } from 'node:fs/promises'; +import { register } from 'node:module'; +import { fileURLToPath } from 'node:url'; + +if (!globalThis.__angularCliTestEsmHookRegistered) { + globalThis.__angularCliTestEsmHookRegistered = true; + register(import.meta.url); +} + +export async function resolve(specifier, context, nextResolve) { + try { + const res = await nextResolve(specifier, context); + if (res.url.endsWith('.md')) { + return { ...res, format: 'module' }; + } + return res; + } catch (err) { + if (err && (err.code === 'ERR_MODULE_NOT_FOUND' || err.code === 'ERR_UNSUPPORTED_DIR_IMPORT')) { + for (const suffix of ['.js', '/index.js', '.json']) { + try { + return await nextResolve(specifier + suffix, context); + } catch {} + } + } + throw err; + } +} + +export async function load(url, context, nextLoad) { + if (url.endsWith('.md')) { + const content = await readFile(fileURLToPath(url), 'utf-8'); + return { + format: 'module', + shortCircuit: true, + source: `export default ${JSON.stringify(content)};`, + }; + } + + return nextLoad(url, context); +} diff --git a/packages/angular/cli/tsconfig-build.json b/packages/angular/cli/tsconfig-build.json new file mode 100644 index 000000000000..dc9987dba856 --- /dev/null +++ b/packages/angular/cli/tsconfig-build.json @@ -0,0 +1,9 @@ +{ + "extends": "../../../tsconfig-build.json", + "compilerOptions": { + "module": "preserve", + "moduleResolution": "bundler", + "target": "es2022", + "types": ["node"] + } +} diff --git a/packages/angular/cli/tsconfig-test.json b/packages/angular/cli/tsconfig-test.json new file mode 100644 index 000000000000..d275aef0a373 --- /dev/null +++ b/packages/angular/cli/tsconfig-test.json @@ -0,0 +1,6 @@ +{ + "extends": "./tsconfig-build.json", + "compilerOptions": { + "types": ["node", "jasmine"] + } +}