ng-checkup 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +107 -2
  3. package/dist/checks/change-detection.d.ts +22 -0
  4. package/dist/checks/change-detection.js +99 -0
  5. package/dist/checks/change-detection.js.map +1 -0
  6. package/dist/checks/circular-deps.d.ts +13 -0
  7. package/dist/checks/circular-deps.js +43 -0
  8. package/dist/checks/circular-deps.js.map +1 -0
  9. package/dist/checks/components.d.ts +19 -0
  10. package/dist/checks/components.js +97 -0
  11. package/dist/checks/components.js.map +1 -0
  12. package/dist/checks/context.d.ts +51 -0
  13. package/dist/checks/context.js +53 -0
  14. package/dist/checks/context.js.map +1 -0
  15. package/dist/checks/deprecated-apis.d.ts +14 -0
  16. package/dist/checks/deprecated-apis.js +141 -0
  17. package/dist/checks/deprecated-apis.js.map +1 -0
  18. package/dist/checks/state.d.ts +13 -0
  19. package/dist/checks/state.js +72 -0
  20. package/dist/checks/state.js.map +1 -0
  21. package/dist/checks/testing.d.ts +13 -0
  22. package/dist/checks/testing.js +98 -0
  23. package/dist/checks/testing.js.map +1 -0
  24. package/dist/checks/type-safety.d.ts +14 -0
  25. package/dist/checks/type-safety.js +105 -0
  26. package/dist/checks/type-safety.js.map +1 -0
  27. package/dist/checks/version.d.ts +12 -0
  28. package/dist/checks/version.js +107 -0
  29. package/dist/checks/version.js.map +1 -0
  30. package/dist/cli.d.ts +28 -0
  31. package/dist/cli.js +166 -0
  32. package/dist/cli.js.map +1 -0
  33. package/dist/deprecations.d.ts +30 -0
  34. package/dist/deprecations.js +279 -0
  35. package/dist/deprecations.js.map +1 -0
  36. package/dist/files.d.ts +29 -0
  37. package/dist/files.js +92 -0
  38. package/dist/files.js.map +1 -0
  39. package/dist/graph.d.ts +44 -0
  40. package/dist/graph.js +151 -0
  41. package/dist/graph.js.map +1 -0
  42. package/dist/index.d.ts +39 -0
  43. package/dist/index.js +126 -0
  44. package/dist/index.js.map +1 -0
  45. package/dist/report/html.d.ts +14 -0
  46. package/dist/report/html.js +210 -0
  47. package/dist/report/html.js.map +1 -0
  48. package/dist/report/markdown.d.ts +8 -0
  49. package/dist/report/markdown.js +55 -0
  50. package/dist/report/markdown.js.map +1 -0
  51. package/dist/report/summary.d.ts +45 -0
  52. package/dist/report/summary.js +65 -0
  53. package/dist/report/summary.js.map +1 -0
  54. package/dist/report/terminal.d.ts +17 -0
  55. package/dist/report/terminal.js +70 -0
  56. package/dist/report/terminal.js.map +1 -0
  57. package/dist/scan.d.ts +119 -0
  58. package/dist/scan.js +270 -0
  59. package/dist/scan.js.map +1 -0
  60. package/dist/types.d.ts +95 -0
  61. package/dist/types.js +9 -0
  62. package/dist/types.js.map +1 -0
  63. package/dist/workspace.d.ts +70 -0
  64. package/dist/workspace.js +155 -0
  65. package/dist/workspace.js.map +1 -0
  66. package/package.json +60 -4
