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.
- package/LICENSE +21 -0
- package/README.md +107 -2
- package/dist/checks/change-detection.d.ts +22 -0
- package/dist/checks/change-detection.js +99 -0
- package/dist/checks/change-detection.js.map +1 -0
- package/dist/checks/circular-deps.d.ts +13 -0
- package/dist/checks/circular-deps.js +43 -0
- package/dist/checks/circular-deps.js.map +1 -0
- package/dist/checks/components.d.ts +19 -0
- package/dist/checks/components.js +97 -0
- package/dist/checks/components.js.map +1 -0
- package/dist/checks/context.d.ts +51 -0
- package/dist/checks/context.js +53 -0
- package/dist/checks/context.js.map +1 -0
- package/dist/checks/deprecated-apis.d.ts +14 -0
- package/dist/checks/deprecated-apis.js +141 -0
- package/dist/checks/deprecated-apis.js.map +1 -0
- package/dist/checks/state.d.ts +13 -0
- package/dist/checks/state.js +72 -0
- package/dist/checks/state.js.map +1 -0
- package/dist/checks/testing.d.ts +13 -0
- package/dist/checks/testing.js +98 -0
- package/dist/checks/testing.js.map +1 -0
- package/dist/checks/type-safety.d.ts +14 -0
- package/dist/checks/type-safety.js +105 -0
- package/dist/checks/type-safety.js.map +1 -0
- package/dist/checks/version.d.ts +12 -0
- package/dist/checks/version.js +107 -0
- package/dist/checks/version.js.map +1 -0
- package/dist/cli.d.ts +28 -0
- package/dist/cli.js +166 -0
- package/dist/cli.js.map +1 -0
- package/dist/deprecations.d.ts +30 -0
- package/dist/deprecations.js +279 -0
- package/dist/deprecations.js.map +1 -0
- package/dist/files.d.ts +29 -0
- package/dist/files.js +92 -0
- package/dist/files.js.map +1 -0
- package/dist/graph.d.ts +44 -0
- package/dist/graph.js +151 -0
- package/dist/graph.js.map +1 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.js +126 -0
- package/dist/index.js.map +1 -0
- package/dist/report/html.d.ts +14 -0
- package/dist/report/html.js +210 -0
- package/dist/report/html.js.map +1 -0
- package/dist/report/markdown.d.ts +8 -0
- package/dist/report/markdown.js +55 -0
- package/dist/report/markdown.js.map +1 -0
- package/dist/report/summary.d.ts +45 -0
- package/dist/report/summary.js +65 -0
- package/dist/report/summary.js.map +1 -0
- package/dist/report/terminal.d.ts +17 -0
- package/dist/report/terminal.js +70 -0
- package/dist/report/terminal.js.map +1 -0
- package/dist/scan.d.ts +119 -0
- package/dist/scan.js +270 -0
- package/dist/scan.js.map +1 -0
- package/dist/types.d.ts +95 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- package/dist/workspace.d.ts +70 -0
- package/dist/workspace.js +155 -0
- package/dist/workspace.js.map +1 -0
- package/package.json +60 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Stefan Kuburovic
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,108 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ng-checkup
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/ng-checkup) [](https://github.com/stefankuburovic/ng-checkup/actions/workflows/ci.yml)
|
|
4
|
+
|
|
5
|
+
A one-minute modernization report for any Angular codebase.
|
|
6
|
+
|
|
7
|
+
Point it at a repository and it tells you how far the code is from current Angular, what will break on the next upgrade, and where the work is: version gap, deprecated APIs, zoneless readiness, standalone and signal adoption, test runner, type safety and circular dependencies. Each area gets a grade, every finding names the files, and the HTML report is a single self-contained page you can hand to a team lead or a client.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx ng-checkup
|
|
11
|
+
npx ng-checkup path/to/app --report checkup.html
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Needs Node 22 or newer.
|
|
15
|
+
|
|
16
|
+
```text
|
|
17
|
+
ng-checkup 0.1.0 legacy-app · Angular CLI · Angular ^15.2.0
|
|
18
|
+
8 TypeScript files, 1 template, 50 lines of application code, analysed in 32 ms
|
|
19
|
+
|
|
20
|
+
[POOR] Angular version & tooling Angular ^15.2.0, 7 majors behind 22.
|
|
21
|
+
[INFO] Deprecated APIs Dependencies are not installed, so deprecations could not be read from the Angular packages.
|
|
22
|
+
[FAIR] Change detection & zoneless readiness Runs on zone.js; 50% of 2 components use OnPush.
|
|
23
|
+
[POOR] Components & templates 2 components: 0% standalone, 0% signal inputs, 0% built-in control flow.
|
|
24
|
+
[FAIR] State management Classic NgRx in 2 files, 0 signal()/computed() calls, 0 signal-store files.
|
|
25
|
+
[POOR] Testing Karma, 1 spec file, 25% of components and services with a spec.
|
|
26
|
+
[POOR] Type safety strictNullChecks off, strictTemplates off, 2 explicit any.
|
|
27
|
+
[FAIR] Circular dependencies 1 group of files importing each other in a loop, 2 files in total.
|
|
28
|
+
|
|
29
|
+
Score 37/100 Average of the graded areas: good counts 100, fair 60, poor 20. Areas marked info are not counted.
|
|
30
|
+
|
|
31
|
+
Top priorities
|
|
32
|
+
1. high Angular 15 is 7 majors behind 22 and out of support (Angular version & tooling)
|
|
33
|
+
2. high `@angular/flex-layout` is end of life (Angular version & tooling)
|
|
34
|
+
3. high Unit tests run on Karma, which is deprecated (Testing)
|
|
35
|
+
4. high End-to-end tests use Protractor, which is end of life (Testing)
|
|
36
|
+
5. high `strictNullChecks` is off (Type safety)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
On a real 1 600-file Nx monorepo it finishes in under two seconds.
|
|
40
|
+
|
|
41
|
+
## What it checks
|
|
42
|
+
|
|
43
|
+
| Area | What it looks at | Graded on |
|
|
44
|
+
| --- | --- | --- |
|
|
45
|
+
| **Angular version & tooling** | Installed version against the latest on npm, mixed `@angular/*` majors, webpack vs. esbuild builder, end-of-life packages (`@angular/flex-layout`, Protractor, TSLint, …) | The 18-month support window: the newest three majors are supported |
|
|
46
|
+
| **Deprecated APIs** | Every Angular and NgRx symbol the code imports that the **installed** packages tag `@deprecated`, plus `*ngIf` / `*ngFor` / `[ngSwitch]` in templates | `high` once a deprecation is two majors old, the earliest Angular may remove it |
|
|
47
|
+
| **Change detection** | Zoneless or zone.js (zoneless is the default from Angular 21), OnPush coverage, `NgZone` usage, zone-only test helpers | Zoneless, or OnPush coverage as the readiness proxy |
|
|
48
|
+
| **Components & templates** | Standalone vs. NgModule, signal inputs/outputs vs. decorators, built-in control flow, `@defer` | Average adoption, with the `ng generate` migration for each gap |
|
|
49
|
+
| **State management** | Classic NgRx, NgRx signal store, Angular signals, `BehaviorSubject` services | Never `poor`: classic NgRx is supported, the check measures the distance to signals |
|
|
50
|
+
| **Testing** | Karma / Jest / Vitest, spec files next to components and services, Protractor / Playwright / Cypress | Karma or Protractor, or no specs at all |
|
|
51
|
+
| **Type safety** | `strictNullChecks`, `strictTemplates`, `noImplicitAny`, explicit `any` per 1 000 lines, `x!`, `@ts-ignore`, `$any()`, `UntypedForm*` | The two flags that let the most runtime errors through |
|
|
52
|
+
| **Circular dependencies** | Import cycles between application files, through barrels and tsconfig `paths` | Groups of files in a loop, with one concrete loop to cut |
|
|
53
|
+
|
|
54
|
+
Defaults that changed between versions are applied per version: a component without `standalone` is standalone from Angular 19 and module-based before; one without `changeDetection` is OnPush from 22 and eager before.
|
|
55
|
+
|
|
56
|
+
## Reports
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npx ng-checkup --report checkup.html # self-contained page, light and dark, prints to PDF cleanly
|
|
60
|
+
npx ng-checkup --report checkup.md # for a pull request or a wiki
|
|
61
|
+
npx ng-checkup --report checkup.json # everything, machine-readable
|
|
62
|
+
npx ng-checkup --verbose # every finding and file in the terminal
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`--report` is repeatable. The HTML page loads nothing from the network: no fonts, no scripts, no images.
|
|
66
|
+
|
|
67
|
+
## In CI
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
npx ng-checkup --fail-on poor --report checkup.md
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Exits with `1` when any area is graded `poor` (or `fair` with `--fail-on fair`), `2` on a usage error, `0` otherwise. Areas graded `info`, the ones the tool could not measure, never fail a run.
|
|
74
|
+
|
|
75
|
+
## What leaves your machine
|
|
76
|
+
|
|
77
|
+
One request: the latest `@angular/core` version from the npm registry, with a three-second timeout. `--offline` skips it and compares against the newest major this release knows about, and the report says so. No source, no file names, no telemetry. The tool reads files and never writes anything except the reports you ask for.
|
|
78
|
+
|
|
79
|
+
## How it works
|
|
80
|
+
|
|
81
|
+
- **Syntax only.** Every `.ts` file is parsed once with the TypeScript compiler API, without a type checker and without building the project. That is what makes it fast and lets it run on a repository whose dependencies are not installed. Identifiers are matched through their imports, so a local helper called `signal` is not counted as Angular's.
|
|
82
|
+
- **Deprecations come from your `node_modules`.** The tool reads the `@deprecated` tags in the installed `@angular/*` and `@ngrx/*` type definitions, following Angular's re-exports between its bundled `.d.ts` chunks, so the report always matches the version you actually run, with Angular's own wording. A function counts as deprecated only when every overload is.
|
|
83
|
+
- **Cycles are counted as groups.** A strongly connected component is one thing to untangle; counting individual cycles grows combinatorially and produces numbers nobody can act on. `import type` and `import()` are ignored, since neither can cause an initialization-order crash.
|
|
84
|
+
|
|
85
|
+
## Limitations
|
|
86
|
+
|
|
87
|
+
- It is static analysis. "Has a spec next to it" is a proxy for coverage, not coverage.
|
|
88
|
+
- A monorepo with several apps is reported as one codebase.
|
|
89
|
+
- No bundle-size analysis yet.
|
|
90
|
+
|
|
91
|
+
## Roadmap
|
|
92
|
+
|
|
93
|
+
- Bundle size and eager-chunk analysis from the build's `stats.json`
|
|
94
|
+
- Per-project breakdown for Nx and multi-project CLI workspaces
|
|
95
|
+
- An effort estimate per finding, in days
|
|
96
|
+
- Ratchet mode: store a baseline and fail CI only when a number goes up
|
|
97
|
+
|
|
98
|
+
## Development
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
npm install
|
|
102
|
+
npm test # builds, then runs the Vitest suite against the fixture apps in test/fixtures
|
|
103
|
+
npm run typecheck
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
MIT
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ComponentFact } from '../scan.js';
|
|
2
|
+
import type { CheckResult } from '../types.js';
|
|
3
|
+
import { type AnalysisContext } from './context.js';
|
|
4
|
+
/**
|
|
5
|
+
* How far the app is from zoneless change detection.
|
|
6
|
+
*
|
|
7
|
+
* Zoneless has been stable since Angular 20.2 and is the default from 21: an
|
|
8
|
+
* app on 21+ runs zoneless unless it calls `provideZoneChangeDetection()`,
|
|
9
|
+
* which is how an app upgraded from an older version keeps zone.js. Before 21
|
|
10
|
+
* it is the other way round, zoneless only with `provideZonelessChangeDetection()`.
|
|
11
|
+
* Whether an app can switch is
|
|
12
|
+
* mostly decided by how much of it already tells Angular when something
|
|
13
|
+
* changed, and OnPush coverage is the best single proxy for that: an OnPush
|
|
14
|
+
* component already has to, an eager one usually relies on zone.js noticing.
|
|
15
|
+
*/
|
|
16
|
+
export declare function changeDetectionCheck(context: AnalysisContext): CheckResult;
|
|
17
|
+
/**
|
|
18
|
+
* Whether a component is checked OnPush. From Angular 22 OnPush is the default
|
|
19
|
+
* and only an explicit `Default` or `Eager` opts out; before it, only an
|
|
20
|
+
* explicit `OnPush` opts in.
|
|
21
|
+
*/
|
|
22
|
+
export declare function isOnPush(component: ComponentFact, onPushByDefault: boolean): boolean;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { applicationFiles, callSites, countCalls, filesImporting, formatCount, percent, plural, specFiles, verb } from './context.js';
|
|
2
|
+
/** Test helpers that only work with zone.js loaded. */
|
|
3
|
+
const ZONE_TEST_HELPERS = ['fakeAsync', 'waitForAsync', 'tick', 'flush', 'flushMicrotasks', 'discardPeriodicTasks'].map((name) => `@angular/core/testing#${name}`);
|
|
4
|
+
/**
|
|
5
|
+
* How far the app is from zoneless change detection.
|
|
6
|
+
*
|
|
7
|
+
* Zoneless has been stable since Angular 20.2 and is the default from 21: an
|
|
8
|
+
* app on 21+ runs zoneless unless it calls `provideZoneChangeDetection()`,
|
|
9
|
+
* which is how an app upgraded from an older version keeps zone.js. Before 21
|
|
10
|
+
* it is the other way round, zoneless only with `provideZonelessChangeDetection()`.
|
|
11
|
+
* Whether an app can switch is
|
|
12
|
+
* mostly decided by how much of it already tells Angular when something
|
|
13
|
+
* changed, and OnPush coverage is the best single proxy for that: an OnPush
|
|
14
|
+
* component already has to, an eager one usually relies on zone.js noticing.
|
|
15
|
+
*/
|
|
16
|
+
export function changeDetectionCheck(context) {
|
|
17
|
+
const application = applicationFiles(context);
|
|
18
|
+
const major = context.workspace.angular.major;
|
|
19
|
+
const zonelessByDefault = major !== undefined && major >= 21;
|
|
20
|
+
const zoneless = zonelessByDefault
|
|
21
|
+
? countCalls(application, '@angular/core#provideZoneChangeDetection') === 0
|
|
22
|
+
: countCalls(application, '@angular/core#provideZonelessChangeDetection', '@angular/core#provideExperimentalZonelessChangeDetection') > 0;
|
|
23
|
+
const usesZoneJs = 'zone.js' in context.workspace.dependencies ||
|
|
24
|
+
context.workspace.polyfills.some((entry) => entry.startsWith('zone.js')) ||
|
|
25
|
+
application.some((file) => file.imports.some((fact) => fact.specifier.startsWith('zone.js')));
|
|
26
|
+
const components = application.flatMap((file) => file.components.map((component) => ({ file: file.path, component })));
|
|
27
|
+
const onPushByDefault = major !== undefined && major >= 22;
|
|
28
|
+
const eager = components.filter(({ component }) => !isOnPush(component, onPushByDefault));
|
|
29
|
+
const onPushShare = percent(components.length - eager.length, components.length);
|
|
30
|
+
const ngZoneFiles = application.filter((file) => file.imports.some((fact) => fact.specifier === '@angular/core' && fact.bindings.some((binding) => binding.imported === 'NgZone' && !binding.typeOnly)));
|
|
31
|
+
const zoneHelpers = countCalls(specFiles(context), ...ZONE_TEST_HELPERS);
|
|
32
|
+
const findings = [];
|
|
33
|
+
if (!zoneless && (major === undefined || major >= 20)) {
|
|
34
|
+
findings.push({
|
|
35
|
+
severity: 'medium',
|
|
36
|
+
message: 'Change detection still runs on zone.js',
|
|
37
|
+
detail: `${zonelessByDefault ? 'Zoneless is the default since Angular 21; this app opts back into zone.js with `provideZoneChangeDetection()`.' : 'Zoneless change detection is stable since Angular 20.2 (`provideZonelessChangeDetection()`) and the default from 21.'} It removes zone.js from the bundle and the cost of patching every async API, but every component then has to notify Angular itself, through signals, \`markForCheck()\`, the async pipe or event bindings. OnPush coverage below shows how much of the app already does.`,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
else if (!zoneless && major !== undefined) {
|
|
41
|
+
findings.push({ severity: 'info', message: 'Zoneless change detection needs Angular 20.2 or newer', detail: 'Upgrade first; OnPush coverage is the preparation that pays off now.' });
|
|
42
|
+
}
|
|
43
|
+
if (components.length > 0 && eager.length > 0) {
|
|
44
|
+
findings.push({
|
|
45
|
+
severity: onPushShare < 50 ? 'medium' : 'low',
|
|
46
|
+
message: `${plural(eager.length, 'component')} ${verb(eager.length, 'uses', 'use')} eager change detection (${100 - onPushShare}%)`,
|
|
47
|
+
detail: onPushByDefault
|
|
48
|
+
? 'OnPush is the default since Angular 22, so these set `ChangeDetectionStrategy.Default` or `Eager` explicitly. Each is a component that relies on being checked on every cycle.'
|
|
49
|
+
: 'Without `changeDetection: ChangeDetectionStrategy.OnPush` a component is checked on every change detection cycle and usually relies on zone.js to notice mutations. These are the components to review before going zoneless.',
|
|
50
|
+
files: eager.slice(0, context.maxExamples).map(({ file, component }) => ({ path: file, line: component.line })),
|
|
51
|
+
count: eager.length,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
if (zoneHelpers > 0) {
|
|
55
|
+
findings.push({
|
|
56
|
+
severity: zoneless ? 'medium' : 'low',
|
|
57
|
+
message: `${formatCount(zoneHelpers)} zone-based test helper calls (\`fakeAsync\`, \`tick\`, \`waitForAsync\`)`,
|
|
58
|
+
detail: 'These only run with zone.js loaded in the test environment. Once zone.js leaves the test setup too, each needs rewriting with the runner\'s fake timers (`vi.useFakeTimers()` and `vi.advanceTimersByTime()` under Vitest).',
|
|
59
|
+
files: callSites(specFiles(context), ...ZONE_TEST_HELPERS).slice(0, context.maxExamples),
|
|
60
|
+
count: callSites(specFiles(context), ...ZONE_TEST_HELPERS).length,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
if (ngZoneFiles.length > 0) {
|
|
64
|
+
findings.push({
|
|
65
|
+
severity: 'low',
|
|
66
|
+
message: `${plural(ngZoneFiles.length, 'file')} ${verb(ngZoneFiles.length, 'injects', 'inject')} \`NgZone\``,
|
|
67
|
+
detail: 'Zoneless, `NgZone` is still injectable but `run()` no longer triggers change detection and `runOutsideAngular()` no longer saves any. Code that relied on either needs a signal or an explicit `markForCheck()` instead.',
|
|
68
|
+
files: filesImporting(ngZoneFiles, ['@angular/core']).slice(0, context.maxExamples),
|
|
69
|
+
count: ngZoneFiles.length,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
const grade = major === undefined && components.length === 0 && !usesZoneJs ? 'info' : zoneless ? 'good' : onPushShare >= 50 ? 'fair' : 'poor';
|
|
73
|
+
return {
|
|
74
|
+
id: 'change-detection',
|
|
75
|
+
title: 'Change detection & zoneless readiness',
|
|
76
|
+
grade,
|
|
77
|
+
summary: zoneless
|
|
78
|
+
? `Zoneless, with ${onPushShare}% of components on OnPush.`
|
|
79
|
+
: `Runs on zone.js; ${onPushShare}% of ${plural(components.length, 'component')} use OnPush.`,
|
|
80
|
+
metrics: [
|
|
81
|
+
{ label: 'Mode', value: zoneless ? 'Zoneless' : 'zone.js', hint: zoneless && zonelessByDefault ? 'Angular 21+ default' : undefined },
|
|
82
|
+
{ label: 'OnPush', value: `${onPushShare}%`, hint: `${formatCount(components.length - eager.length)} of ${plural(components.length, 'component')}` },
|
|
83
|
+
{ label: 'Files injecting NgZone', value: formatCount(ngZoneFiles.length) },
|
|
84
|
+
{ label: 'Zone test helpers', value: formatCount(zoneHelpers) },
|
|
85
|
+
],
|
|
86
|
+
findings,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Whether a component is checked OnPush. From Angular 22 OnPush is the default
|
|
91
|
+
* and only an explicit `Default` or `Eager` opts out; before it, only an
|
|
92
|
+
* explicit `OnPush` opts in.
|
|
93
|
+
*/
|
|
94
|
+
export function isOnPush(component, onPushByDefault) {
|
|
95
|
+
if (component.changeDetection === undefined)
|
|
96
|
+
return onPushByDefault;
|
|
97
|
+
return component.changeDetection === 'OnPush';
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=change-detection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"change-detection.js","sourceRoot":"","sources":["../../src/checks/change-detection.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { CheckResult } from '../types.js';
|
|
2
|
+
import { type AnalysisContext } from './context.js';
|
|
3
|
+
/**
|
|
4
|
+
* Import cycles between application files.
|
|
5
|
+
*
|
|
6
|
+
* A cycle is more than a style issue in an Angular app: whichever file of the
|
|
7
|
+
* loop the bundler evaluates first sees the others' exports as `undefined`,
|
|
8
|
+
* which surfaces as "X is not a function" or "Cannot access X before
|
|
9
|
+
* initialization" only in some load orders, such as one lazy route or one test
|
|
10
|
+
* file run on its own. Cycles also stop the bundler from splitting the files
|
|
11
|
+
* into separate lazy chunks.
|
|
12
|
+
*/
|
|
13
|
+
export declare function circularDepsCheck(context: AnalysisContext): CheckResult;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { formatCount, plural } from './context.js';
|
|
2
|
+
/** How many tangles get a finding of their own; the rest are summarised in one line. */
|
|
3
|
+
const LISTED_TANGLES = 5;
|
|
4
|
+
/**
|
|
5
|
+
* Import cycles between application files.
|
|
6
|
+
*
|
|
7
|
+
* A cycle is more than a style issue in an Angular app: whichever file of the
|
|
8
|
+
* loop the bundler evaluates first sees the others' exports as `undefined`,
|
|
9
|
+
* which surfaces as "X is not a function" or "Cannot access X before
|
|
10
|
+
* initialization" only in some load orders, such as one lazy route or one test
|
|
11
|
+
* file run on its own. Cycles also stop the bundler from splitting the files
|
|
12
|
+
* into separate lazy chunks.
|
|
13
|
+
*/
|
|
14
|
+
export function circularDepsCheck(context) {
|
|
15
|
+
const tangles = context.tangles;
|
|
16
|
+
const filesInTangles = tangles.reduce((sum, tangle) => sum + tangle.files.length, 0);
|
|
17
|
+
const largest = tangles[0]?.files.length ?? 0;
|
|
18
|
+
const findings = tangles.slice(0, LISTED_TANGLES).map((tangle) => ({
|
|
19
|
+
severity: tangle.files.length >= 10 ? 'high' : 'medium',
|
|
20
|
+
message: `${plural(tangle.files.length, 'file')} import each other in a loop`,
|
|
21
|
+
detail: `For example: ${tangle.example.map((path) => `\`${path}\``).join(' → ')}. The group dissolves once every loop in it loses one import; most often the culprit is a barrel (\`index.ts\`) import that drags in a sibling, fixed by importing the file directly.`,
|
|
22
|
+
files: tangle.files.slice(0, context.maxExamples).map((path) => ({ path })),
|
|
23
|
+
count: tangle.files.length,
|
|
24
|
+
}));
|
|
25
|
+
if (tangles.length > LISTED_TANGLES) {
|
|
26
|
+
const rest = tangles.slice(LISTED_TANGLES);
|
|
27
|
+
findings.push({ severity: 'medium', message: `${plural(rest.length, 'more tangle')} with ${plural(rest.reduce((sum, tangle) => sum + tangle.files.length, 0), 'file')}`, detail: 'Smaller than the ones above; the same fix applies.' });
|
|
28
|
+
}
|
|
29
|
+
const grade = tangles.length === 0 ? 'good' : tangles.length <= 3 && filesInTangles <= 10 ? 'fair' : 'poor';
|
|
30
|
+
return {
|
|
31
|
+
id: 'circular-deps',
|
|
32
|
+
title: 'Circular dependencies',
|
|
33
|
+
grade,
|
|
34
|
+
summary: tangles.length === 0 ? 'No import cycles between application files.' : `${plural(tangles.length, 'group')} of files importing each other in a loop, ${plural(filesInTangles, 'file')} in total.`,
|
|
35
|
+
metrics: [
|
|
36
|
+
{ label: 'Tangles', value: formatCount(tangles.length), hint: 'groups of files in a loop' },
|
|
37
|
+
{ label: 'Files involved', value: formatCount(filesInTangles) },
|
|
38
|
+
{ label: 'Largest tangle', value: formatCount(largest), hint: 'files' },
|
|
39
|
+
],
|
|
40
|
+
findings,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=circular-deps.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"circular-deps.js","sourceRoot":"","sources":["../../src/checks/circular-deps.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ComponentFact } from '../scan.js';
|
|
2
|
+
import type { CheckResult } from '../types.js';
|
|
3
|
+
import { type AnalysisContext } from './context.js';
|
|
4
|
+
/**
|
|
5
|
+
* Adoption of the component APIs Angular has moved to: standalone components,
|
|
6
|
+
* signal inputs and outputs, and built-in control flow.
|
|
7
|
+
*
|
|
8
|
+
* Graded on the average adoption across the three, so a codebase is not
|
|
9
|
+
* punished for an API it has no use for (an app without a single `@Input`
|
|
10
|
+
* is not graded on signal inputs). Each lagging area comes with the
|
|
11
|
+
* `ng generate` migration Angular ships for it, because each is mostly
|
|
12
|
+
* mechanical and the report's job is to say how much work is left.
|
|
13
|
+
*/
|
|
14
|
+
export declare function componentsCheck(context: AnalysisContext): CheckResult;
|
|
15
|
+
/**
|
|
16
|
+
* Whether a component is standalone. From Angular 19 that is the default and
|
|
17
|
+
* only `standalone: false` opts out; before it, only `standalone: true` opts in.
|
|
18
|
+
*/
|
|
19
|
+
export declare function isStandalone(component: ComponentFact, standaloneByDefault: boolean): boolean;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { applicationFiles, countCalls, formatCount, percent, plural } from './context.js';
|
|
2
|
+
/** Calls that declare a signal input, including two-way `model()` bindings. */
|
|
3
|
+
const SIGNAL_INPUTS = ['input', 'input.required', 'model', 'model.required'].map((name) => `@angular/core#${name}`);
|
|
4
|
+
/** Calls that declare a function-based output. */
|
|
5
|
+
const SIGNAL_OUTPUTS = ['@angular/core#output', '@angular/core/rxjs-interop#outputFromObservable'];
|
|
6
|
+
/**
|
|
7
|
+
* Adoption of the component APIs Angular has moved to: standalone components,
|
|
8
|
+
* signal inputs and outputs, and built-in control flow.
|
|
9
|
+
*
|
|
10
|
+
* Graded on the average adoption across the three, so a codebase is not
|
|
11
|
+
* punished for an API it has no use for (an app without a single `@Input`
|
|
12
|
+
* is not graded on signal inputs). Each lagging area comes with the
|
|
13
|
+
* `ng generate` migration Angular ships for it, because each is mostly
|
|
14
|
+
* mechanical and the report's job is to say how much work is left.
|
|
15
|
+
*/
|
|
16
|
+
export function componentsCheck(context) {
|
|
17
|
+
const application = applicationFiles(context);
|
|
18
|
+
const major = context.workspace.angular.major;
|
|
19
|
+
const standaloneByDefault = major === undefined || major >= 19;
|
|
20
|
+
const components = application.flatMap((file) => file.components.map((component) => ({ file: file.path, component })));
|
|
21
|
+
if (components.length === 0) {
|
|
22
|
+
return { id: 'components', title: 'Components & templates', grade: 'info', summary: 'No `@Component` classes found.', metrics: [], findings: [] };
|
|
23
|
+
}
|
|
24
|
+
const moduleBased = components.filter(({ component }) => !isStandalone(component, standaloneByDefault));
|
|
25
|
+
const ngModules = application.reduce((sum, file) => sum + file.ngModules, 0);
|
|
26
|
+
const signalInputs = countCalls(application, ...SIGNAL_INPUTS);
|
|
27
|
+
const signalOutputs = countCalls(application, ...SIGNAL_OUTPUTS);
|
|
28
|
+
const decoratorInputs = application.reduce((sum, file) => sum + file.decoratorInputs, 0);
|
|
29
|
+
const decoratorOutputs = application.reduce((sum, file) => sum + file.decoratorOutputs, 0);
|
|
30
|
+
const totals = context.templates.reduce((sum, template) => ({
|
|
31
|
+
legacy: sum.legacy + template.stats.ngIf + template.stats.ngFor + template.stats.ngSwitch,
|
|
32
|
+
modern: sum.modern + template.stats.ifBlocks + template.stats.forBlocks + template.stats.switchBlocks,
|
|
33
|
+
defer: sum.defer + template.stats.deferBlocks,
|
|
34
|
+
}), { legacy: 0, modern: 0, defer: 0 });
|
|
35
|
+
const standaloneShare = percent(components.length - moduleBased.length, components.length);
|
|
36
|
+
const inputs = signalInputs + decoratorInputs;
|
|
37
|
+
const outputs = signalOutputs + decoratorOutputs;
|
|
38
|
+
const signalInputShare = percent(signalInputs, inputs);
|
|
39
|
+
const signalOutputShare = percent(signalOutputs, outputs);
|
|
40
|
+
const controlFlow = totals.legacy + totals.modern;
|
|
41
|
+
const controlFlowShare = percent(totals.modern, controlFlow);
|
|
42
|
+
const findings = [];
|
|
43
|
+
if (moduleBased.length > 0) {
|
|
44
|
+
findings.push({
|
|
45
|
+
severity: standaloneShare < 50 ? 'medium' : 'low',
|
|
46
|
+
message: `${plural(moduleBased.length, 'component')} still declared in NgModules (${plural(ngModules, 'NgModule')} in total)`,
|
|
47
|
+
detail: 'Standalone components are the default since Angular 19 and what every new API assumes. `ng generate @angular/core:standalone` converts declarations, removes the emptied NgModules and switches bootstrapping, in three passes that can be reviewed one at a time.',
|
|
48
|
+
files: moduleBased.slice(0, context.maxExamples).map(({ file, component }) => ({ path: file, line: component.line })),
|
|
49
|
+
count: moduleBased.length,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
if (decoratorInputs + decoratorOutputs > 0) {
|
|
53
|
+
findings.push({
|
|
54
|
+
severity: signalInputShare < 50 ? 'medium' : 'low',
|
|
55
|
+
message: `${plural(decoratorInputs, '`@Input()` decorator')} and ${plural(decoratorOutputs, '`@Output()` decorator')} left`,
|
|
56
|
+
detail: 'Signal inputs (`input()`) make inputs readable in `computed()` and effects without `ngOnChanges`, and are what zoneless change detection tracks. `ng generate @angular/core:signal-input-migration` and `ng generate @angular/core:output-migration` convert most of them; the rest are usually inputs written to from inside the component.',
|
|
57
|
+
count: decoratorInputs + decoratorOutputs,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
const controlFlowDeprecated = context.deprecations.get('@angular/common')?.symbols.has('NgIf') ?? false;
|
|
61
|
+
if (totals.legacy > 0 && !controlFlowDeprecated) {
|
|
62
|
+
// When NgIf is deprecated in the installed version the deprecated-APIs check reports this instead.
|
|
63
|
+
findings.push({
|
|
64
|
+
severity: 'low',
|
|
65
|
+
message: `${plural(totals.legacy, 'use')} of \`*ngIf\`, \`*ngFor\` and \`[ngSwitch]\``,
|
|
66
|
+
detail: 'Built-in control flow (`@if`, `@for`, `@switch`) is available from Angular 17. `ng generate @angular/core:control-flow` migrates most templates automatically.',
|
|
67
|
+
count: totals.legacy,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
const shares = [standaloneShare, ...(inputs > 0 ? [signalInputShare] : []), ...(controlFlow > 0 ? [controlFlowShare] : [])];
|
|
71
|
+
const average = shares.reduce((sum, share) => sum + share, 0) / shares.length;
|
|
72
|
+
const grade = average >= 80 ? 'good' : average >= 40 ? 'fair' : 'poor';
|
|
73
|
+
const metrics = [
|
|
74
|
+
{ label: 'Components', value: formatCount(components.length) },
|
|
75
|
+
{ label: 'Standalone', value: `${standaloneShare}%`, hint: `${formatCount(ngModules)} NgModules` },
|
|
76
|
+
{ label: 'Signal inputs', value: inputs > 0 ? `${signalInputShare}%` : 'n/a', hint: `${formatCount(signalInputs)} of ${formatCount(inputs)}` },
|
|
77
|
+
{ label: 'Signal outputs', value: outputs > 0 ? `${signalOutputShare}%` : 'n/a', hint: `${formatCount(signalOutputs)} of ${formatCount(outputs)}` },
|
|
78
|
+
{ label: 'Built-in control flow', value: controlFlow > 0 ? `${controlFlowShare}%` : 'n/a', hint: `${formatCount(totals.modern)} of ${formatCount(controlFlow)} blocks` },
|
|
79
|
+
{ label: '@defer blocks', value: formatCount(totals.defer) },
|
|
80
|
+
];
|
|
81
|
+
return {
|
|
82
|
+
id: 'components',
|
|
83
|
+
title: 'Components & templates',
|
|
84
|
+
grade,
|
|
85
|
+
summary: `${plural(components.length, 'component')}: ${standaloneShare}% standalone, ${inputs > 0 ? `${signalInputShare}% signal inputs` : 'no inputs'}, ${controlFlow > 0 ? `${controlFlowShare}% built-in control flow` : 'no control flow'}.`,
|
|
86
|
+
metrics,
|
|
87
|
+
findings,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Whether a component is standalone. From Angular 19 that is the default and
|
|
92
|
+
* only `standalone: false` opts out; before it, only `standalone: true` opts in.
|
|
93
|
+
*/
|
|
94
|
+
export function isStandalone(component, standaloneByDefault) {
|
|
95
|
+
return component.standalone ?? standaloneByDefault;
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=components.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"components.js","sourceRoot":"","sources":["../../src/checks/components.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type ts from 'typescript';
|
|
2
|
+
import type { DeprecationIndex } from '../deprecations.js';
|
|
3
|
+
import type { DependencyTangle } from '../graph.js';
|
|
4
|
+
import type { SourceFileFacts, TemplateFact } from '../scan.js';
|
|
5
|
+
import type { CheckResult, FileRef } from '../types.js';
|
|
6
|
+
import type { WorkspaceInfo } from '../workspace.js';
|
|
7
|
+
/**
|
|
8
|
+
* Everything a check may read. Built once by `runCheckup` before any check
|
|
9
|
+
* runs, so checks are pure functions of it: no check reads the disk, which is
|
|
10
|
+
* what lets the tests build a context by hand.
|
|
11
|
+
*/
|
|
12
|
+
export interface AnalysisContext {
|
|
13
|
+
workspace: WorkspaceInfo;
|
|
14
|
+
/** Every scanned TypeScript file, specs included; each check filters what it needs. */
|
|
15
|
+
sources: SourceFileFacts[];
|
|
16
|
+
/** External template files and inline `template:` strings. */
|
|
17
|
+
templates: TemplateFact[];
|
|
18
|
+
/** Empty when dependencies are not installed. */
|
|
19
|
+
deprecations: DeprecationIndex;
|
|
20
|
+
tangles: DependencyTangle[];
|
|
21
|
+
/** Newest Angular major, and whether it came from the registry or the tool's bundled fallback. */
|
|
22
|
+
latestAngular: {
|
|
23
|
+
major: number;
|
|
24
|
+
source: 'registry' | 'bundled';
|
|
25
|
+
};
|
|
26
|
+
/** Effective options of the application tsconfig, `extends` resolved. */
|
|
27
|
+
appCompilerOptions: ts.CompilerOptions;
|
|
28
|
+
/** `angularCompilerOptions` of the application tsconfig, merged along its `extends` chain. */
|
|
29
|
+
angularCompilerOptions: Record<string, unknown>;
|
|
30
|
+
maxExamples: number;
|
|
31
|
+
}
|
|
32
|
+
/** A check: a pure function from the analysis context to a graded result. */
|
|
33
|
+
export type Check = (context: AnalysisContext) => CheckResult;
|
|
34
|
+
/** Total of the line arrays stored under `keys` in each file's `calls` map. */
|
|
35
|
+
export declare function countCalls(files: SourceFileFacts[], ...keys: string[]): number;
|
|
36
|
+
/** The first call site of any of `keys` in each file that has one, for report examples. */
|
|
37
|
+
export declare function callSites(files: SourceFileFacts[], ...keys: string[]): FileRef[];
|
|
38
|
+
/** Files that import any of `modules`, with the line of the first such import. */
|
|
39
|
+
export declare function filesImporting(files: SourceFileFacts[], modules: string[]): FileRef[];
|
|
40
|
+
/** Whole percentage, 0 when the denominator is 0, so an empty codebase never reports NaN. */
|
|
41
|
+
export declare function percent(part: number, whole: number): number;
|
|
42
|
+
/** Thousands separated by a thin space, which reads in every locale a report goes to. */
|
|
43
|
+
export declare function formatCount(value: number): string;
|
|
44
|
+
/** `1 file` / `3 files`. */
|
|
45
|
+
export declare function plural(count: number, singular: string, pluralForm?: string): string;
|
|
46
|
+
/** "uses" for one, "use" for several: the verb that agrees with a count. */
|
|
47
|
+
export declare function verb(count: number, singular: string, pluralForm: string): string;
|
|
48
|
+
/** Non-spec files: application code, as opposed to tests. */
|
|
49
|
+
export declare function applicationFiles(context: AnalysisContext): SourceFileFacts[];
|
|
50
|
+
/** Spec files only. */
|
|
51
|
+
export declare function specFiles(context: AnalysisContext): SourceFileFacts[];
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/** Total of the line arrays stored under `keys` in each file's `calls` map. */
|
|
2
|
+
export function countCalls(files, ...keys) {
|
|
3
|
+
let total = 0;
|
|
4
|
+
for (const file of files)
|
|
5
|
+
for (const key of keys)
|
|
6
|
+
total += file.calls.get(key)?.length ?? 0;
|
|
7
|
+
return total;
|
|
8
|
+
}
|
|
9
|
+
/** The first call site of any of `keys` in each file that has one, for report examples. */
|
|
10
|
+
export function callSites(files, ...keys) {
|
|
11
|
+
const refs = [];
|
|
12
|
+
for (const file of files) {
|
|
13
|
+
const lines = keys.flatMap((key) => file.calls.get(key) ?? []);
|
|
14
|
+
if (lines.length > 0)
|
|
15
|
+
refs.push({ path: file.path, line: Math.min(...lines) });
|
|
16
|
+
}
|
|
17
|
+
return refs;
|
|
18
|
+
}
|
|
19
|
+
/** Files that import any of `modules`, with the line of the first such import. */
|
|
20
|
+
export function filesImporting(files, modules) {
|
|
21
|
+
const refs = [];
|
|
22
|
+
for (const file of files) {
|
|
23
|
+
const first = file.imports.find((fact) => !fact.dynamic && modules.includes(fact.specifier));
|
|
24
|
+
if (first)
|
|
25
|
+
refs.push({ path: file.path, line: first.line });
|
|
26
|
+
}
|
|
27
|
+
return refs;
|
|
28
|
+
}
|
|
29
|
+
/** Whole percentage, 0 when the denominator is 0, so an empty codebase never reports NaN. */
|
|
30
|
+
export function percent(part, whole) {
|
|
31
|
+
return whole === 0 ? 0 : Math.round((part / whole) * 100);
|
|
32
|
+
}
|
|
33
|
+
/** Thousands separated by a thin space, which reads in every locale a report goes to. */
|
|
34
|
+
export function formatCount(value) {
|
|
35
|
+
return value.toLocaleString('en-US').replace(/,/g, ' ');
|
|
36
|
+
}
|
|
37
|
+
/** `1 file` / `3 files`. */
|
|
38
|
+
export function plural(count, singular, pluralForm = `${singular}s`) {
|
|
39
|
+
return `${formatCount(count)} ${count === 1 ? singular : pluralForm}`;
|
|
40
|
+
}
|
|
41
|
+
/** "uses" for one, "use" for several: the verb that agrees with a count. */
|
|
42
|
+
export function verb(count, singular, pluralForm) {
|
|
43
|
+
return count === 1 ? singular : pluralForm;
|
|
44
|
+
}
|
|
45
|
+
/** Non-spec files: application code, as opposed to tests. */
|
|
46
|
+
export function applicationFiles(context) {
|
|
47
|
+
return context.sources.filter((file) => !file.isSpec);
|
|
48
|
+
}
|
|
49
|
+
/** Spec files only. */
|
|
50
|
+
export function specFiles(context) {
|
|
51
|
+
return context.sources.filter((file) => file.isSpec);
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.js","sourceRoot":"","sources":["../../src/checks/context.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { CheckResult } from '../types.js';
|
|
2
|
+
import { type AnalysisContext } from './context.js';
|
|
3
|
+
/**
|
|
4
|
+
* Deprecated Angular and NgRx APIs the code uses, as the installed packages
|
|
5
|
+
* mark them, plus the template-side use of the deprecated structural
|
|
6
|
+
* directives (`*ngIf`, `*ngFor`, `[ngSwitch]`), which no import reveals because
|
|
7
|
+
* templates reach them through `CommonModule`.
|
|
8
|
+
*
|
|
9
|
+
* A symbol deprecated two or more majors before the installed one is `high`:
|
|
10
|
+
* Angular removes deprecated APIs no earlier than two majors after
|
|
11
|
+
* deprecating them, so from then on any `ng update` may be the one that breaks
|
|
12
|
+
* the build.
|
|
13
|
+
*/
|
|
14
|
+
export declare function deprecatedApisCheck(context: AnalysisContext): CheckResult;
|