@buoy-design/cli 0.3.28 → 0.3.30
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/commands/check.d.ts.map +1 -1
- package/dist/commands/check.js +8 -4
- package/dist/commands/check.js.map +1 -1
- package/dist/commands/show.d.ts +1 -1
- package/dist/commands/show.d.ts.map +1 -1
- package/dist/commands/show.js +29 -19
- package/dist/commands/show.js.map +1 -1
- package/dist/services/drift-analysis.d.ts +10 -1
- package/dist/services/drift-analysis.d.ts.map +1 -1
- package/dist/services/drift-analysis.js +323 -19
- package/dist/services/drift-analysis.js.map +1 -1
- package/package.json +3 -3
|
@@ -112,11 +112,20 @@ export declare function applySeverityOverrides(drifts: DriftSignal[], overrides:
|
|
|
112
112
|
*/
|
|
113
113
|
export declare class DriftAnalysisService {
|
|
114
114
|
private config;
|
|
115
|
-
|
|
115
|
+
private projectRoot;
|
|
116
|
+
constructor(config: BuoyConfig, projectRoot?: string);
|
|
116
117
|
/**
|
|
117
118
|
* Run full drift analysis pipeline
|
|
118
119
|
*/
|
|
119
120
|
analyze(options?: DriftAnalysisOptions): Promise<DriftAnalysisResult>;
|
|
121
|
+
private getUsageCollectorGlobs;
|
|
122
|
+
private applyTailwindConfigAliasUsages;
|
|
123
|
+
private resolveTailwindSemanticAliasMap;
|
|
124
|
+
private findTailwindConfigFile;
|
|
125
|
+
private extractLocalImportSpecifiers;
|
|
126
|
+
private resolveLocalImport;
|
|
127
|
+
private extractTailwindColorAliasesFromSource;
|
|
128
|
+
private extractTailwindClassTokens;
|
|
120
129
|
/**
|
|
121
130
|
* Scan barrel files (index.ts) for re-exports and count re-exported components as used.
|
|
122
131
|
* Components re-exported from barrel files are part of the public API.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"drift-analysis.d.ts","sourceRoot":"","sources":["../../src/services/drift-analysis.ts"],"names":[],"mappings":"AACA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAC1E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAGtD,OAAO,EAAmB,SAAS,EAA6B,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"drift-analysis.d.ts","sourceRoot":"","sources":["../../src/services/drift-analysis.ts"],"names":[],"mappings":"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"}
|
|
@@ -15,7 +15,8 @@ import { detectRepeatedPatterns, checkVariantConsistency, checkExampleCompliance
|
|
|
15
15
|
import { glob } from "glob";
|
|
16
16
|
import { minimatch } from "minimatch";
|
|
17
17
|
import { readFile } from "fs/promises";
|
|
18
|
-
import {
|
|
18
|
+
import { existsSync } from "fs";
|
|
19
|
+
import { dirname, extname, resolve } from "path";
|
|
19
20
|
/**
|
|
20
21
|
* Severity order for filtering and sorting (0 = lowest, 2 = highest)
|
|
21
22
|
* Use getSeverityWeight from @buoy-design/core for consistent ordering
|
|
@@ -149,6 +150,118 @@ function ruleMatches(d, rule, onWarning) {
|
|
|
149
150
|
}
|
|
150
151
|
return true;
|
|
151
152
|
}
|
|
153
|
+
function extractTailwindSemanticTokenNameLocal(classToken) {
|
|
154
|
+
const core = classToken.split(":").pop()?.trim() ?? "";
|
|
155
|
+
if (!core)
|
|
156
|
+
return null;
|
|
157
|
+
const normalizedCore = core.replace(/^!/, "").replace(/^-/, "");
|
|
158
|
+
if (!normalizedCore)
|
|
159
|
+
return null;
|
|
160
|
+
const withoutOpacity = normalizedCore.replace(/\/[a-zA-Z0-9.[\]-]+$/, "");
|
|
161
|
+
const match = withoutOpacity.match(/^(?:bg|text|border|ring|fill|stroke|outline|caret|accent|from|via|to)-([a-zA-Z0-9_-]+)$/);
|
|
162
|
+
if (!match?.[1])
|
|
163
|
+
return null;
|
|
164
|
+
const value = match[1];
|
|
165
|
+
if (/^(transparent|current|black|white|inherit)$/.test(value) ||
|
|
166
|
+
/^[a-z]+-\d{2,3}$/.test(value) ||
|
|
167
|
+
/^\[[^\]]+\]$/.test(value)) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
return value;
|
|
171
|
+
}
|
|
172
|
+
function extractNamedObjectBlocks(content, key) {
|
|
173
|
+
const results = [];
|
|
174
|
+
const patterns = [
|
|
175
|
+
new RegExp(`${key}\\s*:\\s*\\{`, "g"),
|
|
176
|
+
new RegExp(`['"]${key}['"]\\s*:\\s*\\{`, "g"),
|
|
177
|
+
];
|
|
178
|
+
for (const pattern of patterns) {
|
|
179
|
+
let match;
|
|
180
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
181
|
+
const openBraceIndex = match.index + match[0].length - 1;
|
|
182
|
+
const block = extractBalancedBracesContent(content, openBraceIndex);
|
|
183
|
+
if (block != null)
|
|
184
|
+
results.push(block);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return results;
|
|
188
|
+
}
|
|
189
|
+
function extractBalancedBracesContent(content, openBraceIndex) {
|
|
190
|
+
if (content[openBraceIndex] !== "{")
|
|
191
|
+
return null;
|
|
192
|
+
let depth = 0;
|
|
193
|
+
for (let i = openBraceIndex; i < content.length; i++) {
|
|
194
|
+
const ch = content[i];
|
|
195
|
+
if (ch === "{")
|
|
196
|
+
depth++;
|
|
197
|
+
if (ch === "}")
|
|
198
|
+
depth--;
|
|
199
|
+
if (depth === 0) {
|
|
200
|
+
return content.slice(openBraceIndex + 1, i);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
function parseColorObjectAliases(colorsBlock) {
|
|
206
|
+
const aliases = new Map();
|
|
207
|
+
// First parse one-level nested scales (e.g., surface: { DEFAULT: "var(--surface)" ... })
|
|
208
|
+
const nestedPattern = /['"]?([a-zA-Z0-9_-]+)['"]?\s*:\s*\{/g;
|
|
209
|
+
let nestedMatch;
|
|
210
|
+
while ((nestedMatch = nestedPattern.exec(colorsBlock)) !== null) {
|
|
211
|
+
const parent = nestedMatch[1];
|
|
212
|
+
const openBraceIndex = nestedMatch.index + nestedMatch[0].length - 1;
|
|
213
|
+
const nestedContent = extractBalancedBracesContent(colorsBlock, openBraceIndex);
|
|
214
|
+
if (!nestedContent)
|
|
215
|
+
continue;
|
|
216
|
+
const nestedStart = openBraceIndex;
|
|
217
|
+
const nestedEnd = nestedStart + nestedContent.length + 2; // braces
|
|
218
|
+
const kvPattern = /['"]?([a-zA-Z0-9_-]+)['"]?\s*:\s*["'`]([^"'`]+)["'`]/g;
|
|
219
|
+
let kv;
|
|
220
|
+
while ((kv = kvPattern.exec(nestedContent)) !== null) {
|
|
221
|
+
const child = kv[1];
|
|
222
|
+
const value = kv[2];
|
|
223
|
+
const refs = extractCssVarRefs(value);
|
|
224
|
+
if (refs.length === 0)
|
|
225
|
+
continue;
|
|
226
|
+
const semantic = child === "DEFAULT" ? parent : `${parent}-${child}`;
|
|
227
|
+
for (const ref of refs)
|
|
228
|
+
addAlias(aliases, semantic, ref);
|
|
229
|
+
}
|
|
230
|
+
// Skip over nested object contents so top-level string scan doesn't double-process nested entries.
|
|
231
|
+
nestedPattern.lastIndex = nestedEnd;
|
|
232
|
+
}
|
|
233
|
+
// Top-level direct string colors (e.g., background: "var(--surface)")
|
|
234
|
+
const topLevelPattern = /['"]?([a-zA-Z0-9_-]+)['"]?\s*:\s*["'`]([^"'`]+)["'`]/g;
|
|
235
|
+
let top;
|
|
236
|
+
while ((top = topLevelPattern.exec(colorsBlock)) !== null) {
|
|
237
|
+
const semantic = top[1];
|
|
238
|
+
const value = top[2];
|
|
239
|
+
const refs = extractCssVarRefs(value);
|
|
240
|
+
if (refs.length === 0)
|
|
241
|
+
continue;
|
|
242
|
+
for (const ref of refs)
|
|
243
|
+
addAlias(aliases, semantic, ref);
|
|
244
|
+
}
|
|
245
|
+
return aliases;
|
|
246
|
+
}
|
|
247
|
+
function extractCssVarRefs(value) {
|
|
248
|
+
const refs = [];
|
|
249
|
+
const pattern = /var\(\s*--([a-zA-Z0-9_-]+)/g;
|
|
250
|
+
let match;
|
|
251
|
+
while ((match = pattern.exec(value)) !== null) {
|
|
252
|
+
if (match[1])
|
|
253
|
+
refs.push(match[1]);
|
|
254
|
+
}
|
|
255
|
+
return refs;
|
|
256
|
+
}
|
|
257
|
+
function addAlias(map, semantic, tokenName) {
|
|
258
|
+
let bucket = map.get(semantic);
|
|
259
|
+
if (!bucket) {
|
|
260
|
+
bucket = new Set();
|
|
261
|
+
map.set(semantic, bucket);
|
|
262
|
+
}
|
|
263
|
+
bucket.add(tokenName);
|
|
264
|
+
}
|
|
152
265
|
/**
|
|
153
266
|
* Apply severity filter to drifts
|
|
154
267
|
*/
|
|
@@ -211,8 +324,10 @@ function isEntryPointComponent(component) {
|
|
|
211
324
|
*/
|
|
212
325
|
export class DriftAnalysisService {
|
|
213
326
|
config;
|
|
214
|
-
|
|
327
|
+
projectRoot;
|
|
328
|
+
constructor(config, projectRoot = process.cwd()) {
|
|
215
329
|
this.config = config;
|
|
330
|
+
this.projectRoot = projectRoot;
|
|
216
331
|
}
|
|
217
332
|
/**
|
|
218
333
|
* Run full drift analysis pipeline
|
|
@@ -221,7 +336,7 @@ export class DriftAnalysisService {
|
|
|
221
336
|
const { onProgress, includeIgnored, minSeverity, filterType, cache, checkVariants, checkTokenUtilities, checkExamples, } = options;
|
|
222
337
|
// Step 1: Scan components
|
|
223
338
|
onProgress?.("Scanning components...");
|
|
224
|
-
const orchestrator = new ScanOrchestrator(this.config,
|
|
339
|
+
const orchestrator = new ScanOrchestrator(this.config, this.projectRoot, {
|
|
225
340
|
cache,
|
|
226
341
|
});
|
|
227
342
|
const { components } = await orchestrator.scanComponents({
|
|
@@ -245,7 +360,7 @@ export class DriftAnalysisService {
|
|
|
245
360
|
// Step 2.2: Check framework sprawl
|
|
246
361
|
onProgress?.("Checking for framework sprawl...");
|
|
247
362
|
const { ProjectDetector } = await import("../detect/project-detector.js");
|
|
248
|
-
const detector = new ProjectDetector(
|
|
363
|
+
const detector = new ProjectDetector(this.projectRoot);
|
|
249
364
|
const projectInfo = await detector.detect();
|
|
250
365
|
if (projectInfo.frameworks.length > 0) {
|
|
251
366
|
const sprawlDrift = engine.checkFrameworkSprawl(projectInfo.frameworks.map((f) => ({ name: f.name, version: f.version })));
|
|
@@ -257,10 +372,13 @@ export class DriftAnalysisService {
|
|
|
257
372
|
// Step 2.3: Scan for unused components and tokens
|
|
258
373
|
onProgress?.("Scanning for unused components and tokens...");
|
|
259
374
|
const { collectUsages } = await import("@buoy-design/core");
|
|
375
|
+
const usageGlobs = this.getUsageCollectorGlobs();
|
|
260
376
|
const componentNames = components.map((c) => c.name);
|
|
261
377
|
const tokenNames = scannedTokens.map((t) => t.name);
|
|
262
378
|
const usageResult = await collectUsages({
|
|
263
|
-
projectRoot:
|
|
379
|
+
projectRoot: this.projectRoot,
|
|
380
|
+
include: usageGlobs.include,
|
|
381
|
+
exclude: usageGlobs.exclude,
|
|
264
382
|
knownComponents: componentNames,
|
|
265
383
|
knownTokens: tokenNames,
|
|
266
384
|
});
|
|
@@ -273,6 +391,9 @@ export class DriftAnalysisService {
|
|
|
273
391
|
for (const tu of usageResult.tokenUsages) {
|
|
274
392
|
tokenUsageMap.set(tu.tokenName, (tokenUsageMap.get(tu.tokenName) || 0) + 1);
|
|
275
393
|
}
|
|
394
|
+
// Tailwind semantic aliases may map class names to differently named CSS vars
|
|
395
|
+
// via imported presets (e.g., bg-background -> var(--surface)).
|
|
396
|
+
await this.applyTailwindConfigAliasUsages(tokenUsageMap);
|
|
276
397
|
// Fix 2: Count barrel file re-exports as usage
|
|
277
398
|
// Components re-exported from barrel files (index.ts) are part of the public API
|
|
278
399
|
await this.scanBarrelReExports(componentUsageMap);
|
|
@@ -348,7 +469,7 @@ export class DriftAnalysisService {
|
|
|
348
469
|
if (this.config.sources.tailwind?.enabled) {
|
|
349
470
|
onProgress?.("Scanning for Tailwind arbitrary values...");
|
|
350
471
|
const tailwindScanner = new TailwindScanner({
|
|
351
|
-
projectRoot:
|
|
472
|
+
projectRoot: this.projectRoot,
|
|
352
473
|
include: this.config.sources.tailwind.files,
|
|
353
474
|
exclude: this.config.sources.tailwind.exclude,
|
|
354
475
|
detectArbitraryValues: true,
|
|
@@ -449,12 +570,195 @@ export class DriftAnalysisService {
|
|
|
449
570
|
summary: calculateDriftSummary(drifts),
|
|
450
571
|
};
|
|
451
572
|
}
|
|
573
|
+
getUsageCollectorGlobs() {
|
|
574
|
+
const include = new Set();
|
|
575
|
+
const exclude = new Set();
|
|
576
|
+
const addMany = (target, values) => {
|
|
577
|
+
if (!values)
|
|
578
|
+
return;
|
|
579
|
+
for (const value of values) {
|
|
580
|
+
if (value?.trim())
|
|
581
|
+
target.add(value);
|
|
582
|
+
}
|
|
583
|
+
};
|
|
584
|
+
const { react, nextjs, vue, svelte, angular, webcomponent, templates, tailwind, tokens, } = this.config.sources;
|
|
585
|
+
for (const source of [react, nextjs, vue, svelte, angular, webcomponent, templates]) {
|
|
586
|
+
if (!source?.enabled)
|
|
587
|
+
continue;
|
|
588
|
+
addMany(include, source.include);
|
|
589
|
+
addMany(exclude, source.exclude);
|
|
590
|
+
}
|
|
591
|
+
if (tailwind?.enabled) {
|
|
592
|
+
addMany(include, tailwind.files);
|
|
593
|
+
addMany(exclude, tailwind.exclude);
|
|
594
|
+
}
|
|
595
|
+
if (tokens?.enabled) {
|
|
596
|
+
addMany(include, tokens.files);
|
|
597
|
+
}
|
|
598
|
+
return {
|
|
599
|
+
include: include.size > 0 ? [...include] : undefined,
|
|
600
|
+
exclude: exclude.size > 0 ? [...exclude] : undefined,
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
async applyTailwindConfigAliasUsages(tokenUsageMap) {
|
|
604
|
+
if (!this.config.sources.tailwind?.enabled)
|
|
605
|
+
return;
|
|
606
|
+
const aliasMap = await this.resolveTailwindSemanticAliasMap();
|
|
607
|
+
if (aliasMap.size === 0)
|
|
608
|
+
return;
|
|
609
|
+
const usageGlobs = this.getUsageCollectorGlobs();
|
|
610
|
+
const include = usageGlobs.include ?? ["**/*.{ts,tsx,js,jsx,vue,svelte,astro,html,mdx,md}"];
|
|
611
|
+
const exclude = usageGlobs.exclude ?? ["**/node_modules/**", "**/dist/**", "**/build/**", "**/.next/**"];
|
|
612
|
+
const files = await glob(include, {
|
|
613
|
+
cwd: this.projectRoot,
|
|
614
|
+
ignore: exclude,
|
|
615
|
+
absolute: false,
|
|
616
|
+
nodir: true,
|
|
617
|
+
});
|
|
618
|
+
for (const file of files) {
|
|
619
|
+
let content = "";
|
|
620
|
+
try {
|
|
621
|
+
content = await readFile(resolve(this.projectRoot, file), "utf-8");
|
|
622
|
+
}
|
|
623
|
+
catch {
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
|
+
for (const classToken of this.extractTailwindClassTokens(content)) {
|
|
627
|
+
const semantic = extractTailwindSemanticTokenNameLocal(classToken);
|
|
628
|
+
if (!semantic)
|
|
629
|
+
continue;
|
|
630
|
+
const mapped = aliasMap.get(semantic);
|
|
631
|
+
if (!mapped || mapped.size === 0)
|
|
632
|
+
continue;
|
|
633
|
+
for (const tokenName of mapped) {
|
|
634
|
+
tokenUsageMap.set(tokenName, (tokenUsageMap.get(tokenName) || 0) + 1);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
async resolveTailwindSemanticAliasMap() {
|
|
640
|
+
const configFile = this.findTailwindConfigFile();
|
|
641
|
+
if (!configFile)
|
|
642
|
+
return new Map();
|
|
643
|
+
const visited = new Set();
|
|
644
|
+
const queue = [configFile];
|
|
645
|
+
const aliases = new Map();
|
|
646
|
+
while (queue.length > 0 && visited.size < 64) {
|
|
647
|
+
const filePath = queue.shift();
|
|
648
|
+
if (visited.has(filePath))
|
|
649
|
+
continue;
|
|
650
|
+
visited.add(filePath);
|
|
651
|
+
let content = "";
|
|
652
|
+
try {
|
|
653
|
+
content = await readFile(filePath, "utf-8");
|
|
654
|
+
}
|
|
655
|
+
catch {
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
this.extractTailwindColorAliasesFromSource(content, aliases);
|
|
659
|
+
for (const specifier of this.extractLocalImportSpecifiers(content)) {
|
|
660
|
+
const resolved = this.resolveLocalImport(filePath, specifier);
|
|
661
|
+
if (resolved && !visited.has(resolved)) {
|
|
662
|
+
queue.push(resolved);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
return aliases;
|
|
667
|
+
}
|
|
668
|
+
findTailwindConfigFile() {
|
|
669
|
+
for (const name of [
|
|
670
|
+
"tailwind.config.ts",
|
|
671
|
+
"tailwind.config.js",
|
|
672
|
+
"tailwind.config.mjs",
|
|
673
|
+
"tailwind.config.cjs",
|
|
674
|
+
]) {
|
|
675
|
+
const path = resolve(this.projectRoot, name);
|
|
676
|
+
if (existsSync(path))
|
|
677
|
+
return path;
|
|
678
|
+
}
|
|
679
|
+
return null;
|
|
680
|
+
}
|
|
681
|
+
extractLocalImportSpecifiers(content) {
|
|
682
|
+
const out = new Set();
|
|
683
|
+
const patterns = [
|
|
684
|
+
/import[\s\S]*?\sfrom\s*["']([^"']+)["']/g,
|
|
685
|
+
/(?:export\s+\{[^}]*\}\s+from|export\s+\*\s+from)\s*["']([^"']+)["']/g,
|
|
686
|
+
/require\(\s*["']([^"']+)["']\s*\)/g,
|
|
687
|
+
];
|
|
688
|
+
for (const pattern of patterns) {
|
|
689
|
+
let match;
|
|
690
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
691
|
+
const spec = match[1];
|
|
692
|
+
if (spec?.startsWith("."))
|
|
693
|
+
out.add(spec);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
return [...out];
|
|
697
|
+
}
|
|
698
|
+
resolveLocalImport(fromFile, specifier) {
|
|
699
|
+
const base = resolve(dirname(fromFile), specifier);
|
|
700
|
+
const candidates = extname(base)
|
|
701
|
+
? [base]
|
|
702
|
+
: [
|
|
703
|
+
base,
|
|
704
|
+
`${base}.ts`,
|
|
705
|
+
`${base}.tsx`,
|
|
706
|
+
`${base}.js`,
|
|
707
|
+
`${base}.mjs`,
|
|
708
|
+
`${base}.cjs`,
|
|
709
|
+
resolve(base, "index.ts"),
|
|
710
|
+
resolve(base, "index.tsx"),
|
|
711
|
+
resolve(base, "index.js"),
|
|
712
|
+
resolve(base, "index.mjs"),
|
|
713
|
+
resolve(base, "index.cjs"),
|
|
714
|
+
];
|
|
715
|
+
for (const candidate of candidates) {
|
|
716
|
+
if (existsSync(candidate))
|
|
717
|
+
return candidate;
|
|
718
|
+
}
|
|
719
|
+
return null;
|
|
720
|
+
}
|
|
721
|
+
extractTailwindColorAliasesFromSource(content, aliases) {
|
|
722
|
+
for (const colorsBlock of extractNamedObjectBlocks(content, "colors")) {
|
|
723
|
+
const parsed = parseColorObjectAliases(colorsBlock);
|
|
724
|
+
for (const [semantic, tokenNames] of parsed) {
|
|
725
|
+
let bucket = aliases.get(semantic);
|
|
726
|
+
if (!bucket) {
|
|
727
|
+
bucket = new Set();
|
|
728
|
+
aliases.set(semantic, bucket);
|
|
729
|
+
}
|
|
730
|
+
for (const tokenName of tokenNames)
|
|
731
|
+
bucket.add(tokenName);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
extractTailwindClassTokens(content) {
|
|
736
|
+
const classTokens = new Set();
|
|
737
|
+
// Generic string literal scan catches JSX, Astro, template literals, and utility wrappers.
|
|
738
|
+
const stringPattern = /(['"`])((?:\\.|(?!\1)[\s\S])*)\1/g;
|
|
739
|
+
let match;
|
|
740
|
+
while ((match = stringPattern.exec(content)) !== null) {
|
|
741
|
+
const raw = match[2];
|
|
742
|
+
if (!raw || (!raw.includes("-") && !raw.includes(":")))
|
|
743
|
+
continue;
|
|
744
|
+
for (const token of raw.split(/\s+/)) {
|
|
745
|
+
if (token)
|
|
746
|
+
classTokens.add(token);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
// Also include extractor output for nested utility helper patterns.
|
|
750
|
+
for (const extracted of extractStaticClassStrings(content)) {
|
|
751
|
+
for (const token of extracted.classes)
|
|
752
|
+
classTokens.add(token);
|
|
753
|
+
}
|
|
754
|
+
return [...classTokens];
|
|
755
|
+
}
|
|
452
756
|
/**
|
|
453
757
|
* Scan barrel files (index.ts) for re-exports and count re-exported components as used.
|
|
454
758
|
* Components re-exported from barrel files are part of the public API.
|
|
455
759
|
*/
|
|
456
760
|
async scanBarrelReExports(componentUsageMap) {
|
|
457
|
-
const cwd =
|
|
761
|
+
const cwd = this.projectRoot;
|
|
458
762
|
const barrelFiles = await glob('**/index.{ts,tsx,js,jsx}', {
|
|
459
763
|
cwd,
|
|
460
764
|
ignore: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/.next/**'],
|
|
@@ -505,7 +809,7 @@ export class DriftAnalysisService {
|
|
|
505
809
|
* Handles React.lazy(() => import('./Component')) and next/dynamic patterns.
|
|
506
810
|
*/
|
|
507
811
|
async scanDynamicImports(componentUsageMap) {
|
|
508
|
-
const cwd =
|
|
812
|
+
const cwd = this.projectRoot;
|
|
509
813
|
const sourceFiles = await glob('**/*.{tsx,jsx,ts,js}', {
|
|
510
814
|
cwd,
|
|
511
815
|
ignore: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/.next/**', '**/*.test.*', '**/*.spec.*'],
|
|
@@ -542,7 +846,7 @@ export class DriftAnalysisService {
|
|
|
542
846
|
async scanTemplateComponentUsage(componentUsageMap, knownComponents) {
|
|
543
847
|
if (knownComponents.length === 0)
|
|
544
848
|
return;
|
|
545
|
-
const cwd =
|
|
849
|
+
const cwd = this.projectRoot;
|
|
546
850
|
const templateFiles = await glob('**/*.{vue,svelte,html}', {
|
|
547
851
|
cwd,
|
|
548
852
|
ignore: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/.next/**'],
|
|
@@ -591,7 +895,7 @@ export class DriftAnalysisService {
|
|
|
591
895
|
* Detects app.component('Name', ...) and Vue.component('Name', ...) calls.
|
|
592
896
|
*/
|
|
593
897
|
async scanAutoRegistration(componentUsageMap) {
|
|
594
|
-
const cwd =
|
|
898
|
+
const cwd = this.projectRoot;
|
|
595
899
|
const sourceFiles = await glob('**/*.{ts,js,tsx,jsx}', {
|
|
596
900
|
cwd,
|
|
597
901
|
ignore: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/.next/**'],
|
|
@@ -621,7 +925,7 @@ export class DriftAnalysisService {
|
|
|
621
925
|
* Components in declarations: [...] are registered and should count as used.
|
|
622
926
|
*/
|
|
623
927
|
async scanNgModuleDeclarations(componentUsageMap) {
|
|
624
|
-
const cwd =
|
|
928
|
+
const cwd = this.projectRoot;
|
|
625
929
|
const moduleFiles = await glob('**/*.module.ts', {
|
|
626
930
|
cwd,
|
|
627
931
|
ignore: ['**/node_modules/**', '**/dist/**', '**/build/**'],
|
|
@@ -666,7 +970,7 @@ export class DriftAnalysisService {
|
|
|
666
970
|
* A component referenced in a .stories file is documented/tested and should count as used.
|
|
667
971
|
*/
|
|
668
972
|
async scanStoryFileUsages(componentUsageMap) {
|
|
669
|
-
const cwd =
|
|
973
|
+
const cwd = this.projectRoot;
|
|
670
974
|
const storyFiles = await glob('**/*.stories.{ts,tsx,js,jsx}', {
|
|
671
975
|
cwd,
|
|
672
976
|
ignore: ['**/node_modules/**', '**/dist/**', '**/build/**'],
|
|
@@ -712,7 +1016,7 @@ export class DriftAnalysisService {
|
|
|
712
1016
|
* mean the component is registered and used by the browser.
|
|
713
1017
|
*/
|
|
714
1018
|
async scanWebComponentRegistrations(componentUsageMap) {
|
|
715
|
-
const cwd =
|
|
1019
|
+
const cwd = this.projectRoot;
|
|
716
1020
|
const files = await glob('**/*.{ts,js}', {
|
|
717
1021
|
cwd,
|
|
718
1022
|
ignore: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/*.d.ts', '**/*.spec.*', '**/*.test.*'],
|
|
@@ -748,7 +1052,7 @@ export class DriftAnalysisService {
|
|
|
748
1052
|
* If we detect Nuxt (nuxt.config.ts exists), mark all components in components/ as used.
|
|
749
1053
|
*/
|
|
750
1054
|
async scanNuxtAutoImports(componentUsageMap, componentNames) {
|
|
751
|
-
const cwd =
|
|
1055
|
+
const cwd = this.projectRoot;
|
|
752
1056
|
// Check for Nuxt config
|
|
753
1057
|
const nuxtConfigs = await glob('nuxt.config.{ts,js,mjs}', { cwd, nodir: true });
|
|
754
1058
|
if (nuxtConfigs.length === 0)
|
|
@@ -764,7 +1068,7 @@ export class DriftAnalysisService {
|
|
|
764
1068
|
* Components imported in .test.tsx/.spec.tsx are actively maintained/tested.
|
|
765
1069
|
*/
|
|
766
1070
|
async scanTestFileUsages(componentUsageMap) {
|
|
767
|
-
const cwd =
|
|
1071
|
+
const cwd = this.projectRoot;
|
|
768
1072
|
const testFiles = await glob('**/*.{test,spec}.{ts,tsx,js,jsx}', {
|
|
769
1073
|
cwd,
|
|
770
1074
|
ignore: ['**/node_modules/**', '**/dist/**', '**/build/**'],
|
|
@@ -805,7 +1109,7 @@ export class DriftAnalysisService {
|
|
|
805
1109
|
* compound component patterns like Component.Sub = SubComponent.
|
|
806
1110
|
*/
|
|
807
1111
|
async scanHOCWrapperUsages(componentUsageMap) {
|
|
808
|
-
const cwd =
|
|
1112
|
+
const cwd = this.projectRoot;
|
|
809
1113
|
const sourceFiles = await glob('**/*.{ts,tsx,js,jsx}', {
|
|
810
1114
|
cwd,
|
|
811
1115
|
ignore: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/.next/**', '**/*.d.ts'],
|
|
@@ -878,7 +1182,7 @@ export class DriftAnalysisService {
|
|
|
878
1182
|
* all components re-exported from that barrel are the product's public API.
|
|
879
1183
|
*/
|
|
880
1184
|
async scanPackageExports(componentUsageMap) {
|
|
881
|
-
const cwd =
|
|
1185
|
+
const cwd = this.projectRoot;
|
|
882
1186
|
try {
|
|
883
1187
|
const pkgContent = await readFile(resolve(cwd, 'package.json'), 'utf-8');
|
|
884
1188
|
const pkg = JSON.parse(pkgContent);
|
|
@@ -949,7 +1253,7 @@ export class DriftAnalysisService {
|
|
|
949
1253
|
async scanComponentAsValueUsages(componentUsageMap, knownComponents) {
|
|
950
1254
|
if (knownComponents.length === 0)
|
|
951
1255
|
return;
|
|
952
|
-
const cwd =
|
|
1256
|
+
const cwd = this.projectRoot;
|
|
953
1257
|
const sourceFiles = await glob('**/*.{tsx,jsx,ts,js}', {
|
|
954
1258
|
cwd,
|
|
955
1259
|
ignore: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/.next/**', '**/*.d.ts'],
|
|
@@ -987,7 +1291,7 @@ export class DriftAnalysisService {
|
|
|
987
1291
|
*/
|
|
988
1292
|
async detectRepeatedPatterns(config) {
|
|
989
1293
|
const occurrences = [];
|
|
990
|
-
const cwd =
|
|
1294
|
+
const cwd = this.projectRoot;
|
|
991
1295
|
// Find all source files
|
|
992
1296
|
const patterns = ["**/*.tsx", "**/*.jsx", "**/*.vue", "**/*.svelte"];
|
|
993
1297
|
const ignore = ["**/node_modules/**", "**/dist/**", "**/.next/**", "**/build/**"];
|