ng-checkup 0.0.0-stage → 0.2.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 +273 -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 +117 -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 +65 -0
- package/dist/cli.js +285 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +47 -0
- package/dist/config.js +93 -0
- package/dist/config.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 +33 -0
- package/dist/files.js +98 -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 +44 -0
- package/dist/index.js +135 -0
- package/dist/index.js.map +1 -0
- package/dist/init.d.ts +78 -0
- package/dist/init.js +220 -0
- package/dist/init.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 +97 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- package/dist/workspace.d.ts +76 -0
- package/dist/workspace.js +156 -0
- package/dist/workspace.js.map +1 -0
- package/package.json +67 -4
- package/schematics/collection.json +10 -0
- package/schematics/ng-add/index.cjs +60 -0
- package/schematics/ng-add/schema.json +29 -0
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,274 @@
|
|
|
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) [](https://github.com/stefankuburovic/ng-checkup/blob/main/LICENSE)
|
|
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. Eight areas get a grade, every finding names the files, and the HTML report is one self-contained page you can hand to your team lead.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx ng-checkup
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+

|
|
14
|
+
|
|
15
|
+
**[See a full example report →](https://stefankuburovic.github.io/ng-checkup/example.html)** (the Angular 12 version of the RealWorld example app)
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Contents
|
|
20
|
+
|
|
21
|
+
- [Quick start](#quick-start)
|
|
22
|
+
- [Set it up in a project](#set-it-up-in-a-project)
|
|
23
|
+
- [Configuration](#configuration)
|
|
24
|
+
- [What it checks](#what-it-checks)
|
|
25
|
+
- [Reports](#reports)
|
|
26
|
+
- [In CI](#in-ci)
|
|
27
|
+
- [Programmatic API](#programmatic-api)
|
|
28
|
+
- [What leaves your machine](#what-leaves-your-machine)
|
|
29
|
+
- [How it works](#how-it-works)
|
|
30
|
+
- [FAQ](#faq)
|
|
31
|
+
- [Roadmap](#roadmap)
|
|
32
|
+
- [Contributing](#contributing)
|
|
33
|
+
|
|
34
|
+
## Quick start
|
|
35
|
+
|
|
36
|
+
Run it in the root of an Angular CLI or Nx workspace. It needs Node 22 or newer and nothing else; your project does not have to build.
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# 1. The summary in your terminal
|
|
40
|
+
npx ng-checkup
|
|
41
|
+
|
|
42
|
+
# 2. The full report as a page to share
|
|
43
|
+
npx ng-checkup --report checkup.html
|
|
44
|
+
|
|
45
|
+
# 3. Fail CI when an area turns poor
|
|
46
|
+
npx ng-checkup --fail-on poor
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Install dependencies first (`npm install`) if you can: the deprecated-API check reads the installed Angular packages, and without them it is skipped.
|
|
50
|
+
|
|
51
|
+
## Set it up in a project
|
|
52
|
+
|
|
53
|
+
To keep the checkup in the project, with a script, a config file and a CI step:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
ng add ng-checkup # Angular CLI: installs it and asks which CI to use
|
|
57
|
+
# or
|
|
58
|
+
npx ng-checkup init # any workspace, including Nx
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Both make the same changes and list what they changed:
|
|
62
|
+
|
|
63
|
+
| File | Change |
|
|
64
|
+
| --- | --- |
|
|
65
|
+
| `package.json` | a `"checkup": "ng-checkup"` script, and `ng-checkup` in `devDependencies` (`init` only; `ng add` installs it itself) |
|
|
66
|
+
| `ng-checkup.config.json` | writes `checkup.html` on every run, and an empty `ignore` list |
|
|
67
|
+
| `.gitignore` | the generated reports, if the repository has a `.gitignore` |
|
|
68
|
+
| `.github/workflows/ng-checkup.yml` | runs on every pull request, fails on a poor area, uploads the reports (when `.github/` exists) |
|
|
69
|
+
| `ng-checkup.gitlab-ci.yml` | the same as a GitLab job; add `include: [{ local: ng-checkup.gitlab-ci.yml }]` to your `.gitlab-ci.yml` (when that file exists) |
|
|
70
|
+
|
|
71
|
+
After that it is `npm run checkup` (or `pnpm checkup`, `yarn checkup`; the lockfile decides). Running either command again changes nothing. It never overwrites a file you wrote: an existing workflow or config is kept, and a different `checkup` script is kept unless you pass `--force`.
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
npx ng-checkup init --dry-run # show the changes, write nothing
|
|
75
|
+
npx ng-checkup init --ci gitlab # pick the CI instead of detecting it (github, gitlab, none)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Configuration
|
|
79
|
+
|
|
80
|
+
`ng-checkup.config.json` in the project root. Every key is optional, and every command-line flag overrides its key.
|
|
81
|
+
|
|
82
|
+
```json
|
|
83
|
+
{
|
|
84
|
+
"reports": ["checkup.html"],
|
|
85
|
+
"failOn": "poor",
|
|
86
|
+
"maxExamples": 5,
|
|
87
|
+
"offline": false,
|
|
88
|
+
"ignore": ["src/legacy", "**/generated", "apps/*/e2e"]
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
| Key | Meaning | Flag |
|
|
93
|
+
| --- | --- | --- |
|
|
94
|
+
| `reports` | report files to write, relative to the config file | `--report` |
|
|
95
|
+
| `failOn` | exit with 1 when an area is `poor` (or `fair`) | `--fail-on` |
|
|
96
|
+
| `maxExamples` | example files listed per finding | `--max-examples` |
|
|
97
|
+
| `offline` | skip the npm lookup of the latest Angular version | `--offline` |
|
|
98
|
+
| `ignore` | paths left out of the analysis; a path matches everything below it, `*` stays within a folder, `**` crosses folders | |
|
|
99
|
+
|
|
100
|
+
An unknown key or a wrong type stops the run with an error, so a typo cannot silently switch off a CI threshold. Use `--config path/to/file.json` for a file elsewhere.
|
|
101
|
+
|
|
102
|
+
## What it checks
|
|
103
|
+
|
|
104
|
+
| Area | It looks at | Poor when |
|
|
105
|
+
| --- | --- | --- |
|
|
106
|
+
| [**Angular version & tooling**](https://github.com/stefankuburovic/ng-checkup/blob/main/docs/checks.md#version) | installed vs. latest Angular, mixed `@angular/*` majors, webpack builder, end-of-life packages | three or more majors behind, i.e. out of support |
|
|
107
|
+
| [**Deprecated APIs**](https://github.com/stefankuburovic/ng-checkup/blob/main/docs/checks.md#deprecated-apis) | every imported Angular/NgRx symbol your **installed** version tags `@deprecated`, plus `*ngIf` / `*ngFor` in templates | something deprecated two majors ago is still used |
|
|
108
|
+
| [**Change detection**](https://github.com/stefankuburovic/ng-checkup/blob/main/docs/checks.md#change-detection) | zoneless or zone.js, OnPush coverage, `NgZone`, zone-only test helpers | zone.js with less than half the components on OnPush |
|
|
109
|
+
| [**Components & templates**](https://github.com/stefankuburovic/ng-checkup/blob/main/docs/checks.md#components) | standalone, signal inputs and outputs, built-in control flow, `@defer` | average adoption below 40% |
|
|
110
|
+
| [**State management**](https://github.com/stefankuburovic/ng-checkup/blob/main/docs/checks.md#state) | classic NgRx, NgRx signal store, signals, `BehaviorSubject` services | the removed `@Effect()` decorator |
|
|
111
|
+
| [**Testing**](https://github.com/stefankuburovic/ng-checkup/blob/main/docs/checks.md#testing) | Karma / Jest / Vitest, specs next to components and services, Protractor / Playwright / Cypress | Karma, Protractor or no specs |
|
|
112
|
+
| [**Type safety**](https://github.com/stefankuburovic/ng-checkup/blob/main/docs/checks.md#type-safety) | `strictNullChecks`, `strictTemplates`, `noImplicitAny`, `any`, `x!`, `@ts-ignore`, `$any()` | both `strictNullChecks` and `strictTemplates` off |
|
|
113
|
+
| [**Circular dependencies**](https://github.com/stefankuburovic/ng-checkup/blob/main/docs/checks.md#circular-deps) | import cycles through relative paths, barrels and tsconfig `paths` | more than three groups or ten files in loops |
|
|
114
|
+
|
|
115
|
+
Each finding says why it matters and which `ng generate` migration fixes it. **[Every check, its grading and its fix →](https://github.com/stefankuburovic/ng-checkup/blob/main/docs/checks.md)**
|
|
116
|
+
|
|
117
|
+
Defaults that changed between versions are applied per version: a component without `standalone` is standalone from Angular 19, one without `changeDetection` is OnPush from 22, and an app without a zone provider is zoneless from 21.
|
|
118
|
+
|
|
119
|
+
## Reports
|
|
120
|
+
|
|
121
|
+

|
|
122
|
+
|
|
123
|
+
| Command | Output |
|
|
124
|
+
| --- | --- |
|
|
125
|
+
| `npx ng-checkup` | summary and top five priorities in the terminal |
|
|
126
|
+
| `npx ng-checkup --verbose` | every finding with its files in the terminal |
|
|
127
|
+
| `npx ng-checkup --report checkup.html` | self-contained page: light and dark mode, prints cleanly to PDF, loads nothing from the network |
|
|
128
|
+
| `npx ng-checkup --report checkup.md` | Markdown for a pull request, an issue or a wiki |
|
|
129
|
+
| `npx ng-checkup --report checkup.json` | everything, machine-readable |
|
|
130
|
+
| `npx ng-checkup --json` | the JSON on stdout, for piping |
|
|
131
|
+
|
|
132
|
+
`--report` is repeatable, so one run can write all three files.
|
|
133
|
+
|
|
134
|
+
<details>
|
|
135
|
+
<summary>All options</summary>
|
|
136
|
+
|
|
137
|
+
```text
|
|
138
|
+
Usage: ng-checkup [path] [options]
|
|
139
|
+
ng-checkup init [path] [init options]
|
|
140
|
+
|
|
141
|
+
Arguments:
|
|
142
|
+
path Repository root (default: current directory)
|
|
143
|
+
|
|
144
|
+
Options:
|
|
145
|
+
-r, --report <file> Write the full report; format from the extension:
|
|
146
|
+
.html, .md or .json. Repeatable.
|
|
147
|
+
--json Print the report as JSON to stdout instead of the summary
|
|
148
|
+
-v, --verbose Print every finding with its files
|
|
149
|
+
--fail-on <grade> Exit with code 1 if any area is graded <grade> or worse:
|
|
150
|
+
poor or fair (for CI)
|
|
151
|
+
--max-examples <n> Example files listed per finding (default 5)
|
|
152
|
+
--offline Do not look up the latest Angular version online
|
|
153
|
+
--config <file> Config file (default: ng-checkup.config.json in the root)
|
|
154
|
+
--no-color Plain output (also off when NO_COLOR is set or not a TTY)
|
|
155
|
+
-h, --help Show this help
|
|
156
|
+
--version Show the version
|
|
157
|
+
|
|
158
|
+
Init options (ng-checkup init):
|
|
159
|
+
--ci <provider> auto (default), github, gitlab or none
|
|
160
|
+
--dry-run Show what would change without writing anything
|
|
161
|
+
--force Replace an existing "checkup" script
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Exit codes: `0` done, `1` `--fail-on` tripped, `2` usage error. Areas graded `info` never fail a run.
|
|
165
|
+
|
|
166
|
+
</details>
|
|
167
|
+
|
|
168
|
+
## In CI
|
|
169
|
+
|
|
170
|
+
`ng add ng-checkup` and `npx ng-checkup init` write this for you. By hand:
|
|
171
|
+
|
|
172
|
+
**GitHub Actions**
|
|
173
|
+
|
|
174
|
+
```yaml
|
|
175
|
+
- uses: actions/setup-node@v4
|
|
176
|
+
with:
|
|
177
|
+
node-version: 22
|
|
178
|
+
- run: npm ci
|
|
179
|
+
- run: npx ng-checkup --fail-on poor --report checkup.md --report checkup.html
|
|
180
|
+
- uses: actions/upload-artifact@v4
|
|
181
|
+
if: always()
|
|
182
|
+
with:
|
|
183
|
+
name: ng-checkup
|
|
184
|
+
path: checkup.*
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
**GitLab CI**
|
|
188
|
+
|
|
189
|
+
```yaml
|
|
190
|
+
ng-checkup:
|
|
191
|
+
image: node:22
|
|
192
|
+
script:
|
|
193
|
+
- npm ci
|
|
194
|
+
- npx ng-checkup --fail-on poor --report checkup.md --report checkup.html
|
|
195
|
+
artifacts:
|
|
196
|
+
when: always
|
|
197
|
+
paths: [checkup.md, checkup.html]
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Start with `--fail-on poor`. `--fail-on fair` is for codebases that are already in good shape and want to stay there.
|
|
201
|
+
|
|
202
|
+
## Programmatic API
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
import { writeFile } from 'node:fs/promises';
|
|
206
|
+
import { runCheckup, renderMarkdown, overallScore } from 'ng-checkup';
|
|
207
|
+
|
|
208
|
+
const report = await runCheckup('path/to/app', { offline: true });
|
|
209
|
+
|
|
210
|
+
console.log(overallScore(report)); // e.g. 45
|
|
211
|
+
for (const check of report.checks) {
|
|
212
|
+
console.log(check.id, check.grade, check.summary);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// The same Markdown the CLI writes, e.g. to post as a pull-request comment.
|
|
216
|
+
await writeFile('checkup.md', renderMarkdown(report));
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
`runCheckup` never writes anything. `renderHtml`, `renderMarkdown`, `overallScore`, `topPriorities`, `loadConfig` and `planInit` (what `init` would change, without writing it) are exported too, along with the `Report`, `CheckResult`, `Finding` and `Grade` types. `Report` is also the schema of the JSON output.
|
|
220
|
+
|
|
221
|
+
## What leaves your machine
|
|
222
|
+
|
|
223
|
+
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, and the report says so. No source, no file names, no telemetry. The tool reads files and writes only the reports you ask for.
|
|
224
|
+
|
|
225
|
+
## How it works
|
|
226
|
+
|
|
227
|
+
- **Syntax only.** Every `.ts` file is parsed once with the TypeScript compiler API, without a type checker and without building your project. That is why it takes seconds on a large monorepo and works on a repository that does not compile. Identifiers are matched through their imports, so a local helper called `signal` is not counted as Angular's.
|
|
228
|
+
- **Deprecations come from your `node_modules`.** It reads the `@deprecated` tags in the installed `@angular/*` and `@ngrx/*` type definitions, following Angular's re-exports between its bundled `.d.ts` chunks, in both the current package layout and the older one. The report always matches the version you actually run, in Angular's own words.
|
|
229
|
+
- **Cycles are counted as groups.** A strongly connected component is one thing to untangle; counting individual cycles grows combinatorially.
|
|
230
|
+
|
|
231
|
+
## FAQ
|
|
232
|
+
|
|
233
|
+
**Does it change my code?** No. It only reads. The fixes it suggests are Angular's own `ng generate` migrations, which you run yourself.
|
|
234
|
+
|
|
235
|
+
**Why is the deprecated-API check "info"?** Dependencies are not installed. Run `npm install` and run the checkup again.
|
|
236
|
+
|
|
237
|
+
**My app is on Angular 12 and deprecated APIs are "good". Is that right?** Yes, for Angular 12: nothing that *Angular 12* deprecates is in use. Each upgrade step deprecates more, and the version check is where that gap shows.
|
|
238
|
+
|
|
239
|
+
**Is "with a spec" my test coverage?** No. It counts components and services with an `x.spec.ts` next to `x.ts`. It needs no test run, which is what keeps the tool fast and usable on a repository it has never seen.
|
|
240
|
+
|
|
241
|
+
**Does it support Nx?** Yes: it reads `project.json` targets and tsconfig `paths`. A workspace with several apps is reported as one codebase for now.
|
|
242
|
+
|
|
243
|
+
**Which Angular versions?** Angular 2 and newer (not AngularJS). Checks whose rules changed between versions (standalone, OnPush, zoneless defaults) apply the rules of the version you run.
|
|
244
|
+
|
|
245
|
+
## Roadmap
|
|
246
|
+
|
|
247
|
+
- Baseline mode: fail CI only when a number gets worse, so legacy code can adopt it on day one
|
|
248
|
+
- Upgrade plan: the steps from your version to the latest, with the migrations each needs
|
|
249
|
+
- Per-project reports for Nx and multi-project workspaces
|
|
250
|
+
- Bundle size from the build's esbuild metafile
|
|
251
|
+
- SARIF output for GitHub code scanning
|
|
252
|
+
|
|
253
|
+
Ideas and bug reports are welcome in [issues](https://github.com/stefankuburovic/ng-checkup/issues).
|
|
254
|
+
|
|
255
|
+
## Contributing
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
git clone https://github.com/stefankuburovic/ng-checkup.git
|
|
259
|
+
cd ng-checkup
|
|
260
|
+
npm install
|
|
261
|
+
npm test # builds, then runs the Vitest suite against the fixture apps in test/fixtures
|
|
262
|
+
npm run typecheck
|
|
263
|
+
node dist/cli.js path/to/some/angular/app
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Each check is one file in `src/checks/` and a pure function of the analysis context, so a new check is a new file plus a fixture that shows it.
|
|
267
|
+
|
|
268
|
+
## Need help acting on the report?
|
|
269
|
+
|
|
270
|
+
I'm [Stefan Kuburovic](https://stefankuburovic.github.io/), and modernizing large Angular codebases is my job. If you'd rather have someone walk through the report with your team, or do the upgrade with you, have a look at the [Angular Health Check](https://stefankuburovic.github.io/#health-check).
|
|
271
|
+
|
|
272
|
+
## License
|
|
273
|
+
|
|
274
|
+
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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"}
|