angular-scan 0.0.2 → 0.0.3

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/src/lib/scan.ts DELETED
@@ -1,176 +0,0 @@
1
- import { isDevMode } from '@angular/core';
2
- import { getNgDebugApi } from './ng-debug';
3
- import { CanvasOverlay } from './overlay/canvas-overlay';
4
- import type { AngularScanOptions, RenderKind } from './types';
5
-
6
- // Profiler event constants
7
- const TemplateUpdateStart = 2;
8
- const ChangeDetectionStart = 12;
9
- const ChangeDetectionEnd = 13;
10
- const ChangeDetectionSyncStart = 14;
11
- const ChangeDetectionSyncEnd = 15;
12
-
13
- /**
14
- * Imperative API for angular-scan.
15
- *
16
- * Use this when you cannot modify application providers (e.g. micro-frontends,
17
- * third-party apps). Call before `bootstrapApplication()`.
18
- *
19
- * ```ts
20
- * // main.ts
21
- * import { scan } from 'angular-scan';
22
- * scan();
23
- * bootstrapApplication(AppComponent, appConfig);
24
- * ```
25
- *
26
- * @returns A teardown function that stops scanning and removes the overlay.
27
- */
28
- export function scan(options: AngularScanOptions = {}): () => void {
29
- if (!isDevMode()) return noop;
30
- if (typeof window === 'undefined') return noop;
31
- if (options.enabled === false) return noop;
32
-
33
- const ng = getNgDebugApi();
34
- if (!ng) {
35
- console.warn(
36
- '[angular-scan] Angular debug APIs (window.ng) not available. ' +
37
- 'Ensure you are running in development mode.'
38
- );
39
- return noop;
40
- }
41
-
42
- const durationMs = options.flashDurationMs ?? 500;
43
- const showBadges = options.showBadges !== false;
44
-
45
- const overlay = new CanvasOverlay();
46
- overlay.attach(document);
47
-
48
- // State for the current tick
49
- let inTick = false;
50
- let inSyncPhase = false;
51
- const mutatedNodes = new Set<Node>();
52
- const tickInstances = new Set<object>();
53
- const renderCounts = new WeakMap<object, number>();
54
- const unnecessaryCounts = new WeakMap<object, number>();
55
- const badges = new Map<Element, HTMLElement>();
56
-
57
- const observer = new MutationObserver(records => {
58
- for (const r of records) {
59
- mutatedNodes.add(r.target);
60
- r.addedNodes.forEach(n => mutatedNodes.add(n));
61
- r.removedNodes.forEach(n => mutatedNodes.add(n));
62
- }
63
- });
64
-
65
- const removeProfiler = ng.ɵsetProfiler((event, instance) => {
66
- switch (event) {
67
- case ChangeDetectionStart:
68
- if (inTick) break;
69
- inTick = true;
70
- mutatedNodes.clear();
71
- tickInstances.clear();
72
- observer.observe(document.body, {
73
- subtree: true, childList: true, attributes: true, characterData: true,
74
- });
75
- break;
76
-
77
- case ChangeDetectionSyncStart:
78
- inSyncPhase = true;
79
- break;
80
-
81
- case ChangeDetectionSyncEnd:
82
- inSyncPhase = false;
83
- break;
84
-
85
- case TemplateUpdateStart:
86
- if (inSyncPhase && instance) tickInstances.add(instance);
87
- break;
88
-
89
- case ChangeDetectionEnd: {
90
- if (!inTick) break;
91
- inTick = false;
92
- inSyncPhase = false;
93
-
94
- const pending = observer.takeRecords();
95
- for (const r of pending) {
96
- mutatedNodes.add(r.target);
97
- r.addedNodes.forEach(n => mutatedNodes.add(n));
98
- r.removedNodes.forEach(n => mutatedNodes.add(n));
99
- }
100
- observer.disconnect();
101
-
102
- // Build mutated host set
103
- const mutatedHosts = new Set<Element>();
104
- for (const node of mutatedNodes) {
105
- let current: Node | null = node;
106
- while (current && current !== document.body) {
107
- if (current instanceof Element) {
108
- try {
109
- if (ng.getComponent(current)) { mutatedHosts.add(current); break; }
110
- } catch { /* ignore */ }
111
- }
112
- current = current.parentNode;
113
- }
114
- }
115
-
116
- // Collect updates without writing to any signals
117
- const updates: Array<{ instance: object; hostEl: Element; kind: RenderKind }> = [];
118
- for (const inst of tickInstances) {
119
- try {
120
- const hostEl = ng.getHostElement(inst);
121
- if (!hostEl) continue;
122
- const kind: RenderKind = mutatedHosts.has(hostEl) ? 'render' : 'unnecessary';
123
- updates.push({ instance: inst, hostEl, kind });
124
- } catch { /* component destroyed */ }
125
- }
126
-
127
- queueMicrotask(() => {
128
- for (const { instance, hostEl, kind } of updates) {
129
- const count = (renderCounts.get(instance) ?? 0) + 1;
130
- renderCounts.set(instance, count);
131
-
132
- if (kind === 'unnecessary') {
133
- unnecessaryCounts.set(instance, (unnecessaryCounts.get(instance) ?? 0) + 1);
134
- }
135
-
136
- overlay.flash(hostEl, kind, durationMs);
137
-
138
- if (showBadges) updateBadge(hostEl, count, kind);
139
- }
140
- });
141
- break;
142
- }
143
- }
144
- });
145
-
146
- function updateBadge(hostEl: Element, count: number, kind: RenderKind): void {
147
- let badge = badges.get(hostEl);
148
- if (!badge) {
149
- badge = document.createElement('div');
150
- badge.setAttribute('aria-hidden', 'true');
151
- badge.setAttribute('role', 'presentation');
152
- badge.style.cssText = [
153
- 'position:absolute', 'top:2px', 'right:2px',
154
- 'z-index:2147483645', 'pointer-events:none',
155
- 'font:bold 9px/13px monospace', 'padding:1px 4px',
156
- 'border-radius:3px', 'min-width:16px', 'text-align:center', 'color:#fff',
157
- ].join(';');
158
- const htmlEl = hostEl as HTMLElement;
159
- if (getComputedStyle(htmlEl).position === 'static') htmlEl.style.position = 'relative';
160
- hostEl.appendChild(badge);
161
- badges.set(hostEl, badge);
162
- }
163
- badge.style.background = kind === 'unnecessary' ? '#f44336' : '#ff9800';
164
- badge.textContent = String(count);
165
- }
166
-
167
- return () => {
168
- removeProfiler();
169
- observer.disconnect();
170
- overlay.detach();
171
- for (const b of badges.values()) b.remove();
172
- badges.clear();
173
- };
174
- }
175
-
176
- function noop(): void {}
@@ -1,197 +0,0 @@
1
- import {
2
- Injectable,
3
- inject,
4
- isDevMode,
5
- OnDestroy,
6
- PLATFORM_ID,
7
- } from '@angular/core';
8
- import { isPlatformBrowser } from '@angular/common';
9
- import { ComponentTracker } from './component-tracker';
10
- import { OverlayService } from './overlay/overlay.service';
11
- import { ANGULAR_SCAN_OPTIONS } from './tokens';
12
- import { getNgDebugApi } from './ng-debug';
13
- import type { NgDebugApi, RenderKind } from './types';
14
-
15
- // Angular profiler event IDs (stable contract in dev mode, used by Angular DevTools)
16
- const TemplateUpdateStart = 2;
17
- const ChangeDetectionStart = 12;
18
- const ChangeDetectionEnd = 13;
19
- const ChangeDetectionSyncStart = 14;
20
- const ChangeDetectionSyncEnd = 15;
21
-
22
- interface PendingUpdate {
23
- instance: object;
24
- hostElement: Element;
25
- kind: RenderKind;
26
- }
27
-
28
- @Injectable({ providedIn: 'root' })
29
- export class ScannerService implements OnDestroy {
30
- private readonly tracker = inject(ComponentTracker);
31
- private readonly overlay = inject(OverlayService);
32
- private readonly options = inject(ANGULAR_SCAN_OPTIONS);
33
- private readonly platformId = inject(PLATFORM_ID);
34
-
35
- private removeProfiler: (() => void) | null = null;
36
- private mutationObserver: MutationObserver | null = null;
37
-
38
- // Accumulated during a single CD tick (raw DOM nodes, no signal writes)
39
- private mutatedNodes = new Set<Node>();
40
- // Component instances visited during the sync (template update) phase
41
- private tickInstances = new Set<object>();
42
-
43
- // Tracks which phase of CD we're in
44
- private inSyncPhase = false;
45
- private inTick = false;
46
-
47
- // The toolbar component instance — excluded from tracking to avoid self-loops
48
- private toolbarInstance: object | null = null;
49
-
50
- initialize(): void {
51
- if (!isDevMode()) return;
52
- if (!isPlatformBrowser(this.platformId)) return;
53
- if (this.options.enabled === false) return;
54
-
55
- const ng = getNgDebugApi();
56
- if (!ng) {
57
- console.warn(
58
- '[angular-scan] Angular debug APIs (window.ng) not available. ' +
59
- 'Ensure you are running in development mode.'
60
- );
61
- return;
62
- }
63
-
64
- this.mutationObserver = new MutationObserver(records => {
65
- for (const r of records) {
66
- this.mutatedNodes.add(r.target);
67
- r.addedNodes.forEach(n => this.mutatedNodes.add(n));
68
- r.removedNodes.forEach(n => this.mutatedNodes.add(n));
69
- }
70
- });
71
-
72
- this.removeProfiler = ng.ɵsetProfiler((event, instance) => {
73
- switch (event) {
74
- case ChangeDetectionStart:
75
- this.onTickStart();
76
- break;
77
- case ChangeDetectionSyncStart:
78
- this.inSyncPhase = true;
79
- break;
80
- case ChangeDetectionSyncEnd:
81
- this.inSyncPhase = false;
82
- break;
83
- case TemplateUpdateStart:
84
- // Only record during the real update phase, not the dev-mode checkNoChanges pass
85
- if (this.inSyncPhase && instance && instance !== this.toolbarInstance) {
86
- this.tickInstances.add(instance);
87
- }
88
- break;
89
- case ChangeDetectionEnd:
90
- this.onTickEnd(ng);
91
- break;
92
- }
93
- });
94
- }
95
-
96
- /** Exclude a component instance from render tracking (used for the toolbar). */
97
- setToolbarInstance(instance: object): void {
98
- this.toolbarInstance = instance;
99
- }
100
-
101
- private onTickStart(): void {
102
- if (this.inTick) return;
103
- this.inTick = true;
104
- this.mutatedNodes.clear();
105
- this.tickInstances.clear();
106
-
107
- this.mutationObserver?.observe(document.body, {
108
- subtree: true,
109
- childList: true,
110
- attributes: true,
111
- characterData: true,
112
- });
113
- }
114
-
115
- private onTickEnd(ng: NgDebugApi): void {
116
- if (!this.inTick) return;
117
- this.inTick = false;
118
- this.inSyncPhase = false;
119
-
120
- // Synchronously flush pending mutation records before disconnecting
121
- const pending = this.mutationObserver?.takeRecords() ?? [];
122
- for (const r of pending) {
123
- this.mutatedNodes.add(r.target);
124
- r.addedNodes.forEach(n => this.mutatedNodes.add(n));
125
- r.removedNodes.forEach(n => this.mutatedNodes.add(n));
126
- }
127
- this.mutationObserver?.disconnect();
128
-
129
- // Build a set of component host elements that had DOM mutations
130
- // Walk mutated nodes UP to find their owning component host element
131
- const mutatedHosts = this.buildMutatedHosts(ng);
132
-
133
- // Collect updates as pure data — NO signal writes here (would trigger CD)
134
- const pendingUpdates: PendingUpdate[] = [];
135
-
136
- for (const instance of this.tickInstances) {
137
- try {
138
- const hostElement = ng.getHostElement(instance);
139
- if (!hostElement) continue;
140
-
141
- const hadMutation = mutatedHosts.has(hostElement);
142
- const kind: RenderKind = hadMutation ? 'render' : 'unnecessary';
143
-
144
- pendingUpdates.push({ instance, hostElement, kind });
145
- } catch {
146
- // Component may have been destroyed during the tick
147
- }
148
- }
149
-
150
- // Defer all signal writes out of the profiler callback to avoid triggering a new CD cycle
151
- queueMicrotask(() => {
152
- for (const update of pendingUpdates) {
153
- const stats = this.tracker.recordRender(update.instance, update.hostElement, update.kind);
154
- this.overlay.onComponentChecked(stats);
155
- }
156
- this.tracker.snapshotTrackedComponents();
157
- });
158
- }
159
-
160
- /**
161
- * Build a Set of host Elements by walking each mutated node up the DOM tree
162
- * until an Angular component boundary is found.
163
- * This is O(mutatedNodes × depth) but depth is typically < 20.
164
- */
165
- private buildMutatedHosts(ng: NgDebugApi): Set<Element> {
166
- const hosts = new Set<Element>();
167
-
168
- for (const node of this.mutatedNodes) {
169
- let current: Node | null = node;
170
-
171
- while (current && current !== document.body) {
172
- if (current instanceof Element) {
173
- try {
174
- const component = ng.getComponent(current);
175
- if (component) {
176
- hosts.add(current);
177
- break;
178
- }
179
- } catch {
180
- // ignore
181
- }
182
- }
183
- current = current.parentNode;
184
- }
185
- }
186
-
187
- return hosts;
188
- }
189
-
190
- ngOnDestroy(): void {
191
- this.removeProfiler?.();
192
- this.removeProfiler = null;
193
- this.mutationObserver?.disconnect();
194
- this.mutationObserver = null;
195
- this.overlay.destroy();
196
- }
197
- }
package/src/lib/tokens.ts DELETED
@@ -1,7 +0,0 @@
1
- import { InjectionToken } from '@angular/core';
2
- import type { AngularScanOptions } from './types';
3
-
4
- export const ANGULAR_SCAN_OPTIONS = new InjectionToken<AngularScanOptions>(
5
- 'ANGULAR_SCAN_OPTIONS',
6
- { providedIn: 'root', factory: () => ({}) }
7
- );
@@ -1,66 +0,0 @@
1
- <div
2
- class="toolbar"
3
- role="complementary"
4
- aria-label="Angular Scan DevTools"
5
- >
6
- <div class="toolbar__header">
7
- <span class="toolbar__logo" aria-hidden="true">◉</span>
8
- <span class="toolbar__title">angular-scan</span>
9
-
10
- <button
11
- class="toolbar__btn"
12
- type="button"
13
- [attr.aria-pressed]="enabled()"
14
- [title]="enabled() ? 'Pause scanning' : 'Resume scanning'"
15
- (click)="toggleEnabled()"
16
- >{{ enabled() ? '⏸' : '▶' }}</button>
17
-
18
- <button
19
- class="toolbar__btn"
20
- type="button"
21
- [attr.aria-pressed]="expanded()"
22
- [title]="expanded() ? 'Collapse inspector' : 'Expand inspector'"
23
- (click)="toggleExpanded()"
24
- >{{ expanded() ? '▲' : '▼' }}</button>
25
- </div>
26
-
27
- <div class="toolbar__stats">
28
- <span class="toolbar__stat" title="Total change detection checks">
29
- <span class="toolbar__stat-label">checks</span>
30
- <strong class="toolbar__stat-value">{{ tracker.totalRenders() }}</strong>
31
- </span>
32
- <span
33
- class="toolbar__stat"
34
- [class.toolbar__stat--warn]="tracker.totalUnnecessary() > 0"
35
- title="Unnecessary renders: component was checked but DOM did not change"
36
- >
37
- <span class="toolbar__stat-label">wasted</span>
38
- <strong class="toolbar__stat-value">{{ tracker.totalUnnecessary() }}</strong>
39
- </span>
40
- </div>
41
-
42
- @if (expanded()) {
43
- <div
44
- class="toolbar__inspector"
45
- role="list"
46
- aria-label="Component render counts"
47
- >
48
- @for (comp of tracker.trackedComponents(); track comp.hostElement) {
49
- <div
50
- class="toolbar__component"
51
- role="listitem"
52
- [class.toolbar__component--warn]="comp.lastRenderKind === 'unnecessary'"
53
- [title]="comp.componentName + ' — ' + comp.totalRenders + ' checks, ' + comp.unnecessaryRenders + ' wasted'"
54
- >
55
- <span class="toolbar__component-name">{{ comp.componentName }}</span>
56
- <span class="toolbar__component-count">{{ comp.totalRenders }}</span>
57
- @if (comp.unnecessaryRenders > 0) {
58
- <span class="toolbar__component-wasted" aria-label="wasted renders">
59
- {{ comp.unnecessaryRenders }}W
60
- </span>
61
- }
62
- </div>
63
- }
64
- </div>
65
- }
66
- </div>
@@ -1,147 +0,0 @@
1
-
2
- :host {
3
- position: fixed;
4
- bottom: 0;
5
- right: 0;
6
- z-index: 2147483647;
7
- font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, monospace;
8
- font-size: 11px;
9
- line-height: 1.4;
10
- }
11
-
12
- .toolbar {
13
- background: #0f1117;
14
- color: #c9d1d9;
15
- border: 1px solid #30363d;
16
- border-bottom: none;
17
- border-right: none;
18
- border-radius: 6px 0 0 0;
19
- min-width: 200px;
20
- max-width: 300px;
21
- box-shadow: -2px -2px 12px rgba(0, 0, 0, 0.5);
22
- }
23
-
24
- .toolbar__header {
25
- display: flex;
26
- align-items: center;
27
- gap: 4px;
28
- padding: 5px 8px;
29
- border-bottom: 1px solid #21262d;
30
- }
31
-
32
- .toolbar__logo {
33
- color: #58a6ff;
34
- font-size: 12px;
35
- }
36
-
37
- .toolbar__title {
38
- font-weight: 600;
39
- color: #58a6ff;
40
- flex: 1;
41
- letter-spacing: 0.02em;
42
- }
43
-
44
- .toolbar__btn {
45
- background: transparent;
46
- border: 1px solid #30363d;
47
- color: #8b949e;
48
- border-radius: 4px;
49
- cursor: pointer;
50
- padding: 2px 6px;
51
- font-size: 10px;
52
- transition: color 0.15s, border-color 0.15s;
53
- }
54
-
55
- .toolbar__btn:hover {
56
- color: #c9d1d9;
57
- border-color: #58a6ff;
58
- }
59
-
60
- .toolbar__btn:focus-visible {
61
- outline: 2px solid #58a6ff;
62
- outline-offset: 2px;
63
- }
64
-
65
- .toolbar__stats {
66
- display: flex;
67
- gap: 12px;
68
- padding: 5px 8px;
69
- }
70
-
71
- .toolbar__stat {
72
- display: flex;
73
- flex-direction: column;
74
- gap: 1px;
75
- }
76
-
77
- .toolbar__stat-label {
78
- color: #8b949e;
79
- font-size: 9px;
80
- text-transform: uppercase;
81
- letter-spacing: 0.05em;
82
- }
83
-
84
- .toolbar__stat-value {
85
- color: #c9d1d9;
86
- }
87
-
88
- .toolbar__stat--warn .toolbar__stat-value {
89
- color: #f85149;
90
- }
91
-
92
- .toolbar__inspector {
93
- max-height: 220px;
94
- overflow-y: auto;
95
- border-top: 1px solid #21262d;
96
- }
97
-
98
- .toolbar__inspector::-webkit-scrollbar {
99
- width: 4px;
100
- }
101
-
102
- .toolbar__inspector::-webkit-scrollbar-track {
103
- background: transparent;
104
- }
105
-
106
- .toolbar__inspector::-webkit-scrollbar-thumb {
107
- background: #30363d;
108
- border-radius: 2px;
109
- }
110
-
111
- .toolbar__component {
112
- display: flex;
113
- align-items: center;
114
- gap: 6px;
115
- padding: 3px 8px;
116
- border-bottom: 1px solid #161b22;
117
- transition: background 0.1s;
118
- }
119
-
120
- .toolbar__component:hover {
121
- background: #161b22;
122
- }
123
-
124
- .toolbar__component--warn {
125
- background: rgba(248, 81, 73, 0.06);
126
- }
127
-
128
- .toolbar__component-name {
129
- flex: 1;
130
- overflow: hidden;
131
- text-overflow: ellipsis;
132
- white-space: nowrap;
133
- color: #8b949e;
134
- }
135
-
136
- .toolbar__component-count {
137
- font-weight: 600;
138
- color: #ffa657;
139
- min-width: 20px;
140
- text-align: right;
141
- }
142
-
143
- .toolbar__component-wasted {
144
- color: #f85149;
145
- font-size: 9px;
146
- font-weight: 600;
147
- }
@@ -1,29 +0,0 @@
1
- import {
2
- Component,
3
- ChangeDetectionStrategy,
4
- inject,
5
- signal,
6
- } from '@angular/core';
7
- import { ComponentTracker } from '../component-tracker';
8
-
9
- @Component({
10
- selector: 'angular-scan-toolbar',
11
- changeDetection: ChangeDetectionStrategy.OnPush,
12
- templateUrl: './toolbar.component.html',
13
- styleUrls: ['./toolbar.component.scss'],
14
- host: { '[style.display]': '"block"' },
15
- })
16
- export class ToolbarComponent {
17
- protected readonly tracker = inject(ComponentTracker);
18
-
19
- protected readonly expanded = signal(false);
20
- protected readonly enabled = signal(true);
21
-
22
- protected toggleExpanded(): void {
23
- this.expanded.update(v => !v);
24
- }
25
-
26
- protected toggleEnabled(): void {
27
- this.enabled.update(v => !v);
28
- }
29
- }
package/src/lib/types.ts DELETED
@@ -1,36 +0,0 @@
1
- /** Whether a render was meaningful (DOM changed) or wasted (DOM unchanged). */
2
- export type RenderKind = 'render' | 'unnecessary';
3
-
4
- /** Accumulated stats for a single component across its lifetime. */
5
- export interface ComponentStats {
6
- componentName: string;
7
- hostElement: Element;
8
- totalRenders: number;
9
- unnecessaryRenders: number;
10
- lastRenderKind: RenderKind;
11
- lastRenderTimestamp: number;
12
- }
13
-
14
- /** Options for configuring angular-scan. */
15
- export interface AngularScanOptions {
16
- /** Set to false to disable scanning entirely. Default: true. */
17
- enabled?: boolean;
18
- /** Duration of the canvas flash in milliseconds. Default: 500. */
19
- flashDurationMs?: number;
20
- /** Show render count badges on component host elements. Default: true. */
21
- showBadges?: boolean;
22
- /** Show the floating toolbar HUD. Default: true. */
23
- showToolbar?: boolean;
24
- }
25
-
26
- /** Shape of the window.ng Angular debug API (dev mode only). */
27
- export interface NgDebugApi {
28
- getComponent: (el: Element) => object | null;
29
- getHostElement: (component: object) => Element | null;
30
- getRootComponents: (el?: Element) => object[];
31
- getOwningComponent: (el: Element) => object | null;
32
- getDirectives: (el: Element) => object[];
33
- ɵsetProfiler: (
34
- callback: ((event: number, instance: object | null, context?: unknown) => void) | null
35
- ) => () => void;
36
- }
package/src/public-api.ts DELETED
@@ -1,7 +0,0 @@
1
- /*
2
- * Public API Surface of angular-scan
3
- */
4
-
5
- export { provideAngularScan } from './lib/provide-angular-scan';
6
- export { scan } from './lib/scan';
7
- export type { AngularScanOptions, ComponentStats, RenderKind } from './lib/types';
package/tsconfig.lib.json DELETED
@@ -1,13 +0,0 @@
1
- /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
2
- /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
3
- {
4
- "extends": "../../tsconfig.json",
5
- "compilerOptions": {
6
- "outDir": "../../out-tsc/lib",
7
- "declaration": true,
8
- "declarationMap": true,
9
- "types": []
10
- },
11
- "include": ["src/**/*.ts"],
12
- "exclude": ["**/*.spec.ts"]
13
- }
@@ -1,11 +0,0 @@
1
- /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
2
- /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
3
- {
4
- "extends": "./tsconfig.lib.json",
5
- "compilerOptions": {
6
- "declarationMap": false
7
- },
8
- "angularCompilerOptions": {
9
- "compilationMode": "partial"
10
- }
11
- }