pkg-scaffold 3.1.3 → 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.
@@ -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;
@@ -1,53 +1,71 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+
1
4
  /**
2
5
  * Base class for all pkg-scaffold plugins.
3
6
  * Defines the contract for ecosystem detection and entry point mapping.
7
+ * Version 3.2.0: Added support for dynamic custom getters.
4
8
  */
5
9
  export class BasePlugin {
6
- constructor(context) {
7
- this.context = context;
8
- }
10
+ constructor(context) {
11
+ this.context = context;
12
+ this.customGetters = new Map();
13
+ }
14
+
15
+ /**
16
+ * Unique identifier for the plugin (e.g., 'nextjs').
17
+ */
18
+ get name() {
19
+ throw new Error('Plugin must implement name getter');
20
+ }
9
21
 
10
- /**
11
- * Unique identifier for the plugin (e.g., 'nextjs').
12
- */
13
- get name() {
14
- throw new Error('Plugin must implement name getter');
15
- }
22
+ /**
23
+ * Returns a list of configuration files that indicate this ecosystem is active.
24
+ */
25
+ getConfigFiles() {
26
+ return [];
27
+ }
16
28
 
17
- /**
18
- * Returns a list of configuration files that indicate this ecosystem is active.
19
- */
20
- getConfigFiles() {
21
- return [];
22
- }
29
+ /**
30
+ * Returns regex patterns for files that should be treated as entry points.
31
+ */
32
+ getRoutePatterns() {
33
+ return [];
34
+ }
23
35
 
24
- /**
25
- * Returns regex patterns for files that should be treated as entry points.
26
- */
27
- getRoutePatterns() {
28
- return [];
29
- }
36
+ /**
37
+ * Returns symbols that are implicitly required/exported by the framework.
38
+ */
39
+ getRequiredSystemContracts() {
40
+ return ['default'];
41
+ }
30
42
 
31
- /**
32
- * Returns symbols that are implicitly required/exported by the framework.
33
- */
34
- getRequiredSystemContracts() {
35
- return ['default'];
36
- }
43
+ /**
44
+ * Version 3.2.0: Dynamic getter for custom plugin properties.
45
+ * @param {string} key - The property key to retrieve
46
+ * @returns {any} The value of the custom property
47
+ */
48
+ get(key) {
49
+ const methodName = `get${key.charAt(0).toUpperCase() + key.slice(1)}`;
50
+ if (typeof this[methodName] === 'function') {
51
+ return this[methodName]();
52
+ }
53
+ return this.customGetters.get(key);
54
+ }
37
55
 
38
- /**
39
- * Optional: Logic to detect if the plugin should be active in the given directory.
40
- */
41
- async isActive(baseDir) {
42
- const configFiles = this.getConfigFiles();
43
- for (const file of configFiles) {
44
- try {
45
- await fs.access(path.join(baseDir, file));
46
- return true;
47
- } catch {
48
- continue;
49
- }
56
+ /**
57
+ * Optional: Logic to detect if the plugin should be active in the given directory.
58
+ */
59
+ async isActive(baseDir) {
60
+ const configFiles = this.getConfigFiles();
61
+ for (const file of configFiles) {
62
+ try {
63
+ await fs.access(path.join(baseDir, file));
64
+ return true;
65
+ } catch {
66
+ continue;
67
+ }
68
+ }
69
+ return false;
50
70
  }
51
- return false;
52
- }
53
71
  }
@@ -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;