Skip to content

Commit eeddbc5

Browse files
committed
refactor(@angular/cli): import markdown files directly for command descriptions
Use TypeScript ambient module declaration for *.md files and register a scoped markdown require extension hook in CommonJS to allow direct import of command long descriptions. Loading markdown directly via module imports prepares the CLI codebase for ESM bundling, where __dirname and runtime filesystem reads are problematic. It simplifies unit tests by avoiding filesystem path resolution while bundling the text content seamlessly during production builds. This eliminates 12 __dirname references across command modules and removes the obsolete longDescriptionPath property.
1 parent 31c0456 commit eeddbc5

27 files changed

Lines changed: 119 additions & 59 deletions

File tree

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

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@
77
*/
88

99
import { schema } from '@angular-devkit/core';
10-
import { readFileSync } from 'node:fs';
11-
import { join, posix, relative } from 'node:path';
1210
import type { ArgumentsCamelCase, Argv, CommandModule as YargsCommandModule } from 'yargs';
1311
import { Parser as yargsParser } from 'yargs/helpers';
1412
import { getAnalyticsUserId } from '../analytics/analytics';
@@ -20,6 +18,7 @@ import { AngularWorkspace } from '../utilities/config';
2018
import { memoize } from '../utilities/memoize';
2119
import { CommandContext, CommandScope, Options, OtherOptions } from './definitions';
2220
import { Option, addSchemaOptionsToCommand } from './utilities/json-schema';
21+
import '../utilities/markdown-loader';
2322

2423
export { CommandScope };
2524
export type { CommandContext, Options, OtherOptions };
@@ -31,8 +30,11 @@ export interface CommandModuleImplementation<T extends {} = {}> extends Omit<
3130
/** Scope in which the command can be executed in. */
3231
scope: CommandScope;
3332

34-
/** Path used to load the long description for the command in JSON help text. */
35-
longDescriptionPath?: string;
33+
/** Long description for the command in JSON help text. */
34+
longDescription?: string;
35+
36+
/** Relative path to the long description file for the command in JSON help text. */
37+
longDescriptionRelativePath?: string;
3638