@@ -0,0 +1,107 @@
1
+ import { majorOf, readInstalledVersion } from '../workspace.js';
2
+ import { plural } from './context.js';
3
+ /**
4
+ * Packages whose presence is a finding of its own, with what replaced them.
5
+ * `runtime` entries ship to users (a dead UI library), the others are tooling.
6
+ * The difference decides the grade: dead runtime code is a liability, dead
7
+ * tooling is friction.
8
+ */
9
+ const END_OF_LIFE_PACKAGES = [
10
+ { name: '@angular/http', runtime: true, note: 'Removed in Angular 8. Replaced by `HttpClient` from `@angular/common/http`.' },
11
+ { name: '@angular/flex-layout', runtime: true, note: 'No longer maintained since Angular 15. Replaced by CSS flexbox/grid or a layout utility library.' },
12
+ { name: 'protractor', runtime: false, note: 'End of life since 2023. Replaced by Playwright or Cypress.' },
13
+ { name: 'tslint', runtime: false, note: 'Deprecated since 2019. Replaced by ESLint with `angular-eslint`.' },
14
+ { name: 'codelyzer', runtime: false, note: 'A set of TSLint rules, so it died with TSLint. Replaced by `angular-eslint`.' },
15
+ { name: 'node-sass', runtime: false, note: 'Deprecated. Replaced by `sass` (Dart Sass), which the Angular CLI already uses.' },
16
+ ];
17
+ /**
18
+ * `@angular/*` packages that do not follow the framework's version numbers, so
19
+ * a different major is not a half-finished upgrade. AngularFire has its own
20
+ * release line, and flex-layout is reported as end of life instead.
21
+ */
22
+ const OWN_VERSIONING = new Set(['@angular/fire', '@angular/flex-layout']);
23
+ /** Builders that compile with webpack, which the esbuild `application` builder replaced as the default in v17. */
24
+ const WEBPACK_BUILDERS = new Set(['@angular-devkit/build-angular:browser', '@nx/angular:webpack-browser', '@angular-builders/custom-webpack:browser']);
25
+ /**
26
+ * Where the codebase stands against the newest Angular release, and whether
27
+ * its toolchain is current.
28
+ *
29
+ * Graded on the support window: Angular releases a major roughly every six
30
+ * months and supports each for 18 (6 active, 12 LTS), so the newest three
31
+ * majors are supported and anything older gets no security fixes. `ng update`
32
+ * moves one major at a time, so the gap is also the number of upgrade steps.
33
+ */
34
+ export function versionCheck(context) {
35
+ const { workspace, latestAngular } = context;
36
+ const current = workspace.angular.major;
37
+ const findings = [];
38
+ const latestLabel = latestAngular.source === 'registry' ? 'npm registry' : 'offline, bundled estimate';
39
+ if (current === undefined) {
40
+ return {
41
+ id: 'version',
42
+ title: 'Angular version & tooling',
43
+ grade: 'info',
44
+ summary: 'No `@angular/core` dependency found in package.json.',
45
+ metrics: [{ label: 'Latest Angular', value: latestAngular.major, hint: latestLabel }],
46
+ findings: [],
47
+ };
48
+ }
49
+ const gap = Math.max(0, latestAngular.major - current);
50
+ const versionLabel = workspace.angular.installed ?? workspace.angular.declared ?? String(current);
51
+ if (gap >= 3) {
52
+ findings.push({
53
+ severity: 'high',
54
+ message: `Angular ${current} is ${gap} majors behind ${latestAngular.major} and out of support`,
55
+ detail: `Angular supports each major for 18 months, so only the newest three get fixes; ${current} receives no security patches. \`ng update\` moves one major at a time, which makes this ${plural(gap, 'sequential upgrade step')}, each with its own migrations to review.`,
56
+ });
57
+ }
58
+ else if (gap === 2) {
59
+ findings.push({
60
+ severity: 'medium',
61
+ message: `Angular ${current} is two majors behind and in its last months of LTS`,
62
+ detail: `Support for ${current} ends when Angular ${latestAngular.major + 1} ships, roughly six months after ${latestAngular.major}. Two \`ng update\` steps bring it current.`,
63
+ });
64
+ }
65
+ else if (gap === 1) {
66
+ findings.push({ severity: 'low', message: `One major behind (${current} → ${latestAngular.major})`, detail: 'Supported, but the next release will make it two.' });
67
+ }
68
+ const mismatched = Object.keys(workspace.dependencies)
69
+ .filter((name) => name.startsWith('@angular/') && !OWN_VERSIONING.has(name))
70
+ .map((name) => ({ name, major: majorOf(readInstalledVersion(workspace.root, name) ?? workspace.dependencies[name]) }))
71
+ .filter((entry) => entry.major !== undefined && entry.major !== current);
72
+ if (mismatched.length > 0) {
73
+ findings.push({
74
+ severity: 'medium',
75
+ message: `${plural(mismatched.length, 'Angular package')} on a different major than @angular/core ${current}`,
76
+ detail: `${mismatched.map((entry) => `${entry.name} ${entry.major}`).join(', ')}. Angular packages are released in lockstep and only tested against the same major; a mixed set is usually a half-finished upgrade.`,
77
+ });
78
+ }
79
+ const webpackTargets = workspace.targets.filter((target) => WEBPACK_BUILDERS.has(target.builder));
80
+ if (webpackTargets.length > 0) {
81
+ findings.push({
82
+ severity: 'medium',
83
+ message: `${plural(webpackTargets.length, 'project')} still built with the webpack builder`,
84
+ detail: `The esbuild-based \`application\` builder has been the default since Angular 17 and builds several times faster. \`ng update @angular/cli --name use-application-builder\` migrates the configuration. Projects: ${webpackTargets.map((target) => target.project).join(', ')}.`,
85
+ });
86
+ }
87
+ const endOfLife = END_OF_LIFE_PACKAGES.filter((entry) => entry.name in workspace.dependencies);
88
+ for (const entry of endOfLife) {
89
+ findings.push({ severity: entry.runtime ? 'high' : 'medium', message: `\`${entry.name}\` is end of life`, detail: entry.note });
90
+ }
91
+ const grade = gap >= 3 || endOfLife.some((entry) => entry.runtime) ? 'poor' : gap === 2 || mismatched.length > 0 || webpackTargets.length > 0 || endOfLife.length > 0 ? 'fair' : 'good';
92
+ const usesApplicationBuilder = workspace.targets.some((target) => /:(application|browser-esbuild)$/.test(target.builder));
93
+ return {
94
+ id: 'version',
95
+ title: 'Angular version & tooling',
96
+ grade,
97
+ summary: gap === 0 ? `On the latest major, Angular ${versionLabel}.` : `Angular ${versionLabel}, ${plural(gap, 'major')} behind ${latestAngular.major}.`,
98
+ metrics: [
99
+ { label: 'Installed', value: versionLabel, hint: workspace.angular.installed ? 'node_modules' : 'package.json range' },
100
+ { label: 'Latest', value: latestAngular.major, hint: latestLabel },
101
+ { label: 'Majors behind', value: gap },
102
+ { label: 'Build system', value: webpackTargets.length > 0 ? 'webpack' : usesApplicationBuilder ? 'esbuild' : 'unknown' },
103
+ ],
104
+ findings,
105
+ };
106
+ }
107
+ //# sourceMappingURL=version.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.js","sourceRoot":"","sources":["../../src/checks/version.ts"],"names":[],"mappings":"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"}
package/dist/cli.d.ts ADDED
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+ import type { Report } from './types.js';
3
+ /** Parsed and validated command-line arguments. */
4
+ interface CliArguments {
5
+ root: string;
6
+ reports: string[];
7
+ json: boolean;
8
+ verbose: boolean;
9
+ failOn?: 'fair' | 'poor';
10
+ maxExamples: number;
11
+ offline: boolean;
12
+ color: boolean;
13
+ }
14
+ /**
15
+ * Parses `argv` (without the node and script entries). Throws an `Error`
16
+ * whose message is meant for the user on anything invalid, which `main`
17
+ * prints with the hint to `--help`. Returns `'help'` or `'version'` for those
18
+ * two flags so `main` can print and exit before doing any work.
19
+ */
20
+ export declare function parseCliArguments(argv: string[], environment?: NodeJS.ProcessEnv, isTty?: boolean): CliArguments | 'help' | 'version';
21
+ /** Renders a report in the format its file extension names. */
22
+ export declare function renderForFile(report: Report, path: string): string;
23
+ /**
24
+ * True when some area is graded `failOn` or worse. `info` never fails a
25
+ * run: it means the area could not be measured, not that it is bad.
26
+ */
27
+ export declare function shouldFail(report: Report, failOn: 'fair' | 'poor' | undefined): boolean;
28
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,166 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, mkdirSync, realpathSync, statSync, writeFileSync } from 'node:fs';
3
+ import { dirname, extname, resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { parseArgs } from 'node:util';
6
+ import { runCheckup, TOOL } from './index.js';
7
+ import { renderHtml } from './report/html.js';
8
+ import { renderMarkdown } from './report/markdown.js';
9
+ import { renderTerminal } from './report/terminal.js';
10
+ const HELP = `Usage: ng-checkup [path] [options]
11
+
12
+ A one-minute modernization report for an Angular codebase. Reads the code,
13
+ never changes it, and sends nothing anywhere except one npm registry lookup
14
+ of the latest Angular version (skip it with --offline).
15
+
16
+ Arguments:
17
+ path Repository root (default: current directory)
18
+
19
+ Options:
20
+ -r, --report <file> Write the full report; format from the extension:
21
+ .html, .md or .json. Repeatable.
22
+ --json Print the report as JSON to stdout instead of the summary
23
+ -v, --verbose Print every finding with its files
24
+ --fail-on <grade> Exit with code 1 if any area is graded <grade> or worse:
25
+ poor or fair (for CI)
26
+ --max-examples <n> Example files listed per finding (default 5)
27
+ --offline Do not look up the latest Angular version online
28
+ --no-color Plain output (also off when NO_COLOR is set or not a TTY)
29
+ -h, --help Show this help
30
+ --version Show the version
31
+
32
+ Examples:
33
+ npx ng-checkup
34
+ npx ng-checkup ../my-app --report checkup.html
35
+ npx ng-checkup --fail-on poor --report checkup.md`;
36
+ /** Grades in order of how bad they are, for `--fail-on`. */
37
+ const SEVERITY_ORDER = ['good', 'fair', 'poor'];
38
+ /**
39
+ * Parses `argv` (without the node and script entries). Throws an `Error`
40
+ * whose message is meant for the user on anything invalid, which `main`
41
+ * prints with the hint to `--help`. Returns `'help'` or `'version'` for those
42
+ * two flags so `main` can print and exit before doing any work.
43
+ */
44
+ export function parseCliArguments(argv, environment = process.env, isTty = Boolean(process.stdout.isTTY)) {
45
+ const { values, positionals } = parseArgs({
46
+ args: argv,
47
+ allowPositionals: true,
48
+ options: {
49
+ report: { type: 'string', short: 'r', multiple: true },
50
+ json: { type: 'boolean' },
51
+ verbose: { type: 'boolean', short: 'v' },
52
+ 'fail-on': { type: 'string' },
53
+ 'max-examples': { type: 'string' },
54
+ offline: { type: 'boolean' },
55
+ 'no-color': { type: 'boolean' },
56
+ help: { type: 'boolean', short: 'h' },
57
+ version: { type: 'boolean' },
58
+ },
59
+ });
60
+ if (values.help)
61
+ return 'help';
62
+ if (values.version)
63
+ return 'version';
64
+ if (positionals.length > 1)
65
+ throw new Error(`Expected one path, got ${positionals.length}: ${positionals.join(' ')}`);
66
+ const root = resolve(positionals[0] ?? '.');
67
+ if (!existsSync(root) || !statSync(root).isDirectory())
68
+ throw new Error(`Not a directory: ${root}`);
69
+ const reports = values.report ?? [];
70
+ for (const report of reports) {
71
+ if (!['.html', '.htm', '.md', '.json'].includes(extname(report).toLowerCase()))
72
+ throw new Error(`Unknown report format for ${report}: use .html, .md or .json`);
73
+ }
74
+ const failOn = values['fail-on'];
75
+ if (failOn !== undefined && failOn !== 'fair' && failOn !== 'poor')
76
+ throw new Error(`--fail-on takes poor or fair, got ${failOn}`);
77
+ const maxExamples = values['max-examples'] === undefined ? 5 : Number(values['max-examples']);
78
+ if (!Number.isInteger(maxExamples) || maxExamples < 0)
79
+ throw new Error(`--max-examples takes a whole number, got ${values['max-examples']}`);
80
+ return {
81
+ root,
82
+ reports,
83
+ json: values.json ?? false,
84
+ verbose: values.verbose ?? false,
85
+ failOn,
86
+ maxExamples,
87
+ offline: values.offline ?? false,
88
+ color: !values['no-color'] && environment['NO_COLOR'] === undefined && isTty,
89
+ };
90
+ }
91
+ /** Renders a report in the format its file extension names. */
92
+ export function renderForFile(report, path) {
93
+ const extension = extname(path).toLowerCase();
94
+ if (extension === '.json')
95
+ return `${JSON.stringify(report, null, 2)}\n`;
96
+ if (extension === '.md')
97
+ return renderMarkdown(report);
98
+ return renderHtml(report);
99
+ }
100
+ /**
101
+ * True when some area is graded `failOn` or worse. `info` never fails a
102
+ * run: it means the area could not be measured, not that it is bad.
103
+ */
104
+ export function shouldFail(report, failOn) {
105
+ if (!failOn)
106
+ return false;
107
+ const threshold = SEVERITY_ORDER.indexOf(failOn);
108
+ return report.checks.some((check) => check.grade !== 'info' && SEVERITY_ORDER.indexOf(check.grade) >= threshold);
109
+ }
110
+ /** Entry point. Exit codes: 0 done, 1 `--fail-on` tripped, 2 usage or runtime error. */
111
+ async function main() {
112
+ let parsed;
113
+ try {
114
+ parsed = parseCliArguments(process.argv.slice(2));
115
+ }
116
+ catch (error) {
117
+ process.stderr.write(`ng-checkup: ${error instanceof Error ? error.message : String(error)}\nRun ng-checkup --help for usage.\n`);
118
+ return 2;
119
+ }
120
+ if (parsed === 'help') {
121
+ process.stdout.write(`${HELP}\n`);
122
+ return 0;
123
+ }
124
+ if (parsed === 'version') {
125
+ process.stdout.write(`${TOOL.version}\n`);
126
+ return 0;
127
+ }
128
+ const report = await runCheckup(parsed.root, { offline: parsed.offline, maxExamples: parsed.maxExamples });
129
+ for (const path of parsed.reports) {
130
+ const target = resolve(path);
131
+ mkdirSync(dirname(target), { recursive: true });
132
+ writeFileSync(target, renderForFile(report, target));
133
+ }
134
+ if (parsed.json) {
135
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
136
+ }
137
+ else {
138
+ process.stdout.write(`${renderTerminal(report, { color: parsed.color, verbose: parsed.verbose })}\n`);
139
+ for (const path of parsed.reports)
140
+ process.stdout.write(`Report written to ${resolve(path)}\n`);
141
+ }
142
+ return shouldFail(report, parsed.failOn) ? 1 : 0;
143
+ }
144
+ /**
145
+ * True when this file is the process entry point. Compared through
146
+ * `realpath` because npm starts the CLI through a symlink in `.bin`, so
147
+ * `argv[1]` names the link while `import.meta.url` names the target. A test
148
+ * importing the parsing helpers is not the entry point and runs nothing.
149
+ */
150
+ function isEntryPoint() {
151
+ try {
152
+ return process.argv[1] !== undefined && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
153
+ }
154
+ catch {
155
+ return false;
156
+ }
157
+ }
158
+ if (isEntryPoint()) {
159
+ main().then((code) => {
160
+ process.exitCode = code;
161
+ }, (error) => {
162
+ process.stderr.write(`ng-checkup failed: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`);
163
+ process.exitCode = 2;
164
+ });
165
+ }
166
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ /** What a `@deprecated` tag says. */
2
+ export interface DeprecationInfo {
3
+ /** The version the tag names, such as `20.0` or `20.2`, when the tag starts with one. */
4
+ since?: string;
5
+ /** The tag's text after the version, trimmed. Can be empty: some tags give no reason. */
6
+ message: string;
7
+ }
8
+ /** The deprecated exports of one import path, such as `@angular/common` or `@angular/common/http`. */
9
+ export interface EntryDeprecations {
10
+ /** Exported name to its deprecation. */
11
+ symbols: Map<string, DeprecationInfo>;
12
+ /** `Owner.member` to its deprecation, for enum members and static class members. */
13
+ members: Map<string, DeprecationInfo>;
14
+ }
15
+ /** Import path to its deprecated exports. */
16
+ export type DeprecationIndex = Map<string, EntryDeprecations>;
17
+ /**
18
+ * Builds the deprecation index from the analysed repository's own `node_modules`.
19
+ *
20
+ * Reading the installed `.d.ts` files rather than shipping a hand-kept list is
21
+ * what keeps the report truthful: it names exactly what the installed version
22
+ * marks, with the version and reason Angular itself wrote. A hand-kept list
23
+ * drifts the moment a release adds or lifts a deprecation (Angular 22, for
24
+ * example, deprecates `ChangeDetectionStrategy.Default` in favour of `Eager`,
25
+ * because OnPush became the default).
26
+ *
27
+ * Returns an empty index when dependencies are not installed; the deprecations
28
+ * check then grades `info` and says why.
29
+ */
30
+ export declare function buildDeprecationIndex(root: string): DeprecationIndex;
@@ -0,0 +1,279 @@
1
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
2
+ import { dirname, join, resolve } from 'node:path';
3
+ import ts from 'typescript';
4
+ import { asRecord, readJson } from './workspace.js';
5
+ /**
6
+ * Package scopes whose type definitions are indexed. Both publish their public
7
+ * API as bundled `.d.ts` files with `@deprecated` tags kept, and both are where
8
+ * an Angular upgrade breaks things.
9
+ */
10
+ const INDEXED_SCOPES = ['@angular', '@ngrx'];
11
+ /**
12
+ * Builds the deprecation index from the analysed repository's own `node_modules`.
13
+ *
14
+ * Reading the installed `.d.ts` files rather than shipping a hand-kept list is
15
+ * what keeps the report truthful: it names exactly what the installed version
16
+ * marks, with the version and reason Angular itself wrote. A hand-kept list
17
+ * drifts the moment a release adds or lifts a deprecation (Angular 22, for
18
+ * example, deprecates `ChangeDetectionStrategy.Default` in favour of `Eager`,
19
+ * because OnPush became the default).
20
+ *
21
+ * Returns an empty index when dependencies are not installed; the deprecations
22
+ * check then grades `info` and says why.
23
+ */
24
+ export function buildDeprecationIndex(root) {
25
+ const index = new Map();
26
+ for (const scope of INDEXED_SCOPES) {
27
+ const scopeDirectory = join(root, 'node_modules', scope);
28
+ if (!existsSync(scopeDirectory))
29
+ continue;
30
+ for (const packageName of safeReaddir(scopeDirectory)) {
31
+ const packageDirectory = join(scopeDirectory, packageName);
32
+ for (const [entry, typesPath] of typeEntriesOf(packageDirectory)) {
33
+ const specifier = entry === '.' ? `${scope}/${packageName}` : `${scope}/${packageName}/${entry.slice(2)}`;
34
+ const deprecations = new DtsReader().exportsOf(typesPath);
35
+ if (deprecations.symbols.size > 0 || deprecations.members.size > 0)
36
+ index.set(specifier, deprecations);
37
+ }
38
+ }
39
+ }
40
+ return index;
41
+ }
42
+ /**
43
+ * Each public entry point of a package with its type definition file.
44
+ *
45
+ * Reads `exports` (Angular 13+ and every NgRx release that matters), skipping
46
+ * wildcard and non-code entries. A package without an `exports` map uses the
47
+ * older layout, where every secondary entry point (`@angular/core/testing`,
48
+ * `@angular/common/http`) is a folder with its own `package.json` naming its
49
+ * `typings`; those are found by `legacyEntriesOf`.
50
+ */
51
+ function typeEntriesOf(packageDirectory) {
52
+ const packageJson = readJson(join(packageDirectory, 'package.json'));
53
+ if (!packageJson)
54
+ return [];
55
+ const entries = [];
56
+ for (const [entry, target] of Object.entries(asRecord(packageJson['exports']))) {
57
+ if (entry.includes('*') || entry === './package.json' || !entry.startsWith('.'))
58
+ continue;
59
+ const types = asRecord(target)['types'] ?? asRecord(target)['typings'];
60
+ if (typeof types === 'string' && types.endsWith('.d.ts'))
61
+ entries.push([entry, resolve(packageDirectory, types)]);
62
+ }
63
+ if (entries.length === 0)
64
+ entries.push(...legacyEntriesOf(packageDirectory, '.', 0));
65
+ return entries.filter(([, path]) => existsSync(path));
66
+ }
67
+ /**
68
+ * Folders that never hold an entry point in the pre-`exports` layout: build
69
+ * formats, schematics and nested dependencies. Skipping them keeps the walk to
70
+ * a few dozen directories per package.
71
+ */
72
+ const NON_ENTRY_DIRECTORIES = new Set(['node_modules', 'schematics', 'bundles', 'esm5', 'esm2015', 'esm2020', 'fesm5', 'fesm2015', 'fesm2020', 'src', 'locales', 'third_party', 'migrations']);
73
+ /**
74
+ * Entry points of a package in the pre-`exports` layout (Angular 12 and
75
+ * older): this folder's `package.json` `typings`, then each subfolder's, up to
76
+ * three levels deep (`@angular/common/http/testing` is the deepest Angular has).
77
+ *
78
+ * @param entry The entry as an `exports` key would spell it: `.`, `./testing`.
79
+ */
80
+ function legacyEntriesOf(directory, entry, depth) {
81
+ const entries = [];
82
+ const packageJson = readJson(join(directory, 'package.json'));
83
+ const types = packageJson?.['types'] ?? packageJson?.['typings'];
84
+ if (typeof types === 'string')
85
+ entries.push([entry, resolve(directory, types)]);
86
+ if (depth >= 3)
87
+ return entries;
88
+ for (const name of safeReaddir(directory)) {
89
+ if (NON_ENTRY_DIRECTORIES.has(name) || name.startsWith('.'))
90
+ continue;
91
+ const child = join(directory, name);
92
+ if (!existsSync(join(child, 'package.json')))
93
+ continue;
94
+ entries.push(...legacyEntriesOf(child, `${entry}/${name}`, depth + 1));
95
+ }
96
+ return entries;
97
+ }
98
+ /**
99
+ * Reads deprecations out of a bundle of `.d.ts` files, following re-exports
100
+ * between them.
101
+ *
102
+ * Angular 20+ ships each entry point as a thin `.d.ts` that re-exports from
103
+ * shared chunks, often under a different name (`export { a as NgIf } from
104
+ * './_common_module-chunk.js'`), so the deprecation sits on a declaration in
105
+ * one file and the public name lives in another. One reader instance memoises
106
+ * per file, which matters because many entry points share the same chunks.
107
+ */
108
+ class DtsReader {
109
+ cache = new Map();
110
+ /** The deprecated names `path` exports, under the names it exports them as. */
111
+ exportsOf(path, depth = 0) {
112
+ const cached = this.cache.get(path);
113
+ if (cached)
114
+ return cached;
115
+ const result = { symbols: new Map(), members: new Map() };
116
+ // Set before recursing, so a re-export cycle between chunks ends here instead of overflowing.
117
+ this.cache.set(path, result);
118
+ if (depth > 8 || !existsSync(path))
119
+ return result;
120
+ const source = ts.createSourceFile(path, readFileSync(path, 'utf8'), ts.ScriptTarget.Latest, true);
121
+ const local = localDeprecations(source);
122
+ /** Names imported from sibling chunks, which a later `export { x }` may re-export. */
123
+ const importedFrom = new Map();
124
+ const exportAs = (from, original, exported) => {
125
+ const symbol = from.symbols.get(original);
126
+ if (symbol)
127
+ result.symbols.set(exported, symbol);
128
+ for (const [member, info] of from.members) {
129
+ if (member.startsWith(`${original}.`))
130
+ result.members.set(`${exported}${member.slice(original.length)}`, info);
131
+ }
132
+ };
133
+ for (const statement of source.statements) {
134
+ if (ts.isImportDeclaration(statement) && ts.isStringLiteralLike(statement.moduleSpecifier) && statement.moduleSpecifier.text.startsWith('.')) {
135
+ const file = siblingDts(path, statement.moduleSpecifier.text);
136
+ const named = statement.importClause?.namedBindings;
137
+ if (named && ts.isNamedImports(named)) {
138
+ for (const element of named.elements)
139
+ importedFrom.set(element.name.text, { file, name: (element.propertyName ?? element.name).text });
140
+ }
141
+ continue;
142
+ }
143
+ if (ts.isExportDeclaration(statement)) {
144
+ const moduleSpecifier = statement.moduleSpecifier;
145
+ const target = moduleSpecifier && ts.isStringLiteralLike(moduleSpecifier) && moduleSpecifier.text.startsWith('.') ? this.exportsOf(siblingDts(path, moduleSpecifier.text), depth + 1) : undefined;
146
+ const clause = statement.exportClause;
147
+ if (!clause) {
148
+ // `export * from './x.js'`: everything x exports, under the same names.
149
+ if (target) {
150
+ for (const [name, info] of target.symbols)
151
+ result.symbols.set(name, info);
152
+ for (const [name, info] of target.members)
153
+ result.members.set(name, info);
154
+ }
155
+ continue;
156
+ }
157
+ if (!ts.isNamedExports(clause))
158
+ continue;
159
+ for (const element of clause.elements) {
160
+ const original = (element.propertyName ?? element.name).text;
161
+ const exported = element.name.text;
162
+ if (target) {
163
+ exportAs(target, original, exported);
164
+ }
165
+ else if (local.symbols.has(original) || [...local.members.keys()].some((key) => key.startsWith(`${original}.`))) {
166
+ exportAs(local, original, exported);
167
+ }
168
+ else {
169
+ const imported = importedFrom.get(original);
170
+ if (imported)
171
+ exportAs(this.exportsOf(imported.file, depth + 1), imported.name, exported);
172
+ }
173
+ }
174
+ continue;
175
+ }
176
+ // `export declare class X` and friends, exported under their own name.
177
+ if (ts.canHaveModifiers(statement) && ts.getModifiers(statement)?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword)) {
178
+ for (const name of declaredNames(statement))
179
+ exportAs(local, name, name);
180
+ }
181
+ }
182
+ return result;
183
+ }
184
+ }
185
+ /**
186
+ * Deprecated top-level declarations of one file, by their local name, plus
187
+ * deprecated enum members and static class members.
188
+ *
189
+ * A name counts as deprecated only when **every** declaration of it carries the
190
+ * tag. Overloaded functions often deprecate one signature (an old parameter
191
+ * shape) while the function itself is fine, and an interface merged with a
192
+ * const (how Angular declares decorators) can deprecate one half. Flagging those
193
+ * would accuse every caller of using a deprecated API.
194
+ */
195
+ function localDeprecations(source) {
196
+ const declarations = new Map();
197
+ const members = new Map();
198
+ for (const statement of source.statements) {
199
+ const info = deprecationOf(statement);
200
+ for (const name of declaredNames(statement)) {
201
+ const list = declarations.get(name) ?? [];
202
+ list.push(info);
203
+ declarations.set(name, list);
204
+ }
205
+ if (ts.isEnumDeclaration(statement)) {
206
+ for (const member of statement.members) {
207
+ const memberInfo = deprecationOf(member);
208
+ if (memberInfo && ts.isIdentifier(member.name))
209
+ members.set(`${statement.name.text}.${member.name.text}`, memberInfo);
210
+ }
211
+ }
212
+ if (ts.isClassDeclaration(statement) && statement.name) {
213
+ const byMember = new Map();
214
+ for (const member of statement.members) {
215
+ const isStatic = ts.canHaveModifiers(member) && ts.getModifiers(member)?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword);
216
+ if (!isStatic || !member.name || !ts.isIdentifier(member.name))
217
+ continue;
218
+ const list = byMember.get(member.name.text) ?? [];
219
+ list.push(deprecationOf(member));
220
+ byMember.set(member.name.text, list);
221
+ }
222
+ for (const [name, list] of byMember) {
223
+ const first = list[0];
224
+ if (first && list.every(Boolean))
225
+ members.set(`${statement.name.text}.${name}`, first);
226
+ }
227
+ }
228
+ }
229
+ const symbols = new Map();
230
+ for (const [name, list] of declarations) {
231
+ // `ɵ` marks Angular's private API. Its deprecations are internal bookkeeping, not upgrade advice.
232
+ if (name.startsWith('ɵ'))
233
+ continue;
234
+ const first = list[0];
235
+ if (first && list.every(Boolean))
236
+ symbols.set(name, first);
237
+ }
238
+ return { symbols, members };
239
+ }
240
+ /** The names a top-level statement declares. Empty for anything that declares none. */
241
+ function declaredNames(statement) {
242
+ if (ts.isVariableStatement(statement)) {
243
+ return statement.declarationList.declarations.flatMap((declaration) => (ts.isIdentifier(declaration.name) ? [declaration.name.text] : []));
244
+ }
245
+ if ((ts.isFunctionDeclaration(statement) || ts.isClassDeclaration(statement) || ts.isInterfaceDeclaration(statement) || ts.isTypeAliasDeclaration(statement) || ts.isEnumDeclaration(statement)) &&
246
+ statement.name) {
247
+ return [statement.name.text];
248
+ }
249
+ return [];
250
+ }
251
+ /** The node's `@deprecated` tag, parsed, or undefined when it has none. */
252
+ function deprecationOf(node) {
253
+ const tag = ts.getJSDocDeprecatedTag(node);
254
+ if (!tag)
255
+ return undefined;
256
+ const text = (ts.getTextOfJSDocComment(tag.comment) ?? '').replace(/\s+/g, ' ').trim();
257
+ // Angular writes the version three ways: `20.0 Use …`, `from v19.0.0, use …`, `since v17`.
258
+ const version = /^(?:(?:from|since|in)\s+)?v?(\d+(?:\.\d+){0,2})\b[:.,]?\s*/i.exec(text);
259
+ return version ? { since: version[1], message: text.slice(version[0].length) } : { since: undefined, message: text };
260
+ }
261
+ /** `./_chunk.js` next to `from`, as the `.d.ts` that types it. */
262
+ function siblingDts(from, specifier) {
263
+ const base = resolve(dirname(from), specifier);
264
+ if (base.endsWith('.d.ts'))
265
+ return base;
266
+ if (base.endsWith('.js') || base.endsWith('.mjs'))
267
+ return base.replace(/\.m?js$/, '.d.ts');
268
+ return existsSync(`${base}.d.ts`) ? `${base}.d.ts` : join(base, 'index.d.ts');
269
+ }
270
+ /** `readdirSync` that treats an unreadable directory as empty. */
271
+ function safeReaddir(directory) {
272
+ try {
273
+ return readdirSync(directory);
274
+ }
275
+ catch {
276
+ return [];
277
+ }
278
+ }
279
+ //# sourceMappingURL=deprecations.js.map