@controlfront/detect 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/bin/cfb.js +202 -0
  2. package/package.json +64 -0
  3. package/src/commands/baseline.js +198 -0
  4. package/src/commands/init.js +309 -0
  5. package/src/commands/login.js +71 -0
  6. package/src/commands/logout.js +44 -0
  7. package/src/commands/scan.js +1547 -0
  8. package/src/commands/snapshot.js +191 -0
  9. package/src/commands/sync.js +127 -0
  10. package/src/config/baseUrl.js +49 -0
  11. package/src/data/tailwind-core-spec.js +149 -0
  12. package/src/engine/runRules.js +210 -0
  13. package/src/lib/collectDeclaredTokensAuto.js +67 -0
  14. package/src/lib/collectTokenMatches.js +330 -0
  15. package/src/lib/collectTokenMatches.js.regex +252 -0
  16. package/src/lib/loadRules.js +73 -0
  17. package/src/rules/core/no-hardcoded-colors.js +28 -0
  18. package/src/rules/core/no-hardcoded-spacing.js +29 -0
  19. package/src/rules/core/no-inline-styles.js +28 -0
  20. package/src/utils/authorId.js +106 -0
  21. package/src/utils/buildAIContributions.js +224 -0
  22. package/src/utils/buildBlameData.js +388 -0
  23. package/src/utils/buildDeclaredCssVars.js +185 -0
  24. package/src/utils/buildDeclaredJson.js +214 -0
  25. package/src/utils/buildFileChanges.js +372 -0
  26. package/src/utils/buildRuntimeUsage.js +337 -0
  27. package/src/utils/detectDeclaredDrift.js +59 -0
  28. package/src/utils/extractImports.js +178 -0
  29. package/src/utils/fileExtensions.js +65 -0
  30. package/src/utils/generateInsights.js +332 -0
  31. package/src/utils/getAllFiles.js +63 -0
  32. package/src/utils/getCommitMetaData.js +102 -0
  33. package/src/utils/getLine.js +14 -0
  34. package/src/utils/resolveProjectForFolder/index.js +47 -0
  35. package/src/utils/twClassify.js +138 -0
