pkg-scaffold 3.2.0 → 3.3.2
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/bin/cli.js +52 -59
- package/package.json +8 -6
- package/src/EngineContext.js +24 -20
- package/src/api/HeadlessAPI.js +376 -0
- package/src/api/PluginSDK.js +299 -0
- package/src/ast/ASTAnalyzer.js +3 -2
- package/src/ast/OxcAnalyzer.js +17 -7
- package/src/index.js +119 -148
- package/src/performance/SecretDetector.js +378 -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 +122 -0
- package/src/resolution/PathMapper.js +12 -2
- package/src/resolution/WorkSpaceGraph.js +10 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
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
|
+
export class CircularDetector {
|
|
15
|
+
constructor(context) {
|
|
16
|
+
this.context = context;
|
|
17
|
+
this.cycles = [];
|
|
18
|
+
}
|
|
19
|
+
|
|
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
|
+
detectCycles(graph, context = null) {
|
|
26
|
+
if (context) this.context = context;
|
|
27
|
+
this.cwd = context?.cwd || this.context?.cwd || process.cwd();
|
|
28
|
+
this.cycles = [];
|
|
29
|
+
let index = 0;
|
|
30
|
+
const stack = [];
|
|
31
|
+
const indices = new Map();
|
|
32
|
+
const lowlink = new Map();
|
|
33
|
+
const onStack = new Set();
|
|
34
|
+
|
|
35
|
+
const strongconnect = (v) => {
|
|
36
|
+
indices.set(v, index);
|
|
37
|
+
lowlink.set(v, index);
|
|
38
|
+
index++;
|
|
39
|
+
stack.push(v);
|
|
40
|
+
onStack.add(v);
|
|
41
|
+
|
|
42
|
+
const node = graph.get(v);
|
|
43
|
+
if (node && node.outgoingEdges) {
|
|
44
|
+
for (const w of node.outgoingEdges) {
|
|
45
|
+
if (!indices.has(w)) {
|
|
46
|
+
strongconnect(w);
|
|
47
|
+
lowlink.set(v, Math.min(lowlink.get(v), lowlink.get(w)));
|
|
48
|
+
} else if (onStack.has(w)) {
|
|
49
|
+
lowlink.set(v, Math.min(lowlink.get(v), indices.get(w)));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (lowlink.get(v) === indices.get(v)) {
|
|
55
|
+
const component = [];
|
|
56
|
+
let w;
|
|
57
|
+
do {
|
|
58
|
+
w = stack.pop();
|
|
59
|
+
onStack.delete(w);
|
|
60
|
+
component.push(w);
|
|
61
|
+
} while (w !== v);
|
|
62
|
+
|
|
63
|
+
if (component.length > 1) {
|
|
64
|
+
this.cycles.push(component.reverse());
|
|
65
|
+
} else {
|
|
66
|
+
// Check for self-loops
|
|
67
|
+
const node = graph.get(v);
|
|
68
|
+
if (node && node.outgoingEdges && node.outgoingEdges.has(v)) {
|
|
69
|
+
this.cycles.push([v]);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
for (const v of graph.keys()) {
|
|
76
|
+
if (!indices.has(v)) {
|
|
77
|
+
strongconnect(v);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return this.cycles;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Formats cycles for reporting with file paths
|
|
86
|
+
*/
|
|
87
|
+
formatCycles() {
|
|
88
|
+
return this.cycles.map(cycle => {
|
|
89
|
+
const paths = cycle.map(p => {
|
|
90
|
+
// Extract relative path for readability
|
|
91
|
+
let rel = p.replace(this.context.cwd, '').replace(/^\//, '');
|
|
92
|
+
// Convert absolute Windows paths
|
|
93
|
+
if (rel.includes(':\\')) {
|
|
94
|
+
rel = rel.split(':\\')[1] || rel;
|
|
95
|
+
}
|
|
96
|
+
return rel;
|
|
97
|
+
});
|
|
98
|
+
if (cycle.length === 1) return `${paths[0]} -> (self-loop)`;
|
|
99
|
+
return paths.join(' -> ') + ' -> ' + paths[0];
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Gets detailed cycle information
|
|
105
|
+
*/
|
|
106
|
+
getCycleDetails() {
|
|
107
|
+
return this.cycles.map((cycle, idx) => ({
|
|
108
|
+
cycleId: idx + 1,
|
|
109
|
+
files: cycle.map(p => {
|
|
110
|
+
let rel = p.replace(this.context.cwd, '').replace(/^\//, '');
|
|
111
|
+
if (rel.includes(':\\')) {
|
|
112
|
+
rel = rel.split(':\\')[1] || rel;
|
|
113
|
+
}
|
|
114
|
+
return rel;
|
|
115
|
+
}),
|
|
116
|
+
length: cycle.length,
|
|
117
|
+
isSelfLoop: cycle.length === 1
|
|
118
|
+
}));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export default CircularDetector;
|
|
@@ -25,15 +25,25 @@ export class PathMapper {
|
|
|
25
25
|
const rawText = await fs.readFile(configPath, 'utf8');
|
|
26
26
|
|
|
27
27
|
// Strip inline single-line and block comments before parsing
|
|
28
|
-
|
|
28
|
+
// Improved regex to handle more edge cases in tsconfig comments
|
|
29
|
+
const jsonCleanText = rawText
|
|
30
|
+
.replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, '$1')
|
|
31
|
+
.replace(/,(\s*[\]}])/g, '$1'); // Remove trailing commas
|
|
32
|
+
|
|
29
33
|
const tsconfig = JSON.parse(jsonCleanText);
|
|
30
34
|
|
|
31
35
|
if (!tsconfig.compilerOptions) return;
|
|
32
36
|
|
|
33
37
|
const opts = tsconfig.compilerOptions;
|
|
38
|
+
|
|
39
|
+
// v6 Path Resolution Fix (Knip Issue #1794)
|
|
40
|
+
// Ensure baseUrl is correctly resolved relative to the tsconfig file location
|
|
41
|
+
const configDir = path.dirname(configPath);
|
|
34
42
|
if (opts.baseUrl) {
|
|
35
43
|
this.baseUrl = opts.baseUrl;
|
|
36
|
-
this.absoluteBaseUrl = path.resolve(
|
|
44
|
+
this.absoluteBaseUrl = path.resolve(configDir, this.baseUrl);
|
|
45
|
+
} else {
|
|
46
|
+
this.absoluteBaseUrl = configDir;
|
|
37
47
|
}
|
|
38
48
|
|
|
39
49
|
if (opts.paths) {
|
|
@@ -21,6 +21,16 @@ export class WorkspaceGraph {
|
|
|
21
21
|
const pnpmWorkspacePath = path.join(this.context.cwd, 'pnpm-workspace.yaml');
|
|
22
22
|
|
|
23
23
|
let workspaceGlobs = [];
|
|
24
|
+
this.hoistedDependencies = new Set();
|
|
25
|
+
|
|
26
|
+
// Load hoisted dependencies from root package.json (Knip Issue #1792 fix)
|
|
27
|
+
try {
|
|
28
|
+
const rootPkg = JSON.parse(await fs.readFile(rootPackageJsonPath, 'utf8'));
|
|
29
|
+
const deps = { ...rootPkg.dependencies, ...rootPkg.devDependencies };
|
|
30
|
+
Object.keys(deps).forEach(d => this.hoistedDependencies.add(d));
|
|
31
|
+
} catch (e) {
|
|
32
|
+
// No root package.json or unreadable
|
|
33
|
+
}
|
|
24
34
|
|
|
25
35
|
// Protocol A: Check for pnpm workspace configurations
|
|
26
36
|
try {
|