@foxlight/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1484 @@
1
+ // src/registry.ts
2
+ var ComponentRegistry = class {
3
+ components = /* @__PURE__ */ new Map();
4
+ imports = [];
5
+ bundleInfo = /* @__PURE__ */ new Map();
6
+ health = /* @__PURE__ */ new Map();
7
+ // -----------------------------------------------------------
8
+ // Component CRUD
9
+ // -----------------------------------------------------------
10
+ /** Register a discovered component. */
11
+ addComponent(component) {
12
+ this.components.set(component.id, component);
13
+ }
14
+ /** Register multiple components at once. */
15
+ addComponents(components) {
16
+ for (const c of components) {
17
+ this.addComponent(c);
18
+ }
19
+ }
20
+ /** Get a component by ID. */
21
+ getComponent(id) {
22
+ return this.components.get(id);
23
+ }
24
+ /** Get all registered components. */
25
+ getAllComponents() {
26
+ return Array.from(this.components.values());
27
+ }
28
+ /** Check if a component exists in the registry. */
29
+ hasComponent(id) {
30
+ return this.components.has(id);
31
+ }
32
+ /** Remove a component from the registry. */
33
+ removeComponent(id) {
34
+ return this.components.delete(id);
35
+ }
36
+ /** Total number of registered components. */
37
+ get size() {
38
+ return this.components.size;
39
+ }
40
+ // -----------------------------------------------------------
41
+ // Import graph
42
+ // -----------------------------------------------------------
43
+ /** Add an import edge to the graph. */
44
+ addImport(edge) {
45
+ this.imports.push(edge);
46
+ }
47
+ /** Add multiple import edges. */
48
+ addImports(edges) {
49
+ this.imports.push(...edges);
50
+ }
51
+ /** Get all imports originating from a file. */
52
+ getImportsFrom(filePath) {
53
+ return this.imports.filter((e) => e.source === filePath);
54
+ }
55
+ /** Get all imports targeting a file or package. */
56
+ getImportsTo(target) {
57
+ return this.imports.filter((e) => e.target === target);
58
+ }
59
+ /** Get the full import graph. */
60
+ getAllImports() {
61
+ return [...this.imports];
62
+ }
63
+ // -----------------------------------------------------------
64
+ // Bundle info
65
+ // -----------------------------------------------------------
66
+ /** Set bundle size info for a component. */
67
+ setBundleInfo(info) {
68
+ this.bundleInfo.set(info.componentId, info);
69
+ }
70
+ /** Get bundle info for a component. */
71
+ getBundleInfo(id) {
72
+ return this.bundleInfo.get(id);
73
+ }
74
+ /** Get all bundle info entries. */
75
+ getAllBundleInfo() {
76
+ return Array.from(this.bundleInfo.values());
77
+ }
78
+ // -----------------------------------------------------------
79
+ // Health scores
80
+ // -----------------------------------------------------------
81
+ /** Set health score for a component. */
82
+ setHealth(h) {
83
+ this.health.set(h.componentId, h);
84
+ }
85
+ /** Get health score for a component. */
86
+ getHealth(id) {
87
+ return this.health.get(id);
88
+ }
89
+ /** Get all health scores. */
90
+ getAllHealth() {
91
+ return Array.from(this.health.values());
92
+ }
93
+ // -----------------------------------------------------------
94
+ // Relationship queries
95
+ // -----------------------------------------------------------
96
+ /** Find components that use the given component (parents). */
97
+ getConsumers(id) {
98
+ const component = this.components.get(id);
99
+ if (!component) return [];
100
+ return component.usedBy.map((parentId) => this.components.get(parentId)).filter((c) => c !== void 0);
101
+ }
102
+ /** Find components that the given component renders (children). */
103
+ getDependents(id) {
104
+ const component = this.components.get(id);
105
+ if (!component) return [];
106
+ return component.children.map((childId) => this.components.get(childId)).filter((c) => c !== void 0);
107
+ }
108
+ /** Get components that have no parents (top-level / page components). */
109
+ getRootComponents() {
110
+ return this.getAllComponents().filter((c) => c.usedBy.length === 0);
111
+ }
112
+ /** Get components that have no children (leaf / primitive components). */
113
+ getLeafComponents() {
114
+ return this.getAllComponents().filter((c) => c.children.length === 0);
115
+ }
116
+ /**
117
+ * Find all components reachable from the given component (full subtree).
118
+ * Uses BFS to avoid stack overflow on deep trees.
119
+ */
120
+ getSubtree(id) {
121
+ const visited = /* @__PURE__ */ new Set();
122
+ const queue = [id];
123
+ const result = [];
124
+ while (queue.length > 0) {
125
+ const currentId = queue.shift();
126
+ if (visited.has(currentId)) continue;
127
+ visited.add(currentId);
128
+ const component = this.components.get(currentId);
129
+ if (!component) continue;
130
+ result.push(component);
131
+ for (const childId of component.children) {
132
+ if (!visited.has(childId)) {
133
+ queue.push(childId);
134
+ }
135
+ }
136
+ }
137
+ return result;
138
+ }
139
+ // -----------------------------------------------------------
140
+ // Snapshot & diff
141
+ // -----------------------------------------------------------
142
+ /** Create a point-in-time snapshot of the registry. */
143
+ createSnapshot(commitSha, branch) {
144
+ return {
145
+ id: `snap_${Date.now()}_${commitSha.slice(0, 8)}`,
146
+ commitSha,
147
+ branch,
148
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
149
+ components: this.getAllComponents(),
150
+ imports: this.getAllImports(),
151
+ bundleInfo: this.getAllBundleInfo(),
152
+ health: this.getAllHealth()
153
+ };
154
+ }
155
+ /** Load a snapshot into the registry, replacing current state. */
156
+ loadSnapshot(snapshot) {
157
+ this.clear();
158
+ this.addComponents(snapshot.components);
159
+ this.addImports(snapshot.imports);
160
+ for (const bi of snapshot.bundleInfo) {
161
+ this.setBundleInfo(bi);
162
+ }
163
+ for (const h of snapshot.health) {
164
+ this.setHealth(h);
165
+ }
166
+ }
167
+ /** Compute the diff between two snapshots. */
168
+ static diff(base, head) {
169
+ const baseIds = new Set(base.components.map((c) => c.id));
170
+ const headIds = new Set(head.components.map((c) => c.id));
171
+ const added = head.components.filter((c) => !baseIds.has(c.id));
172
+ const removed = base.components.filter((c) => !headIds.has(c.id));
173
+ const modified = [];
174
+ for (const headComp of head.components) {
175
+ if (!baseIds.has(headComp.id)) continue;
176
+ const baseComp = base.components.find((c) => c.id === headComp.id);
177
+ if (!baseComp) continue;
178
+ const mod = diffComponent(baseComp, headComp);
179
+ if (mod) {
180
+ modified.push(mod);
181
+ }
182
+ }
183
+ const bundleDiff = [];
184
+ const headBundleMap = new Map(head.bundleInfo.map((b) => [b.componentId, b]));
185
+ for (const baseBi of base.bundleInfo) {
186
+ const headBi = headBundleMap.get(baseBi.componentId);
187
+ if (!headBi) continue;
188
+ bundleDiff.push({
189
+ componentId: baseBi.componentId,
190
+ before: baseBi.selfSize,
191
+ after: headBi.selfSize,
192
+ delta: {
193
+ raw: headBi.selfSize.raw - baseBi.selfSize.raw,
194
+ gzip: headBi.selfSize.gzip - baseBi.selfSize.gzip
195
+ }
196
+ });
197
+ }
198
+ const healthDiff = [];
199
+ const headHealthMap = new Map(head.health.map((h) => [h.componentId, h]));
200
+ for (const baseH of base.health) {
201
+ const headH = headHealthMap.get(baseH.componentId);
202
+ if (!headH) continue;
203
+ healthDiff.push({
204
+ componentId: baseH.componentId,
205
+ beforeScore: baseH.score,
206
+ afterScore: headH.score,
207
+ delta: headH.score - baseH.score
208
+ });
209
+ }
210
+ return {
211
+ base: { id: base.id, commitSha: base.commitSha },
212
+ head: { id: head.id, commitSha: head.commitSha },
213
+ components: { added, removed, modified },
214
+ bundleDiff,
215
+ healthDiff
216
+ };
217
+ }
218
+ // -----------------------------------------------------------
219
+ // Utility
220
+ // -----------------------------------------------------------
221
+ /** Clear all data from the registry. */
222
+ clear() {
223
+ this.components.clear();
224
+ this.imports = [];
225
+ this.bundleInfo.clear();
226
+ this.health.clear();
227
+ }
228
+ };
229
+ function diffComponent(base, head) {
230
+ const changes = [];
231
+ const baseProps = new Set(base.props.map((p) => p.name));
232
+ const headProps = new Set(head.props.map((p) => p.name));
233
+ const propsAdded = head.props.filter((p) => !baseProps.has(p.name)).map((p) => p.name);
234
+ const propsRemoved = base.props.filter((p) => !headProps.has(p.name)).map((p) => p.name);
235
+ const propsModified = [];
236
+ for (const headProp of head.props) {
237
+ if (!baseProps.has(headProp.name)) continue;
238
+ const baseProp = base.props.find((p) => p.name === headProp.name);
239
+ if (!baseProp) continue;
240
+ if (baseProp.type !== headProp.type || baseProp.required !== headProp.required) {
241
+ propsModified.push(headProp.name);
242
+ }
243
+ }
244
+ if (base.children.length !== head.children.length) {
245
+ changes.push("children changed");
246
+ }
247
+ if (base.dependencies.length !== head.dependencies.length) {
248
+ changes.push("dependencies changed");
249
+ }
250
+ if (base.framework !== head.framework) {
251
+ changes.push(`framework changed: ${base.framework} \u2192 ${head.framework}`);
252
+ }
253
+ const hasChanges = propsAdded.length > 0 || propsRemoved.length > 0 || propsModified.length > 0 || changes.length > 0;
254
+ if (!hasChanges) return null;
255
+ return {
256
+ componentId: head.id,
257
+ changes,
258
+ propsAdded,
259
+ propsRemoved,
260
+ propsModified
261
+ };
262
+ }
263
+
264
+ // src/dependency-graph.ts
265
+ var DependencyGraph = class _DependencyGraph {
266
+ nodes = /* @__PURE__ */ new Map();
267
+ /** Build the graph from a list of import edges. */
268
+ static fromImports(edges) {
269
+ const graph = new _DependencyGraph();
270
+ for (const edge of edges) {
271
+ graph.addEdge(edge.source, edge.target);
272
+ }
273
+ return graph;
274
+ }
275
+ /** Add a directed edge from source to target. */
276
+ addEdge(source, target) {
277
+ this.ensureNode(source);
278
+ this.ensureNode(target);
279
+ this.nodes.get(source).outgoing.add(target);
280
+ this.nodes.get(target).incoming.add(source);
281
+ }
282
+ /** Get all direct dependencies of a module. */
283
+ getDependencies(id) {
284
+ const node = this.nodes.get(id);
285
+ return node ? Array.from(node.outgoing) : [];
286
+ }
287
+ /** Get all modules that directly depend on a module. */
288
+ getDependents(id) {
289
+ const node = this.nodes.get(id);
290
+ return node ? Array.from(node.incoming) : [];
291
+ }
292
+ /**
293
+ * Get all modules transitively affected if the given module changes.
294
+ * Walks the "incoming" edges (dependents) recursively.
295
+ */
296
+ getImpactedModules(id) {
297
+ const visited = /* @__PURE__ */ new Set();
298
+ const queue = [id];
299
+ while (queue.length > 0) {
300
+ const current = queue.shift();
301
+ if (visited.has(current)) continue;
302
+ visited.add(current);
303
+ const node = this.nodes.get(current);
304
+ if (!node) continue;
305
+ for (const dep of node.incoming) {
306
+ if (!visited.has(dep)) {
307
+ queue.push(dep);
308
+ }
309
+ }
310
+ }
311
+ visited.delete(id);
312
+ return Array.from(visited);
313
+ }
314
+ /**
315
+ * Get all transitive dependencies of a module.
316
+ * Walks "outgoing" edges recursively.
317
+ */
318
+ getTransitiveDependencies(id) {
319
+ const visited = /* @__PURE__ */ new Set();
320
+ const queue = [id];
321
+ while (queue.length > 0) {
322
+ const current = queue.shift();
323
+ if (visited.has(current)) continue;
324
+ visited.add(current);
325
+ const node = this.nodes.get(current);
326
+ if (!node) continue;
327
+ for (const dep of node.outgoing) {
328
+ if (!visited.has(dep)) {
329
+ queue.push(dep);
330
+ }
331
+ }
332
+ }
333
+ visited.delete(id);
334
+ return Array.from(visited);
335
+ }
336
+ /**
337
+ * Detect cycles in the graph.
338
+ * Returns an array of cycles, where each cycle is an array of module IDs.
339
+ */
340
+ detectCycles() {
341
+ const cycles = [];
342
+ const visited = /* @__PURE__ */ new Set();
343
+ const inStack = /* @__PURE__ */ new Set();
344
+ const stack = [];
345
+ const dfs = (nodeId) => {
346
+ if (inStack.has(nodeId)) {
347
+ const cycleStart = stack.indexOf(nodeId);
348
+ if (cycleStart !== -1) {
349
+ cycles.push([...stack.slice(cycleStart), nodeId]);
350
+ }
351
+ return;
352
+ }
353
+ if (visited.has(nodeId)) return;
354
+ visited.add(nodeId);
355
+ inStack.add(nodeId);
356
+ stack.push(nodeId);
357
+ const node = this.nodes.get(nodeId);
358
+ if (node) {
359
+ for (const dep of node.outgoing) {
360
+ dfs(dep);
361
+ }
362
+ }
363
+ stack.pop();
364
+ inStack.delete(nodeId);
365
+ };
366
+ for (const nodeId of this.nodes.keys()) {
367
+ if (!visited.has(nodeId)) {
368
+ dfs(nodeId);
369
+ }
370
+ }
371
+ return cycles;
372
+ }
373
+ /**
374
+ * Topological sort of the graph.
375
+ * Returns null if the graph has cycles.
376
+ */
377
+ topologicalSort() {
378
+ const inDegree = /* @__PURE__ */ new Map();
379
+ for (const [id, node] of this.nodes) {
380
+ inDegree.set(id, node.incoming.size);
381
+ }
382
+ const queue = [];
383
+ for (const [id, degree] of inDegree) {
384
+ if (degree === 0) queue.push(id);
385
+ }
386
+ const result = [];
387
+ while (queue.length > 0) {
388
+ const current = queue.shift();
389
+ result.push(current);
390
+ const node = this.nodes.get(current);
391
+ if (!node) continue;
392
+ for (const dep of node.outgoing) {
393
+ const newDegree = (inDegree.get(dep) ?? 1) - 1;
394
+ inDegree.set(dep, newDegree);
395
+ if (newDegree === 0) {
396
+ queue.push(dep);
397
+ }
398
+ }
399
+ }
400
+ return result.length === this.nodes.size ? result : null;
401
+ }
402
+ /**
403
+ * Find shared dependencies between two modules.
404
+ * Useful for understanding which code is shared vs. unique.
405
+ */
406
+ getSharedDependencies(idA, idB) {
407
+ const depsA = new Set(this.getTransitiveDependencies(idA));
408
+ const depsB = new Set(this.getTransitiveDependencies(idB));
409
+ return Array.from(depsA).filter((d) => depsB.has(d));
410
+ }
411
+ /**
412
+ * Find dependencies unique to a module (not shared with any other top-level module).
413
+ * Used for calculating "exclusive" bundle size.
414
+ */
415
+ getExclusiveDependencies(id, allTopLevel) {
416
+ const myDeps = new Set(this.getTransitiveDependencies(id));
417
+ const otherDeps = /* @__PURE__ */ new Set();
418
+ for (const otherId of allTopLevel) {
419
+ if (otherId === id) continue;
420
+ for (const dep of this.getTransitiveDependencies(otherId)) {
421
+ otherDeps.add(dep);
422
+ }
423
+ }
424
+ return Array.from(myDeps).filter((d) => !otherDeps.has(d));
425
+ }
426
+ /** Get all node IDs in the graph. */
427
+ getAllNodes() {
428
+ return Array.from(this.nodes.keys());
429
+ }
430
+ /** Get the total number of nodes. */
431
+ get nodeCount() {
432
+ return this.nodes.size;
433
+ }
434
+ /** Get the total number of edges. */
435
+ get edgeCount() {
436
+ let count = 0;
437
+ for (const node of this.nodes.values()) {
438
+ count += node.outgoing.size;
439
+ }
440
+ return count;
441
+ }
442
+ ensureNode(id) {
443
+ if (!this.nodes.has(id)) {
444
+ this.nodes.set(id, { id, outgoing: /* @__PURE__ */ new Set(), incoming: /* @__PURE__ */ new Set() });
445
+ }
446
+ }
447
+ };
448
+
449
+ // src/config.ts
450
+ import { existsSync } from "fs";
451
+ import { readFile } from "fs/promises";
452
+ import { resolve, join } from "path";
453
+ var CONFIG_FILENAMES = [
454
+ "foxlight.config.ts",
455
+ "foxlight.config.js",
456
+ "foxlight.config.mjs",
457
+ "foxlight.config.json"
458
+ ];
459
+ var DEFAULT_INCLUDE = [
460
+ "src/**/*.{tsx,jsx,vue,svelte}",
461
+ "components/**/*.{tsx,jsx,vue,svelte}",
462
+ "app/**/*.{tsx,jsx,vue,svelte}",
463
+ "pages/**/*.{tsx,jsx,vue,svelte}"
464
+ ];
465
+ var DEFAULT_EXCLUDE = [
466
+ "**/node_modules/**",
467
+ "**/dist/**",
468
+ "**/build/**",
469
+ "**/.next/**",
470
+ "**/.nuxt/**",
471
+ "**/*.test.*",
472
+ "**/*.spec.*",
473
+ "**/*.stories.*"
474
+ ];
475
+ async function loadConfig(rootDir) {
476
+ const resolvedRoot = resolve(rootDir);
477
+ for (const filename of CONFIG_FILENAMES) {
478
+ const configPath = join(resolvedRoot, filename);
479
+ if (existsSync(configPath)) {
480
+ const config = await loadConfigFile(configPath);
481
+ return mergeWithDefaults(resolvedRoot, config);
482
+ }
483
+ }
484
+ const framework = await detectFramework(resolvedRoot);
485
+ return mergeWithDefaults(resolvedRoot, { framework });
486
+ }
487
+ function createDefaultConfig(rootDir) {
488
+ return {
489
+ rootDir: resolve(rootDir),
490
+ include: DEFAULT_INCLUDE,
491
+ exclude: DEFAULT_EXCLUDE
492
+ };
493
+ }
494
+ async function detectFramework(rootDir) {
495
+ const pkgPath = join(rootDir, "package.json");
496
+ if (!existsSync(pkgPath)) return "unknown";
497
+ try {
498
+ const raw = await readFile(pkgPath, "utf-8");
499
+ const pkg = JSON.parse(raw);
500
+ const allDeps = {
501
+ ...pkg["dependencies"],
502
+ ...pkg["devDependencies"]
503
+ };
504
+ if ("react" in allDeps) return "react";
505
+ if ("vue" in allDeps) return "vue";
506
+ if ("svelte" in allDeps) return "svelte";
507
+ if ("@angular/core" in allDeps) return "angular";
508
+ if ("lit" in allDeps || "lit-element" in allDeps) return "web-component";
509
+ } catch {
510
+ }
511
+ return "unknown";
512
+ }
513
+ async function loadConfigFile(configPath) {
514
+ if (configPath.endsWith(".json")) {
515
+ const raw = await readFile(configPath, "utf-8");
516
+ return JSON.parse(raw);
517
+ }
518
+ try {
519
+ const mod = await import(configPath);
520
+ return mod.default ?? {};
521
+ } catch {
522
+ return {};
523
+ }
524
+ }
525
+ function mergeWithDefaults(rootDir, partial) {
526
+ return {
527
+ rootDir,
528
+ include: partial.include ?? DEFAULT_INCLUDE,
529
+ exclude: partial.exclude ?? DEFAULT_EXCLUDE,
530
+ framework: partial.framework,
531
+ storybook: partial.storybook,
532
+ costModel: partial.costModel,
533
+ baselines: partial.baselines,
534
+ plugins: partial.plugins
535
+ };
536
+ }
537
+
538
+ // src/health-scorer.ts
539
+ var DEFAULT_WEIGHTS = {
540
+ bundleSize: 0.25,
541
+ testCoverage: 0.2,
542
+ accessibility: 0.15,
543
+ freshness: 0.15,
544
+ performance: 0.15,
545
+ reliability: 0.1
546
+ };
547
+ var BUNDLE_THRESHOLDS = {
548
+ good: 10240,
549
+ // ≤10 KB
550
+ warning: 50240
551
+ // ≤50 KB
552
+ };
553
+ var COVERAGE_THRESHOLDS = {
554
+ good: 80,
555
+ warning: 50
556
+ };
557
+ var FRESHNESS_THRESHOLDS = {
558
+ good: 90,
559
+ // Modified within 90 days
560
+ warning: 365
561
+ // Modified within a year
562
+ };
563
+ function computeComponentHealth(input, weights = DEFAULT_WEIGHTS) {
564
+ const metrics = computeMetrics(input);
565
+ const score = computeOverallScore(metrics, weights);
566
+ return {
567
+ componentId: input.component.id,
568
+ score,
569
+ metrics,
570
+ computedAt: (/* @__PURE__ */ new Date()).toISOString()
571
+ };
572
+ }
573
+ function computeAllHealth(inputs, weights = DEFAULT_WEIGHTS) {
574
+ return inputs.map((input) => computeComponentHealth(input, weights));
575
+ }
576
+ function computeMetrics(input) {
577
+ return {
578
+ bundleSize: scoreBundleSize(input.bundleInfo),
579
+ testCoverage: scoreTestCoverage(input.testCoverage),
580
+ accessibility: scoreAccessibility(input.accessibilityScore),
581
+ freshness: scoreFreshness(input.daysSinceModified),
582
+ performance: scorePerformance(input.renderTimeMs),
583
+ reliability: scoreReliability(input.errorRate)
584
+ };
585
+ }
586
+ function computeOverallScore(metrics, weights) {
587
+ const weighted = metrics.bundleSize.score * weights.bundleSize + metrics.testCoverage.score * weights.testCoverage + metrics.accessibility.score * weights.accessibility + metrics.freshness.score * weights.freshness + metrics.performance.score * weights.performance + metrics.reliability.score * weights.reliability;
588
+ return Math.round(weighted);
589
+ }
590
+ function scoreBundleSize(bundleInfo) {
591
+ if (!bundleInfo) {
592
+ return {
593
+ score: 50,
594
+ value: "unknown",
595
+ label: "Bundle size not measured",
596
+ level: "warning"
597
+ };
598
+ }
599
+ const gzipBytes = bundleInfo.selfSize.gzip;
600
+ const score = gzipBytes <= BUNDLE_THRESHOLDS.good ? 100 : gzipBytes <= BUNDLE_THRESHOLDS.warning ? Math.round(
601
+ 100 - (gzipBytes - BUNDLE_THRESHOLDS.good) / (BUNDLE_THRESHOLDS.warning - BUNDLE_THRESHOLDS.good) * 50
602
+ ) : Math.max(
603
+ 0,
604
+ Math.round(
605
+ 50 - (gzipBytes - BUNDLE_THRESHOLDS.warning) / BUNDLE_THRESHOLDS.warning * 50
606
+ )
607
+ );
608
+ return {
609
+ score,
610
+ value: formatBytesCompact(gzipBytes),
611
+ label: "Gzip size",
612
+ level: levelFromScore(score)
613
+ };
614
+ }
615
+ function scoreTestCoverage(coverage) {
616
+ if (coverage === void 0) {
617
+ return {
618
+ score: 0,
619
+ value: "no data",
620
+ label: "Test coverage not available",
621
+ level: "critical"
622
+ };
623
+ }
624
+ const clamped = Math.max(0, Math.min(100, coverage));
625
+ return {
626
+ score: Math.round(clamped),
627
+ value: `${clamped.toFixed(0)}%`,
628
+ label: "Test coverage",
629
+ level: clamped >= COVERAGE_THRESHOLDS.good ? "good" : clamped >= COVERAGE_THRESHOLDS.warning ? "warning" : "critical"
630
+ };
631
+ }
632
+ function scoreAccessibility(a11yScore) {
633
+ if (a11yScore === void 0) {
634
+ return {
635
+ score: 50,
636
+ value: "not scanned",
637
+ label: "Accessibility not measured",
638
+ level: "warning"
639
+ };
640
+ }
641
+ const clamped = Math.max(0, Math.min(100, a11yScore));
642
+ return {
643
+ score: Math.round(clamped),
644
+ value: `${clamped.toFixed(0)}/100`,
645
+ label: "Accessibility score",
646
+ level: levelFromScore(Math.round(clamped))
647
+ };
648
+ }
649
+ function scoreFreshness(daysSinceModified) {
650
+ if (daysSinceModified === void 0) {
651
+ return {
652
+ score: 50,
653
+ value: "unknown",
654
+ label: "Last modification date unknown",
655
+ level: "warning"
656
+ };
657
+ }
658
+ const score = daysSinceModified <= FRESHNESS_THRESHOLDS.good ? 100 : daysSinceModified <= FRESHNESS_THRESHOLDS.warning ? Math.round(
659
+ 100 - (daysSinceModified - FRESHNESS_THRESHOLDS.good) / (FRESHNESS_THRESHOLDS.warning - FRESHNESS_THRESHOLDS.good) * 50
660
+ ) : Math.max(
661
+ 0,
662
+ Math.round(
663
+ 50 - (daysSinceModified - FRESHNESS_THRESHOLDS.warning) / FRESHNESS_THRESHOLDS.warning * 50
664
+ )
665
+ );
666
+ return {
667
+ score,
668
+ value: daysSinceModified <= 1 ? "today" : `${daysSinceModified}d ago`,
669
+ label: "Last modified",
670
+ level: levelFromScore(score)
671
+ };
672
+ }
673
+ function scorePerformance(renderTimeMs) {
674
+ if (renderTimeMs === void 0) {
675
+ return {
676
+ score: 50,
677
+ value: "not profiled",
678
+ label: "Render performance not measured",
679
+ level: "warning"
680
+ };
681
+ }
682
+ const score = renderTimeMs <= 16 ? 100 : renderTimeMs <= 50 ? Math.round(100 - (renderTimeMs - 16) / 34 * 20) : renderTimeMs <= 200 ? Math.round(80 - (renderTimeMs - 50) / 150 * 50) : Math.max(0, Math.round(30 - (renderTimeMs - 200) / 500 * 30));
683
+ return {
684
+ score,
685
+ value: `${renderTimeMs.toFixed(0)}ms`,
686
+ label: "Render time",
687
+ level: levelFromScore(score)
688
+ };
689
+ }
690
+ function scoreReliability(errorRate) {
691
+ if (errorRate === void 0) {
692
+ return {
693
+ score: 50,
694
+ value: "no data",
695
+ label: "Error rate not tracked",
696
+ level: "warning"
697
+ };
698
+ }
699
+ const pct = errorRate * 100;
700
+ const score = pct <= 0 ? 100 : pct <= 1 ? Math.round(100 - pct * 20) : pct <= 5 ? Math.round(80 - (pct - 1) / 4 * 50) : Math.max(0, Math.round(30 - (pct - 5) / 10 * 30));
701
+ return {
702
+ score,
703
+ value: `${pct.toFixed(2)}%`,
704
+ label: "Error rate",
705
+ level: levelFromScore(score)
706
+ };
707
+ }
708
+ function levelFromScore(score) {
709
+ if (score >= 80) return "good";
710
+ if (score >= 50) return "warning";
711
+ return "critical";
712
+ }
713
+ function formatBytesCompact(bytes) {
714
+ if (bytes === 0) return "0 B";
715
+ const units = ["B", "KB", "MB", "GB"];
716
+ const i = Math.floor(Math.log(Math.abs(bytes)) / Math.log(1024));
717
+ const value = bytes / Math.pow(1024, i);
718
+ return `${value.toFixed(value >= 100 ? 0 : value >= 10 ? 1 : 2)} ${units[i]}`;
719
+ }
720
+
721
+ // src/cost-estimator.ts
722
+ var COST_MODELS = {
723
+ vercel: {
724
+ provider: "vercel",
725
+ invocationCostPer1M: 0.6,
726
+ bandwidthCostPerGB: 0.15,
727
+ storageCostPerGB: 0.023,
728
+ edgeCostPer1M: 2,
729
+ baseCost: 0
730
+ },
731
+ netlify: {
732
+ provider: "netlify",
733
+ invocationCostPer1M: 2,
734
+ bandwidthCostPerGB: 0.2,
735
+ storageCostPerGB: 0.025,
736
+ baseCost: 0
737
+ },
738
+ aws: {
739
+ provider: "aws",
740
+ invocationCostPer1M: 0.2,
741
+ bandwidthCostPerGB: 0.09,
742
+ storageCostPerGB: 0.023,
743
+ edgeCostPer1M: 0.6,
744
+ baseCost: 0
745
+ },
746
+ cloudflare: {
747
+ provider: "cloudflare",
748
+ invocationCostPer1M: 0.5,
749
+ bandwidthCostPerGB: 0,
750
+ // Free egress
751
+ storageCostPerGB: 0.015,
752
+ edgeCostPer1M: 0.5,
753
+ baseCost: 5
754
+ }
755
+ };
756
+ var DEFAULT_TRAFFIC = {
757
+ monthlyPageViews: 1e5,
758
+ invocationsPerPageView: 1.5,
759
+ edgeRatio: 0.3
760
+ };
761
+ function estimateCostImpact(currentBundleInfo, updatedBundleInfo, costModel, traffic = DEFAULT_TRAFFIC) {
762
+ const currentTotalSize = aggregateTotalSize(currentBundleInfo);
763
+ const updatedTotalSize = aggregateTotalSize(updatedBundleInfo);
764
+ const currentCosts = computeMonthlyCosts(currentTotalSize, costModel, traffic);
765
+ const updatedCosts = computeMonthlyCosts(updatedTotalSize, costModel, traffic);
766
+ const breakdown = [
767
+ {
768
+ category: "bandwidth",
769
+ description: `Bandwidth cost for serving ${formatGB(updatedTotalSize.gzip * traffic.monthlyPageViews)} per month`,
770
+ currentCost: currentCosts.bandwidth,
771
+ projectedCost: updatedCosts.bandwidth,
772
+ delta: updatedCosts.bandwidth - currentCosts.bandwidth
773
+ },
774
+ {
775
+ category: "invocations",
776
+ description: `${formatNumber(traffic.monthlyPageViews * traffic.invocationsPerPageView)} monthly function invocations`,
777
+ currentCost: currentCosts.invocations,
778
+ projectedCost: updatedCosts.invocations,
779
+ delta: updatedCosts.invocations - currentCosts.invocations
780
+ },
781
+ {
782
+ category: "storage",
783
+ description: "CDN/hosting asset storage",
784
+ currentCost: currentCosts.storage,
785
+ projectedCost: updatedCosts.storage,
786
+ delta: updatedCosts.storage - currentCosts.storage
787
+ },
788
+ {
789
+ category: "edge",
790
+ description: `Edge function invocations (${(traffic.edgeRatio * 100).toFixed(0)}% of traffic)`,
791
+ currentCost: currentCosts.edge,
792
+ projectedCost: updatedCosts.edge,
793
+ delta: updatedCosts.edge - currentCosts.edge
794
+ },
795
+ {
796
+ category: "base",
797
+ description: "Base platform cost",
798
+ currentCost: costModel.baseCost,
799
+ projectedCost: costModel.baseCost,
800
+ delta: 0
801
+ }
802
+ ];
803
+ const currentMonthlyCost = Object.values(currentCosts).reduce((a, b) => a + b, 0) + costModel.baseCost;
804
+ const projectedMonthlyCost = Object.values(updatedCosts).reduce((a, b) => a + b, 0) + costModel.baseCost;
805
+ return {
806
+ monthlyDelta: projectedMonthlyCost - currentMonthlyCost,
807
+ breakdown,
808
+ currentMonthlyCost,
809
+ projectedMonthlyCost
810
+ };
811
+ }
812
+ function estimateMonthlyCost(bundleInfo, costModel, traffic = DEFAULT_TRAFFIC) {
813
+ const totalSize = aggregateTotalSize(bundleInfo);
814
+ const costs = computeMonthlyCosts(totalSize, costModel, traffic);
815
+ return Object.values(costs).reduce((a, b) => a + b, 0) + costModel.baseCost;
816
+ }
817
+ function computeMonthlyCosts(totalSize, model, traffic) {
818
+ const monthlyBandwidthGB = totalSize.gzip * traffic.monthlyPageViews / (1024 * 1024 * 1024);
819
+ const bandwidth = monthlyBandwidthGB * model.bandwidthCostPerGB;
820
+ const totalInvocations = traffic.monthlyPageViews * traffic.invocationsPerPageView;
821
+ const invocations = totalInvocations / 1e6 * model.invocationCostPer1M;
822
+ const storageGB = totalSize.raw / (1024 * 1024 * 1024);
823
+ const storage = storageGB * model.storageCostPerGB;
824
+ const edgeInvocations = totalInvocations * traffic.edgeRatio;
825
+ const edge = model.edgeCostPer1M ? edgeInvocations / 1e6 * model.edgeCostPer1M : 0;
826
+ return { bandwidth, invocations, storage, edge };
827
+ }
828
+ function aggregateTotalSize(bundleInfo) {
829
+ let raw = 0;
830
+ let gzip = 0;
831
+ for (const info of bundleInfo) {
832
+ raw += info.selfSize.raw;
833
+ gzip += info.selfSize.gzip;
834
+ }
835
+ return { raw, gzip };
836
+ }
837
+ function formatGB(bytes) {
838
+ const gb = bytes / (1024 * 1024 * 1024);
839
+ if (gb >= 1) return `${gb.toFixed(2)} GB`;
840
+ const mb = bytes / (1024 * 1024);
841
+ if (mb >= 1) return `${mb.toFixed(1)} MB`;
842
+ const kb = bytes / 1024;
843
+ return `${kb.toFixed(0)} KB`;
844
+ }
845
+ function formatNumber(n) {
846
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
847
+ if (n >= 1e3) return `${(n / 1e3).toFixed(0)}K`;
848
+ return String(Math.round(n));
849
+ }
850
+
851
+ // src/upgrade-analyzer.ts
852
+ import { execFile } from "child_process";
853
+ import { promisify } from "util";
854
+ import { readFile as readFile2 } from "fs/promises";
855
+ import { join as join2 } from "path";
856
+ var execFileAsync = promisify(execFile);
857
+ async function analyzeUpgrade(options) {
858
+ const { rootDir, packageName, affectedComponents = [] } = options;
859
+ const currentVersion = await getCurrentVersion(rootDir, packageName);
860
+ const targetVersion = options.targetVersion ?? await getLatestVersion(packageName);
861
+ const checks = [];
862
+ checks.push(checkSemverRisk(currentVersion, targetVersion));
863
+ checks.push(checkComponentImpact(packageName, affectedComponents));
864
+ checks.push(await checkPeerDependencies(rootDir, packageName, targetVersion));
865
+ checks.push(await checkDeprecation(packageName, targetVersion));
866
+ const risk = computeOverallRisk(checks);
867
+ return {
868
+ packageName,
869
+ fromVersion: currentVersion,
870
+ toVersion: targetVersion,
871
+ risk,
872
+ checks
873
+ };
874
+ }
875
+ async function analyzeUpgrades(rootDir, packages, allComponents) {
876
+ const results = [];
877
+ for (const pkg of packages) {
878
+ const affectedComponents = allComponents?.filter((c) => c.dependencies.includes(pkg.name));
879
+ const preview = await analyzeUpgrade({
880
+ rootDir,
881
+ packageName: pkg.name,
882
+ targetVersion: pkg.targetVersion,
883
+ affectedComponents
884
+ });
885
+ results.push(preview);
886
+ }
887
+ return results;
888
+ }
889
+ function checkSemverRisk(currentVersion, targetVersion) {
890
+ const current = parseSemver(currentVersion);
891
+ const target = parseSemver(targetVersion);
892
+ if (!current || !target) {
893
+ return {
894
+ name: "Semver Analysis",
895
+ status: "warn",
896
+ summary: `Could not parse versions: ${currentVersion} \u2192 ${targetVersion}`
897
+ };
898
+ }
899
+ if (target.major > current.major) {
900
+ return {
901
+ name: "Semver Analysis",
902
+ status: "fail",
903
+ summary: `Major version bump (${currentVersion} \u2192 ${targetVersion}). Likely contains breaking changes.`,
904
+ details: "Major version bumps often require code changes. Review the changelog carefully."
905
+ };
906
+ }
907
+ if (target.minor > current.minor) {
908
+ return {
909
+ name: "Semver Analysis",
910
+ status: "warn",
911
+ summary: `Minor version bump (${currentVersion} \u2192 ${targetVersion}). May contain new features and deprecations.`
912
+ };
913
+ }
914
+ return {
915
+ name: "Semver Analysis",
916
+ status: "pass",
917
+ summary: `Patch version bump (${currentVersion} \u2192 ${targetVersion}). Bug fixes only.`
918
+ };
919
+ }
920
+ function checkComponentImpact(packageName, affectedComponents) {
921
+ if (affectedComponents.length === 0) {
922
+ return {
923
+ name: "Component Impact",
924
+ status: "pass",
925
+ summary: `No components directly import ${packageName}.`
926
+ };
927
+ }
928
+ const names = affectedComponents.map((c) => c.name).slice(0, 10);
929
+ const more = affectedComponents.length > 10 ? ` and ${affectedComponents.length - 10} more` : "";
930
+ const status = affectedComponents.length > 20 ? "fail" : affectedComponents.length > 5 ? "warn" : "pass";
931
+ return {
932
+ name: "Component Impact",
933
+ status,
934
+ summary: `${affectedComponents.length} component(s) directly import ${packageName}.`,
935
+ details: `Affected: ${names.join(", ")}${more}`
936
+ };
937
+ }
938
+ async function checkPeerDependencies(rootDir, packageName, targetVersion) {
939
+ try {
940
+ const { stdout } = await execFileAsync(
941
+ "npm",
942
+ ["view", `${packageName}@${targetVersion}`, "peerDependencies", "--json"],
943
+ { cwd: rootDir, timeout: 1e4 }
944
+ );
945
+ if (!stdout.trim()) {
946
+ return {
947
+ name: "Peer Dependencies",
948
+ status: "pass",
949
+ summary: "No peer dependency requirements."
950
+ };
951
+ }
952
+ const peerDeps = JSON.parse(stdout);
953
+ const peerList = Object.entries(peerDeps).map(([name, range]) => `${name}@${range}`).join(", ");
954
+ return {
955
+ name: "Peer Dependencies",
956
+ status: "warn",
957
+ summary: `Requires peer dependencies: ${peerList}`,
958
+ details: "Verify that your installed versions satisfy these peer dependency ranges."
959
+ };
960
+ } catch {
961
+ return {
962
+ name: "Peer Dependencies",
963
+ status: "warn",
964
+ summary: "Could not check peer dependencies (npm view failed)."
965
+ };
966
+ }
967
+ }
968
+ async function checkDeprecation(packageName, targetVersion) {
969
+ try {
970
+ const { stdout } = await execFileAsync(
971
+ "npm",
972
+ ["view", `${packageName}@${targetVersion}`, "deprecated", "--json"],
973
+ { timeout: 1e4 }
974
+ );
975
+ if (stdout.trim() && stdout.trim() !== "undefined") {
976
+ return {
977
+ name: "Deprecation",
978
+ status: "fail",
979
+ summary: `Version ${targetVersion} is deprecated.`,
980
+ details: stdout.trim().replace(/^"|"$/g, "")
981
+ };
982
+ }
983
+ return {
984
+ name: "Deprecation",
985
+ status: "pass",
986
+ summary: "Target version is not deprecated."
987
+ };
988
+ } catch {
989
+ return {
990
+ name: "Deprecation",
991
+ status: "pass",
992
+ summary: "No deprecation notice found."
993
+ };
994
+ }
995
+ }
996
+ async function getCurrentVersion(rootDir, packageName) {
997
+ try {
998
+ const pkgPath = join2(rootDir, "package.json");
999
+ const raw = await readFile2(pkgPath, "utf-8");
1000
+ const pkg = JSON.parse(raw);
1001
+ const allDeps = {
1002
+ ...pkg["dependencies"],
1003
+ ...pkg["devDependencies"]
1004
+ };
1005
+ const version = allDeps[packageName];
1006
+ return version?.replace(/^[\^~>=<]+/, "") ?? "0.0.0";
1007
+ } catch {
1008
+ return "0.0.0";
1009
+ }
1010
+ }
1011
+ async function getLatestVersion(packageName) {
1012
+ try {
1013
+ const { stdout } = await execFileAsync("npm", ["view", packageName, "version"], {
1014
+ timeout: 1e4
1015
+ });
1016
+ return stdout.trim();
1017
+ } catch {
1018
+ return "0.0.0";
1019
+ }
1020
+ }
1021
+ function parseSemver(version) {
1022
+ const cleaned = version.replace(/^[\^~>=<]+/, "");
1023
+ const match = cleaned.match(/^(\d+)\.(\d+)\.(\d+)/);
1024
+ if (!match) return null;
1025
+ return {
1026
+ major: parseInt(match[1], 10),
1027
+ minor: parseInt(match[2], 10),
1028
+ patch: parseInt(match[3], 10)
1029
+ };
1030
+ }
1031
+ function computeOverallRisk(checks) {
1032
+ const hasFail = checks.some((c) => c.status === "fail");
1033
+ const warnCount = checks.filter((c) => c.status === "warn").length;
1034
+ if (hasFail) return "high";
1035
+ if (warnCount >= 2) return "medium";
1036
+ return "low";
1037
+ }
1038
+
1039
+ // src/import-parser.ts
1040
+ function extractImportsFromScript(script, filePath) {
1041
+ const imports = [];
1042
+ const importRegex = /import\s+(?:(?:type\s+)?(?:(\{[^}]+\})|(\w+)(?:\s*,\s*(\{[^}]+\}))?|(\*\s+as\s+\w+)))\s+from\s+['"]([^'"]+)['"]/g;
1043
+ let match;
1044
+ while ((match = importRegex.exec(script)) !== null) {
1045
+ const target = match[5];
1046
+ const specifiers = [];
1047
+ if (match[2]) {
1048
+ specifiers.push({ imported: "default", local: match[2] });
1049
+ }
1050
+ const namedBlock = match[1] ?? match[3];
1051
+ if (namedBlock) {
1052
+ const names = namedBlock.replace(/[{}]/g, "").split(",").map((s) => s.trim()).filter(Boolean);
1053
+ for (const name of names) {
1054
+ const parts = name.split(/\s+as\s+/);
1055
+ specifiers.push({
1056
+ imported: parts[0].replace(/^type\s+/, "").trim(),
1057
+ local: (parts[1] ?? parts[0]).trim()
1058
+ });
1059
+ }
1060
+ }
1061
+ if (match[4]) {
1062
+ const nsName = match[4].replace("* as ", "").trim();
1063
+ specifiers.push({ imported: "*", local: nsName });
1064
+ }
1065
+ imports.push({
1066
+ source: filePath,
1067
+ target,
1068
+ specifiers,
1069
+ typeOnly: /import\s+type\s/.test(match[0])
1070
+ });
1071
+ }
1072
+ return imports;
1073
+ }
1074
+
1075
+ // src/coverage-analyzer.ts
1076
+ import { readFile as readFile3 } from "fs/promises";
1077
+ import { existsSync as existsSync2 } from "fs";
1078
+ import { join as join3 } from "path";
1079
+ async function loadCoverageData(projectRoot, customCoveragePath) {
1080
+ const coveragePath = customCoveragePath || join3(projectRoot, "coverage", "coverage-final.json");
1081
+ if (!existsSync2(coveragePath)) {
1082
+ return {};
1083
+ }
1084
+ try {
1085
+ const raw = await readFile3(coveragePath, "utf-8");
1086
+ return JSON.parse(raw);
1087
+ } catch {
1088
+ return {};
1089
+ }
1090
+ }
1091
+ function calculateFileCoverage(coverage) {
1092
+ const statementsCovered = Object.values(coverage.s).filter((count) => count > 0).length;
1093
+ const statementsTotal = Object.keys(coverage.s).length;
1094
+ const functionsCovered = Object.values(coverage.f).filter((count) => count > 0).length;
1095
+ const functionsTotal = Object.keys(coverage.f).length;
1096
+ const totalCoveration = statementsTotal + functionsTotal;
1097
+ const coveredCount = statementsCovered + functionsCovered;
1098
+ const percentage = totalCoveration > 0 ? Math.round(coveredCount / totalCoveration * 100) : 0;
1099
+ return {
1100
+ statements: { covered: statementsCovered, total: statementsTotal },
1101
+ functions: { covered: functionsCovered, total: functionsTotal },
1102
+ percentage
1103
+ };
1104
+ }
1105
+ function mapCoverageToComponents(coverageData, componentFilePaths) {
1106
+ const components = /* @__PURE__ */ new Map();
1107
+ let totalStatementsCovered = 0;
1108
+ let totalStatementsTotal = 0;
1109
+ for (const [filePath, coverage] of Object.entries(coverageData)) {
1110
+ if (!componentFilePaths.has(filePath) && !componentFilePaths.has(`./${filePath}`)) {
1111
+ continue;
1112
+ }
1113
+ const { statements, functions, percentage } = calculateFileCoverage(coverage);
1114
+ const componentId = filePath;
1115
+ const componentCoverage = {
1116
+ componentId,
1117
+ filePath,
1118
+ statementsCovered: statements.covered,
1119
+ statementsTotal: statements.total,
1120
+ functionsCovered: functions.covered,
1121
+ functionsTotal: functions.total,
1122
+ percentage,
1123
+ isCovered: percentage > 0
1124
+ };
1125
+ components.set(filePath, componentCoverage);
1126
+ totalStatementsCovered += statements.covered;
1127
+ totalStatementsTotal += statements.total;
1128
+ }
1129
+ const overallPercentage = totalStatementsTotal > 0 ? Math.round(totalStatementsCovered / totalStatementsTotal * 100) : 0;
1130
+ return {
1131
+ components,
1132
+ totalStatementsCovered,
1133
+ totalStatementsTotal,
1134
+ overallPercentage
1135
+ };
1136
+ }
1137
+ function getComponentCoverage(componentFilePath, coverage) {
1138
+ const componentCoverage = coverage.components.get(componentFilePath);
1139
+ return componentCoverage?.percentage ?? 0;
1140
+ }
1141
+ function findUncoveredComponents(coverage) {
1142
+ return Array.from(coverage.components.values()).filter((c) => c.percentage === 0);
1143
+ }
1144
+ function findLowCoverageComponents(coverage, threshold = 50) {
1145
+ return Array.from(coverage.components.values()).filter(
1146
+ (c) => c.percentage > 0 && c.percentage < threshold
1147
+ );
1148
+ }
1149
+ function summarizeCoverage(coverage) {
1150
+ const uncovered = findUncoveredComponents(coverage);
1151
+ const lowCoverage = findLowCoverageComponents(coverage, 50);
1152
+ return [
1153
+ `Overall Coverage: ${coverage.overallPercentage}%`,
1154
+ `Statements: ${coverage.totalStatementsCovered}/${coverage.totalStatementsTotal}`,
1155
+ `Components with 0% coverage: ${uncovered.length}`,
1156
+ `Components with <50% coverage: ${lowCoverage.length}`
1157
+ ].join("\n");
1158
+ }
1159
+
1160
+ // src/dead-code-detector.ts
1161
+ function detectDeadCode(registry) {
1162
+ const allComponents = registry.getAllComponents();
1163
+ const allImports = registry.getAllImports();
1164
+ const unusedComponents = [];
1165
+ const orphanedComponents = [];
1166
+ const unusedExports = [];
1167
+ const importedIds = /* @__PURE__ */ new Set();
1168
+ for (const imp of allImports) {
1169
+ importedIds.add(imp.target);
1170
+ }
1171
+ for (const component of allComponents) {
1172
+ const isImported = importedIds.has(component.filePath) || importedIds.has(component.id);
1173
+ if (component.exportKind === "re-export" && !isImported) {
1174
+ unusedExports.push({
1175
+ filePath: component.filePath,
1176
+ exportName: component.name,
1177
+ reason: "re_exported_unused"
1178
+ });
1179
+ }
1180
+ const hasConsumers = component.usedBy && component.usedBy.length > 0;
1181
+ if (!isImported && !hasConsumers) {
1182
+ if (component.exportKind === "default") {
1183
+ unusedComponents.push({
1184
+ id: component.id,
1185
+ name: component.name,
1186
+ filePath: component.filePath,
1187
+ reason: "never_imported"
1188
+ });
1189
+ } else if (component.exportKind === "named") {
1190
+ unusedComponents.push({
1191
+ id: component.id,
1192
+ name: component.name,
1193
+ filePath: component.filePath,
1194
+ reason: "unused_export"
1195
+ });
1196
+ unusedExports.push({
1197
+ filePath: component.filePath,
1198
+ exportName: component.name,
1199
+ reason: "exported_but_unused"
1200
+ });
1201
+ }
1202
+ }
1203
+ if (!isImported && hasConsumers) {
1204
+ const allConsumersAreUnused = component.usedBy.every((consumerId) => {
1205
+ const consumer = registry.getComponent(consumerId);
1206
+ return consumer && !isComponentUsed(consumer, registry);
1207
+ });
1208
+ if (allConsumersAreUnused) {
1209
+ orphanedComponents.push({
1210
+ id: component.id,
1211
+ name: component.name,
1212
+ filePath: component.filePath,
1213
+ reason: "orphaned"
1214
+ });
1215
+ }
1216
+ }
1217
+ }
1218
+ let totalPotentialBytes = 0;
1219
+ for (const unused of unusedComponents) {
1220
+ const bundleInfo = registry.getBundleInfo(unused.id);
1221
+ if (bundleInfo) {
1222
+ totalPotentialBytes += bundleInfo.exclusiveSize.gzip;
1223
+ }
1224
+ }
1225
+ return {
1226
+ unusedComponents,
1227
+ orphanedComponents,
1228
+ unusedExports,
1229
+ totalPotentialBytes
1230
+ };
1231
+ }
1232
+ function isComponentUsed(component, registry) {
1233
+ if (!component.usedBy || component.usedBy.length === 0) {
1234
+ return false;
1235
+ }
1236
+ for (const consumerId of component.usedBy) {
1237
+ const consumer = registry.getComponent(consumerId);
1238
+ if (consumer && isComponentUsed(consumer, registry)) {
1239
+ return true;
1240
+ }
1241
+ }
1242
+ return false;
1243
+ }
1244
+ function findSafeRemovalCandidates(report) {
1245
+ return report.unusedComponents.filter((c) => c.reason !== "orphaned");
1246
+ }
1247
+ function formatDeadCodeReport(report) {
1248
+ const lines = [];
1249
+ if (report.unusedComponents.length > 0) {
1250
+ lines.push(`
1251
+ \u274C Unused Components (${report.unusedComponents.length}):`);
1252
+ for (const comp of report.unusedComponents.slice(0, 10)) {
1253
+ lines.push(` - ${comp.name} (${comp.filePath})`);
1254
+ }
1255
+ if (report.unusedComponents.length > 10) {
1256
+ lines.push(` ... and ${report.unusedComponents.length - 10} more`);
1257
+ }
1258
+ }
1259
+ if (report.orphanedComponents.length > 0) {
1260
+ lines.push(`
1261
+ \u{1F517} Orphaned Components (${report.orphanedComponents.length}):`);
1262
+ for (const comp of report.orphanedComponents.slice(0, 5)) {
1263
+ lines.push(` - ${comp.name} (${comp.filePath})`);
1264
+ }
1265
+ if (report.orphanedComponents.length > 5) {
1266
+ lines.push(` ... and ${report.orphanedComponents.length - 5} more`);
1267
+ }
1268
+ }
1269
+ if (report.unusedExports.length > 0) {
1270
+ lines.push(`
1271
+ \u26A0\uFE0F Unused Exports (${report.unusedExports.length}):`);
1272
+ for (const exp of report.unusedExports.slice(0, 10)) {
1273
+ lines.push(` - ${exp.exportName} from ${exp.filePath}`);
1274
+ }
1275
+ }
1276
+ if (report.totalPotentialBytes > 0) {
1277
+ const kb = (report.totalPotentialBytes / 1024).toFixed(1);
1278
+ lines.push(`
1279
+ \u{1F4BE} Potential Savings: ~${kb} KB`);
1280
+ }
1281
+ return lines.join("\n");
1282
+ }
1283
+
1284
+ // src/api-snapshot.ts
1285
+ function createAPISnapshot(components, hash) {
1286
+ const components_map = /* @__PURE__ */ new Map();
1287
+ for (const comp of components) {
1288
+ components_map.set(comp.id, {
1289
+ componentId: comp.id,
1290
+ name: comp.name,
1291
+ filePath: comp.filePath,
1292
+ exportKind: comp.exportKind,
1293
+ props: comp.props,
1294
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1295
+ });
1296
+ }
1297
+ return {
1298
+ components: components_map,
1299
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1300
+ hash
1301
+ };
1302
+ }
1303
+ function snapshotToJSON(snapshot) {
1304
+ const data = {
1305
+ timestamp: snapshot.timestamp,
1306
+ hash: snapshot.hash,
1307
+ components: Array.from(snapshot.components.values())
1308
+ };
1309
+ return JSON.stringify(data, null, 2);
1310
+ }
1311
+ function snapshotFromJSON(json) {
1312
+ const data = JSON.parse(json);
1313
+ const components = new Map(data.components.map((c) => [c.componentId, c]));
1314
+ return {
1315
+ components,
1316
+ timestamp: data.timestamp,
1317
+ hash: data.hash
1318
+ };
1319
+ }
1320
+ function compareSnapshots(oldSnapshot, newSnapshot) {
1321
+ const addedComponents = [];
1322
+ const removedComponents = [];
1323
+ const modifiedComponents = [];
1324
+ const breakingChanges = [];
1325
+ const nonBreakingChanges = [];
1326
+ for (const [id, oldComponent] of oldSnapshot.components) {
1327
+ if (!newSnapshot.components.has(id)) {
1328
+ removedComponents.push(oldComponent);
1329
+ breakingChanges.push({
1330
+ componentId: id,
1331
+ componentName: oldComponent.name,
1332
+ changeType: "export_removed",
1333
+ description: `${oldComponent.name} export was removed`,
1334
+ affectedItems: [oldComponent.filePath],
1335
+ severity: "critical"
1336
+ });
1337
+ }
1338
+ }
1339
+ for (const [id, newComponent] of newSnapshot.components) {
1340
+ const oldComponent = oldSnapshot.components.get(id);
1341
+ if (!oldComponent) {
1342
+ addedComponents.push(newComponent);
1343
+ } else {
1344
+ const changes = detectComponentChanges(oldComponent, newComponent);
1345
+ if (changes.length > 0) {
1346
+ modifiedComponents.push({
1347
+ component: newComponent,
1348
+ changes
1349
+ });
1350
+ for (const change of changes) {
1351
+ if (change.type === "removed" || change.type === "modified" && change.field === "export_kind") {
1352
+ breakingChanges.push({
1353
+ componentId: id,
1354
+ componentName: newComponent.name,
1355
+ changeType: change.field === "export_kind" ? "export_kind_changed" : "prop_removed",
1356
+ description: `${newComponent.name}: ${change.field} changed`,
1357
+ affectedItems: [newComponent.filePath],
1358
+ severity: change.field === "export_kind" ? "high" : "medium"
1359
+ });
1360
+ } else {
1361
+ nonBreakingChanges.push(change);
1362
+ }
1363
+ }
1364
+ }
1365
+ }
1366
+ }
1367
+ return {
1368
+ addedComponents,
1369
+ removedComponents,
1370
+ modifiedComponents,
1371
+ breaking: breakingChanges,
1372
+ nonBreaking: nonBreakingChanges
1373
+ };
1374
+ }
1375
+ function detectComponentChanges(oldComponent, newComponent) {
1376
+ const changes = [];
1377
+ if (oldComponent.exportKind !== newComponent.exportKind) {
1378
+ changes.push({
1379
+ type: "modified",
1380
+ field: "export_kind",
1381
+ oldValue: oldComponent.exportKind,
1382
+ newValue: newComponent.exportKind
1383
+ });
1384
+ }
1385
+ const oldPropNames = new Set(oldComponent.props.map((p) => p.name));
1386
+ const newPropNames = new Set(newComponent.props.map((p) => p.name));
1387
+ for (const propName of oldPropNames) {
1388
+ if (!newPropNames.has(propName)) {
1389
+ changes.push({
1390
+ type: "removed",
1391
+ field: "prop",
1392
+ oldValue: propName
1393
+ });
1394
+ }
1395
+ }
1396
+ for (const propName of newPropNames) {
1397
+ if (!oldPropNames.has(propName)) {
1398
+ const newProp = newComponent.props.find((p) => p.name === propName);
1399
+ if (newProp?.required) {
1400
+ changes.push({
1401
+ type: "added",
1402
+ field: "prop",
1403
+ newValue: `${propName} (required)`
1404
+ });
1405
+ } else {
1406
+ changes.push({
1407
+ type: "added",
1408
+ field: "prop",
1409
+ newValue: propName
1410
+ });
1411
+ }
1412
+ }
1413
+ }
1414
+ return changes;
1415
+ }
1416
+ function formatAPIChangeSummary(summary) {
1417
+ const lines = [];
1418
+ if (summary.addedComponents.length > 0) {
1419
+ lines.push(`
1420
+ \u2728 Added Components (${summary.addedComponents.length}):`);
1421
+ for (const comp of summary.addedComponents.slice(0, 5)) {
1422
+ lines.push(` + ${comp.name}`);
1423
+ }
1424
+ }
1425
+ if (summary.removedComponents.length > 0) {
1426
+ lines.push(`
1427
+ \u{1F5D1}\uFE0F Removed Components (${summary.removedComponents.length}):`);
1428
+ for (const comp of summary.removedComponents.slice(0, 5)) {
1429
+ lines.push(` - ${comp.name}`);
1430
+ }
1431
+ }
1432
+ if (summary.breaking.length > 0) {
1433
+ lines.push(`
1434
+ \u26A0\uFE0F BREAKING CHANGES (${summary.breaking.length}):`);
1435
+ for (const change of summary.breaking.slice(0, 10)) {
1436
+ const icon = change.severity === "critical" ? "\u{1F6A8}" : "\u26A0\uFE0F ";
1437
+ lines.push(
1438
+ ` ${icon} [${change.severity.toUpperCase()}] ${change.componentName}: ${change.description}`
1439
+ );
1440
+ }
1441
+ if (summary.breaking.length > 10) {
1442
+ lines.push(` ... and ${summary.breaking.length - 10} more`);
1443
+ }
1444
+ }
1445
+ if (summary.modifiedComponents.length > 0 && summary.breaking.length === 0) {
1446
+ lines.push(`
1447
+ \u{1F504} Modified Components (${summary.modifiedComponents.length}):`);
1448
+ for (const mod of summary.modifiedComponents.slice(0, 5)) {
1449
+ lines.push(` ~ ${mod.component.name}`);
1450
+ }
1451
+ }
1452
+ return lines.length > 0 ? lines.join("\n") : "No API changes detected.";
1453
+ }
1454
+ export {
1455
+ COST_MODELS,
1456
+ ComponentRegistry,
1457
+ DEFAULT_TRAFFIC,
1458
+ DEFAULT_WEIGHTS,
1459
+ DependencyGraph,
1460
+ analyzeUpgrade,
1461
+ analyzeUpgrades,
1462
+ compareSnapshots,
1463
+ computeAllHealth,
1464
+ computeComponentHealth,
1465
+ createAPISnapshot,
1466
+ createDefaultConfig,
1467
+ detectDeadCode,
1468
+ detectFramework,
1469
+ estimateCostImpact,
1470
+ estimateMonthlyCost,
1471
+ extractImportsFromScript,
1472
+ findLowCoverageComponents,
1473
+ findSafeRemovalCandidates,
1474
+ findUncoveredComponents,
1475
+ formatAPIChangeSummary,
1476
+ formatDeadCodeReport,
1477
+ getComponentCoverage,
1478
+ loadConfig,
1479
+ loadCoverageData,
1480
+ mapCoverageToComponents,
1481
+ snapshotFromJSON,
1482
+ snapshotToJSON,
1483
+ summarizeCoverage
1484
+ };