pkg-scaffold 3.2.0 → 3.3.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/LICENSE +211 -21
- package/NOTICE +13 -0
- package/package.json +8 -6
- package/src/api/HeadlessAPI.js +365 -0
- package/src/api/PluginSDK.js +299 -0
- package/src/plugins/KnipAdapter.js +106 -0
- package/src/plugins/PluginRegistry.js +22 -4
- package/src/plugins/ecosystems/BackendServices.js +59 -0
- package/src/plugins/ecosystems/ModernFrameworks.js +157 -0
- package/src/resolution/CircularDetector.js +71 -0
- package/src/resolution/PathMapper.js +12 -2
- package/src/resolution/WorkSpaceGraph.js +10 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* Plugin SDK for pkg-scaffold v4.0.0
|
|
4
|
+
* ============================================================================
|
|
5
|
+
* Provides utilities and helpers for developing custom plugins that extend
|
|
6
|
+
* pkg-scaffold's analysis and healing capabilities.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { BasePlugin } from '../plugins/BasePlugin.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Extended plugin base class with SDK utilities
|
|
13
|
+
*/
|
|
14
|
+
export class PluginSDKBase extends BasePlugin {
|
|
15
|
+
constructor(context) {
|
|
16
|
+
super(context);
|
|
17
|
+
this.hooks = new Map();
|
|
18
|
+
this.transformers = [];
|
|
19
|
+
this.validators = [];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Register a hook for a specific lifecycle event
|
|
24
|
+
* @param {string} eventName - Event name (e.g., 'analyze:start', 'refactor:complete')
|
|
25
|
+
* @param {Function} handler - Handler function
|
|
26
|
+
*/
|
|
27
|
+
registerHook(eventName, handler) {
|
|
28
|
+
if (!this.hooks.has(eventName)) {
|
|
29
|
+
this.hooks.set(eventName, []);
|
|
30
|
+
}
|
|
31
|
+
this.hooks.get(eventName).push(handler);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Emit a hook event
|
|
36
|
+
* @param {string} eventName - Event name
|
|
37
|
+
* @param {Object} data - Event data
|
|
38
|
+
*/
|
|
39
|
+
async emitHook(eventName, data) {
|
|
40
|
+
const handlers = this.hooks.get(eventName) || [];
|
|
41
|
+
for (const handler of handlers) {
|
|
42
|
+
await handler(data);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Register a code transformer
|
|
48
|
+
* @param {Function} transformer - Transformer function
|
|
49
|
+
*/
|
|
50
|
+
registerTransformer(transformer) {
|
|
51
|
+
this.transformers.push(transformer);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Register a validator
|
|
56
|
+
* @param {Function} validator - Validator function
|
|
57
|
+
*/
|
|
58
|
+
registerValidator(validator) {
|
|
59
|
+
this.validators.push(validator);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Apply all registered transformers to a code string
|
|
64
|
+
* @param {string} code - Source code
|
|
65
|
+
* @param {string} filePath - File path
|
|
66
|
+
* @returns {Promise<string>} Transformed code
|
|
67
|
+
*/
|
|
68
|
+
async applyTransformers(code, filePath) {
|
|
69
|
+
let result = code;
|
|
70
|
+
for (const transformer of this.transformers) {
|
|
71
|
+
result = await transformer(result, filePath);
|
|
72
|
+
}
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Run all validators on a code string
|
|
78
|
+
* @param {string} code - Source code
|
|
79
|
+
* @param {string} filePath - File path
|
|
80
|
+
* @returns {Promise<Array>} Validation errors
|
|
81
|
+
*/
|
|
82
|
+
async runValidators(code, filePath) {
|
|
83
|
+
const errors = [];
|
|
84
|
+
for (const validator of this.validators) {
|
|
85
|
+
const result = await validator(code, filePath);
|
|
86
|
+
if (result && result.length > 0) {
|
|
87
|
+
errors.push(...result);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return errors;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* SDK utilities for plugin development
|
|
96
|
+
*/
|
|
97
|
+
export class PluginSDK {
|
|
98
|
+
/**
|
|
99
|
+
* Create a custom plugin class
|
|
100
|
+
* @param {Object} config - Plugin configuration
|
|
101
|
+
* @returns {Class} Plugin class
|
|
102
|
+
*/
|
|
103
|
+
static createPlugin(config) {
|
|
104
|
+
return class CustomPlugin extends PluginSDKBase {
|
|
105
|
+
get name() {
|
|
106
|
+
return config.name;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
getConfigFiles() {
|
|
110
|
+
return config.configFiles || [];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
getRoutePatterns() {
|
|
114
|
+
return config.routePatterns || [];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
getRequiredSystemContracts() {
|
|
118
|
+
return config.requiredContracts || ['default'];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async isActive(baseDir) {
|
|
122
|
+
if (config.isActive) {
|
|
123
|
+
return await config.isActive(baseDir);
|
|
124
|
+
}
|
|
125
|
+
return super.isActive(baseDir);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async initialize() {
|
|
129
|
+
if (config.initialize) {
|
|
130
|
+
await config.initialize(this.context);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async analyze(node, filePath) {
|
|
135
|
+
if (config.analyze) {
|
|
136
|
+
return await config.analyze(node, filePath, this.context);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async transform(code, filePath) {
|
|
141
|
+
if (config.transform) {
|
|
142
|
+
return await config.transform(code, filePath, this.context);
|
|
143
|
+
}
|
|
144
|
+
return code;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async validate(code, filePath) {
|
|
148
|
+
if (config.validate) {
|
|
149
|
+
return await config.validate(code, filePath, this.context);
|
|
150
|
+
}
|
|
151
|
+
return [];
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Create a CSS-in-JS analyzer plugin
|
|
158
|
+
* @param {Object} config - Configuration for CSS-in-JS analysis
|
|
159
|
+
* @returns {Class} Plugin class
|
|
160
|
+
*/
|
|
161
|
+
static createCSSInJSPlugin(config = {}) {
|
|
162
|
+
const cssLibraries = config.libraries || [
|
|
163
|
+
'styled-components',
|
|
164
|
+
'emotion',
|
|
165
|
+
'@emotion/react',
|
|
166
|
+
'@emotion/styled',
|
|
167
|
+
'linaria',
|
|
168
|
+
'vanilla-extract'
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
return this.createPlugin({
|
|
172
|
+
name: config.name || 'css-in-js-analyzer',
|
|
173
|
+
configFiles: [],
|
|
174
|
+
routePatterns: [/\.(tsx?|jsx?)$/],
|
|
175
|
+
requiredContracts: [],
|
|
176
|
+
|
|
177
|
+
async analyze(node, filePath) {
|
|
178
|
+
// Track CSS-in-JS imports
|
|
179
|
+
for (const lib of cssLibraries) {
|
|
180
|
+
if (node.explicitImports.has(lib)) {
|
|
181
|
+
node.cssInJsLibraries = node.cssInJsLibraries || new Set();
|
|
182
|
+
node.cssInJsLibraries.add(lib);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Detect styled component definitions
|
|
187
|
+
const styledPattern = /(?:styled|css|keyframes)\s*\.\w+|styled\(\w+\)/g;
|
|
188
|
+
for (const match of (node.rawCode || '').matchAll(styledPattern)) {
|
|
189
|
+
node.styledComponentUsages = node.styledComponentUsages || [];
|
|
190
|
+
node.styledComponentUsages.push(match[0]);
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
async validate(code, filePath) {
|
|
195
|
+
const errors = [];
|
|
196
|
+
// Check for unused CSS-in-JS definitions
|
|
197
|
+
const unusedStylesPattern = /(?:const|let|var)\s+(\w+)\s*=\s*(?:styled|css)\./g;
|
|
198
|
+
const matches = [...code.matchAll(unusedStylesPattern)];
|
|
199
|
+
|
|
200
|
+
for (const match of matches) {
|
|
201
|
+
const styleName = match[1];
|
|
202
|
+
const usagePattern = new RegExp(`\\b${styleName}\\b`);
|
|
203
|
+
if (!usagePattern.test(code.substring(match.index + match[0].length))) {
|
|
204
|
+
errors.push({
|
|
205
|
+
type: 'unused-style',
|
|
206
|
+
name: styleName,
|
|
207
|
+
line: code.substring(0, match.index).split('\n').length,
|
|
208
|
+
message: `Unused CSS-in-JS definition: ${styleName}`
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return errors;
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Create an asset tracking plugin
|
|
220
|
+
* @param {Object} config - Configuration for asset tracking
|
|
221
|
+
* @returns {Class} Plugin class
|
|
222
|
+
*/
|
|
223
|
+
static createAssetTrackingPlugin(config = {}) {
|
|
224
|
+
const assetExtensions = config.extensions || [
|
|
225
|
+
'.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp',
|
|
226
|
+
'.mp4', '.webm', '.mp3', '.wav',
|
|
227
|
+
'.woff', '.woff2', '.ttf', '.eot'
|
|
228
|
+
];
|
|
229
|
+
|
|
230
|
+
return this.createPlugin({
|
|
231
|
+
name: config.name || 'asset-tracker',
|
|
232
|
+
configFiles: [],
|
|
233
|
+
routePatterns: [/\.(tsx?|jsx?)$/],
|
|
234
|
+
|
|
235
|
+
async analyze(node, filePath) {
|
|
236
|
+
node.assetReferences = node.assetReferences || new Set();
|
|
237
|
+
|
|
238
|
+
// Track asset imports
|
|
239
|
+
const assetImportPattern = /import\s+(?:\*\s+as\s+\w+|[\w\s,{}]+)\s+from\s+['"]([^'"]+(?:${assetExtensions.join('|')}))['"]/g;
|
|
240
|
+
for (const match of (node.rawCode || '').matchAll(assetImportPattern)) {
|
|
241
|
+
node.assetReferences.add(match[1]);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Track asset requires
|
|
245
|
+
const assetRequirePattern = /require\s*\(\s*['"]([^'"]+(?:${assetExtensions.join('|')}))['"]\s*\)/g;
|
|
246
|
+
for (const match of (node.rawCode || '').matchAll(assetRequirePattern)) {
|
|
247
|
+
node.assetReferences.add(match[1]);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Track asset URLs in strings
|
|
251
|
+
const assetUrlPattern = /['"]([^'"]*(?:${assetExtensions.join('|')}))['"]/g;
|
|
252
|
+
for (const match of (node.rawCode || '').matchAll(assetUrlPattern)) {
|
|
253
|
+
node.assetReferences.add(match[1]);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Create a monorepo awareness plugin
|
|
261
|
+
* @param {Object} config - Configuration for monorepo support
|
|
262
|
+
* @returns {Class} Plugin class
|
|
263
|
+
*/
|
|
264
|
+
static createMonorepoPlugin(config = {}) {
|
|
265
|
+
return this.createPlugin({
|
|
266
|
+
name: config.name || 'monorepo-aware',
|
|
267
|
+
configFiles: config.configFiles || ['nx.json', 'pnpm-workspace.yaml', 'lerna.json'],
|
|
268
|
+
|
|
269
|
+
async analyze(node, filePath) {
|
|
270
|
+
// Track workspace package references
|
|
271
|
+
node.workspaceReferences = node.workspaceReferences || new Set();
|
|
272
|
+
|
|
273
|
+
// Detect workspace imports (e.g., @workspace/package-name)
|
|
274
|
+
const workspacePattern = /@[\w-]+\/[\w-]+/g;
|
|
275
|
+
for (const match of (node.rawCode || '').matchAll(workspacePattern)) {
|
|
276
|
+
node.workspaceReferences.add(match[0]);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Create a circular dependency detector plugin
|
|
284
|
+
* @param {Object} config - Configuration
|
|
285
|
+
* @returns {Class} Plugin class
|
|
286
|
+
*/
|
|
287
|
+
static createCircularDepPlugin(config = {}) {
|
|
288
|
+
return this.createPlugin({
|
|
289
|
+
name: config.name || 'circular-dep-detector',
|
|
290
|
+
|
|
291
|
+
async analyze(node, filePath) {
|
|
292
|
+
node.potentialCycles = node.potentialCycles || [];
|
|
293
|
+
// Cycle detection will be handled by the main engine
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export default PluginSDK;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* Knip Plugin Adapter for pkg-scaffold v4.0.0
|
|
4
|
+
* ============================================================================
|
|
5
|
+
* This adapter allows pkg-scaffold to use existing Knip plugins without
|
|
6
|
+
* requiring knip as a dependency. It implements the Knip plugin interface
|
|
7
|
+
* internally to ensure full compatibility.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import fs from 'fs/promises';
|
|
12
|
+
|
|
13
|
+
export class KnipAdapter {
|
|
14
|
+
constructor(context) {
|
|
15
|
+
this.context = context;
|
|
16
|
+
this.knipPlugins = new Map();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Discovers and loads Knip plugins from the project's node_modules
|
|
21
|
+
* or a specified directory.
|
|
22
|
+
*/
|
|
23
|
+
async discoverPlugins(projectRoot) {
|
|
24
|
+
// Knip plugins are typically named 'knip-plugin-*' or are part of knip's core
|
|
25
|
+
// We look for common Knip plugin patterns in node_modules
|
|
26
|
+
const nodeModulesPath = path.join(projectRoot, 'node_modules');
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const dirs = await fs.readdir(nodeModulesPath);
|
|
30
|
+
for (const dir of dirs) {
|
|
31
|
+
if (dir.startsWith('knip-plugin-') || dir === '@knip/plugin') {
|
|
32
|
+
await this.loadPlugin(path.join(nodeModulesPath, dir));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
} catch (e) {
|
|
36
|
+
// node_modules not found or unreadable
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Loads a specific Knip plugin and wraps it for pkg-scaffold
|
|
42
|
+
*/
|
|
43
|
+
async loadPlugin(pluginPath) {
|
|
44
|
+
try {
|
|
45
|
+
const pluginModule = await import(pluginPath);
|
|
46
|
+
const plugin = pluginModule.default || pluginModule;
|
|
47
|
+
|
|
48
|
+
if (plugin.name && (plugin.config || plugin.entry)) {
|
|
49
|
+
this.knipPlugins.set(plugin.name, this.wrapKnipPlugin(plugin));
|
|
50
|
+
if (this.context.verbose) {
|
|
51
|
+
console.log(`[KnipAdapter] Successfully integrated Knip plugin: ${plugin.name}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
} catch (e) {
|
|
55
|
+
// Failed to load plugin
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Wraps a Knip plugin to match the pkg-scaffold BasePlugin interface
|
|
61
|
+
*/
|
|
62
|
+
wrapKnipPlugin(knipPlugin) {
|
|
63
|
+
return {
|
|
64
|
+
name: `knip-${knipPlugin.name}`,
|
|
65
|
+
isKnipWrapped: true,
|
|
66
|
+
|
|
67
|
+
getConfigFiles: () => {
|
|
68
|
+
if (Array.isArray(knipPlugin.config)) return knipPlugin.config;
|
|
69
|
+
if (typeof knipPlugin.config === 'string') return [knipPlugin.config];
|
|
70
|
+
return [];
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
getRoutePatterns: () => {
|
|
74
|
+
if (Array.isArray(knipPlugin.entry)) return knipPlugin.entry.map(e => new RegExp(e.replace('*', '.*')));
|
|
75
|
+
if (typeof knipPlugin.entry === 'string') return [new RegExp(knipPlugin.entry.replace('*', '.*'))];
|
|
76
|
+
return [];
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
isActive: async (baseDir) => {
|
|
80
|
+
const configFiles = Array.isArray(knipPlugin.config) ? knipPlugin.config : [knipPlugin.config];
|
|
81
|
+
for (const file of configFiles) {
|
|
82
|
+
if (!file) continue;
|
|
83
|
+
try {
|
|
84
|
+
await fs.access(path.join(baseDir, file));
|
|
85
|
+
return true;
|
|
86
|
+
} catch {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return false;
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
// Map other Knip plugin properties to pkg-scaffold
|
|
94
|
+
get: (key) => knipPlugin[key]
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Returns all integrated Knip plugins
|
|
100
|
+
*/
|
|
101
|
+
getPlugins() {
|
|
102
|
+
return Array.from(this.knipPlugins.values());
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export default KnipAdapter;
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import fs from 'fs/promises';
|
|
3
3
|
import { pathToFileURL } from 'url';
|
|
4
|
+
import { KnipAdapter } from './KnipAdapter.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Advanced Plugin Registry supporting Builtin, Custom, and Knip-style plugins.
|
|
7
|
-
* Version
|
|
8
|
+
* Version 4.0.0: Enhanced with Modern Frameworks, Backend Services, and Standalone Knip Integration.
|
|
8
9
|
*/
|
|
9
10
|
export class PluginRegistry {
|
|
10
11
|
constructor(context) {
|
|
11
12
|
this.context = context;
|
|
12
13
|
this.plugins = new Map();
|
|
13
14
|
this.config = null;
|
|
15
|
+
this.knipAdapter = new KnipAdapter(context);
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
async init(projectRoot) {
|
|
@@ -35,14 +37,21 @@ export class PluginRegistry {
|
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
if (this.config.supportKnipPlugins) {
|
|
38
|
-
await this.initKnipAdapter();
|
|
40
|
+
await this.initKnipAdapter(projectRoot);
|
|
39
41
|
}
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
async loadBuiltinPlugins() {
|
|
45
|
+
// Core Ecosystems
|
|
43
46
|
const { NextJsPlugin } = await import('./ecosystems/NextJsPlugin.js');
|
|
44
47
|
const { NuxtPlugin, RemixPlugin, SvelteKitPlugin, AstroPlugin } = await import('./ecosystems/GenericPlugins.js');
|
|
45
48
|
const { TypeScriptPlugin } = await import('./ecosystems/TypeScriptPlugin.js');
|
|
49
|
+
|
|
50
|
+
// Modern Frameworks (New in v4.0)
|
|
51
|
+
const { ReactPlugin, VuePlugin, SveltePlugin, AngularPlugin } = await import('./ecosystems/ModernFrameworks.js');
|
|
52
|
+
|
|
53
|
+
// Backend Services (New in v4.0)
|
|
54
|
+
const { GraphQLPlugin, DatabasePlugin } = await import('./ecosystems/BackendServices.js');
|
|
46
55
|
|
|
47
56
|
const builtins = [
|
|
48
57
|
new NextJsPlugin(this.context),
|
|
@@ -50,7 +59,13 @@ export class PluginRegistry {
|
|
|
50
59
|
new RemixPlugin(this.context),
|
|
51
60
|
new SvelteKitPlugin(this.context),
|
|
52
61
|
new AstroPlugin(this.context),
|
|
53
|
-
new TypeScriptPlugin(this.context)
|
|
62
|
+
new TypeScriptPlugin(this.context),
|
|
63
|
+
new ReactPlugin(this.context),
|
|
64
|
+
new VuePlugin(this.context),
|
|
65
|
+
new SveltePlugin(this.context),
|
|
66
|
+
new AngularPlugin(this.context),
|
|
67
|
+
new GraphQLPlugin(this.context),
|
|
68
|
+
new DatabasePlugin(this.context)
|
|
54
69
|
];
|
|
55
70
|
|
|
56
71
|
builtins.forEach(p => {
|
|
@@ -82,8 +97,11 @@ export class PluginRegistry {
|
|
|
82
97
|
}
|
|
83
98
|
}
|
|
84
99
|
|
|
85
|
-
async initKnipAdapter() {
|
|
100
|
+
async initKnipAdapter(projectRoot) {
|
|
86
101
|
this.context.knipCompatible = true;
|
|
102
|
+
await this.knipAdapter.discoverPlugins(projectRoot);
|
|
103
|
+
const knipPlugins = this.knipAdapter.getPlugins();
|
|
104
|
+
knipPlugins.forEach(p => this.register(p));
|
|
87
105
|
}
|
|
88
106
|
|
|
89
107
|
register(plugin) {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* Backend Services Plugins for pkg-scaffold v4.0.0
|
|
4
|
+
* ============================================================================
|
|
5
|
+
* Built-in support for GraphQL, REST APIs, and Databases.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { BasePlugin } from '../BasePlugin.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* GraphQL Ecosystem Plugin
|
|
12
|
+
*/
|
|
13
|
+
export class GraphQLPlugin extends BasePlugin {
|
|
14
|
+
get name() {
|
|
15
|
+
return 'graphql';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
getConfigFiles() {
|
|
19
|
+
return ['package.json', 'graphql.config.js', '.graphqlconfig'];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async isActive(baseDir) {
|
|
23
|
+
try {
|
|
24
|
+
const pkgJson = JSON.parse(await require('fs').promises.readFile(require('path').join(baseDir, 'package.json'), 'utf8'));
|
|
25
|
+
return !!(pkgJson.dependencies?.graphql || pkgJson.devDependencies?.graphql || pkgJson.dependencies?.['@apollo/client']);
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async analyze(node, filePath) {
|
|
32
|
+
// Detect GraphQL tagged templates
|
|
33
|
+
const gqlPattern = /gql\s*`([\s\S]*?)`/g;
|
|
34
|
+
const matches = node.rawCode?.match(gqlPattern) || [];
|
|
35
|
+
if (matches.length > 0) {
|
|
36
|
+
node.graphqlQueries = matches;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Database Ecosystem Plugin (Prisma, Drizzle, TypeORM)
|
|
43
|
+
*/
|
|
44
|
+
export class DatabasePlugin extends BasePlugin {
|
|
45
|
+
get name() {
|
|
46
|
+
return 'database';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
getConfigFiles() {
|
|
50
|
+
return ['package.json', 'prisma/schema.prisma', 'drizzle.config.ts', 'ormconfig.json'];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async analyze(node, filePath) {
|
|
54
|
+
// Detect DB usage
|
|
55
|
+
if (node.explicitImports.has('@prisma/client') || node.explicitImports.has('drizzle-orm')) {
|
|
56
|
+
node.usesDatabase = true;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* Modern Frameworks Plugins for pkg-scaffold v4.0.0
|
|
4
|
+
* ============================================================================
|
|
5
|
+
* Built-in support for React, Vue, Svelte, and Angular.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { BasePlugin } from '../BasePlugin.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* React Ecosystem Plugin
|
|
12
|
+
*/
|
|
13
|
+
export class ReactPlugin extends BasePlugin {
|
|
14
|
+
get name() {
|
|
15
|
+
return 'react';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
getConfigFiles() {
|
|
19
|
+
return ['package.json'];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async isActive(baseDir) {
|
|
23
|
+
try {
|
|
24
|
+
const pkgJson = JSON.parse(await require('fs').promises.readFile(require('path').join(baseDir, 'package.json'), 'utf8'));
|
|
25
|
+
return !!(pkgJson.dependencies?.react || pkgJson.devDependencies?.react);
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
getRoutePatterns() {
|
|
32
|
+
return [/\.(tsx?|jsx?)$/];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
getRequiredSystemContracts() {
|
|
36
|
+
return ['default', 'Component', 'PureComponent', 'Fragment', 'useEffect', 'useState', 'useContext', 'useReducer', 'useCallback', 'useMemo', 'useRef', 'useImperativeHandle', 'useLayoutEffect', 'useDebugValue'];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async analyze(node, filePath) {
|
|
40
|
+
if (node.explicitImports.has('react')) {
|
|
41
|
+
node.isReactComponent = true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Detect JSX
|
|
45
|
+
if (node.rawCode && (node.rawCode.includes('</') || node.rawCode.includes('/>'))) {
|
|
46
|
+
node.hasJSX = true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Detect Hooks
|
|
50
|
+
const hookMatches = node.rawCode?.match(/use[A-Z]\w+/g) || [];
|
|
51
|
+
if (hookMatches.length > 0) {
|
|
52
|
+
node.reactHooks = new Set(hookMatches);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Vue Ecosystem Plugin
|
|
59
|
+
*/
|
|
60
|
+
export class VuePlugin extends BasePlugin {
|
|
61
|
+
get name() {
|
|
62
|
+
return 'vue';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
getConfigFiles() {
|
|
66
|
+
return ['package.json', 'vue.config.js', 'vite.config.ts', 'vite.config.js'];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async isActive(baseDir) {
|
|
70
|
+
try {
|
|
71
|
+
const pkgJson = JSON.parse(await require('fs').promises.readFile(require('path').join(baseDir, 'package.json'), 'utf8'));
|
|
72
|
+
return !!(pkgJson.dependencies?.vue || pkgJson.devDependencies?.vue);
|
|
73
|
+
} catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
getRoutePatterns() {
|
|
79
|
+
return [/\.vue$/, /\.(tsx?|jsx?)$/];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async analyze(node, filePath) {
|
|
83
|
+
if (filePath.endsWith('.vue')) {
|
|
84
|
+
node.isVueSFC = true;
|
|
85
|
+
// Extract template/script/style sections
|
|
86
|
+
const templateMatch = node.rawCode?.match(/<template>([\s\S]*)<\/template>/);
|
|
87
|
+
if (templateMatch) node.vueTemplate = templateMatch[1];
|
|
88
|
+
|
|
89
|
+
const scriptMatch = node.rawCode?.match(/<script(?: setup)?(?: lang=['"]\w+['"])?>([\s\S]*)<\/script>/);
|
|
90
|
+
if (scriptMatch) node.vueScript = scriptMatch[1];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Svelte Ecosystem Plugin
|
|
97
|
+
*/
|
|
98
|
+
export class SveltePlugin extends BasePlugin {
|
|
99
|
+
get name() {
|
|
100
|
+
return 'svelte';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
getConfigFiles() {
|
|
104
|
+
return ['package.json', 'svelte.config.js'];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async isActive(baseDir) {
|
|
108
|
+
try {
|
|
109
|
+
const pkgJson = JSON.parse(await require('fs').promises.readFile(require('path').join(baseDir, 'package.json'), 'utf8'));
|
|
110
|
+
return !!(pkgJson.dependencies?.svelte || pkgJson.devDependencies?.svelte);
|
|
111
|
+
} catch {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
getRoutePatterns() {
|
|
117
|
+
return [/\.svelte$/, /\.(tsx?|jsx?)$/];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async analyze(node, filePath) {
|
|
121
|
+
if (filePath.endsWith('.svelte')) {
|
|
122
|
+
node.isSvelteComponent = true;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Angular Ecosystem Plugin
|
|
129
|
+
*/
|
|
130
|
+
export class AngularPlugin extends BasePlugin {
|
|
131
|
+
get name() {
|
|
132
|
+
return 'angular';
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
getConfigFiles() {
|
|
136
|
+
return ['package.json', 'angular.json'];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async isActive(baseDir) {
|
|
140
|
+
try {
|
|
141
|
+
const pkgJson = JSON.parse(await require('fs').promises.readFile(require('path').join(baseDir, 'package.json'), 'utf8'));
|
|
142
|
+
return !!(pkgJson.dependencies?.['@angular/core'] || pkgJson.devDependencies?.['@angular/core']);
|
|
143
|
+
} catch {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
getRoutePatterns() {
|
|
149
|
+
return [/\.ts$/];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async analyze(node, filePath) {
|
|
153
|
+
if (node.rawCode?.includes('@Component') || node.rawCode?.includes('@Injectable') || node.rawCode?.includes('@NgModule')) {
|
|
154
|
+
node.isAngularEntity = true;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|