3739
/** Object declaring the options the command accepts, or a function accepting and returning a yargs instance. */
3840
builder(argv: Argv): Promise<Argv<T>> | Argv<T>;
@@ -50,7 +52,8 @@ export interface FullDescribe {
5052
export abstract class CommandModule<T extends {} = {}> implements CommandModuleImplementation<T> {
5153
abstract readonly command: string;
5254
abstract readonly describe: string | false;
53-
abstract readonly longDescriptionPath?: string;
55+
readonly longDescription?: string;
56+
readonly longDescriptionRelativePath?: string;
5457
protected readonly shouldReportAnalytics: boolean = true;
5558
readonly scope: CommandScope = CommandScope.Both;
5659

@@ -68,23 +71,22 @@ export abstract class CommandModule<T extends {} = {}> implements CommandModuleI
6871
* `false` will result in a hidden command.
6972
*/
7073
public get fullDescribe(): FullDescribe | false {
71-
return this.describe === false
72-
? false
73-
: {
74-
describe: this.describe,
75-
...(this.longDescriptionPath
76-
? {
77-
longDescriptionRelativePath: relative(
78-
join(__dirname, '../../../../'),
79-
this.longDescriptionPath,
80-
).replace(/\\/g, posix.sep),
81-
longDescription: readFileSync(this.longDescriptionPath, 'utf8').replace(
82-
/\r\n/g,
83-
'\n',
84-
),
85-
}
86-
: {}),
87-
};
74+
if (this.describe === false) {
75+
return false;
76+
}
77+
78+
const description: FullDescribe = {
79+
describe: this.describe,
80+
};
81+
82+
if (this.longDescription) {
83+
description.longDescription = this.longDescription.replace(/\r\n/g, '\n');
84+
description.longDescriptionRelativePath =
85+
this.longDescriptionRelativePath ??
86+
`@angular/cli/src/commands/${this.commandName}/long-description.md`;
87+
}
88+
89+
return description;
8890
}
8991

9092
protected get commandName(): string {

packages/angular/cli/src/commands/add/cli.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ import { NgAddSaveDependency, PackageManifest, PackageMetadata } from '../../pac
2929
import { assertIsError } from '../../utilities/error';
3030
import { isTTY } from '../../utilities/tty';
3131
import { VERSION } from '../../utilities/version';
32+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
33+
// @ts-ignore strict-deps: Markdown files are asset dependencies bundled/loaded at runtime
34+
import longDescription from './long-description.md';
3235

3336
class CommandError extends Error {}
3437

@@ -100,7 +103,7 @@ export default class AddCommandModule
100103
{
101104
command = 'add <collection>';
102105
describe = 'Adds support for an external library to your project.';
103-
longDescriptionPath = join(__dirname, 'long-description.md');
106+
override longDescription = longDescription;
104107
protected override allowPrivateSchematics = true;
105108
private readonly schematicName = 'ng-add';
106109
private rootRequire = createRequire(this.context.root + '/');

packages/angular/cli/src/commands/analytics/cli.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import { join } from 'node:path';
109
import { Argv } from 'yargs';
1110
import {
1211
CommandModule,
@@ -18,6 +17,9 @@ import {
1817
demandCommandFailureMessage,
1918
} from '../../command-builder/utilities/command';
2019
import { AnalyticsInfoCommandModule } from './info/cli';
20+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
21+
// @ts-ignore strict-deps: Markdown files are asset dependencies bundled/loaded at runtime
22+
import longDescription from './long-description.md';
2123
import {
2224
AnalyticsDisableModule,
2325
AnalyticsEnableModule,
@@ -30,7 +32,7 @@ export default class AnalyticsCommandModule
3032
{
3133
command = 'analytics';
3234
describe = 'Configures the gathering of Angular CLI usage metrics.';
33-
longDescriptionPath = join(__dirname, 'long-description.md');
35+
override longDescription = longDescription;
3436

3537
builder(localYargs: Argv): Argv {
3638
const subcommands = [

packages/angular/cli/src/commands/analytics/info/cli.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ export class AnalyticsInfoCommandModule
2020
{
2121
command = 'info';
2222
describe = 'Prints analytics gathering and reporting configuration in the console.';
23-
longDescriptionPath?: string;
2423

2524
builder(localYargs: Argv): Argv {
2625
return localYargs.strict();

packages/angular/cli/src/commands/analytics/settings/cli.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,6 @@ abstract class AnalyticsSettingModule
2626
extends CommandModule<AnalyticsCommandArgs>
2727
implements CommandModuleImplementation<AnalyticsCommandArgs>
2828
{
29-
longDescriptionPath?: string;
30-
3129
builder(localYargs: Argv): Argv<AnalyticsCommandArgs> {
3230
return localYargs
3331
.option('global', {

packages/angular/cli/src/commands/build/cli.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import { join } from 'node:path';
109
import { ArchitectCommandModule } from '../../command-builder/architect-command-module';
1110
import { CommandModuleImplementation } from '../../command-builder/command-module';
1211
import { RootCommands } from '../command-config';
12+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
13+
// @ts-ignore strict-deps: Markdown files are asset dependencies bundled/loaded at runtime
14+
import longDescription from './long-description.md';
1315

1416
export default class BuildCommandModule
1517
extends ArchitectCommandModule
@@ -20,5 +22,5 @@ export default class BuildCommandModule
2022
aliases = RootCommands['build'].aliases;
2123
describe =
2224
'Compiles an Angular application or library into an output directory named dist/ at the given output path.';
23-
longDescriptionPath = join(__dirname, 'long-description.md');
25+
override longDescription = longDescription;
2426
}

packages/angular/cli/src/commands/cache/clean/cli.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import { getCacheConfig } from '../utilities';
1818
export class CacheCleanModule extends CommandModule implements CommandModuleImplementation {
1919
command = 'clean';
2020
describe = 'Deletes persistent disk cache from disk.';
21-
longDescriptionPath: string | undefined;
2221
override scope = CommandScope.In;
2322

2423
builder(localYargs: Argv): Argv {

packages/angular/cli/src/commands/cache/cli.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import { join } from 'node:path';
109
import { Argv } from 'yargs';
1110
import {
1211
CommandModule,
@@ -20,6 +19,9 @@ import {
2019
} from '../../command-builder/utilities/command';
2120
import { CacheCleanModule } from './clean/cli';
2221
import { CacheInfoCommandModule } from './info/cli';
22+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
23+
// @ts-ignore strict-deps: Markdown files are asset dependencies bundled/loaded at runtime
24+
import longDescription from './long-description.md';
2325
import { CacheDisableModule, CacheEnableModule } from './settings/cli';
2426

2527
export default class CacheCommandModule
@@ -28,7 +30,7 @@ export default class CacheCommandModule
2830
{
2931
command = 'cache';
3032
describe = 'Configure persistent disk cache and retrieve cache statistics.';
31-
longDescriptionPath = join(__dirname, 'long-description.md');
33+
override longDescription = longDescription;
3234
override scope = CommandScope.In;
3335

3436
builder(localYargs: Argv): Argv {

packages/angular/cli/src/commands/cache/info/cli.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import { getCacheConfig } from '../utilities';
2121
export class CacheInfoCommandModule extends CommandModule implements CommandModuleImplementation {
2222
command = 'info';
2323
describe = 'Prints persistent disk cache configuration and statistics in the console.';
24-
longDescriptionPath?: string | undefined;
2524
override scope = CommandScope.In;
2625

2726
builder(localYargs: Argv): Argv {

packages/angular/cli/src/commands/cache/settings/cli.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ export class CacheDisableModule extends CommandModule implements CommandModuleIm
1818
command = 'disable';
1919
aliases = 'off';
2020
describe = 'Disables persistent disk cache for all projects in the workspace.';
21-
longDescriptionPath: string | undefined;
2221
override scope = CommandScope.In;
2322

2423
builder(localYargs: Argv): Argv {
@@ -34,7 +33,6 @@ export class CacheEnableModule extends CommandModule implements CommandModuleImp
3433
command = 'enable';
3534
aliases = 'on';
3635
describe = 'Enables disk cache for all projects in the workspace.';
37-
longDescriptionPath: string | undefined;
3836
override scope = CommandScope.In;
3937

4038
builder(localYargs: Argv): Argv {

0 commit comments

Comments
 (0)