@buoy-design/cli 0.4.1 → 0.5.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/README.md +37 -21
- package/dist/cloud/config.d.ts +5 -0
- package/dist/cloud/config.d.ts.map +1 -1
- package/dist/cloud/config.js.map +1 -1
- package/dist/commands/ignore.d.ts +7 -3
- package/dist/commands/ignore.d.ts.map +1 -1
- package/dist/commands/ignore.js +81 -78
- package/dist/commands/ignore.js.map +1 -1
- package/dist/commands/index.d.ts +1 -0
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +1 -0
- package/dist/commands/index.js.map +1 -1
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +108 -33
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/rescue.d.ts +3 -0
- package/dist/commands/rescue.d.ts.map +1 -0
- package/dist/commands/rescue.js +147 -0
- package/dist/commands/rescue.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/rescue/report.d.ts +9 -0
- package/dist/rescue/report.d.ts.map +1 -0
- package/dist/rescue/report.js +107 -0
- package/dist/rescue/report.js.map +1 -0
- package/dist/rescue/store.d.ts +9 -0
- package/dist/rescue/store.d.ts.map +1 -0
- package/dist/rescue/store.js +50 -0
- package/dist/rescue/store.js.map +1 -0
- package/dist/rescue/types.d.ts +76 -0
- package/dist/rescue/types.d.ts.map +1 -0
- package/dist/rescue/types.js +2 -0
- package/dist/rescue/types.js.map +1 -0
- package/dist/rescue/workflow.d.ts +37 -0
- package/dist/rescue/workflow.d.ts.map +1 -0
- package/dist/rescue/workflow.js +355 -0
- package/dist/rescue/workflow.js.map +1 -0
- package/dist/services/drift-analysis.d.ts +3 -1
- package/dist/services/drift-analysis.d.ts.map +1 -1
- package/dist/services/drift-analysis.js +179 -98
- package/dist/services/drift-analysis.js.map +1 -1
- package/dist/utils/upgrade-hints.d.ts +13 -2
- package/dist/utils/upgrade-hints.d.ts.map +1 -1
- package/dist/utils/upgrade-hints.js +43 -5
- package/dist/utils/upgrade-hints.js.map +1 -1
- package/package.json +3 -3
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { ScanOrchestrator } from "../scan/orchestrator.js";
|
|
12
12
|
import { getSeverityWeight } from "@buoy-design/core";
|
|
13
|
-
import { TailwindScanner, extractStaticClassStrings } from "@buoy-design/scanners";
|
|
13
|
+
import { TailwindScanner, extractStaticClassStrings, } from "@buoy-design/scanners";
|
|
14
14
|
import { detectRepeatedPatterns, checkVariantConsistency, checkExampleCompliance, detectTokenUtilities, checkTokenUtilityUsage, } from "@buoy-design/core";
|
|
15
15
|
import { glob } from "glob";
|
|
16
16
|
import { minimatch } from "minimatch";
|
|
@@ -94,7 +94,10 @@ export function applyPromoteRules(drifts, rules, onWarning) {
|
|
|
94
94
|
export function applyEnforceRules(drifts, rules, onWarning) {
|
|
95
95
|
if (rules.length === 0)
|
|
96
96
|
return drifts;
|
|
97
|
-
const promoteRules = rules.map((rule) => ({
|
|
97
|
+
const promoteRules = rules.map((rule) => ({
|
|
98
|
+
...rule,
|
|
99
|
+
to: "critical",
|
|
100
|
+
}));
|
|
98
101
|
return applyPromoteRules(drifts, promoteRules, onWarning);
|
|
99
102
|
}
|
|
100
103
|
function ruleMatches(d, rule, onWarning) {
|
|
@@ -313,11 +316,11 @@ const ENTRY_POINT_PATTERNS = [
|
|
|
313
316
|
function isEntryPointComponent(component) {
|
|
314
317
|
const source = component.source;
|
|
315
318
|
// Only file-based sources (react, vue, svelte) have a path field
|
|
316
|
-
if (source.type ===
|
|
319
|
+
if (source.type === "figma" || source.type === "storybook")
|
|
317
320
|
return false;
|
|
318
321
|
// Prefix with "/" so regexes match both "app/foo/page.tsx" and "/app/foo/page.tsx"
|
|
319
|
-
const location = `/${source.path ||
|
|
320
|
-
return ENTRY_POINT_PATTERNS.some(pattern => pattern.test(location));
|
|
322
|
+
const location = `/${source.path || ""}`;
|
|
323
|
+
return ENTRY_POINT_PATTERNS.some((pattern) => pattern.test(location));
|
|
321
324
|
}
|
|
322
325
|
/**
|
|
323
326
|
* Known CSS variable prefixes that are framework-internal implementation details.
|
|
@@ -327,37 +330,25 @@ function isEntryPointComponent(component) {
|
|
|
327
330
|
const FRAMEWORK_INTERNAL_PREFIXES = {
|
|
328
331
|
// Tailwind CSS — utility internals and plugin variables
|
|
329
332
|
tailwind: [
|
|
330
|
-
|
|
333
|
+
"--tw-", // all Tailwind internals (ring, shadow, gradient, transform, filter, prose, etc.)
|
|
331
334
|
],
|
|
332
335
|
// Radix UI — component internal sizing/animation variables
|
|
333
|
-
radix: [
|
|
334
|
-
'--radix-',
|
|
335
|
-
],
|
|
336
|
+
radix: ["--radix-"],
|
|
336
337
|
// Chakra UI — generated theme CSS variables
|
|
337
|
-
chakra: [
|
|
338
|
-
'--chakra-',
|
|
339
|
-
],
|
|
338
|
+
chakra: ["--chakra-"],
|
|
340
339
|
// Mantine — theme system variables
|
|
341
|
-
mantine: [
|
|
342
|
-
'--mantine-',
|
|
343
|
-
],
|
|
340
|
+
mantine: ["--mantine-"],
|
|
344
341
|
// Material UI — theme CSS variables
|
|
345
|
-
mui: [
|
|
346
|
-
'--mui-',
|
|
347
|
-
'--md-',
|
|
348
|
-
'--joy-',
|
|
349
|
-
],
|
|
342
|
+
mui: ["--mui-", "--md-", "--joy-"],
|
|
350
343
|
// Ant Design — component theme variables
|
|
351
|
-
antd: [
|
|
352
|
-
'--ant-',
|
|
353
|
-
],
|
|
344
|
+
antd: ["--ant-"],
|
|
354
345
|
};
|
|
355
346
|
/**
|
|
356
347
|
* Check if a token name matches a known framework-internal CSS variable prefix.
|
|
357
348
|
* These tokens are consumed by the framework itself, not by user components.
|
|
358
349
|
*/
|
|
359
350
|
function isFrameworkInternalToken(tokenName, detectedFrameworks) {
|
|
360
|
-
const name = tokenName.startsWith(
|
|
351
|
+
const name = tokenName.startsWith("--") ? tokenName : `--${tokenName}`;
|
|
361
352
|
for (const [framework, prefixes] of Object.entries(FRAMEWORK_INTERNAL_PREFIXES)) {
|
|
362
353
|
// Match detected framework names loosely (e.g., "tailwindcss" matches "tailwind")
|
|
363
354
|
const isDetected = [...detectedFrameworks].some((f) => f.includes(framework) || framework.includes(f));
|
|
@@ -395,7 +386,9 @@ export class DriftAnalysisService {
|
|
|
395
386
|
});
|
|
396
387
|
// Step 2: Scan tokens (before analysis, so suggestions can be generated)
|
|
397
388
|
onProgress?.("Scanning tokens...");
|
|
398
|
-
const { tokens: scannedTokens } = await orchestrator.scanTokens({
|
|
389
|
+
const { tokens: scannedTokens } = await orchestrator.scanTokens({
|
|
390
|
+
onProgress,
|
|
391
|
+
});
|
|
399
392
|
// Step 2.1: Run semantic diff analysis
|
|
400
393
|
onProgress?.("Analyzing drift...");
|
|
401
394
|
const { SemanticDiffEngine } = await import("@buoy-design/core/analysis");
|
|
@@ -414,7 +407,10 @@ export class DriftAnalysisService {
|
|
|
414
407
|
const detector = new ProjectDetector(this.projectRoot);
|
|
415
408
|
const projectInfo = await detector.detect();
|
|
416
409
|
if (projectInfo.frameworks.length > 0) {
|
|
417
|
-
const sprawlDrift = engine.checkFrameworkSprawl(projectInfo.frameworks.map((f) => ({
|
|
410
|
+
const sprawlDrift = engine.checkFrameworkSprawl(projectInfo.frameworks.map((f) => ({
|
|
411
|
+
name: f.name,
|
|
412
|
+
version: f.version,
|
|
413
|
+
})));
|
|
418
414
|
if (sprawlDrift) {
|
|
419
415
|
drifts.push(...applySeverityOverrides([sprawlDrift], this.config.drift.severity));
|
|
420
416
|
onProgress?.(`Framework sprawl detected: ${projectInfo.frameworks.map((f) => f.name).join(", ")}`);
|
|
@@ -482,7 +478,7 @@ export class DriftAnalysisService {
|
|
|
482
478
|
await this.scanComponentAsValueUsages(componentUsageMap, componentNames);
|
|
483
479
|
// Fix 1: Exempt entry point components (pages, routes, layouts)
|
|
484
480
|
// These are rendered by the framework router, not imported by other components
|
|
485
|
-
const nonEntryPointComponents = components.filter(c => !isEntryPointComponent(c));
|
|
481
|
+
const nonEntryPointComponents = components.filter((c) => !isEntryPointComponent(c));
|
|
486
482
|
// Check for unused components (excluding entry points)
|
|
487
483
|
const unusedComponentDrifts = engine.checkUnusedComponents(nonEntryPointComponents, componentUsageMap);
|
|
488
484
|
if (unusedComponentDrifts.length > 0) {
|
|
@@ -496,7 +492,7 @@ export class DriftAnalysisService {
|
|
|
496
492
|
const detectedNames = new Set(projectInfo.frameworks.map((f) => f.name.toLowerCase()));
|
|
497
493
|
// Tailwind and CSS libraries are source configs, not component frameworks — detect from config/deps/tokens
|
|
498
494
|
if (this.config.sources.tailwind?.enabled)
|
|
499
|
-
detectedNames.add(
|
|
495
|
+
detectedNames.add("tailwind");
|
|
500
496
|
for (const tokenFile of projectInfo.tokens) {
|
|
501
497
|
detectedNames.add(tokenFile.type.toLowerCase());
|
|
502
498
|
}
|
|
@@ -515,7 +511,8 @@ export class DriftAnalysisService {
|
|
|
515
511
|
const canonicalPatterns = this.config.sources.tokens?.canonical ?? [];
|
|
516
512
|
const classifiedComponents = classifyComponents(components);
|
|
517
513
|
const classifiedTokens = classifyTokens(scannedTokens, canonicalPatterns);
|
|
518
|
-
if (classifiedComponents.canonical.length > 0 &&
|
|
514
|
+
if (classifiedComponents.canonical.length > 0 &&
|
|
515
|
+
classifiedComponents.code.length > 0) {
|
|
519
516
|
onProgress?.(`Comparing ${classifiedComponents.code.length} code components against ${classifiedComponents.canonical.length} design components...`);
|
|
520
517
|
const componentDiff = engine.compareComponents(classifiedComponents.code, classifiedComponents.canonical);
|
|
521
518
|
if (componentDiff.drifts.length > 0) {
|
|
@@ -523,7 +520,8 @@ export class DriftAnalysisService {
|
|
|
523
520
|
onProgress?.(`Found ${componentDiff.drifts.length} cross-source component issues`);
|
|
524
521
|
}
|
|
525
522
|
}
|
|
526
|
-
if (classifiedTokens.canonical.length > 0 &&
|
|
523
|
+
if (classifiedTokens.canonical.length > 0 &&
|
|
524
|
+
classifiedTokens.code.length > 0) {
|
|
527
525
|
onProgress?.(`Comparing ${classifiedTokens.code.length} code tokens against ${classifiedTokens.canonical.length} design tokens...`);
|
|
528
526
|
const tokenDiff = engine.compareTokens(classifiedTokens.code, classifiedTokens.canonical);
|
|
529
527
|
if (tokenDiff.drifts.length > 0) {
|
|
@@ -560,7 +558,8 @@ export class DriftAnalysisService {
|
|
|
560
558
|
}
|
|
561
559
|
}
|
|
562
560
|
// Step 2.7: Phase 4.1 - Cross-Variant Consistency Checking
|
|
563
|
-
const variantCheckEnabled = checkVariants ??
|
|
561
|
+
const variantCheckEnabled = checkVariants ??
|
|
562
|
+
this.config.drift?.types?.["value-divergence"]?.checkVariants;
|
|
564
563
|
if (variantCheckEnabled) {
|
|
565
564
|
onProgress?.("Checking variant consistency...");
|
|
566
565
|
const variantDrifts = checkVariantConsistency(components);
|
|
@@ -570,7 +569,8 @@ export class DriftAnalysisService {
|
|
|
570
569
|
}
|
|
571
570
|
}
|
|
572
571
|
// Step 2.8: Phase 4.2 - Token Utility Function Detection
|
|
573
|
-
const tokenUtilityCheckEnabled = checkTokenUtilities ??
|
|
572
|
+
const tokenUtilityCheckEnabled = checkTokenUtilities ??
|
|
573
|
+
this.config.drift?.types?.["hardcoded-value"]?.checkUtilities;
|
|
574
574
|
if (tokenUtilityCheckEnabled) {
|
|
575
575
|
onProgress?.("Detecting token utility functions...");
|
|
576
576
|
const utilityAnalysis = detectTokenUtilities(components);
|
|
@@ -584,7 +584,8 @@ export class DriftAnalysisService {
|
|
|
584
584
|
}
|
|
585
585
|
}
|
|
586
586
|
// Step 2.9: Phase 4.3 - Example Code vs Production Code Analysis
|
|
587
|
-
const exampleCheckEnabled = checkExamples ??
|
|
587
|
+
const exampleCheckEnabled = checkExamples ??
|
|
588
|
+
this.config.drift?.types?.["missing-documentation"]?.checkExamples;
|
|
588
589
|
if (exampleCheckEnabled) {
|
|
589
590
|
onProgress?.("Analyzing example code compliance...");
|
|
590
591
|
const exampleDrifts = checkExampleCompliance(components);
|
|
@@ -621,7 +622,7 @@ export class DriftAnalysisService {
|
|
|
621
622
|
let ignoredCount = 0;
|
|
622
623
|
if (!includeIgnored) {
|
|
623
624
|
const { loadIgnoreList, filterIgnored } = await import("../commands/ignore.js");
|
|
624
|
-
const ignoreList = await loadIgnoreList();
|
|
625
|
+
const ignoreList = await loadIgnoreList(this.projectRoot);
|
|
625
626
|
const filtered = filterIgnored(drifts, ignoreList);
|
|
626
627
|
drifts = filtered.newDrifts;
|
|
627
628
|
ignoredCount = filtered.ignoredCount;
|
|
@@ -633,6 +634,7 @@ export class DriftAnalysisService {
|
|
|
633
634
|
drifts,
|
|
634
635
|
components,
|
|
635
636
|
tokenCount: scannedTokens.length,
|
|
637
|
+
tokens: scannedTokens,
|
|
636
638
|
ignoredCount,
|
|
637
639
|
summary: calculateDriftSummary(drifts),
|
|
638
640
|
};
|
|
@@ -649,7 +651,15 @@ export class DriftAnalysisService {
|
|
|
649
651
|
}
|
|
650
652
|
};
|
|
651
653
|
const { react, nextjs, vue, svelte, angular, webcomponent, templates, tailwind, tokens, } = this.config.sources;
|
|
652
|
-
for (const source of [
|
|
654
|
+
for (const source of [
|
|
655
|
+
react,
|
|
656
|
+
nextjs,
|
|
657
|
+
vue,
|
|
658
|
+
svelte,
|
|
659
|
+
angular,
|
|
660
|
+
webcomponent,
|
|
661
|
+
templates,
|
|
662
|
+
]) {
|
|
653
663
|
if (!source?.enabled)
|
|
654
664
|
continue;
|
|
655
665
|
addMany(include, source.include);
|
|
@@ -674,8 +684,15 @@ export class DriftAnalysisService {
|
|
|
674
684
|
if (aliasMap.size === 0)
|
|
675
685
|
return;
|
|
676
686
|
const usageGlobs = this.getUsageCollectorGlobs();
|
|
677
|
-
const include = usageGlobs.include ?? [
|
|
678
|
-
|
|
687
|
+
const include = usageGlobs.include ?? [
|
|
688
|
+
"**/*.{ts,tsx,js,jsx,vue,svelte,astro,html,mdx,md}",
|
|
689
|
+
];
|
|
690
|
+
const exclude = usageGlobs.exclude ?? [
|
|
691
|
+
"**/node_modules/**",
|
|
692
|
+
"**/dist/**",
|
|
693
|
+
"**/build/**",
|
|
694
|
+
"**/.next/**",
|
|
695
|
+
];
|
|
679
696
|
const files = await glob(include, {
|
|
680
697
|
cwd: this.projectRoot,
|
|
681
698
|
ignore: exclude,
|
|
@@ -826,24 +843,32 @@ export class DriftAnalysisService {
|
|
|
826
843
|
*/
|
|
827
844
|
async scanBarrelReExports(componentUsageMap) {
|
|
828
845
|
const cwd = this.projectRoot;
|
|
829
|
-
const barrelFiles = await glob(
|
|
846
|
+
const barrelFiles = await glob("**/index.{ts,tsx,js,jsx}", {
|
|
830
847
|
cwd,
|
|
831
|
-
ignore: [
|
|
848
|
+
ignore: [
|
|
849
|
+
"**/node_modules/**",
|
|
850
|
+
"**/dist/**",
|
|
851
|
+
"**/build/**",
|
|
852
|
+
"**/.next/**",
|
|
853
|
+
],
|
|
832
854
|
nodir: true,
|
|
833
855
|
maxDepth: 6,
|
|
834
856
|
});
|
|
835
857
|
for (const barrelFile of barrelFiles.slice(0, 200)) {
|
|
836
858
|
try {
|
|
837
|
-
const content = await readFile(resolve(cwd, barrelFile),
|
|
859
|
+
const content = await readFile(resolve(cwd, barrelFile), "utf-8");
|
|
838
860
|
// Check for named re-exports: export { Button, Card } from './components'
|
|
839
861
|
const namedPattern = /export\s*\{\s*([^}]+)\s*\}\s*from/g;
|
|
840
862
|
let match;
|
|
841
863
|
while ((match = namedPattern.exec(content)) !== null) {
|
|
842
|
-
const names = match[1]
|
|
864
|
+
const names = match[1]
|
|
865
|
+
.split(",")
|
|
866
|
+
.map((n) => {
|
|
843
867
|
// Handle "default as Name" and "Name as Alias"
|
|
844
868
|
const parts = n.trim().split(/\s+as\s+/);
|
|
845
|
-
return (parts[1] || parts[0] ||
|
|
846
|
-
})
|
|
869
|
+
return (parts[1] || parts[0] || "").trim();
|
|
870
|
+
})
|
|
871
|
+
.filter((n) => n && /^[A-Z]/.test(n)); // Only PascalCase (component names)
|
|
847
872
|
for (const name of names) {
|
|
848
873
|
componentUsageMap.set(name, (componentUsageMap.get(name) || 0) + 1);
|
|
849
874
|
}
|
|
@@ -859,8 +884,8 @@ export class DriftAnalysisService {
|
|
|
859
884
|
const wildcardPattern = /export\s*\*\s*from\s*['"]\.\/([^'"]+)['"]/g;
|
|
860
885
|
while ((match = wildcardPattern.exec(content)) !== null) {
|
|
861
886
|
const moduleName = match[1];
|
|
862
|
-
const segments = moduleName.split(
|
|
863
|
-
const last = segments[segments.length - 1] ||
|
|
887
|
+
const segments = moduleName.split("/");
|
|
888
|
+
const last = segments[segments.length - 1] || "";
|
|
864
889
|
if (/^[A-Z]/.test(last)) {
|
|
865
890
|
componentUsageMap.set(last, (componentUsageMap.get(last) || 0) + 1);
|
|
866
891
|
}
|
|
@@ -877,23 +902,30 @@ export class DriftAnalysisService {
|
|
|
877
902
|
*/
|
|
878
903
|
async scanDynamicImports(componentUsageMap) {
|
|
879
904
|
const cwd = this.projectRoot;
|
|
880
|
-
const sourceFiles = await glob(
|
|
905
|
+
const sourceFiles = await glob("**/*.{tsx,jsx,ts,js}", {
|
|
881
906
|
cwd,
|
|
882
|
-
ignore: [
|
|
907
|
+
ignore: [
|
|
908
|
+
"**/node_modules/**",
|
|
909
|
+
"**/dist/**",
|
|
910
|
+
"**/build/**",
|
|
911
|
+
"**/.next/**",
|
|
912
|
+
"**/*.test.*",
|
|
913
|
+
"**/*.spec.*",
|
|
914
|
+
],
|
|
883
915
|
nodir: true,
|
|
884
916
|
maxDepth: 6,
|
|
885
917
|
});
|
|
886
918
|
for (const file of sourceFiles.slice(0, 200)) {
|
|
887
919
|
try {
|
|
888
|
-
const content = await readFile(resolve(cwd, file),
|
|
889
|
-
if (!content.includes(
|
|
920
|
+
const content = await readFile(resolve(cwd, file), "utf-8");
|
|
921
|
+
if (!content.includes("import("))
|
|
890
922
|
continue; // Quick check before regex
|
|
891
923
|
const dynamicPattern = /import\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
892
924
|
let match;
|
|
893
925
|
while ((match = dynamicPattern.exec(content)) !== null) {
|
|
894
926
|
const modulePath = match[1];
|
|
895
|
-
const segments = modulePath.split(
|
|
896
|
-
const last = (segments[segments.length - 1] ||
|
|
927
|
+
const segments = modulePath.split("/");
|
|
928
|
+
const last = (segments[segments.length - 1] || "").replace(/\.(tsx?|jsx?)$/, "");
|
|
897
929
|
if (/^[A-Z]/.test(last)) {
|
|
898
930
|
componentUsageMap.set(last, (componentUsageMap.get(last) || 0) + 1);
|
|
899
931
|
}
|
|
@@ -914,9 +946,14 @@ export class DriftAnalysisService {
|
|
|
914
946
|
if (knownComponents.length === 0)
|
|
915
947
|
return;
|
|
916
948
|
const cwd = this.projectRoot;
|
|
917
|
-
const templateFiles = await glob(
|
|
949
|
+
const templateFiles = await glob("**/*.{vue,svelte,html}", {
|
|
918
950
|
cwd,
|
|
919
|
-
ignore: [
|
|
951
|
+
ignore: [
|
|
952
|
+
"**/node_modules/**",
|
|
953
|
+
"**/dist/**",
|
|
954
|
+
"**/build/**",
|
|
955
|
+
"**/.next/**",
|
|
956
|
+
],
|
|
920
957
|
nodir: true,
|
|
921
958
|
maxDepth: 8,
|
|
922
959
|
});
|
|
@@ -924,15 +961,15 @@ export class DriftAnalysisService {
|
|
|
924
961
|
// Build a kebab-case lookup for Vue/Angular (MyComponent -> my-component)
|
|
925
962
|
const kebabMap = new Map();
|
|
926
963
|
for (const name of knownComponents) {
|
|
927
|
-
const kebab = name.replace(/([a-z0-9])([A-Z])/g,
|
|
964
|
+
const kebab = name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
928
965
|
kebabMap.set(kebab, name);
|
|
929
966
|
// Angular selectors often use app- prefix: ButtonComponent -> app-button
|
|
930
|
-
const angularSelector = `app-${kebab.replace(/-component$/,
|
|
967
|
+
const angularSelector = `app-${kebab.replace(/-component$/, "")}`;
|
|
931
968
|
kebabMap.set(angularSelector, name);
|
|
932
969
|
}
|
|
933
970
|
for (const file of templateFiles.slice(0, 300)) {
|
|
934
971
|
try {
|
|
935
|
-
const content = await readFile(resolve(cwd, file),
|
|
972
|
+
const content = await readFile(resolve(cwd, file), "utf-8");
|
|
936
973
|
// Match PascalCase component tags: <MyComponent or <MyComponent>
|
|
937
974
|
const pascalPattern = /<([A-Z][a-zA-Z0-9]*)\s*/g;
|
|
938
975
|
let match;
|
|
@@ -963,16 +1000,21 @@ export class DriftAnalysisService {
|
|
|
963
1000
|
*/
|
|
964
1001
|
async scanAutoRegistration(componentUsageMap) {
|
|
965
1002
|
const cwd = this.projectRoot;
|
|
966
|
-
const sourceFiles = await glob(
|
|
1003
|
+
const sourceFiles = await glob("**/*.{ts,js,tsx,jsx}", {
|
|
967
1004
|
cwd,
|
|
968
|
-
ignore: [
|
|
1005
|
+
ignore: [
|
|
1006
|
+
"**/node_modules/**",
|
|
1007
|
+
"**/dist/**",
|
|
1008
|
+
"**/build/**",
|
|
1009
|
+
"**/.next/**",
|
|
1010
|
+
],
|
|
969
1011
|
nodir: true,
|
|
970
1012
|
maxDepth: 4,
|
|
971
1013
|
});
|
|
972
1014
|
for (const file of sourceFiles.slice(0, 100)) {
|
|
973
1015
|
try {
|
|
974
|
-
const content = await readFile(resolve(cwd, file),
|
|
975
|
-
if (!content.includes(
|
|
1016
|
+
const content = await readFile(resolve(cwd, file), "utf-8");
|
|
1017
|
+
if (!content.includes(".component("))
|
|
976
1018
|
continue;
|
|
977
1019
|
// Match: app.component('Name', ...) or Vue.component('Name', ...)
|
|
978
1020
|
const pattern = /\.component\(\s*['"]([A-Z][a-zA-Z0-9]*)['"]/g;
|
|
@@ -993,16 +1035,16 @@ export class DriftAnalysisService {
|
|
|
993
1035
|
*/
|
|
994
1036
|
async scanNgModuleDeclarations(componentUsageMap) {
|
|
995
1037
|
const cwd = this.projectRoot;
|
|
996
|
-
const moduleFiles = await glob(
|
|
1038
|
+
const moduleFiles = await glob("**/*.module.ts", {
|
|
997
1039
|
cwd,
|
|
998
|
-
ignore: [
|
|
1040
|
+
ignore: ["**/node_modules/**", "**/dist/**", "**/build/**"],
|
|
999
1041
|
nodir: true,
|
|
1000
1042
|
maxDepth: 8,
|
|
1001
1043
|
});
|
|
1002
1044
|
for (const file of moduleFiles.slice(0, 50)) {
|
|
1003
1045
|
try {
|
|
1004
|
-
const content = await readFile(resolve(cwd, file),
|
|
1005
|
-
if (!content.includes(
|
|
1046
|
+
const content = await readFile(resolve(cwd, file), "utf-8");
|
|
1047
|
+
if (!content.includes("declarations"))
|
|
1006
1048
|
continue;
|
|
1007
1049
|
// Match declarations: [Component1, Component2, ...]
|
|
1008
1050
|
const declMatch = content.match(/declarations\s*:\s*\[([\s\S]*?)\]/);
|
|
@@ -1015,7 +1057,7 @@ export class DriftAnalysisService {
|
|
|
1015
1057
|
}
|
|
1016
1058
|
}
|
|
1017
1059
|
// Also match imports: [...] and exports: [...] arrays
|
|
1018
|
-
for (const key of [
|
|
1060
|
+
for (const key of ["imports", "exports"]) {
|
|
1019
1061
|
const match = content.match(new RegExp(`${key}\\s*:\\s*\\[([\\s\\S]*?)\\]`));
|
|
1020
1062
|
if (match) {
|
|
1021
1063
|
const names = match[1].match(/\b([A-Z][a-zA-Z]+(?:Component|Module))\b/g);
|
|
@@ -1038,23 +1080,26 @@ export class DriftAnalysisService {
|
|
|
1038
1080
|
*/
|
|
1039
1081
|
async scanStoryFileUsages(componentUsageMap) {
|
|
1040
1082
|
const cwd = this.projectRoot;
|
|
1041
|
-
const storyFiles = await glob(
|
|
1083
|
+
const storyFiles = await glob("**/*.stories.{ts,tsx,js,jsx}", {
|
|
1042
1084
|
cwd,
|
|
1043
|
-
ignore: [
|
|
1085
|
+
ignore: ["**/node_modules/**", "**/dist/**", "**/build/**"],
|
|
1044
1086
|
nodir: true,
|
|
1045
1087
|
maxDepth: 8,
|
|
1046
1088
|
});
|
|
1047
1089
|
for (const file of storyFiles.slice(0, 200)) {
|
|
1048
1090
|
try {
|
|
1049
|
-
const content = await readFile(resolve(cwd, file),
|
|
1091
|
+
const content = await readFile(resolve(cwd, file), "utf-8");
|
|
1050
1092
|
// Match named imports: import { Button, Card } from '...'
|
|
1051
1093
|
const importPattern = /import\s*\{\s*([^}]+)\s*\}\s*from/g;
|
|
1052
1094
|
let match;
|
|
1053
1095
|
while ((match = importPattern.exec(content)) !== null) {
|
|
1054
|
-
const names = match[1]
|
|
1096
|
+
const names = match[1]
|
|
1097
|
+
.split(",")
|
|
1098
|
+
.map((n) => {
|
|
1055
1099
|
const parts = n.trim().split(/\s+as\s+/);
|
|
1056
|
-
return (parts[0] ||
|
|
1057
|
-
})
|
|
1100
|
+
return (parts[0] || "").trim();
|
|
1101
|
+
})
|
|
1102
|
+
.filter((n) => n && /^[A-Z]/.test(n));
|
|
1058
1103
|
for (const name of names) {
|
|
1059
1104
|
componentUsageMap.set(name, (componentUsageMap.get(name) || 0) + 1);
|
|
1060
1105
|
}
|
|
@@ -1084,15 +1129,22 @@ export class DriftAnalysisService {
|
|
|
1084
1129
|
*/
|
|
1085
1130
|
async scanWebComponentRegistrations(componentUsageMap) {
|
|
1086
1131
|
const cwd = this.projectRoot;
|
|
1087
|
-
const files = await glob(
|
|
1132
|
+
const files = await glob("**/*.{ts,js}", {
|
|
1088
1133
|
cwd,
|
|
1089
|
-
ignore: [
|
|
1134
|
+
ignore: [
|
|
1135
|
+
"**/node_modules/**",
|
|
1136
|
+
"**/dist/**",
|
|
1137
|
+
"**/build/**",
|
|
1138
|
+
"**/*.d.ts",
|
|
1139
|
+
"**/*.spec.*",
|
|
1140
|
+
"**/*.test.*",
|
|
1141
|
+
],
|
|
1090
1142
|
nodir: true,
|
|
1091
1143
|
maxDepth: 8,
|
|
1092
1144
|
});
|
|
1093
1145
|
for (const file of files.slice(0, 500)) {
|
|
1094
1146
|
try {
|
|
1095
|
-
const content = await readFile(resolve(cwd, file),
|
|
1147
|
+
const content = await readFile(resolve(cwd, file), "utf-8");
|
|
1096
1148
|
// Match customElements.define('tag-name', ClassName)
|
|
1097
1149
|
const definePattern = /customElements\.define\s*\(\s*['"][^'"]+['"]\s*,\s*([A-Z][a-zA-Z0-9]*)/g;
|
|
1098
1150
|
let match;
|
|
@@ -1121,7 +1173,10 @@ export class DriftAnalysisService {
|
|
|
1121
1173
|
async scanNuxtAutoImports(componentUsageMap, componentNames) {
|
|
1122
1174
|
const cwd = this.projectRoot;
|
|
1123
1175
|
// Check for Nuxt config
|
|
1124
|
-
const nuxtConfigs = await glob(
|
|
1176
|
+
const nuxtConfigs = await glob("nuxt.config.{ts,js,mjs}", {
|
|
1177
|
+
cwd,
|
|
1178
|
+
nodir: true,
|
|
1179
|
+
});
|
|
1125
1180
|
if (nuxtConfigs.length === 0)
|
|
1126
1181
|
return;
|
|
1127
1182
|
// In Nuxt, all components in components/ are auto-imported
|
|
@@ -1136,23 +1191,26 @@ export class DriftAnalysisService {
|
|
|
1136
1191
|
*/
|
|
1137
1192
|
async scanTestFileUsages(componentUsageMap) {
|
|
1138
1193
|
const cwd = this.projectRoot;
|
|
1139
|
-
const testFiles = await glob(
|
|
1194
|
+
const testFiles = await glob("**/*.{test,spec}.{ts,tsx,js,jsx}", {
|
|
1140
1195
|
cwd,
|
|
1141
|
-
ignore: [
|
|
1196
|
+
ignore: ["**/node_modules/**", "**/dist/**", "**/build/**"],
|
|
1142
1197
|
nodir: true,
|
|
1143
1198
|
maxDepth: 8,
|
|
1144
1199
|
});
|
|
1145
1200
|
for (const file of testFiles.slice(0, 300)) {
|
|
1146
1201
|
try {
|
|
1147
|
-
const content = await readFile(resolve(cwd, file),
|
|
1202
|
+
const content = await readFile(resolve(cwd, file), "utf-8");
|
|
1148
1203
|
// Match named imports: import { Button, Card } from '...'
|
|
1149
1204
|
const importPattern = /import\s*\{\s*([^}]+)\s*\}\s*from/g;
|
|
1150
1205
|
let match;
|
|
1151
1206
|
while ((match = importPattern.exec(content)) !== null) {
|
|
1152
|
-
const names = match[1]
|
|
1207
|
+
const names = match[1]
|
|
1208
|
+
.split(",")
|
|
1209
|
+
.map((n) => {
|
|
1153
1210
|
const parts = n.trim().split(/\s+as\s+/);
|
|
1154
|
-
return (parts[0] ||
|
|
1155
|
-
})
|
|
1211
|
+
return (parts[0] || "").trim();
|
|
1212
|
+
})
|
|
1213
|
+
.filter((n) => n && /^[A-Z]/.test(n));
|
|
1156
1214
|
for (const name of names) {
|
|
1157
1215
|
componentUsageMap.set(name, (componentUsageMap.get(name) || 0) + 1);
|
|
1158
1216
|
}
|
|
@@ -1177,15 +1235,21 @@ export class DriftAnalysisService {
|
|
|
1177
1235
|
*/
|
|
1178
1236
|
async scanHOCWrapperUsages(componentUsageMap) {
|
|
1179
1237
|
const cwd = this.projectRoot;
|
|
1180
|
-
const sourceFiles = await glob(
|
|
1238
|
+
const sourceFiles = await glob("**/*.{ts,tsx,js,jsx}", {
|
|
1181
1239
|
cwd,
|
|
1182
|
-
ignore: [
|
|
1240
|
+
ignore: [
|
|
1241
|
+
"**/node_modules/**",
|
|
1242
|
+
"**/dist/**",
|
|
1243
|
+
"**/build/**",
|
|
1244
|
+
"**/.next/**",
|
|
1245
|
+
"**/*.d.ts",
|
|
1246
|
+
],
|
|
1183
1247
|
nodir: true,
|
|
1184
1248
|
maxDepth: 8,
|
|
1185
1249
|
});
|
|
1186
1250
|
for (const file of sourceFiles.slice(0, 500)) {
|
|
1187
1251
|
try {
|
|
1188
|
-
const content = await readFile(resolve(cwd, file),
|
|
1252
|
+
const content = await readFile(resolve(cwd, file), "utf-8");
|
|
1189
1253
|
// forwardRef(Component) / React.forwardRef(Component)
|
|
1190
1254
|
const forwardRefPattern = /(?:React\.)?forwardRef\s*[(<]\s*(?:function\s+)?([A-Z][a-zA-Z0-9]*)/g;
|
|
1191
1255
|
let match;
|
|
@@ -1251,31 +1315,37 @@ export class DriftAnalysisService {
|
|
|
1251
1315
|
async scanPackageExports(componentUsageMap) {
|
|
1252
1316
|
const cwd = this.projectRoot;
|
|
1253
1317
|
try {
|
|
1254
|
-
const pkgContent = await readFile(resolve(cwd,
|
|
1318
|
+
const pkgContent = await readFile(resolve(cwd, "package.json"), "utf-8");
|
|
1255
1319
|
const pkg = JSON.parse(pkgContent);
|
|
1256
1320
|
// Detect if this is a component library by checking for:
|
|
1257
1321
|
// - exports field with ./ entries
|
|
1258
1322
|
// - main/module pointing to index file
|
|
1259
1323
|
// - "react" or "vue" in peerDependencies (library pattern)
|
|
1260
1324
|
const hasPeerReact = pkg.peerDependencies?.react || pkg.peerDependencies?.vue;
|
|
1261
|
-
const hasExports = pkg.exports && typeof pkg.exports ===
|
|
1262
|
-
const mainEntry = pkg.main || pkg.module ||
|
|
1325
|
+
const hasExports = pkg.exports && typeof pkg.exports === "object";
|
|
1326
|
+
const mainEntry = pkg.main || pkg.module || "";
|
|
1263
1327
|
const isLibraryPattern = hasPeerReact && (hasExports || mainEntry);
|
|
1264
1328
|
if (!isLibraryPattern)
|
|
1265
1329
|
return;
|
|
1266
1330
|
// Scan the root barrel file(s) for exports — these are the public API
|
|
1267
|
-
const rootBarrels = await glob(
|
|
1331
|
+
const rootBarrels = await glob("src/index.{ts,tsx,js,jsx}", {
|
|
1332
|
+
cwd,
|
|
1333
|
+
nodir: true,
|
|
1334
|
+
});
|
|
1268
1335
|
for (const barrel of rootBarrels) {
|
|
1269
1336
|
try {
|
|
1270
|
-
const content = await readFile(resolve(cwd, barrel),
|
|
1337
|
+
const content = await readFile(resolve(cwd, barrel), "utf-8");
|
|
1271
1338
|
// Named exports: export { Button, Card } from '...'
|
|
1272
1339
|
const namedPattern = /export\s*\{\s*([^}]+)\s*\}\s*from/g;
|
|
1273
1340
|
let match;
|
|
1274
1341
|
while ((match = namedPattern.exec(content)) !== null) {
|
|
1275
|
-
const names = match[1]
|
|
1342
|
+
const names = match[1]
|
|
1343
|
+
.split(",")
|
|
1344
|
+
.map((n) => {
|
|
1276
1345
|
const parts = n.trim().split(/\s+as\s+/);
|
|
1277
|
-
return (parts[1] || parts[0] ||
|
|
1278
|
-
})
|
|
1346
|
+
return (parts[1] || parts[0] || "").trim();
|
|
1347
|
+
})
|
|
1348
|
+
.filter((n) => n && /^[A-Z]/.test(n));
|
|
1279
1349
|
for (const name of names) {
|
|
1280
1350
|
componentUsageMap.set(name, (componentUsageMap.get(name) || 0) + 1);
|
|
1281
1351
|
}
|
|
@@ -1289,8 +1359,8 @@ export class DriftAnalysisService {
|
|
|
1289
1359
|
const wildcardPattern = /export\s*\*\s*from\s*['"]\.\/([^'"]+)['"]/g;
|
|
1290
1360
|
while ((match = wildcardPattern.exec(content)) !== null) {
|
|
1291
1361
|
const moduleName = match[1];
|
|
1292
|
-
const segments = moduleName.split(
|
|
1293
|
-
const last = segments[segments.length - 1] ||
|
|
1362
|
+
const segments = moduleName.split("/");
|
|
1363
|
+
const last = segments[segments.length - 1] || "";
|
|
1294
1364
|
if (/^[A-Z]/.test(last)) {
|
|
1295
1365
|
componentUsageMap.set(last, (componentUsageMap.get(last) || 0) + 1);
|
|
1296
1366
|
}
|
|
@@ -1321,9 +1391,15 @@ export class DriftAnalysisService {
|
|
|
1321
1391
|
if (knownComponents.length === 0)
|
|
1322
1392
|
return;
|
|
1323
1393
|
const cwd = this.projectRoot;
|
|
1324
|
-
const sourceFiles = await glob(
|
|
1394
|
+
const sourceFiles = await glob("**/*.{tsx,jsx,ts,js}", {
|
|
1325
1395
|
cwd,
|
|
1326
|
-
ignore: [
|
|
1396
|
+
ignore: [
|
|
1397
|
+
"**/node_modules/**",
|
|
1398
|
+
"**/dist/**",
|
|
1399
|
+
"**/build/**",
|
|
1400
|
+
"**/.next/**",
|
|
1401
|
+
"**/*.d.ts",
|
|
1402
|
+
],
|
|
1327
1403
|
nodir: true,
|
|
1328
1404
|
maxDepth: 8,
|
|
1329
1405
|
});
|
|
@@ -1331,7 +1407,7 @@ export class DriftAnalysisService {
|
|
|
1331
1407
|
const knownSet = new Set(knownComponents);
|
|
1332
1408
|
for (const file of sourceFiles.slice(0, 500)) {
|
|
1333
1409
|
try {
|
|
1334
|
-
const content = await readFile(resolve(cwd, file),
|
|
1410
|
+
const content = await readFile(resolve(cwd, file), "utf-8");
|
|
1335
1411
|
// Match component names used as values in these contexts:
|
|
1336
1412
|
// 1. JSX prop value: ={ComponentName} or ={ComponentName}
|
|
1337
1413
|
// 2. Object property: : ComponentName, or : ComponentName}
|
|
@@ -1361,7 +1437,12 @@ export class DriftAnalysisService {
|
|
|
1361
1437
|
const cwd = this.projectRoot;
|
|
1362
1438
|
// Find all source files
|
|
1363
1439
|
const patterns = ["**/*.tsx", "**/*.jsx", "**/*.vue", "**/*.svelte"];
|
|
1364
|
-
const ignore = [
|
|
1440
|
+
const ignore = [
|
|
1441
|
+
"**/node_modules/**",
|
|
1442
|
+
"**/dist/**",
|
|
1443
|
+
"**/.next/**",
|
|
1444
|
+
"**/build/**",
|
|
1445
|
+
];
|
|
1365
1446
|
const files = await glob(patterns, { cwd, ignore, absolute: true });
|
|
1366
1447
|
for (const file of files) {
|
|
1367
1448
|
try {
|