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.
- package/LICENSE +211 -21
- package/NOTICE +13 -0
- package/package.json +9 -7
- package/pkg-scaffold/config.json +2 -1
- package/src/api/HeadlessAPI.js +365 -0
- package/src/api/PluginSDK.js +299 -0
- package/src/plugins/BasePlugin.js +59 -41
- package/src/plugins/KnipAdapter.js +106 -0
- package/src/plugins/PluginRegistry.js +103 -70
- package/src/plugins/ecosystems/BackendServices.js +59 -0
- package/src/plugins/ecosystems/ModernFrameworks.js +157 -0
- package/src/plugins/ecosystems/TypeScriptPlugin.js +56 -0
- package/src/resolution/CircularDetector.js +71 -0
- package/src/resolution/PathMapper.js +12 -2
- package/src/resolution/WorkSpaceGraph.js +10 -0
|
@@ -1,95 +1,128 @@
|
|
|
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.
|
|
8
|
+
* Version 4.0.0: Enhanced with Modern Frameworks, Backend Services, and Standalone Knip Integration.
|
|
7
9
|
*/
|
|
8
10
|
export class PluginRegistry {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
async init(projectRoot) {
|
|
16
|
-
const configPath = path.join(projectRoot, 'pkg-scaffold', 'config.json');
|
|
17
|
-
try {
|
|
18
|
-
const configRaw = await fs.readFile(configPath, 'utf8');
|
|
19
|
-
this.config = JSON.parse(configRaw);
|
|
20
|
-
} catch (e) {
|
|
21
|
-
this.config = { useBuiltinPlugins: true, useCustomPlugins: true, supportKnipPlugins: true };
|
|
11
|
+
constructor(context) {
|
|
12
|
+
this.context = context;
|
|
13
|
+
this.plugins = new Map();
|
|
14
|
+
this.config = null;
|
|
15
|
+
this.knipAdapter = new KnipAdapter(context);
|
|
22
16
|
}
|
|
23
17
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
18
|
+
async init(projectRoot) {
|
|
19
|
+
const configPath = path.join(projectRoot, 'pkg-scaffold', 'config.json');
|
|
20
|
+
try {
|
|
21
|
+
const configRaw = await fs.readFile(configPath, 'utf8');
|
|
22
|
+
this.config = JSON.parse(configRaw);
|
|
23
|
+
} catch (e) {
|
|
24
|
+
this.config = {
|
|
25
|
+
useBuiltinPlugins: true,
|
|
26
|
+
useCustomPlugins: true,
|
|
27
|
+
supportKnipPlugins: true
|
|
28
|
+
};
|
|
29
|
+
}
|
|
27
30
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
if (this.config.useBuiltinPlugins) {
|
|
32
|
+
await this.loadBuiltinPlugins();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (this.config.useCustomPlugins) {
|
|
36
|
+
await this.loadCustomPlugins(projectRoot);
|
|
37
|
+
}
|
|
31
38
|
|
|
32
|
-
|
|
33
|
-
|
|
39
|
+
if (this.config.supportKnipPlugins) {
|
|
40
|
+
await this.initKnipAdapter(projectRoot);
|
|
41
|
+
}
|
|
34
42
|
}
|
|
35
|
-
}
|
|
36
43
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
44
|
+
async loadBuiltinPlugins() {
|
|
45
|
+
// Core Ecosystems
|
|
46
|
+
const { NextJsPlugin } = await import('./ecosystems/NextJsPlugin.js');
|
|
47
|
+
const { NuxtPlugin, RemixPlugin, SvelteKitPlugin, AstroPlugin } = await import('./ecosystems/GenericPlugins.js');
|
|
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');
|
|
40
55
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
56
|
+
const builtins = [
|
|
57
|
+
new NextJsPlugin(this.context),
|
|
58
|
+
new NuxtPlugin(this.context),
|
|
59
|
+
new RemixPlugin(this.context),
|
|
60
|
+
new SvelteKitPlugin(this.context),
|
|
61
|
+
new AstroPlugin(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)
|
|
69
|
+
];
|
|
48
70
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
71
|
+
builtins.forEach(p => {
|
|
72
|
+
if (!this.config.enabledPlugins || this.config.enabledPlugins.includes(p.name)) {
|
|
73
|
+
this.register(p);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async loadCustomPlugins(projectRoot) {
|
|
79
|
+
const pluginsDir = path.join(projectRoot, 'pkg-scaffold', 'plugins');
|
|
80
|
+
try {
|
|
81
|
+
const files = await fs.readdir(pluginsDir);
|
|
82
|
+
for (const file of files) {
|
|
83
|
+
if (file.endsWith('.js') || file.endsWith('.mjs')) {
|
|
84
|
+
const pluginModule = await import(pathToFileURL(path.join(pluginsDir, file)).href);
|
|
85
|
+
const PluginClass = pluginModule.default || pluginModule;
|
|
86
|
+
const pluginInstance = new PluginClass(this.context);
|
|
55
87
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
88
|
+
const version = pluginInstance.get('version');
|
|
89
|
+
if (version && this.context.verbose) {
|
|
90
|
+
console.log(`[PluginRegistry] Loaded ${pluginInstance.name} v${version}`);
|
|
91
|
+
}
|
|
92
|
+
this.register(pluginInstance);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
} catch (e) {
|
|
96
|
+
// No custom plugins or dir missing
|
|
65
97
|
}
|
|
66
|
-
}
|
|
67
|
-
} catch (e) {
|
|
68
|
-
// No custom plugins or dir missing
|
|
69
98
|
}
|
|
70
|
-
}
|
|
71
99
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
100
|
+
async initKnipAdapter(projectRoot) {
|
|
101
|
+
this.context.knipCompatible = true;
|
|
102
|
+
await this.knipAdapter.discoverPlugins(projectRoot);
|
|
103
|
+
const knipPlugins = this.knipAdapter.getPlugins();
|
|
104
|
+
knipPlugins.forEach(p => this.register(p));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
register(plugin) {
|
|
108
|
+
this.plugins.set(plugin.name, plugin);
|
|
109
|
+
}
|
|
77
110
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
111
|
+
getPlugins() {
|
|
112
|
+
return Array.from(this.plugins.values());
|
|
113
|
+
}
|
|
81
114
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
115
|
+
getPlugin(name) {
|
|
116
|
+
return this.plugins.get(name);
|
|
117
|
+
}
|
|
85
118
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
119
|
+
async getActivePlugins(baseDir) {
|
|
120
|
+
const active = [];
|
|
121
|
+
for (const plugin of this.plugins.values()) {
|
|
122
|
+
if (await plugin.isActive(baseDir)) {
|
|
123
|
+
active.push(plugin);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return active;
|
|
92
127
|
}
|
|
93
|
-
return active;
|
|
94
|
-
}
|
|
95
128
|
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import { BasePlugin } from '../BasePlugin.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* TypeScript Plugin for pkg-scaffold.
|
|
7
|
+
* Handles tsconfig.json detection and TypeScript-specific entry points.
|
|
8
|
+
*/
|
|
9
|
+
export class TypeScriptPlugin extends BasePlugin {
|
|
10
|
+
get name() {
|
|
11
|
+
return 'typescript';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
getConfigFiles() {
|
|
15
|
+
return ['tsconfig.json', 'tsconfig.base.json', 'tsconfig.eslint.json'];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
getRoutePatterns() {
|
|
19
|
+
// Common TypeScript entry points and declaration files
|
|
20
|
+
return [
|
|
21
|
+
/src\/index\.ts$/,
|
|
22
|
+
/src\/main\.ts$/,
|
|
23
|
+
/src\/lib\.ts$/,
|
|
24
|
+
/.*\.d\.ts$/
|
|
25
|
+
];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
getRequiredSystemContracts() {
|
|
29
|
+
// TypeScript specific implicit exports or requirements
|
|
30
|
+
return ['default'];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Custom Getter for v3.2.0: Get the compiler version from the project.
|
|
35
|
+
*/
|
|
36
|
+
async getCompilerVersion() {
|
|
37
|
+
try {
|
|
38
|
+
const packageJsonPath = path.join(this.context.cwd, 'package.json');
|
|
39
|
+
const content = await fs.readFile(packageJsonPath, 'utf8');
|
|
40
|
+
const pkg = JSON.parse(content);
|
|
41
|
+
return pkg.devDependencies?.typescript || pkg.dependencies?.typescript || 'unknown';
|
|
42
|
+
} catch {
|
|
43
|
+
return 'not installed';
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async isActive(baseDir) {
|
|
48
|
+
for (const file of this.getConfigFiles()) {
|
|
49
|
+
try {
|
|
50
|
+
await fs.access(path.join(baseDir, file));
|
|
51
|
+
return true;
|
|
52
|
+
} catch {}
|
|
53
|
+
}
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
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 from the lovely DreamLong"
|
|
8
|
+
* ============================================================================
|
|
9
|
+
* Implements a high-performance Tarjan-based or DFS-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
|
|
22
|
+
* @param {Map} graph - The codebase dependency graph
|
|
23
|
+
* @returns {Array} List of detected cycles
|
|
24
|
+
*/
|
|
25
|
+
detectCycles(graph) {
|
|
26
|
+
this.cycles = [];
|
|
27
|
+
const visited = new Set();
|
|
28
|
+
const stack = new Set();
|
|
29
|
+
const path = [];
|
|
30
|
+
|
|
31
|
+
for (const filePath of graph.keys()) {
|
|
32
|
+
if (!visited.has(filePath)) {
|
|
33
|
+
this.dfs(filePath, graph, visited, stack, path);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return this.cycles;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
dfs(node, graph, visited, stack, path) {
|
|
41
|
+
visited.add(node);
|
|
42
|
+
stack.add(node);
|
|
43
|
+
path.push(node);
|
|
44
|
+
|
|
45
|
+
const edges = graph.get(node)?.outgoingEdges || [];
|
|
46
|
+
for (const neighbor of edges) {
|
|
47
|
+
if (stack.has(neighbor)) {
|
|
48
|
+
// Cycle detected
|
|
49
|
+
const cycleStartIndex = path.indexOf(neighbor);
|
|
50
|
+
const cycle = path.slice(cycleStartIndex);
|
|
51
|
+
this.cycles.push(cycle);
|
|
52
|
+
} else if (!visited.has(neighbor)) {
|
|
53
|
+
this.dfs(neighbor, graph, visited, stack, path);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
stack.delete(node);
|
|
58
|
+
path.pop();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Formats cycles for reporting
|
|
63
|
+
*/
|
|
64
|
+
formatCycles() {
|
|
65
|
+
return this.cycles.map(cycle => {
|
|
66
|
+
return cycle.join(' -> ') + ' -> ' + cycle[0];
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
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 {
|