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.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +273 -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 +117 -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 +65 -0
  31. package/dist/cli.js +285 -0
  32. package/dist/cli.js.map +1 -0
  33. package/dist/config.d.ts +47 -0
  34. package/dist/config.js +93 -0
  35. package/dist/config.js.map +1 -0
  36. package/dist/deprecations.d.ts +30 -0
  37. package/dist/deprecations.js +279 -0
  38. package/dist/deprecations.js.map +1 -0
  39. package/dist/files.d.ts +33 -0
  40. package/dist/files.js +98 -0
  41. package/dist/files.js.map +1 -0
  42. package/dist/graph.d.ts +44 -0
  43. package/dist/graph.js +151 -0
  44. package/dist/graph.js.map +1 -0
  45. package/dist/index.d.ts +44 -0
  46. package/dist/index.js +135 -0
  47. package/dist/index.js.map +1 -0
  48. package/dist/init.d.ts +78 -0
  49. package/dist/init.js +220 -0
  50. package/dist/init.js.map +1 -0
  51. package/dist/report/html.d.ts +14 -0
  52. package/dist/report/html.js +210 -0
  53. package/dist/report/html.js.map +1 -0
  54. package/dist/report/markdown.d.ts +8 -0
  55. package/dist/report/markdown.js +55 -0
  56. package/dist/report/markdown.js.map +1 -0
  57. package/dist/report/summary.d.ts +45 -0
  58. package/dist/report/summary.js +65 -0
  59. package/dist/report/summary.js.map +1 -0
  60. package/dist/report/terminal.d.ts +17 -0
  61. package/dist/report/terminal.js +70 -0
  62. package/dist/report/terminal.js.map +1 -0
  63. package/dist/scan.d.ts +119 -0
  64. package/dist/scan.js +270 -0
  65. package/dist/scan.js.map +1 -0
  66. package/dist/types.d.ts +97 -0
  67. package/dist/types.js +9 -0
  68. package/dist/types.js.map +1 -0
  69. package/dist/workspace.d.ts +76 -0
  70. package/dist/workspace.js +156 -0
  71. package/dist/workspace.js.map +1 -0
  72. package/package.json +67 -4
  73. package/schematics/collection.json +10 -0
  74. package/schematics/ng-add/index.cjs +60 -0
  75. package/schematics/ng-add/schema.json +29 -0