@@ -0,0 +1,337 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { classifyTailwindClass } from "./twClassify.js";
4
+ import { parse } from "@babel/parser";
5
+ import traverseModule from "@babel/traverse";
6
+ const traverse = traverseModule.default;
7
+
8
+ const DEBUG = false;
9
+
10
+ /**
11
+ * Recursively scans files and extracts runtime token usage from classNames, classes, CSS var references, and inline styles.
12
+ * Returns an array of objects:
13
+ * { type: "className" | "css-var" | "inline-style", name: string, file: string, ... }
14
+ */
15
+ function camelToKebab(str) {
16
+ return str.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
17
+ }
18
+
19
+ // Note: File discovery now happens in scan.js and files are passed in directly.
20
+ export async function buildRuntimeUsage({ projectRoot, files }) {
21
+ if (!files?.length) return [];
22
+
23
+ const tokenUsages = [];
24
+ const classRegex = /class(?:Name)?=["'`]([^"'`]+)["'`]/g;
25
+ const cssVarRegex = /var\(--([\w-]+)\)/g;
26
+
27
+ for (const file of files) {
28
+ let content;
29
+ try {
30
+ content = await fs.promises.readFile(file, "utf8");
31
+ } catch {
32
+ continue;
33
+ }
34
+
35
+ // Detect styled-components or emotion usage
36
+ let detectedSource = "unknown";
37
+ if (/import\s+styled\s+from\s+['"]styled-components['"]/.test(content)) {
38
+ detectedSource = "styled-components";
39
+ } else if (/import\s+styled\s+from\s+['"]@emotion\/styled['"]/.test(content)) {
40
+ detectedSource = "emotion";
41
+ }
42
+
43
+ // Extract Tailwind or token-like classes
44
+ // Helper to extract content inside balanced brackets (handles nesting)
45
+ function extractBalanced(source, startIndex, openChar = '[', closeChar = ']') {
46
+ let depth = 0, start = -1;
47
+ for (let i = startIndex; i < source.length; i++) {
48
+ const ch = source[i];
49
+ if (ch === openChar) {
50
+ if (start === -1) start = i;
51
+ depth++;
52
+ } else if (ch === closeChar) {
53
+ depth--;
54
+ if (depth === 0) return { block: source.slice(start + 1, i), end: i };
55
+ }
56
+ }
57
+ return null;
58
+ }
59
+
60
+ let classMatch;
61
+ while ((classMatch = classRegex.exec(content))) {
62
+ const classString = classMatch[1];
63
+ const fullMatch = classMatch[0]; // entire matched attribute string, e.g. className="..."
64
+ const classes = [];
65
+ let buffer = '';
66
+ let i = 0;
67
+ while (i < classString.length) {
68
+ const ch = classString[i];
69
+ if (ch === '[') {
70
+ const balanced = extractBalanced(classString, i);
71
+ if (balanced) {
72
+ buffer += `[${balanced.block}]`;
73
+ i = balanced.end + 1;
74
+ continue;
75
+ }
76
+ }
77
+ if (/\s/.test(ch)) {
78
+ if (buffer.trim()) classes.push(buffer.trim());
79
+ buffer = '';
80
+ } else {
81
+ buffer += ch;
82
+ }
83
+ i++;
84
+ }
85
+ if (buffer.trim()) classes.push(buffer.trim());
86
+
87
+ // Calculate line number for classMatch
88
+ const line = content.slice(0, classMatch.index).split('\n').length;
89
+
90
+ for (const cls of classes) {
91
+ const classification = classifyTailwindClass(cls);
92
+ tokenUsages.push({
93
+ type: "className",
94
+ name: cls,
95
+ file,
96
+ classification: classification.classification,
97
+ isTailwindDefault: classification.isTailwindDefault,
98
+ tokenBacked: classification.tokenBacked,
99
+ line,
100
+ instance: fullMatch,
101
+ });
102
+ }
103
+ }
104
+
105
+ // Extract CSS var usages
106
+ let varMatch;
107
+ while ((varMatch = cssVarRegex.exec(content))) {
108
+ const line = content.slice(0, varMatch.index).split('\n').length;
109
+ tokenUsages.push({ type: "css-var", name: `--${varMatch[1]}`, file, line });
110
+ }
111
+
112
+ // Extract inline styles using AST
113
+ try {
114
+ const ast = parse(content, {
115
+ sourceType: "module",
116
+ plugins: ["jsx", "typescript"],
117
+ locations: true,
118
+ });
119
+
120
+ traverse(ast, {
121
+ JSXAttribute(path) {
122
+ if (path.node.name?.name === "style" && path.node.value?.expression?.type === "ObjectExpression") {
123
+ const objExpr = path.node.value.expression;
124
+ const props = objExpr.properties;
125
+ const stylePairs = [];
126
+ for (const prop of props) {
127
+ if (!prop.key) continue;
128
+ const keyName = prop.key.name || prop.key.value;
129
+ if (!keyName) continue;
130
+ const key = camelToKebab(keyName);
131
+ let value = null;
132
+ if (prop.value.type === "StringLiteral") {
133
+ value = prop.value.value;
134
+ } else if (prop.value.type === "NumericLiteral") {
135
+ value = `${prop.value.value}px`;
136
+ } else if (prop.value.type === "TemplateLiteral") {
137
+ value = prop.value.quasis.map((q) => q.value.raw).join("");
138
+ } else if (prop.value.type === "Identifier") {
139
+ value = prop.value.name;
140
+ }
141
+ if (key && value) {
142
+ stylePairs.push(`${key}: ${value}`);
143
+ }
144
+ }
145
+ const instanceString = `style={{ ${stylePairs.join(", ")} }}`;
146
+ for (const prop of props) {
147
+ if (!prop.key) continue;
148
+ const keyName = prop.key.name || prop.key.value;
149
+ if (!keyName) continue;
150
+ const key = camelToKebab(keyName);
151
+ let value = null;
152
+ if (prop.value.type === "StringLiteral") {
153
+ value = prop.value.value;
154
+ } else if (prop.value.type === "NumericLiteral") {
155
+ value = `${prop.value.value}px`;
156
+ } else if (prop.value.type === "TemplateLiteral") {
157
+ value = prop.value.quasis.map((q) => q.value.raw).join("");
158
+ } else if (prop.value.type === "Identifier") {
159
+ value = prop.value.name;
160
+ }
161
+ if (key && value) {
162
+ const cssVarMatch = value.match(/var\(--([\w-]+)\)/);
163
+ if (cssVarMatch) {
164
+ tokenUsages.push({
165
+ type: "css-var",
166
+ name: `--${cssVarMatch[1]}`,
167
+ file,
168
+ line: path.node.loc?.start?.line || null,
169
+ });
170
+ } else {
171
+ tokenUsages.push({
172
+ type: "inline-style",
173
+ name: key,
174
+ value,
175
+ file,
176
+ line: path.node.loc?.start?.line || null,
177
+ instance: instanceString,
178
+ });
179
+ }
180
+ }
181
+ }
182
+ }
183
+ },
184
+ ObjectExpression(path) {
185
+ const parent = path.parent;
186
+ if (
187
+ parent &&
188
+ parent.type === "CallExpression" &&
189
+ parent.callee &&
190
+ parent.callee.object &&
191
+ parent.callee.object.name === "styled"
192
+ ) {
193
+ const props = path.node.properties;
194
+ const stylePairs = [];
195
+ for (const prop of props) {
196
+ if (!prop.key) continue;
197
+ const keyName = prop.key.name || prop.key.value;
198
+ if (!keyName) continue;
199
+ const key = camelToKebab(keyName);
200
+ let value = null;
201
+ if (prop.value.type === "StringLiteral") {
202
+ value = prop.value.value;
203
+ } else if (prop.value.type === "NumericLiteral") {
204
+ value = `${prop.value.value}px`;
205
+ } else if (prop.value.type === "TemplateLiteral") {
206
+ value = prop.value.quasis.map((q) => q.value.raw).join("");
207
+ } else if (prop.value.type === "Identifier") {
208
+ value = prop.value.name;
209
+ }
210
+ if (key && value) {
211
+ stylePairs.push(`${key}: ${value}`);
212
+ }
213
+ }
214
+ const instanceString = `styled.div({ ${stylePairs.join(", ")} })`;
215
+ for (const prop of props) {
216
+ if (!prop.key) continue;
217
+ const keyName = prop.key.name || prop.key.value;
218
+ if (!keyName) continue;
219
+ const key = camelToKebab(keyName);
220
+ let value = null;
221
+ if (prop.value.type === "StringLiteral") {
222
+ value = prop.value.value;
223
+ } else if (prop.value.type === "NumericLiteral") {
224
+ value = `${prop.value.value}px`;
225
+ } else if (prop.value.type === "TemplateLiteral") {
226
+ value = prop.value.quasis.map((q) => q.value.raw).join("");
227
+ } else if (prop.value.type === "Identifier") {
228
+ value = prop.value.name;
229
+ }
230
+ if (key && value) {
231
+ let sourceLabel = detectedSource === "styled-components"
232
+ ? "styled-components"
233
+ : detectedSource === "emotion"
234
+ ? "emotion"
235
+ : "unknown";
236
+ tokenUsages.push({
237
+ type: "inline-style",
238
+ name: key,
239
+ value,
240
+ file,
241
+ source: sourceLabel,
242
+ line: path.node.loc?.start?.line || null,
243
+ instance: instanceString,
244
+ });
245
+ }
246
+ }
247
+ }
248
+ },
249
+ TaggedTemplateExpression(path) {
250
+ const tag = path.node.tag;
251
+ if (
252
+ tag.type === "MemberExpression" &&
253
+ tag.object &&
254
+ tag.object.name === "styled"
255
+ ) {
256
+ const template = path.node.quasi.quasis.map(q => q.value.raw).join("");
257
+ const regex = /([\w-]+)\s*:\s*([^;]+);?/g;
258
+ let match;
259
+ const stylePairs = [];
260
+ while ((match = regex.exec(template))) {
261
+ const key = match[1];
262
+ const value = match[2].trim();
263
+ stylePairs.push(`${key}: ${value}`);
264
+ }
265
+ const instanceString = `styled.div\`\n${template.trim()}\``;
266
+ let match2;
267
+ regex.lastIndex = 0;
268
+ while ((match2 = regex.exec(template))) {
269
+ const key = match2[1];
270
+ const value = match2[2].trim();
271
+ let sourceLabel = detectedSource === "styled-components"
272
+ ? "styled-components"
273
+ : detectedSource === "emotion"
274
+ ? "emotion-template"
275
+ : "unknown";
276
+ tokenUsages.push({
277
+ type: "inline-style",
278
+ name: key,
279
+ value,
280
+ file,
281
+ source: sourceLabel,
282
+ line: path.node.loc?.start?.line || null,
283
+ instance: instanceString,
284
+ });
285
+ }
286
+ }
287
+ },
288
+ });
289
+ } catch (err) {
290
+ if (DEBUG) console.warn(`⚠️ AST parse failed for ${file}:`, err.message);
291
+ }
292
+
293
+ // Step 4: Detect Tailwind inline overrides in runtime usage
294
+ // Look for @theme inline { ... } blocks and extract --var-name: value; pairs
295
+ const themeInlineRegex = /@theme\s+inline\s*{([^}]+)}/g;
296
+ let themeInlineMatch;
297
+ while ((themeInlineMatch = themeInlineRegex.exec(content))) {
298
+ const block = themeInlineMatch[1];
299
+ const line = content.slice(0, themeInlineMatch.index).split('\n').length;
300
+ // Find all --var-name: value; inside the block
301
+ const varDeclRegex = /(--[\w-]+)\s*:\s*([^;]+);/g;
302
+ let varDeclMatch;
303
+ while ((varDeclMatch = varDeclRegex.exec(block))) {
304
+ const varName = varDeclMatch[1];
305
+ const value = varDeclMatch[2];
306
+ tokenUsages.push({
307
+ type: "tailwind-inline",
308
+ name: varName,
309
+ value: value.trim(),
310
+ file,
311
+ theme: "tailwind-inline",
312
+ source: "css",
313
+ line,
314
+ });
315
+ if (DEBUG) console.log("🌈 Detected Tailwind inline var:", varName, "in", file);
316
+ }
317
+ }
318
+ }
319
+
320
+ const inlineStylesCount = tokenUsages.filter(
321
+ (t) => t.type === "inline-style"
322
+ ).length;
323
+ if (DEBUG) console.log(`✅ Runtime usage tokens detected: ${tokenUsages.length}`);
324
+ if (DEBUG && tokenUsages.length) {
325
+ console.log("🧩 Sample runtime usage tokens:", tokenUsages.slice(0, 5));
326
+ }
327
+ if (DEBUG) console.log(`🎨 Inline styles detected: ${inlineStylesCount}`);
328
+ if (DEBUG && inlineStylesCount) {
329
+ console.log(
330
+ "🖌️ Sample inline styles:",
331
+ tokenUsages.filter((t) => t.type === "inline-style").slice(0, 5)
332
+ );
333
+ }
334
+
335
+ // Return only the raw detections (no enrichment or inference)
336
+ return tokenUsages;
337
+ }
@@ -0,0 +1,59 @@
1
+ export function detectDeclaredDrift(declaredJson, declaredCssVars) {
2
+ const drifted = [];
3
+
4
+ const jsonMap = new Map();
5
+ for (const t of declaredJson) {
6
+ jsonMap.set(t.cssVar, t.value);
7
+ }
8
+
9
+ for (const css of declaredCssVars) {
10
+ if (jsonMap.has(css.cssVar)) {
11
+ const declaredValue = jsonMap.get(css.cssVar);
12
+
13
+ const normalize = (v) =>
14
+ v
15
+ ?.trim()
16
+ .replace(/^var\(/, "")
17
+ .replace(/\)$/, "")
18
+ .replace(/^--/, "")
19
+ .toLowerCase();
20
+
21
+ const declaredNorm = normalize(declaredValue);
22
+ const foundNorm = normalize(css.value);
23
+ const cssVarNorm = normalize(css.cssVar);
24
+
25
+ const isSelfRef = css.value.trim() === `var(${css.cssVar})`;
26
+ const isIdentical = css.value.trim() === declaredValue.trim();
27
+ const isAliasSelfRef = foundNorm === declaredNorm || foundNorm === cssVarNorm;
28
+
29
+ // Skip if it's a self-reference or points to itself indirectly
30
+ if (
31
+ isSelfRef ||
32
+ isIdentical ||
33
+ isAliasSelfRef ||
34
+ css.value.includes(`var(${css.cssVar})`)
35
+ ) {
36
+ continue;
37
+ }
38
+
39
+ if (declaredValue !== css.value) {
40
+ drifted.push({
41
+ name: css.name,
42
+ cssVar: css.cssVar,
43
+ declaredValue,
44
+ actualValue: css.value,
45
+ file: css.file,
46
+ source: css.source,
47
+ category: css.category,
48
+ theme: css.theme,
49
+ isTailwindOverride:
50
+ css.source === "tailwind" ||
51
+ (css.category && css.category.startsWith("tailwind")),
52
+ driftFromDeclared: { declared: declaredValue, actual: css.value },
53
+ });
54
+ }
55
+ }
56
+ }
57
+
58
+ return drifted;
59
+ }
@@ -0,0 +1,178 @@
1
+ /**
2
+ * extractImports.js
3
+ *
4
+ * Extract import statements from JavaScript/TypeScript files.
5
+ * Returns relative paths that the file imports.
6
+ */
7
+
8
+ import fs from "fs";
9
+ import path from "path";
10
+ import { IMPORTABLE_EXTENSIONS, RESOLVABLE_EXTENSIONS } from "./fileExtensions.js";
11
+
12
+ const IMPORT_PATTERNS = [
13
+ // ES6 imports
14
+ /import\s+(?:(?:[\w*\s{},]*)\s+from\s+)?['"]([^'"]+)['"]/g,
15
+ // Dynamic imports
16
+ /import\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
17
+ // CommonJS require
18
+ /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
19
+ // TypeScript type imports
20
+ /import\s+type\s+(?:[\w*\s{},]*)\s+from\s+['"]([^'"]+)['"]/g,
21
+ ];
22
+
23
+ /**
24
+ * Extract import paths from file content.
25
+ * @param {string} content - File content
26
+ * @returns {string[]} - Array of imported module paths
27
+ */
28
+ function parseImports(content) {
29
+ const imports = new Set();
30
+
31
+ for (const pattern of IMPORT_PATTERNS) {
32
+ const matches = content.matchAll(pattern);
33
+ for (const match of matches) {
34
+ const importPath = match[1];
35
+ // Only include relative imports (exclude node_modules)
36
+ if (importPath.startsWith('.') || importPath.startsWith('/')) {
37
+ imports.add(importPath);
38
+ }
39
+ }
40
+ }
41
+
42
+ return Array.from(imports);
43
+ }
44
+
45
+ /**
46
+ * Resolve import path to absolute path (optimized with caching).
47
+ */
48
+ function resolveImportPath(fromFile, importPath, repoRoot, fileSetCache) {
49
+ const fromDir = path.dirname(fromFile);
50
+ let resolved = path.resolve(fromDir, importPath);
51
+
52
+ // Check cache first (Set lookup is O(1))
53
+ const relativeResolved = path.relative(repoRoot, resolved).replace(/\\/g, '/');
54
+ if (fileSetCache.has(relativeResolved)) {
55
+ return relativeResolved;
56
+ }
57
+
58
+ // Try extensions
59
+ for (const ext of RESOLVABLE_EXTENSIONS) {
60
+ const candidate = resolved + ext;
61
+ const relativeCandidate = path.relative(repoRoot, candidate).replace(/\\/g, '/');
62
+
63
+ if (fileSetCache.has(relativeCandidate)) {
64
+ return relativeCandidate;
65
+ }
66
+ }
67
+
68
+ // Try index files in directory (only if directory exists in cache)
69
+ const relativeDir = path.relative(repoRoot, resolved).replace(/\\/g, '/');
70
+ for (const ext of ['.js', '.ts', '.jsx', '.tsx']) {
71
+ const indexPath = `${relativeDir}/index${ext}`;
72
+ if (fileSetCache.has(indexPath)) {
73
+ return indexPath;
74
+ }
75
+ }
76
+
77
+ return null;
78
+ }
79
+
80
+ /**
81
+ * Extract imports from a file (optimized).
82
+ */
83
+ export function extractImports(filePath, repoRoot, fileSetCache) {
84
+ const ext = path.extname(filePath).toLowerCase();
85
+
86
+ if (!IMPORTABLE_EXTENSIONS.includes(ext)) {
87
+ return [];
88
+ }
89
+
90
+ try {
91
+ const content = fs.readFileSync(filePath, 'utf8');
92
+ const importPaths = parseImports(content);
93
+
94
+ // Resolve all import paths using cache
95
+ const resolved = importPaths
96
+ .map(imp => resolveImportPath(filePath, imp, repoRoot, fileSetCache))
97
+ .filter(Boolean);
98
+
99
+ return resolved;
100
+ } catch (err) {
101
+ // Don't log every failure, just return empty
102
+ return [];
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Build bidirectional dependency map for all files (optimized).
108
+ */
109
+ export function buildDependencyMap(files, repoRoot) {
110
+ const startTime = Date.now();
111
+
112
+ // Pre-build a Set of all files for O(1) lookups
113
+ const fileSet = new Set(files.map(f => f.replace(/\\/g, '/')));
114
+
115
+ const depMap = {};
116
+
117
+ // Initialize map
118
+ for (const file of files) {
119
+ const normalizedFile = file.replace(/\\/g, '/');
120
+ depMap[normalizedFile] = { imports: [], imported_by: [] };
121
+ }
122
+
123
+ // Track stats
124
+ let processedCount = 0;
125
+ let skippedCount = 0;
126
+ let importsFoundCount = 0;
127
+
128
+ // Batch process importable files only
129
+ const importableFiles = files.filter(file => {
130
+ const ext = path.extname(file).toLowerCase();
131
+ return IMPORTABLE_EXTENSIONS.includes(ext);
132
+ });
133
+
134
+ console.log(`📦 Processing ${importableFiles.length} importable files...`);
135
+
136
+ // Process in batches to show progress
137
+ const batchSize = 100;
138
+ for (let i = 0; i < importableFiles.length; i += batchSize) {
139
+ const batch = importableFiles.slice(i, i + batchSize);
140
+
141
+ for (const file of batch) {
142
+ const normalizedFile = file.replace(/\\/g, '/');
143
+ const absolutePath = path.resolve(repoRoot, file);
144
+ const imports = extractImports(absolutePath, repoRoot, fileSet);
145
+
146
+ if (imports.length > 0) {
147
+ importsFoundCount++;
148
+ }
149
+ processedCount++;
150
+
151
+ depMap[normalizedFile].imports = imports;
152
+
153
+ // Build reverse mapping
154
+ for (const importedFile of imports) {
155
+ if (depMap[importedFile]) {
156
+ if (!depMap[importedFile].imported_by.includes(normalizedFile)) {
157
+ depMap[importedFile].imported_by.push(normalizedFile);
158
+ }
159
+ }
160
+ }
161
+ }
162
+
163
+ // Progress indicator
164
+ if ((i + batchSize) % 500 === 0 || i + batchSize >= importableFiles.length) {
165
+ const progress = Math.min(i + batchSize, importableFiles.length);
166
+ console.log(`📦 Progress: ${progress}/${importableFiles.length} files processed`);
167
+ }
168
+ }
169
+
170
+ skippedCount = files.length - processedCount;
171
+
172
+ const elapsed = Date.now() - startTime;
173
+ console.log(`📦 Dependency stats: ${processedCount} importable files, ${importsFoundCount} with imports, ${skippedCount} skipped (${elapsed}ms)`);
174
+
175
+ return depMap;
176
+ }
177
+
178
+ export default extractImports;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * fileExtensions.js
3
+ *
4
+ * Shared file extension whitelist used across scanner and dependency extraction.
5
+ * Ensures consistency in which files are processed.
6
+ */
7
+
8
+ export const SCANNABLE_EXTENSIONS = [
9
+ ".js",
10
+ ".ts",
11
+ ".jsx",
12
+ ".tsx",
13
+ ".css",
14
+ ".scss",
15
+ ".sass",
16
+ ".less",
17
+ ".styl",
18
+ ".vue",
19
+ ".svelte",
20
+ ".astro",
21
+ ".html",
22
+ ".htm",
23
+ ".mdx",
24
+ ".md",
25
+ ".json",
26
+ ".jsonc",
27
+ ".yml",
28
+ ".yaml",
29
+ ".cjs",
30
+ ".mjs",
31
+ ];
32
+
33
+ // Files that can contain import statements (JS/TS/framework files + MDX)
34
+ export const IMPORTABLE_EXTENSIONS = [
35
+ ".js",
36
+ ".ts",
37
+ ".jsx",
38
+ ".tsx",
39
+ ".mjs",
40
+ ".cjs",
41
+ ".vue",
42
+ ".svelte",
43
+ ".astro",
44
+ ".mdx", // MDX can have imports
45
+ ];
46
+
47
+ // Extensions to try when resolving import paths
48
+ // (empty string first to handle imports with explicit extensions)
49
+ export const RESOLVABLE_EXTENSIONS = [
50
+ "",
51
+ ".js",
52
+ ".ts",
53
+ ".jsx",
54
+ ".tsx",
55
+ ".mjs",
56
+ ".cjs",
57
+ ".vue",
58
+ ".svelte",
59
+ ".astro",
60
+ ".mdx",
61
+ ".json",
62
+ ".jsonc",
63
+ ];
64
+
65
+ export default SCANNABLE_EXTENSIONS;