pkg-scaffold 3.3.2 → 3.3.4
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 +63 -22
- package/bin/cli.js +5 -5
- package/package.json +45 -25
- package/pnpm-workspace.yaml +2 -0
- package/src/EngineContext.js +14 -0
- package/src/ast/ASTAnalyzer.js +243 -116
- package/src/ast/BarrelParser.js +29 -17
- package/src/ast/DeadCodeDetector.js +73 -0
- package/src/ast/MagicDetector.js +3 -1
- package/src/ast/OxcAnalyzer.js +193 -81
- package/src/index.js +10 -11
- package/src/plugins/PluginRegistry.js +1 -0
- package/src/plugins/ecosystems/MorePlugins.js +184 -0
- package/src/plugins/ecosystems/PluginLoader.js +20 -0
- package/src/resolution/CircularDetector.js +3 -34
- package/src/resolution/ConfigLoader.js +34 -6
- package/src/resolution/WorkSpaceGraph.js +15 -1
package/src/ast/OxcAnalyzer.js
CHANGED
|
@@ -1,124 +1,236 @@
|
|
|
1
|
-
let oxcParser;
|
|
2
|
-
try {
|
|
3
|
-
oxcParser = await import('oxc-parser');
|
|
4
|
-
} catch (e) {
|
|
5
|
-
// OXC is optional; will fall back to TypeScript in ASTAnalyzer
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
import fs from 'fs/promises';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* High-Performance AST Analyzer using OXC (Rust-based)
|
|
12
|
-
* Designed to outpace Knip v6 by utilizing the fastest parser in the JS ecosystem.
|
|
13
|
-
* Includes automatic fallback if OXC is not available in the environment.
|
|
14
|
-
*/
|
|
15
1
|
export class OxcAnalyzer {
|
|
16
2
|
constructor(context) {
|
|
17
3
|
this.context = context;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
4
|
+
try {
|
|
5
|
+
this.oxc = require("oxc-parser");
|
|
6
|
+
this.isAvailable = true;
|
|
7
|
+
} catch (e) {
|
|
8
|
+
this.isAvailable = false;
|
|
23
9
|
if (this.context.verbose) {
|
|
24
|
-
console.warn(
|
|
10
|
+
console.warn("[OxcAnalyzer] oxc-parser not found, falling back to TypeScript compiler API.");
|
|
25
11
|
}
|
|
26
|
-
return false;
|
|
27
12
|
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
parseFile(filePath, content, fileNode) {
|
|
16
|
+
if (!this.isAvailable) return false;
|
|
28
17
|
|
|
29
18
|
try {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
19
|
+
if (this.context.verbose) {
|
|
20
|
+
console.log(`[OXC] Fast-parsing: ${filePath}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const ast = this.oxc.parseSync(content, {
|
|
24
|
+
sourceType: "module",
|
|
33
25
|
sourceFilename: filePath,
|
|
34
|
-
|
|
26
|
+
ecmaVersion: "latest",
|
|
35
27
|
});
|
|
36
28
|
|
|
37
|
-
|
|
29
|
+
// Initialize new properties for JSX and Decorator analysis
|
|
30
|
+
fileNode.jsxComponents = new Set();
|
|
31
|
+
fileNode.jsxProps = new Set();
|
|
32
|
+
fileNode.decorators = new Set();
|
|
33
|
+
|
|
34
|
+
this.walkOxcAst(ast.program, fileNode, content);
|
|
38
35
|
return true;
|
|
39
|
-
} catch (
|
|
36
|
+
} catch (e) {
|
|
40
37
|
if (this.context.verbose) {
|
|
41
|
-
console.
|
|
38
|
+
console.warn(`[OXC] Failed to parse ${filePath}: ${e.message}`);
|
|
42
39
|
}
|
|
43
40
|
return false;
|
|
44
41
|
}
|
|
45
42
|
}
|
|
46
43
|
|
|
47
|
-
|
|
44
|
+
walkOxcAst(node, fileNode, content) {
|
|
48
45
|
if (!node) return;
|
|
49
46
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
47
|
+
switch (node.type) {
|
|
48
|
+
case "ImportDeclaration":
|
|
49
|
+
this.handleImportDeclaration(node, fileNode);
|
|
50
|
+
break;
|
|
51
|
+
case "ExportNamedDeclaration":
|
|
52
|
+
case "ExportDefaultDeclaration":
|
|
53
|
+
case "ExportAllDeclaration":
|
|
54
|
+
this.handleExportDeclaration(node, fileNode, content);
|
|
55
|
+
break;
|
|
56
|
+
case "CallExpression":
|
|
57
|
+
this.handleCallExpression(node, fileNode);
|
|
58
|
+
break;
|
|
59
|
+
case "JSXElement":
|
|
60
|
+
case "JSXFragment": // Consider fragments as well
|
|
61
|
+
this.handleJsxElement(node, fileNode);
|
|
62
|
+
break;
|
|
63
|
+
case "Decorator":
|
|
64
|
+
this.handleDecorator(node, fileNode);
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Traverse children
|
|
69
|
+
for (const key in node) {
|
|
70
|
+
if (node[key] && typeof node[key] === "object") {
|
|
71
|
+
if (Array.isArray(node[key])) {
|
|
72
|
+
node[key].forEach((child) => this.walkOxcAst(child, fileNode, content));
|
|
73
|
+
} else {
|
|
74
|
+
this.walkOxcAst(node[key], fileNode, content);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
handleImportDeclaration(node, fileNode) {
|
|
81
|
+
const specifier = node.source.value;
|
|
82
|
+
fileNode.explicitImports.add(specifier);
|
|
83
|
+
|
|
84
|
+
if (node.specifiers) {
|
|
85
|
+
node.specifiers.forEach((spec) => {
|
|
86
|
+
if (spec.type === "ImportSpecifier") {
|
|
87
|
+
const importedName = spec.imported.name;
|
|
88
|
+
const localName = spec.local.name;
|
|
89
|
+
fileNode.importedSymbols.add(`${specifier}:${importedName}`);
|
|
90
|
+
} else if (spec.type === "ImportDefaultSpecifier") {
|
|
57
91
|
fileNode.importedSymbols.add(`${specifier}:default`);
|
|
58
|
-
} else if (
|
|
92
|
+
} else if (spec.type === "ImportNamespaceSpecifier") {
|
|
59
93
|
fileNode.importedSymbols.add(`${specifier}:*`);
|
|
60
94
|
}
|
|
61
95
|
});
|
|
62
96
|
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
handleExportDeclaration(node, fileNode, content) {
|
|
100
|
+
if (node.type === "ExportDefaultDeclaration") {
|
|
101
|
+
fileNode.internalExports.set("default", { type: "default", start: node.start, end: node.end });
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
63
104
|
|
|
64
|
-
if (node.type ===
|
|
65
|
-
if (node.
|
|
66
|
-
|
|
105
|
+
if (node.type === "ExportAllDeclaration") {
|
|
106
|
+
if (node.exported && node.exported.type === "ExportNamespaceSpecifier") {
|
|
107
|
+
// export * as name from 'module'
|
|
108
|
+
const name = node.exported.name;
|
|
109
|
+
fileNode.internalExports.set(name, { type: "re-export-namespace", source: node.source.value, originalName: "*", start: node.start, end: node.end });
|
|
110
|
+
} else {
|
|
111
|
+
// export * from 'module'
|
|
112
|
+
fileNode.internalExports.set("*", { type: "re-export-all", source: node.source.value });
|
|
67
113
|
}
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (node.source) {
|
|
118
|
+
// Re-export
|
|
119
|
+
const specifier = node.source.value;
|
|
68
120
|
if (node.specifiers) {
|
|
69
|
-
node.specifiers.forEach(
|
|
70
|
-
|
|
121
|
+
node.specifiers.forEach((spec) => {
|
|
122
|
+
const exportedName = spec.exported.name;
|
|
123
|
+
const localName = spec.local.name;
|
|
124
|
+
fileNode.internalExports.set(exportedName, {
|
|
125
|
+
type: "re-export",
|
|
126
|
+
source: specifier,
|
|
127
|
+
originalName: localName,
|
|
128
|
+
start: node.start,
|
|
129
|
+
end: node.end,
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
} else if (node.declaration) {
|
|
134
|
+
// Direct export
|
|
135
|
+
const decl = node.declaration;
|
|
136
|
+
if (decl.type === "VariableDeclaration") {
|
|
137
|
+
decl.declarations.forEach((d) => {
|
|
138
|
+
if (d.id.type === "Identifier") {
|
|
139
|
+
fileNode.internalExports.set(d.id.name, { type: "variable", start: d.start, end: d.end });
|
|
140
|
+
} else if (d.id.type === "ObjectPattern") {
|
|
141
|
+
d.id.properties.forEach((p) => {
|
|
142
|
+
if (p.type === "Property" && p.value.type === "Identifier") {
|
|
143
|
+
fileNode.internalExports.set(p.value.name, { type: "variable", start: p.start, end: p.end });
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
} else if (d.id.type === "ArrayPattern") {
|
|
147
|
+
d.id.elements.forEach((e) => {
|
|
148
|
+
if (e && e.type === "Identifier") {
|
|
149
|
+
fileNode.internalExports.set(e.name, { type: "variable", start: e.start, end: e.end });
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
}
|
|
71
153
|
});
|
|
154
|
+
} else if (decl.id && decl.id.name) {
|
|
155
|
+
let type = "unknown";
|
|
156
|
+
if (decl.type === "FunctionDeclaration") type = "function";
|
|
157
|
+
else if (decl.type === "ClassDeclaration") type = "class";
|
|
158
|
+
else if (decl.type === "TSEnumDeclaration") type = "enum";
|
|
159
|
+
else if (decl.type === "TSInterfaceDeclaration") type = "interface";
|
|
160
|
+
else if (decl.type === "TSTypeAliasDeclaration") type = "type";
|
|
161
|
+
else if (decl.type === "TSModuleDeclaration") type = "namespace";
|
|
162
|
+
|
|
163
|
+
const exportInfo = { type, start: decl.start, end: decl.end };
|
|
164
|
+
fileNode.internalExports.set(decl.id.name, exportInfo);
|
|
165
|
+
|
|
166
|
+
if (decl.type === "TSEnumDeclaration") {
|
|
167
|
+
exportInfo.members = decl.members.map((m) => m.id.name || (m.id.type === "Identifier" ? m.id.name : ""));
|
|
168
|
+
} else if (decl.type === "TSInterfaceDeclaration" || decl.type === "ClassDeclaration") {
|
|
169
|
+
exportInfo.members = decl.body.body.filter((m) => m.key && m.key.name).map((m) => m.key.name);
|
|
170
|
+
}
|
|
72
171
|
}
|
|
172
|
+
} else if (node.specifiers) {
|
|
173
|
+
node.specifiers.forEach((spec) => {
|
|
174
|
+
const exportedName = spec.exported.name;
|
|
175
|
+
const localName = spec.local.name;
|
|
176
|
+
fileNode.internalExports.set(exportedName, {
|
|
177
|
+
type: "export",
|
|
178
|
+
originalName: localName,
|
|
179
|
+
start: node.start,
|
|
180
|
+
end: node.end,
|
|
181
|
+
});
|
|
182
|
+
});
|
|
73
183
|
}
|
|
184
|
+
}
|
|
74
185
|
|
|
75
|
-
|
|
76
|
-
|
|
186
|
+
handleCallExpression(node, fileNode) {
|
|
187
|
+
if (node.callee.type === "Import" && node.arguments.length > 0 && node.arguments[0].type === "StringLiteral") {
|
|
188
|
+
const specifier = node.arguments[0].value;
|
|
189
|
+
fileNode.explicitImports.add(specifier);
|
|
190
|
+
fileNode.dynamicImports.add(specifier);
|
|
191
|
+
} else if (node.callee.type === "Identifier" && node.callee.name === "require" && node.arguments.length > 0 && node.arguments[0].type === "StringLiteral") {
|
|
192
|
+
fileNode.explicitImports.add(node.arguments[0].value);
|
|
77
193
|
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
handleJsxElement(node, fileNode) {
|
|
197
|
+
const getElementName = (nameNode) => {
|
|
198
|
+
if (nameNode.type === "JSXIdentifier") return nameNode.name;
|
|
199
|
+
if (nameNode.type === "JSXMemberExpression") return `${getElementName(nameNode.object)}.${nameNode.property.name}`;
|
|
200
|
+
return "unknown";
|
|
201
|
+
};
|
|
78
202
|
|
|
79
|
-
if (node.
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
memberName = decl.declarations[0].id.name;
|
|
88
|
-
} else if (decl.id) {
|
|
89
|
-
memberName = decl.id.name;
|
|
90
|
-
}
|
|
91
|
-
if (memberName) {
|
|
92
|
-
fileNode.namespaceMembers = fileNode.namespaceMembers || new Set();
|
|
93
|
-
fileNode.namespaceMembers.add(`${namespaceName}.${memberName}`);
|
|
94
|
-
}
|
|
203
|
+
if (node.openingElement) {
|
|
204
|
+
const tagName = getElementName(node.openingElement.name);
|
|
205
|
+
fileNode.jsxComponents.add(tagName);
|
|
206
|
+
|
|
207
|
+
if (node.openingElement.attributes) {
|
|
208
|
+
node.openingElement.attributes.forEach(attr => {
|
|
209
|
+
if (attr.type === "JSXAttribute" && attr.name.type === "JSXIdentifier") {
|
|
210
|
+
fileNode.jsxProps.add(`${tagName}:${attr.name.name}`);
|
|
95
211
|
}
|
|
96
212
|
});
|
|
97
213
|
}
|
|
98
214
|
}
|
|
99
|
-
|
|
100
|
-
for (const key in node) {
|
|
101
|
-
const child = node[key];
|
|
102
|
-
if (child && typeof child === 'object') {
|
|
103
|
-
if (Array.isArray(child)) {
|
|
104
|
-
child.forEach(c => this.walkOxcNode(c, fileNode));
|
|
105
|
-
} else {
|
|
106
|
-
this.walkOxcNode(child, fileNode);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
215
|
}
|
|
111
216
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
217
|
+
handleDecorator(node, fileNode) {
|
|
218
|
+
const getDecoratorName = (expr) => {
|
|
219
|
+
if (expr.type === "Identifier") return expr.name;
|
|
220
|
+
if (expr.type === "CallExpression" && expr.callee.type === "Identifier") return expr.callee.name;
|
|
221
|
+
if (expr.type === "CallExpression" && expr.callee.type === "MemberExpression" && expr.callee.property.type === "Identifier") return expr.callee.property.name;
|
|
222
|
+
return "unknown";
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const decoratorName = getDecoratorName(node.expression);
|
|
226
|
+
fileNode.decorators.add(decoratorName);
|
|
227
|
+
|
|
228
|
+
// Optionally, extract decorator arguments
|
|
229
|
+
if (node.expression.type === "CallExpression") {
|
|
230
|
+
node.expression.arguments.forEach(arg => {
|
|
231
|
+
// Further analysis of arguments can be done here if needed
|
|
232
|
+
// e.g., if (arg.type === "StringLiteral") fileNode.decoratorArgs.add(`${decoratorName}:${arg.value}`);
|
|
233
|
+
});
|
|
122
234
|
}
|
|
123
235
|
}
|
|
124
236
|
}
|
package/src/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { DeadCodeDetector } from "./ast/DeadCodeDetector.js";
|
|
2
|
+
import { OxcAnalyzer } from "./ast/OxcAnalyzer.js";
|
|
1
3
|
/**
|
|
2
4
|
* ============================================================================
|
|
3
5
|
* 📦 pkg-scaffold v3.3.2: Unified Architectural Refactoring Orchestrator
|
|
@@ -47,6 +49,7 @@ export class RefactoringEngine {
|
|
|
47
49
|
|
|
48
50
|
// Stage 3: Wire official AST Syntax parsers and framework processors
|
|
49
51
|
this.analyzer = new ASTAnalyzer(this.context);
|
|
52
|
+
this.oxcAnalyzer = new OxcAnalyzer(this.context);
|
|
50
53
|
this.barrelParser = new BarrelParser(this.context, this.resolver);
|
|
51
54
|
this.magicDetector = new MagicDetector(this.context);
|
|
52
55
|
|
|
@@ -110,16 +113,7 @@ export class RefactoringEngine {
|
|
|
110
113
|
}
|
|
111
114
|
}
|
|
112
115
|
|
|
113
|
-
|
|
114
|
-
if (sourceCodeFilesList.length > 0) {
|
|
115
|
-
try {
|
|
116
|
-
this.analyzer.initProgram(sourceCodeFilesList);
|
|
117
|
-
} catch (e) {
|
|
118
|
-
if (this.context.verbose) {
|
|
119
|
-
console.warn('Warning: Failed to initialize TypeScript program:', e.message);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
116
|
+
|
|
123
117
|
|
|
124
118
|
// Pass 3: Process source file tokens using high-performance concurrent workers
|
|
125
119
|
let parallelParseCompleted = false;
|
|
@@ -139,7 +133,12 @@ export class RefactoringEngine {
|
|
|
139
133
|
this.hydrateNodeFromCache(node, cacheManifest[filePath]);
|
|
140
134
|
} else if (!parallelParseCompleted) {
|
|
141
135
|
this.context.metrics.cacheMisses++;
|
|
142
|
-
await
|
|
136
|
+
const fileContent = await fs.readFile(filePath, 'utf8'); // Read file content here
|
|
137
|
+
if (this.oxcAnalyzer.isAvailable) {
|
|
138
|
+
this.oxcAnalyzer.parseFile(filePath, fileContent, node);
|
|
139
|
+
} else {
|
|
140
|
+
this.analyzer.parseFile(filePath, fileContent, node);
|
|
141
|
+
}
|
|
143
142
|
}
|
|
144
143
|
|
|
145
144
|
this.magicDetector.injectVirtualConsumerEdges(filePath, node, activeFrameworkEcosystems);
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { BasePlugin } from '../BasePlugin.js';
|
|
4
|
+
|
|
5
|
+
export class TailwindPlugin extends BasePlugin {
|
|
6
|
+
get name() { return 'tailwind'; }
|
|
7
|
+
getConfigFiles() { return ['tailwind.config.js', 'tailwind.config.ts', 'tailwind.config.cjs', 'tailwind.config.mjs']; }
|
|
8
|
+
async isActive(baseDir) {
|
|
9
|
+
try {
|
|
10
|
+
const pkgJson = JSON.parse(await fs.readFile(path.join(baseDir, 'package.json'), 'utf8'));
|
|
11
|
+
return !!(pkgJson.dependencies?.tailwindcss || pkgJson.devDependencies?.tailwindcss);
|
|
12
|
+
} catch { return false; }
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class PostcssPlugin extends BasePlugin {
|
|
17
|
+
get name() { return 'postcss'; }
|
|
18
|
+
getConfigFiles() { return ['postcss.config.js', 'postcss.config.cjs', 'postcss.config.mjs']; }
|
|
19
|
+
async isActive(baseDir) {
|
|
20
|
+
try {
|
|
21
|
+
const pkgJson = JSON.parse(await fs.readFile(path.join(baseDir, 'package.json'), 'utf8'));
|
|
22
|
+
return !!(pkgJson.dependencies?.postcss || pkgJson.devDependencies?.postcss);
|
|
23
|
+
} catch { return false; }
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class JestPlugin extends BasePlugin {
|
|
28
|
+
get name() { return 'jest'; }
|
|
29
|
+
getConfigFiles() { return ['jest.config.js', 'jest.config.ts', 'jest.config.mjs', 'jest.config.cjs', 'package.json']; }
|
|
30
|
+
getRoutePatterns() { return [/\.(test|spec)\.[jt]sx?$/]; }
|
|
31
|
+
async isActive(baseDir) {
|
|
32
|
+
try {
|
|
33
|
+
const pkgJson = JSON.parse(await fs.readFile(path.join(baseDir, 'package.json'), 'utf8'));
|
|
34
|
+
return !!(pkgJson.dependencies?.jest || pkgJson.devDependencies?.jest);
|
|
35
|
+
} catch { return false; }
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class VitestPlugin extends BasePlugin {
|
|
40
|
+
get name() { return 'vitest'; }
|
|
41
|
+
getConfigFiles() { return ['vitest.config.ts', 'vitest.config.js', 'vitest.config.mjs', 'vitest.workspace.ts']; }
|
|
42
|
+
getRoutePatterns() { return [/\.(test|spec)\.[jt]sx?$/]; }
|
|
43
|
+
async isActive(baseDir) {
|
|
44
|
+
try {
|
|
45
|
+
const pkgJson = JSON.parse(await fs.readFile(path.join(baseDir, 'package.json'), 'utf8'));
|
|
46
|
+
return !!(pkgJson.dependencies?.vitest || pkgJson.devDependencies?.vitest);
|
|
47
|
+
} catch { return false; }
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export class PlaywrightPlugin extends BasePlugin {
|
|
52
|
+
get name() { return 'playwright'; }
|
|
53
|
+
getConfigFiles() { return ['playwright.config.ts', 'playwright.config.js']; }
|
|
54
|
+
getRoutePatterns() { return [/.*\.spec\.[jt]s$/]; }
|
|
55
|
+
async isActive(baseDir) {
|
|
56
|
+
try {
|
|
57
|
+
const pkgJson = JSON.parse(await fs.readFile(path.join(baseDir, 'package.json'), 'utf8'));
|
|
58
|
+
return !!(pkgJson.dependencies?.['@playwright/test'] || pkgJson.devDependencies?.['@playwright/test']);
|
|
59
|
+
} catch { return false; }
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export class CypressPlugin extends BasePlugin {
|
|
64
|
+
get name() { return 'cypress'; }
|
|
65
|
+
getConfigFiles() { return ['cypress.config.ts', 'cypress.config.js']; }
|
|
66
|
+
getRoutePatterns() { return [/cypress\/e2e\/.*\.cy\.[jt]s$/]; }
|
|
67
|
+
async isActive(baseDir) {
|
|
68
|
+
try {
|
|
69
|
+
const pkgJson = JSON.parse(await fs.readFile(path.join(baseDir, 'package.json'), 'utf8'));
|
|
70
|
+
return !!(pkgJson.dependencies?.cypress || pkgJson.devDependencies?.cypress);
|
|
71
|
+
} catch { return false; }
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export class StorybookPlugin extends BasePlugin {
|
|
76
|
+
get name() { return 'storybook'; }
|
|
77
|
+
getConfigFiles() { return ['.storybook/main.js', '.storybook/main.ts', '.storybook/preview.js', '.storybook/preview.ts']; }
|
|
78
|
+
getRoutePatterns() { return [/\.stories\.[jt]sx?$/]; }
|
|
79
|
+
async isActive(baseDir) {
|
|
80
|
+
try {
|
|
81
|
+
const pkgJson = JSON.parse(await fs.readFile(path.join(baseDir, 'package.json'), 'utf8'));
|
|
82
|
+
return !!(pkgJson.dependencies?.storybook || pkgJson.devDependencies?.storybook || pkgJson.devDependencies?.['@storybook/react']);
|
|
83
|
+
} catch { return false; }
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export class EslintPlugin extends BasePlugin {
|
|
88
|
+
get name() { return 'eslint'; }
|
|
89
|
+
getConfigFiles() { return ['.eslintrc', '.eslintrc.js', '.eslintrc.cjs', '.eslintrc.json', 'eslint.config.js', 'eslint.config.mjs']; }
|
|
90
|
+
async isActive(baseDir) {
|
|
91
|
+
try {
|
|
92
|
+
const pkgJson = JSON.parse(await fs.readFile(path.join(baseDir, 'package.json'), 'utf8'));
|
|
93
|
+
return !!(pkgJson.dependencies?.eslint || pkgJson.devDependencies?.eslint);
|
|
94
|
+
} catch { return false; }
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export class PrettierPlugin extends BasePlugin {
|
|
99
|
+
get name() { return 'prettier'; }
|
|
100
|
+
getConfigFiles() { return ['.prettierrc', '.prettierrc.js', '.prettierrc.cjs', '.prettierrc.json', 'prettier.config.js']; }
|
|
101
|
+
async isActive(baseDir) {
|
|
102
|
+
try {
|
|
103
|
+
const pkgJson = JSON.parse(await fs.readFile(path.join(baseDir, 'package.json'), 'utf8'));
|
|
104
|
+
return !!(pkgJson.dependencies?.prettier || pkgJson.devDependencies?.prettier);
|
|
105
|
+
} catch { return false; }
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export class HuskyPlugin extends BasePlugin {
|
|
110
|
+
get name() { return 'husky'; }
|
|
111
|
+
getConfigFiles() { return ['.husky/pre-commit', '.husky/pre-push']; }
|
|
112
|
+
async isActive(baseDir) {
|
|
113
|
+
try {
|
|
114
|
+
const pkgJson = JSON.parse(await fs.readFile(path.join(baseDir, 'package.json'), 'utf8'));
|
|
115
|
+
return !!(pkgJson.dependencies?.husky || pkgJson.devDependencies?.husky);
|
|
116
|
+
} catch { return false; }
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export class LintStagedPlugin extends BasePlugin {
|
|
121
|
+
get name() { return 'lint-staged'; }
|
|
122
|
+
getConfigFiles() { return ['.lintstagedrc', '.lintstagedrc.js', '.lintstagedrc.json', 'lint-staged.config.js', 'package.json']; }
|
|
123
|
+
async isActive(baseDir) {
|
|
124
|
+
try {
|
|
125
|
+
const pkgJson = JSON.parse(await fs.readFile(path.join(baseDir, 'package.json'), 'utf8'));
|
|
126
|
+
return !!(pkgJson.dependencies?.['lint-staged'] || pkgJson.devDependencies?.['lint-staged']);
|
|
127
|
+
} catch { return false; }
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export class CommitlintPlugin extends BasePlugin {
|
|
132
|
+
get name() { return 'commitlint'; }
|
|
133
|
+
getConfigFiles() { return ['.commitlintrc', '.commitlintrc.js', '.commitlintrc.json', 'commitlint.config.js', 'package.json']; }
|
|
134
|
+
async isActive(baseDir) {
|
|
135
|
+
try {
|
|
136
|
+
const pkgJson = JSON.parse(await fs.readFile(path.join(baseDir, 'package.json'), 'utf8'));
|
|
137
|
+
return !!(pkgJson.dependencies?.['@commitlint/cli'] || pkgJson.devDependencies?.['@commitlint/cli']);
|
|
138
|
+
} catch { return false; }
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export class BabelPlugin extends BasePlugin {
|
|
143
|
+
get name() { return 'babel'; }
|
|
144
|
+
getConfigFiles() { return ['.babelrc', '.babelrc.js', '.babelrc.json', 'babel.config.js', 'babel.config.json', 'package.json']; }
|
|
145
|
+
async isActive(baseDir) {
|
|
146
|
+
try {
|
|
147
|
+
const pkgJson = JSON.parse(await fs.readFile(path.join(baseDir, 'package.json'), 'utf8'));
|
|
148
|
+
return !!(pkgJson.dependencies?.['@babel/core'] || pkgJson.devDependencies?.['@babel/core']);
|
|
149
|
+
} catch { return false; }
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export class RollupPlugin extends BasePlugin {
|
|
154
|
+
get name() { return 'rollup'; }
|
|
155
|
+
getConfigFiles() { return ['rollup.config.js', 'rollup.config.mjs', 'rollup.config.ts']; }
|
|
156
|
+
async isActive(baseDir) {
|
|
157
|
+
try {
|
|
158
|
+
const pkgJson = JSON.parse(await fs.readFile(path.join(baseDir, 'package.json'), 'utf8'));
|
|
159
|
+
return !!(pkgJson.dependencies?.rollup || pkgJson.devDependencies?.rollup);
|
|
160
|
+
} catch { return false; }
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export class WebpackPlugin extends BasePlugin {
|
|
165
|
+
get name() { return 'webpack'; }
|
|
166
|
+
getConfigFiles() { return ['webpack.config.js', 'webpack.config.ts', 'webpack.config.mjs', 'webpack.config.cjs']; }
|
|
167
|
+
async isActive(baseDir) {
|
|
168
|
+
try {
|
|
169
|
+
const pkgJson = JSON.parse(await fs.readFile(path.join(baseDir, 'package.json'), 'utf8'));
|
|
170
|
+
return !!(pkgJson.dependencies?.webpack || pkgJson.devDependencies?.webpack);
|
|
171
|
+
} catch { return false; }
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export class GithubActionsPlugin extends BasePlugin {
|
|
176
|
+
get name() { return 'github-actions'; }
|
|
177
|
+
getConfigFiles() { return ['.github/workflows/*.yml', '.github/workflows/*.yaml']; }
|
|
178
|
+
async isActive(baseDir) {
|
|
179
|
+
try {
|
|
180
|
+
await fs.access(path.join(baseDir, '.github/workflows'));
|
|
181
|
+
return true;
|
|
182
|
+
} catch { return false; }
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { TailwindPlugin, PostcssPlugin, JestPlugin, VitestPlugin, PlaywrightPlugin, CypressPlugin, StorybookPlugin, EslintPlugin, PrettierPlugin, HuskyPlugin, LintStagedPlugin, CommitlintPlugin, BabelPlugin, RollupPlugin, WebpackPlugin, GithubActionsPlugin } from './MorePlugins.js';
|
|
2
|
+
|
|
3
|
+
export function loadAdditionalPlugins(registry) {
|
|
4
|
+
registry.register(new TailwindPlugin(registry.context));
|
|
5
|
+
registry.register(new PostcssPlugin(registry.context));
|
|
6
|
+
registry.register(new JestPlugin(registry.context));
|
|
7
|
+
registry.register(new VitestPlugin(registry.context));
|
|
8
|
+
registry.register(new PlaywrightPlugin(registry.context));
|
|
9
|
+
registry.register(new CypressPlugin(registry.context));
|
|
10
|
+
registry.register(new StorybookPlugin(registry.context));
|
|
11
|
+
registry.register(new EslintPlugin(registry.context));
|
|
12
|
+
registry.register(new PrettierPlugin(registry.context));
|
|
13
|
+
registry.register(new HuskyPlugin(registry.context));
|
|
14
|
+
registry.register(new LintStagedPlugin(registry.context));
|
|
15
|
+
registry.register(new CommitlintPlugin(registry.context));
|
|
16
|
+
registry.register(new BabelPlugin(registry.context));
|
|
17
|
+
registry.register(new RollupPlugin(registry.context));
|
|
18
|
+
registry.register(new WebpackPlugin(registry.context));
|
|
19
|
+
registry.register(new GithubActionsPlugin(registry.context));
|
|
20
|
+
}
|
|
@@ -1,31 +1,14 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ============================================================================
|
|
3
|
-
* Circular Dependency Detector for pkg-scaffold v3.3.0
|
|
4
|
-
*
|
|
5
|
-
* Copyright (C) 2026 DreamLongYT
|
|
6
|
-
* Licensed under the Apache License, Version 2.0.
|
|
7
|
-
* "The Original Code was made by DreamLongYT"
|
|
8
|
-
* ============================================================================
|
|
9
|
-
* Implements a high-performance Tarjan-based algorithm to
|
|
10
|
-
* detect circular dependencies in the codebase graph.
|
|
11
|
-
* Addresses Knip Issue #1734.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
1
|
export class CircularDetector {
|
|
15
2
|
constructor(context) {
|
|
16
3
|
this.context = context;
|
|
17
4
|
this.cycles = [];
|
|
18
5
|
}
|
|
19
6
|
|
|
20
|
-
/**
|
|
21
|
-
* Detects cycles in the provided dependency graph using Tarjan's SCC algorithm
|
|
22
|
-
* @param {Map} graph - The codebase dependency graph
|
|
23
|
-
* @returns {Array} List of detected cycles
|
|
24
|
-
*/
|
|
25
7
|
detectCycles(graph, context = null) {
|
|
26
8
|
if (context) this.context = context;
|
|
27
9
|
this.cwd = context?.cwd || this.context?.cwd || process.cwd();
|
|
28
10
|
this.cycles = [];
|
|
11
|
+
|
|
29
12
|
let index = 0;
|
|
30
13
|
const stack = [];
|
|
31
14
|
const indices = new Map();
|
|
@@ -63,7 +46,6 @@ export class CircularDetector {
|
|
|
63
46
|
if (component.length > 1) {
|
|
64
47
|
this.cycles.push(component.reverse());
|
|
65
48
|
} else {
|
|
66
|
-
// Check for self-loops
|
|
67
49
|
const node = graph.get(v);
|
|
68
50
|
if (node && node.outgoingEdges && node.outgoingEdges.has(v)) {
|
|
69
51
|
this.cycles.push([v]);
|
|
@@ -81,18 +63,11 @@ export class CircularDetector {
|
|
|
81
63
|
return this.cycles;
|
|
82
64
|
}
|
|
83
65
|
|
|
84
|
-
/**
|
|
85
|
-
* Formats cycles for reporting with file paths
|
|
86
|
-
*/
|
|
87
66
|
formatCycles() {
|
|
88
67
|
return this.cycles.map(cycle => {
|
|
89
68
|
const paths = cycle.map(p => {
|
|
90
|
-
// Extract relative path for readability
|
|
91
69
|
let rel = p.replace(this.context.cwd, '').replace(/^\//, '');
|
|
92
|
-
|
|
93
|
-
if (rel.includes(':\\')) {
|
|
94
|
-
rel = rel.split(':\\')[1] || rel;
|
|
95
|
-
}
|
|
70
|
+
if (rel.includes(':\\')) rel = rel.split(':\\')[1] || rel;
|
|
96
71
|
return rel;
|
|
97
72
|
});
|
|
98
73
|
if (cycle.length === 1) return `${paths[0]} -> (self-loop)`;
|
|
@@ -100,17 +75,12 @@ export class CircularDetector {
|
|
|
100
75
|
});
|
|
101
76
|
}
|
|
102
77
|
|
|
103
|
-
/**
|
|
104
|
-
* Gets detailed cycle information
|
|
105
|
-
*/
|
|
106
78
|
getCycleDetails() {
|
|
107
79
|
return this.cycles.map((cycle, idx) => ({
|
|
108
80
|
cycleId: idx + 1,
|
|
109
81
|
files: cycle.map(p => {
|
|
110
82
|
let rel = p.replace(this.context.cwd, '').replace(/^\//, '');
|
|
111
|
-
if (rel.includes(':\\'))
|
|
112
|
-
rel = rel.split(':\\')[1] || rel;
|
|
113
|
-
}
|
|
83
|
+
if (rel.includes(':\\')) rel = rel.split(':\\')[1] || rel;
|
|
114
84
|
return rel;
|
|
115
85
|
}),
|
|
116
86
|
length: cycle.length,
|
|
@@ -118,5 +88,4 @@ export class CircularDetector {
|
|
|
118
88
|
}));
|
|
119
89
|
}
|
|
120
90
|
}
|
|
121
|
-
|
|
122
91
|
export default CircularDetector;
|