@@ -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;
@@ -0,0 +1,141 @@
1
+ import { isSpecFile } from '../files.js';
2
+ import { formatCount, plural } 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 function deprecatedApisCheck(context) {
15
+ const { deprecations, workspace } = context;
16
+ const legacyTemplates = context.templates.filter((template) => template.firstLegacyLine !== undefined);
17
+ const legacyUses = legacyTemplates.reduce((sum, template) => sum + template.stats.ngIf + template.stats.ngFor + template.stats.ngSwitch, 0);
18
+ if (deprecations.size === 0) {
19
+ return {
20
+ id: 'deprecated-apis',
21
+ title: 'Deprecated APIs',
22
+ grade: 'info',
23
+ summary: 'Dependencies are not installed, so deprecations could not be read from the Angular packages.',
24
+ metrics: [{ label: 'Legacy template directives', value: formatCount(legacyUses) }],
25
+ findings: [
26
+ {
27
+ severity: 'info',
28
+ message: 'Run `npm install` (or your package manager) and re-run for this check',
29
+ detail: 'Deprecations are read from the `@deprecated` tags of the installed `@angular/*` and `@ngrx/*` type definitions, so they always match the version the project actually uses.',
30
+ },
31
+ ],
32
+ };
33
+ }
34
+ const usages = new Map();
35
+ const add = (module, name, info, ref) => {
36
+ const key = `${module}#${name}`;
37
+ const usage = usages.get(key) ?? { module, name, info, refs: [] };
38
+ if (!usage.refs.some((existing) => existing.path === ref.path))
39
+ usage.refs.push(ref);
40
+ usages.set(key, usage);
41
+ };
42
+ for (const file of context.sources) {
43
+ for (const fact of file.imports) {
44
+ const entry = deprecations.get(fact.specifier);
45
+ if (!entry)
46
+ continue;
47
+ for (const binding of fact.bindings) {
48
+ const info = entry.symbols.get(binding.imported);
49
+ if (info)
50
+ add(fact.specifier, binding.imported, info, { path: file.path, line: fact.line });
51
+ }
52
+ }
53
+ for (const [key, lines] of file.memberAccesses) {
54
+ const [module, member] = splitKey(key);
55
+ const info = deprecations.get(module)?.members.get(member);
56
+ if (info)
57
+ add(module, member, info, { path: file.path, line: lines[0] });
58
+ }
59
+ }
60
+ const current = workspace.angular.major;
61
+ const isOverdue = (info) => current !== undefined && info.since !== undefined && current - Number.parseInt(info.since, 10) >= 2;
62
+ const sorted = [...usages.values()];
63
+ const groups = groupByDeprecation(sorted).sort((left, right) => Number(isOverdue(right.info)) - Number(isOverdue(left.info)) || right.refs.length - left.refs.length);
64
+ const findings = groups.map((group) => ({
65
+ severity: isOverdue(group.info) ? 'high' : 'medium',
66
+ message: `${group.names.map((name) => `\`${name}\``).join(', ')} from ${group.module}, used in ${plural(group.refs.length, 'file')}${testShare(group.refs)}`,
67
+ detail: describe(group.info),
68
+ files: group.refs.slice(0, context.maxExamples),
69
+ count: group.refs.length,
70
+ }));
71
+ const ngIf = deprecations.get('@angular/common')?.symbols.get('NgIf');
72
+ if (ngIf && legacyUses > 0) {
73
+ findings.push({
74
+ severity: isOverdue(ngIf) ? 'high' : 'medium',
75
+ message: `${plural(legacyUses, 'use')} of \`*ngIf\`, \`*ngFor\` and \`[ngSwitch]\` in ${plural(legacyTemplates.length, 'template')}`,
76
+ detail: `${describe(ngIf)} \`ng generate @angular/core:control-flow\` migrates most templates to \`@if\`, \`@for\` and \`@switch\` automatically; what it leaves behind is usually an \`ngFor\` with a custom \`trackBy\` or a template reference it cannot follow.`,
77
+ files: legacyTemplates.slice(0, context.maxExamples).map((template) => ({ path: template.path, line: template.firstLegacyLine })),
78
+ count: legacyTemplates.length,
79
+ });
80
+ }
81
+ const affectedFiles = new Set(sorted.flatMap((usage) => usage.refs.map((ref) => ref.path)));
82
+ const grade = findings.length === 0 ? 'good' : findings.some((finding) => finding.severity === 'high') ? 'poor' : 'fair';
83
+ return {
84
+ id: 'deprecated-apis',
85
+ title: 'Deprecated APIs',
86
+ grade,
87
+ summary: findings.length === 0
88
+ ? current !== undefined && current < context.latestAngular.major
89
+ ? `Nothing deprecated as of Angular ${current} is in use; APIs deprecated in later versions surface at each upgrade step.`
90
+ : 'No deprecated Angular or NgRx API in use.'
91
+ : `${plural(usages.size, 'deprecated symbol')} in ${plural(affectedFiles.size, 'file')}${ngIf && legacyUses > 0 ? ', plus deprecated control flow in templates' : ''}.`,
92
+ metrics: [
93
+ { label: 'Deprecated symbols', value: usages.size },
94
+ { label: 'Files affected', value: formatCount(affectedFiles.size) },
95
+ { label: 'Legacy template directives', value: formatCount(legacyUses), hint: ngIf ? 'deprecated' : 'not deprecated in this version' },
96
+ ],
97
+ findings,
98
+ };
99
+ }
100
+ /**
101
+ * Merges usages that share a module, a version and a message. A package
102
+ * deprecated as a whole tags every export with the same sentence
103
+ * (`@angular/animations`: `trigger`, `state`, `style`, `animate`, …), and one
104
+ * finding per export would fill the top priorities with a single decision.
105
+ */
106
+ function groupByDeprecation(usages) {
107
+ const groups = new Map();
108
+ for (const usage of usages) {
109
+ const key = `${usage.module}\u0000${usage.info.since ?? ''}\u0000${usage.info.message}`;
110
+ const group = groups.get(key) ?? { module: usage.module, names: [], info: usage.info, refs: [] };
111
+ group.names.push(usage.name);
112
+ for (const ref of usage.refs)
113
+ if (!group.refs.some((existing) => existing.path === ref.path))
114
+ group.refs.push(ref);
115
+ groups.set(key, group);
116
+ }
117
+ return [...groups.values()];
118
+ }
119
+ /**
120
+ * " (all in tests)" or " (12 in tests)" when some of the files are specs, since
121
+ * a deprecated testing module is a smaller job than one in shipped code.
122
+ * Empty when none are.
123
+ */
124
+ function testShare(refs) {
125
+ const inTests = refs.filter((ref) => isSpecFile(ref.path)).length;
126
+ if (inTests === 0)
127
+ return '';
128
+ return inTests === refs.length ? ' (all in tests)' : ` (${formatCount(inTests)} in tests)`;
129
+ }
130
+ /** `module#Owner.member` split at the `#`. Module names never contain one. */
131
+ function splitKey(key) {
132
+ const separator = key.indexOf('#');
133
+ return [key.slice(0, separator), key.slice(separator + 1)];
134
+ }
135
+ /** "Deprecated since 20.0: Use the `@if` block instead." in the form the reports show. */
136
+ function describe(info) {
137
+ const since = info.since ? `Deprecated since ${info.since}` : 'Deprecated';
138
+ const message = info.message.replace(/\.?$/, '.');
139
+ return info.message ? `${since}: ${message}` : `${since}.`;
140
+ }
141
+ //# sourceMappingURL=deprecated-apis.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deprecated-apis.js","sourceRoot":"","sources":["../../src/checks/deprecated-apis.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
+ * How application state is held: classic NgRx, the NgRx signal store,
5
+ * Angular signals, or services wrapping a `BehaviorSubject`.
6
+ *
7
+ * Never graded `poor`. Classic NgRx is supported and not deprecated, and a
8
+ * `BehaviorSubject` service is a legitimate pattern, so neither is a defect;
9
+ * the check reports how far the codebase has moved towards signals, which is
10
+ * the direction zoneless change detection rewards, and how big the remaining
11
+ * move would be.
12
+ */
13
+ export declare function stateCheck(context: AnalysisContext): CheckResult;
@@ -0,0 +1,72 @@
1
+ import { applicationFiles, countCalls, filesImporting, formatCount, plural, verb } from './context.js';
2
+ /** The classic, action-based NgRx packages. */
3
+ const CLASSIC_NGRX = ['@ngrx/store', '@ngrx/effects', '@ngrx/entity', '@ngrx/router-store', '@ngrx/component-store'];
4
+ /**
5
+ * How application state is held: classic NgRx, the NgRx signal store,
6
+ * Angular signals, or services wrapping a `BehaviorSubject`.
7
+ *
8
+ * Never graded `poor`. Classic NgRx is supported and not deprecated, and a
9
+ * `BehaviorSubject` service is a legitimate pattern, so neither is a defect;
10
+ * the check reports how far the codebase has moved towards signals, which is
11
+ * the direction zoneless change detection rewards, and how big the remaining
12
+ * move would be.
13
+ */
14
+ export function stateCheck(context) {
15
+ const application = applicationFiles(context);
16
+ const classicFiles = filesImporting(application, CLASSIC_NGRX);
17
+ const signalStoreFiles = filesImporting(application, ['@ngrx/signals']);
18
+ const signals = countCalls(application, '@angular/core#signal', '@angular/core#computed', '@angular/core#linkedSignal');
19
+ const toSignal = countCalls(application, '@angular/core/rxjs-interop#toSignal');
20
+ const subjectServices = application.filter((file) => file.injectables > 0 && (file.constructions.get('rxjs#BehaviorSubject')?.length ?? 0) > 0);
21
+ const reducers = countCalls(application, '@ngrx/store#createReducer');
22
+ const effects = countCalls(application, '@ngrx/effects#createEffect');
23
+ const selectors = countCalls(application, '@ngrx/store#createSelector', '@ngrx/store#createFeatureSelector');
24
+ const actions = countCalls(application, '@ngrx/store#createAction', '@ngrx/store#createActionGroup');
25
+ const removedEffectDecorators = countCalls(application, '@ngrx/effects#Effect');
26
+ const findings = [];
27
+ if (removedEffectDecorators > 0) {
28
+ findings.push({
29
+ severity: 'high',
30
+ message: `${plural(removedEffectDecorators, '`@Effect()` decorator')} from NgRx`,
31
+ detail: 'The `@Effect()` decorator was removed in NgRx 15. These have to become `createEffect()` before NgRx can be upgraded past 14.',
32
+ count: removedEffectDecorators,
33
+ });
34
+ }
35
+ if (classicFiles.length > 0) {
36
+ findings.push({
37
+ severity: 'low',
38
+ message: `Classic NgRx in ${plural(classicFiles.length, 'file')}: ${plural(reducers, 'reducer')}, ${plural(effects, 'effect')}, ${plural(selectors, 'selector')}, ${plural(actions, 'action creator')}`,
39
+ detail: 'Classic NgRx is supported, so this is a choice rather than a defect. Moving a feature to `@ngrx/signals` removes the action, reducer and effect boilerplate and fits zoneless change detection without the async pipe; the cost is a rewrite per feature, and the action log that the devtools show goes with it.',
40
+ files: classicFiles.slice(0, context.maxExamples),
41
+ count: classicFiles.length,
42
+ });
43
+ }
44
+ if (subjectServices.length > 0) {
45
+ findings.push({
46
+ severity: 'low',
47
+ message: `${plural(subjectServices.length, 'service')} ${verb(subjectServices.length, 'holds', 'hold')} state in a \`BehaviorSubject\``,
48
+ detail: 'Each is a candidate for a `signal()`-based service: templates read it without the async pipe, `computed()` replaces `combineLatest` chains, and zoneless change detection tracks it automatically. `toObservable()` keeps existing RxJS consumers working during the move.',
49
+ files: subjectServices.slice(0, context.maxExamples).map((file) => ({ path: file.path, line: file.constructions.get('rxjs#BehaviorSubject')?.[0] })),
50
+ count: subjectServices.length,
51
+ });
52
+ }
53
+ const usesSignals = signals + signalStoreFiles.length > 0;
54
+ const nothingToGrade = classicFiles.length === 0 && signalStoreFiles.length === 0 && signals === 0 && subjectServices.length === 0;
55
+ const grade = nothingToGrade ? 'info' : removedEffectDecorators > 0 ? 'poor' : usesSignals && classicFiles.length === 0 ? 'good' : 'fair';
56
+ return {
57
+ id: 'state',
58
+ title: 'State management',
59
+ grade,
60
+ summary: nothingToGrade
61
+ ? 'No state-management library or signal usage found.'
62
+ : `${classicFiles.length > 0 ? `Classic NgRx in ${plural(classicFiles.length, 'file')}` : 'No classic NgRx'}, ${formatCount(signals)} signal()/computed() calls, ${plural(signalStoreFiles.length, 'signal-store file')}.`,
63
+ metrics: [
64
+ { label: 'Classic NgRx files', value: formatCount(classicFiles.length) },
65
+ { label: 'Signal store files', value: formatCount(signalStoreFiles.length) },
66
+ { label: 'signal() / computed()', value: formatCount(signals), hint: `${formatCount(toSignal)} toSignal()` },
67
+ { label: 'BehaviorSubject services', value: formatCount(subjectServices.length) },
68
+ ],
69
+ findings,
70
+ };
71
+ }
72
+ //# sourceMappingURL=state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.js","sourceRoot":"","sources":["../../src/checks/state.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
+ * The unit-test runner, how much of the code has a spec next to it, and the
5
+ * end-to-end setup.
6
+ *
7
+ * "Has a spec next to it" is a proxy for coverage, not coverage: it counts a
8
+ * component or service as tested when `x.spec.ts` sits beside `x.ts`. It needs
9
+ * no test run and no coverage report, which is what lets this tool run in a
10
+ * minute on a repository it has never seen; real coverage is lower or higher
11
+ * depending on what those specs assert.
12
+ */
13
+ export declare function testingCheck(context: AnalysisContext): CheckResult;
@@ -0,0 +1,98 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { applicationFiles, formatCount, percent, plural, specFiles } from './context.js';
4
+ /**
5
+ * The unit-test runner, how much of the code has a spec next to it, and the
6
+ * end-to-end setup.
7
+ *
8
+ * "Has a spec next to it" is a proxy for coverage, not coverage: it counts a
9
+ * component or service as tested when `x.spec.ts` sits beside `x.ts`. It needs
10
+ * no test run and no coverage report, which is what lets this tool run in a
11
+ * minute on a repository it has never seen; real coverage is lower or higher
12
+ * depending on what those specs assert.
13
+ */
14
+ export function testingCheck(context) {
15
+ const { workspace } = context;
16
+ const runners = detectRunners(context);
17
+ const specs = specFiles(context);
18
+ const testable = applicationFiles(context).filter((file) => file.components.length > 0 || file.injectables > 0);
19
+ const specPaths = new Set(specs.map((file) => file.path));
20
+ const untested = testable.filter((file) => !specPaths.has(file.path.replace(/\.ts$/, '.spec.ts')));
21
+ const coverage = percent(testable.length - untested.length, testable.length);
22
+ const jasmineCalls = specs.reduce((sum, file) => sum + file.jasmineCalls, 0);
23
+ const protractor = 'protractor' in workspace.dependencies || workspace.targets.some((target) => target.builder.endsWith(':protractor'));
24
+ const e2e = protractor ? 'Protractor' : '@playwright/test' in workspace.dependencies ? 'Playwright' : 'cypress' in workspace.dependencies ? 'Cypress' : undefined;
25
+ const findings = [];
26
+ if (runners.has('Karma')) {
27
+ findings.push({
28
+ severity: 'high',
29
+ message: 'Unit tests run on Karma, which is deprecated',
30
+ detail: `Karma was deprecated in 2023 and receives no further development. Vitest is the Angular CLI's default runner since v21 (\`@angular/build:unit-test\`). ${specs.length === 0
31
+ ? 'With no specs written yet, moving costs only the configuration, and the first specs can be written for Vitest directly.'
32
+ : `Moving means replacing ${plural(jasmineCalls, 'Jasmine-specific call')} (\`spyOn\`, \`jasmine.*\`) with their Vitest equivalents across ${plural(specs.length, 'spec file')}.`}`,
33
+ });
34
+ }
35
+ if (specs.length === 0 && testable.length > 0) {
36
+ findings.push({ severity: 'high', message: 'No unit tests found', detail: `${plural(testable.length, 'component and service file')} without a single spec. Every refactoring and upgrade step is verified by hand.` });
37
+ }
38
+ else if (testable.length > 0 && coverage < 60) {
39
+ findings.push({
40
+ severity: coverage < 30 ? 'medium' : 'low',
41
+ message: `${100 - coverage}% of components and services have no spec file next to them`,
42
+ detail: 'Counted by file name (`x.spec.ts` beside `x.ts`), so a spec kept elsewhere is missed and a spec that asserts nothing still counts. It is a lower bound on where an upgrade would be verified by hand.',
43
+ files: untested.slice(0, context.maxExamples).map((file) => ({ path: file.path })),
44
+ count: untested.length,
45
+ });
46
+ }
47
+ if (protractor) {
48
+ findings.push({ severity: 'high', message: 'End-to-end tests use Protractor, which is end of life', detail: 'Protractor reached end of life in 2023 and gets no fixes, including for new browser versions. Playwright and Cypress are the usual replacements.' });
49
+ }
50
+ else if (!e2e && testable.length > 0) {
51
+ findings.push({ severity: 'info', message: 'No end-to-end test setup found', detail: 'Neither Playwright nor Cypress is a dependency. A handful of smoke tests over the main flows is the cheapest safety net for an upgrade.' });
52
+ }
53
+ const grade = testable.length === 0 ? 'info' : runners.has('Karma') || protractor || specs.length === 0 ? 'poor' : coverage < 30 ? 'fair' : 'good';
54
+ return {
55
+ id: 'testing',
56
+ title: 'Testing',
57
+ grade,
58
+ summary: `${runners.size > 0 ? [...runners].join(' + ') : 'No unit-test runner detected'}, ${plural(specs.length, 'spec file')}, ${coverage}% of components and services with a spec.`,
59
+ metrics: [
60
+ { label: 'Unit-test runner', value: runners.size > 0 ? [...runners].join(' + ') : 'none' },
61
+ { label: 'Spec files', value: formatCount(specs.length) },
62
+ { label: 'With a spec', value: `${coverage}%`, hint: `${formatCount(testable.length - untested.length)} of ${plural(testable.length, 'component/service file')}` },
63
+ { label: 'End-to-end', value: e2e ?? 'none' },
64
+ ],
65
+ findings,
66
+ };
67
+ }
68
+ /**
69
+ * Runners in use, from the test targets' builders first and the dependencies
70
+ * second. Both are needed: an Nx or CLI workspace names the runner in its
71
+ * builder, while a hand-rolled setup only shows it as a dependency and a config
72
+ * file. A repository can report two during a migration.
73
+ */
74
+ function detectRunners(context) {
75
+ const { workspace } = context;
76
+ const runners = new Set();
77
+ for (const target of workspace.targets) {
78
+ const builder = target.builder;
79
+ if (builder.endsWith(':karma'))
80
+ runners.add('Karma');
81
+ else if (builder.endsWith(':jest') || builder === '@angular-builders/jest:run')
82
+ runners.add('Jest');
83
+ else if (builder.endsWith(':vitest') || builder === '@nx/vite:test' || builder === '@nx/vitest:test')
84
+ runners.add('Vitest');
85
+ else if (builder === '@angular/build:unit-test')
86
+ runners.add(target.options['runner'] === 'karma' ? 'Karma' : 'Vitest');
87
+ }
88
+ if (runners.size === 0) {
89
+ if ('karma' in workspace.dependencies || existsSync(join(workspace.root, 'karma.conf.js')))
90
+ runners.add('Karma');
91
+ if ('jest' in workspace.dependencies)
92
+ runners.add('Jest');
93
+ if ('vitest' in workspace.dependencies)
94
+ runners.add('Vitest');
95
+ }
96
+ return runners;
97
+ }
98
+ //# sourceMappingURL=testing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testing.js","sourceRoot":"","sources":["../../src/checks/testing.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
+ * How much of the code the compiler actually checks: the strictness flags of
5
+ * the application tsconfig, and the escape hatches the code uses to get
6
+ * around them.
7
+ *
8
+ * `strictNullChecks` and `strictTemplates` carry the grade. They are the two
9
+ * flags whose absence lets the most common runtime errors through (an
10
+ * `undefined` dereference, a mistyped template binding), and both are the
11
+ * ones large codebases most often have switched off because turning them on
12
+ * is a campaign rather than an edit.
13
+ */
14
+ export declare function typeSafetyCheck(context: AnalysisContext): CheckResult;