entkapp 5.2.4 → 5.3.1
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 +30 -37
- package/bin/cli.js +3 -3
- package/entkapp/config.json +18 -2
- package/entkapp/plugins/README.md +52 -12
- package/package.json +2 -2
- package/schema.json +87 -3
- package/src/ast/SecretScanner.js +112 -50
- package/src/index.js +36 -1
- package/src/resolution/ConfigLoader.js +88 -2
- package/src/resolution/DepencyResolver.js +15 -0
- package/src/resolution/DependencyProfiler.js +41 -23
- package/src/resolution/PathMapper.js +42 -9
- package/src/resolution/TSConfigLoader.js +96 -10
- package/src/resolution/WorkSpaceGraph.js +178 -3
- package/src/resolution/WorkspaceDiagnostic.js +137 -3
|
@@ -1,4 +1,90 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* ConfigLoader
|
|
6
|
+
* Loads and merges entkapp configuration from multiple sources:
|
|
7
|
+
* - entkapp/config.json (project config)
|
|
8
|
+
* - entkapp.config.js / entkapp.config.mjs (JS config)
|
|
9
|
+
* - package.json "entkapp" key
|
|
10
|
+
* - CLI flags (passed as overrides)
|
|
11
|
+
*/
|
|
1
12
|
export class ConfigLoader {
|
|
2
|
-
constructor(cwd) {
|
|
3
|
-
|
|
13
|
+
constructor(cwd) {
|
|
14
|
+
this.cwd = cwd;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async loadConfig(overrides = {}) {
|
|
18
|
+
let config = this._defaultConfig();
|
|
19
|
+
|
|
20
|
+
// 1. Try entkapp/config.json
|
|
21
|
+
const jsonConfigPath = path.join(this.cwd, 'entkapp', 'config.json');
|
|
22
|
+
try {
|
|
23
|
+
const raw = await fs.readFile(jsonConfigPath, 'utf8');
|
|
24
|
+
// Strip comments from JSON (JSONC support)
|
|
25
|
+
const stripped = raw.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
26
|
+
const parsed = JSON.parse(stripped);
|
|
27
|
+
config = this._merge(config, parsed);
|
|
28
|
+
} catch (e) {}
|
|
29
|
+
|
|
30
|
+
// 2. Try entkapp.config.js / entkapp.config.mjs
|
|
31
|
+
for (const configFile of ['entkapp.config.mjs', 'entkapp.config.js', 'entkapp.config.cjs']) {
|
|
32
|
+
const jsConfigPath = path.join(this.cwd, configFile);
|
|
33
|
+
try {
|
|
34
|
+
const mod = await import(jsConfigPath);
|
|
35
|
+
const jsConfig = mod.default || mod;
|
|
36
|
+
if (typeof jsConfig === 'object') {
|
|
37
|
+
config = this._merge(config, jsConfig);
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
} catch (e) {}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 3. Try package.json "entkapp" key
|
|
44
|
+
const pkgPath = path.join(this.cwd, 'package.json');
|
|
45
|
+
try {
|
|
46
|
+
const pkg = JSON.parse(await fs.readFile(pkgPath, 'utf8'));
|
|
47
|
+
if (pkg.entkapp && typeof pkg.entkapp === 'object') {
|
|
48
|
+
config = this._merge(config, pkg.entkapp);
|
|
49
|
+
}
|
|
50
|
+
} catch (e) {}
|
|
51
|
+
|
|
52
|
+
// 4. Apply CLI overrides
|
|
53
|
+
config = this._merge(config, overrides);
|
|
54
|
+
|
|
55
|
+
return config;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
_defaultConfig() {
|
|
59
|
+
return {
|
|
60
|
+
interface: 'CLI',
|
|
61
|
+
useBuiltinPlugins: true,
|
|
62
|
+
useCustomPlugins: true,
|
|
63
|
+
options: {
|
|
64
|
+
verbose: false,
|
|
65
|
+
fastMode: true,
|
|
66
|
+
selfHealing: true
|
|
67
|
+
},
|
|
68
|
+
enabledPlugins: [],
|
|
69
|
+
ignoreDependencies: ['entkapp', '@types/*'],
|
|
70
|
+
exclude: ['node_modules', '.git', 'dist', 'build', 'coverage'],
|
|
71
|
+
entryPoints: [],
|
|
72
|
+
workspace: false
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
_merge(base, override) {
|
|
77
|
+
if (!override || typeof override !== 'object') return base;
|
|
78
|
+
const result = { ...base };
|
|
79
|
+
for (const key of Object.keys(override)) {
|
|
80
|
+
if (key === 'options' && typeof override[key] === 'object') {
|
|
81
|
+
result.options = { ...(base.options || {}), ...override[key] };
|
|
82
|
+
} else if (Array.isArray(override[key])) {
|
|
83
|
+
result[key] = override[key];
|
|
84
|
+
} else {
|
|
85
|
+
result[key] = override[key];
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
4
90
|
}
|
|
@@ -25,8 +25,23 @@ export class DependencyResolver {
|
|
|
25
25
|
resolveModulePath(sourceFile, specifier) {
|
|
26
26
|
const cleanSource = this.normalizePath(sourceFile);
|
|
27
27
|
|
|
28
|
+
// Check if it's a workspace package reference
|
|
29
|
+
if (this.context.isWorkspaceEnabled && this.workspaceGraph && this.workspaceGraph.isLocalWorkspaceSpecifier(specifier)) {
|
|
30
|
+
const match = this.workspaceGraph.getWorkspacePackageMatch(specifier);
|
|
31
|
+
if (match && match.entryPoints && match.entryPoints.length > 0) {
|
|
32
|
+
// Return the first entry point for the workspace package
|
|
33
|
+
return this.normalizePath(match.entryPoints[0]);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
28
37
|
// UPGRADE: Use PathMapper for sophisticated resolution (TS-to-JS, aliases, etc.)
|
|
29
38
|
if (this.pathMapper) {
|
|
39
|
+
// Allow pathMapper to resolve aliases directly from specifier
|
|
40
|
+
const aliasResolved = this.pathMapper.resolvePath(specifier);
|
|
41
|
+
if (aliasResolved && aliasResolved !== specifier && existsSync(aliasResolved)) {
|
|
42
|
+
return this.normalizePath(aliasResolved);
|
|
43
|
+
}
|
|
44
|
+
|
|
30
45
|
const dir = path.dirname(cleanSource);
|
|
31
46
|
const target = path.resolve(dir, specifier);
|
|
32
47
|
const resolved = this.pathMapper.resolvePath(target);
|
|
@@ -146,9 +146,44 @@ export class DependencyProfiler {
|
|
|
146
146
|
const usedPackages = new Set();
|
|
147
147
|
const usedBinaries = new Set(); // NEW: Track which binaries are actually used
|
|
148
148
|
|
|
149
|
-
// 1. Scan package.json scripts
|
|
149
|
+
// 1. Scan package.json scripts (Root)
|
|
150
|
+
await this._scanDirectoryForConfigs(projectRoot, usedPackages, usedBinaries);
|
|
151
|
+
|
|
152
|
+
// 1.5 Scan Workspace package.json scripts and configs
|
|
153
|
+
if (this.context.isWorkspaceEnabled && this.context.monorepoPackageRoots) {
|
|
154
|
+
for (const workspaceRoot of this.context.monorepoPackageRoots) {
|
|
155
|
+
await this._scanDirectoryForConfigs(workspaceRoot, usedPackages, usedBinaries);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// 2. Scan CI workflows
|
|
160
|
+
try {
|
|
161
|
+
const githubWorkflows = path.join(projectRoot, '.github/workflows');
|
|
162
|
+
const files = await fs.readdir(githubWorkflows).catch(() => []);
|
|
163
|
+
for (const file of files) {
|
|
164
|
+
if (file.endsWith('.yml') || file.endsWith('.yaml')) {
|
|
165
|
+
const content = await fs.readFile(path.join(githubWorkflows, file), 'utf8');
|
|
166
|
+
this.extractPackagesFromScript(content, usedPackages, usedBinaries);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
} catch (e) {}
|
|
170
|
+
|
|
171
|
+
// 4. Identify Unused Binaries (Root)
|
|
172
|
+
await this._identifyUnusedBinaries(projectRoot, usedBinaries);
|
|
173
|
+
|
|
174
|
+
// 4.5 Identify Unused Binaries (Workspaces)
|
|
175
|
+
if (this.context.isWorkspaceEnabled && this.context.monorepoPackageRoots) {
|
|
176
|
+
for (const workspaceRoot of this.context.monorepoPackageRoots) {
|
|
177
|
+
await this._identifyUnusedBinaries(workspaceRoot, usedBinaries);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return usedPackages;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async _scanDirectoryForConfigs(dir, usedPackages, usedBinaries) {
|
|
150
185
|
try {
|
|
151
|
-
const pkgJsonPath = path.join(
|
|
186
|
+
const pkgJsonPath = path.join(dir, 'package.json');
|
|
152
187
|
const pkg = JSON.parse(await fs.readFile(pkgJsonPath, 'utf8'));
|
|
153
188
|
|
|
154
189
|
if (pkg.scripts) {
|
|
@@ -157,7 +192,6 @@ export class DependencyProfiler {
|
|
|
157
192
|
}
|
|
158
193
|
}
|
|
159
194
|
|
|
160
|
-
// Detect @types/* usage
|
|
161
195
|
if (pkg.dependencies || pkg.devDependencies) {
|
|
162
196
|
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
163
197
|
for (const depName of Object.keys(allDeps)) {
|
|
@@ -168,21 +202,8 @@ export class DependencyProfiler {
|
|
|
168
202
|
}
|
|
169
203
|
} catch (e) {}
|
|
170
204
|
|
|
171
|
-
// 2. Scan CI workflows
|
|
172
205
|
try {
|
|
173
|
-
const
|
|
174
|
-
const files = await fs.readdir(githubWorkflows).catch(() => []);
|
|
175
|
-
for (const file of files) {
|
|
176
|
-
if (file.endsWith('.yml') || file.endsWith('.yaml')) {
|
|
177
|
-
const content = await fs.readFile(path.join(githubWorkflows, file), 'utf8');
|
|
178
|
-
this.extractPackagesFromScript(content, usedPackages, usedBinaries);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
} catch (e) {}
|
|
182
|
-
|
|
183
|
-
// 3. Scan config files
|
|
184
|
-
try {
|
|
185
|
-
const dirEntries = await fs.readdir(projectRoot, { withFileTypes: true });
|
|
206
|
+
const dirEntries = await fs.readdir(dir, { withFileTypes: true });
|
|
186
207
|
for (const entry of dirEntries) {
|
|
187
208
|
if (!entry.isFile()) continue;
|
|
188
209
|
const fileName = entry.name;
|
|
@@ -194,15 +215,14 @@ export class DependencyProfiler {
|
|
|
194
215
|
}
|
|
195
216
|
}
|
|
196
217
|
} catch (e) {}
|
|
218
|
+
}
|
|
197
219
|
|
|
198
|
-
|
|
199
|
-
// Scan node_modules/.bin for all available binaries
|
|
220
|
+
async _identifyUnusedBinaries(dir, usedBinaries) {
|
|
200
221
|
try {
|
|
201
|
-
const binDir = path.join(
|
|
222
|
+
const binDir = path.join(dir, 'node_modules', '.bin');
|
|
202
223
|
const availableBinaries = await fs.readdir(binDir).catch(() => []);
|
|
203
224
|
|
|
204
225
|
for (const bin of availableBinaries) {
|
|
205
|
-
// Skip hidden files or package manager internal bins
|
|
206
226
|
if (bin.startsWith('.') || ['npm', 'pnpm', 'yarn', 'bun'].includes(bin)) continue;
|
|
207
227
|
|
|
208
228
|
if (!usedBinaries.has(bin)) {
|
|
@@ -210,8 +230,6 @@ export class DependencyProfiler {
|
|
|
210
230
|
}
|
|
211
231
|
}
|
|
212
232
|
} catch (e) {}
|
|
213
|
-
|
|
214
|
-
return usedPackages;
|
|
215
233
|
}
|
|
216
234
|
|
|
217
235
|
extractPackagesFromScript(script, packageCollector, binaryCollector) {
|
|
@@ -1,13 +1,35 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import { TSConfigLoader } from './TSConfigLoader.js';
|
|
3
4
|
|
|
4
5
|
export class PathMapper {
|
|
5
6
|
constructor(context) {
|
|
6
7
|
this.context = context;
|
|
8
|
+
this.aliasMappers = []; // list of alias mapping functions
|
|
7
9
|
}
|
|
8
10
|
|
|
9
|
-
async loadMappings() {
|
|
10
|
-
//
|
|
11
|
+
async loadMappings(tsconfigFilename = 'tsconfig.json') {
|
|
12
|
+
// Load root tsconfig
|
|
13
|
+
const loader = new TSConfigLoader(this.context.cwd);
|
|
14
|
+
const config = loader.load();
|
|
15
|
+
if (config) {
|
|
16
|
+
const mapper = loader.getAliasMapper(config);
|
|
17
|
+
this.aliasMappers.push(mapper);
|
|
18
|
+
if (this.context.verbose) console.log(`[PathMapper] Loaded root tsconfig aliases`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Load workspace tsconfigs if available
|
|
22
|
+
if (this.context.isWorkspaceEnabled && this.context.monorepoPackageRoots) {
|
|
23
|
+
for (const root of this.context.monorepoPackageRoots) {
|
|
24
|
+
const wsLoader = new TSConfigLoader(root);
|
|
25
|
+
const wsConfig = wsLoader.load();
|
|
26
|
+
if (wsConfig) {
|
|
27
|
+
const wsMapper = wsLoader.getAliasMapper(wsConfig);
|
|
28
|
+
this.aliasMappers.push(wsMapper);
|
|
29
|
+
if (this.context.verbose) console.log(`[PathMapper] Loaded workspace tsconfig aliases from ${root}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
11
33
|
}
|
|
12
34
|
|
|
13
35
|
/**
|
|
@@ -18,25 +40,36 @@ export class PathMapper {
|
|
|
18
40
|
resolvePath(p) {
|
|
19
41
|
if (!p || typeof p !== 'string') return p;
|
|
20
42
|
|
|
43
|
+
let resolvedP = p;
|
|
44
|
+
|
|
45
|
+
// Try alias mappers first
|
|
46
|
+
for (const mapper of this.aliasMappers) {
|
|
47
|
+
const mapped = mapper(resolvedP);
|
|
48
|
+
if (mapped !== resolvedP && fs.existsSync(mapped)) {
|
|
49
|
+
resolvedP = mapped;
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
21
54
|
// FIX 1: If the import ends with .js, translate it to .ts for the search
|
|
22
|
-
if (
|
|
23
|
-
const tsPath =
|
|
55
|
+
if (resolvedP.endsWith('.js')) {
|
|
56
|
+
const tsPath = resolvedP.slice(0, -3) + '.ts';
|
|
24
57
|
if (fs.existsSync(tsPath)) return tsPath;
|
|
25
58
|
}
|
|
26
59
|
|
|
27
60
|
// FIX 2: If the import ends with .jsx, translate it to .tsx for the search
|
|
28
|
-
if (
|
|
29
|
-
const tsxPath =
|
|
61
|
+
if (resolvedP.endsWith('.jsx')) {
|
|
62
|
+
const tsxPath = resolvedP.slice(0, -4) + '.tsx';
|
|
30
63
|
if (fs.existsSync(tsxPath)) return tsxPath;
|
|
31
64
|
}
|
|
32
65
|
|
|
33
66
|
// FIX 3: Support for directory imports (z.B. ./adapters -> ./adapters/index.ts)
|
|
34
67
|
try {
|
|
35
|
-
const stat = fs.statSync(
|
|
68
|
+
const stat = fs.statSync(resolvedP);
|
|
36
69
|
if (stat.isDirectory()) {
|
|
37
70
|
const extensions = ['.ts', '.tsx', '.js', '.jsx'];
|
|
38
71
|
for (const ext of extensions) {
|
|
39
|
-
const indexPath = path.join(
|
|
72
|
+
const indexPath = path.join(resolvedP, `index${ext}`);
|
|
40
73
|
if (fs.existsSync(indexPath)) return indexPath;
|
|
41
74
|
}
|
|
42
75
|
}
|
|
@@ -44,6 +77,6 @@ export class PathMapper {
|
|
|
44
77
|
// File does not exist or is not a directory, continue with default
|
|
45
78
|
}
|
|
46
79
|
|
|
47
|
-
return
|
|
80
|
+
return resolvedP;
|
|
48
81
|
}
|
|
49
82
|
}
|
|
@@ -9,39 +9,102 @@ export class TSConfigLoader {
|
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Lädt und parst die tsconfig.json.
|
|
12
|
+
* Unterstützt JSONC (Kommentare), extends, und project references.
|
|
13
|
+
* @param {string} [filename='tsconfig.json'] - Name der tsconfig-Datei
|
|
12
14
|
* @returns {Object|null} Parsed config oder null.
|
|
13
15
|
*/
|
|
14
|
-
load() {
|
|
15
|
-
const configPath = path.join(this.targetDir,
|
|
16
|
+
load(filename = 'tsconfig.json') {
|
|
17
|
+
const configPath = path.join(this.targetDir, filename);
|
|
16
18
|
if (!fs.existsSync(configPath)) return null;
|
|
17
19
|
|
|
18
20
|
try {
|
|
19
21
|
const content = fs.readFileSync(configPath, 'utf8');
|
|
20
|
-
const
|
|
22
|
+
const readResult = ts.readConfigFile(configPath, ts.sys.readFile);
|
|
21
23
|
|
|
22
|
-
if (
|
|
24
|
+
if (readResult.error) {
|
|
23
25
|
return null;
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
const parsed = ts.parseJsonConfigFileContent(
|
|
27
|
-
|
|
29
|
+
readResult.config,
|
|
28
30
|
ts.sys,
|
|
29
31
|
this.targetDir
|
|
30
32
|
);
|
|
31
33
|
|
|
34
|
+
// Attach raw config for reference inspection
|
|
35
|
+
parsed._rawConfig = readResult.config;
|
|
36
|
+
|
|
32
37
|
return parsed;
|
|
33
38
|
} catch (e) {
|
|
34
39
|
return null;
|
|
35
40
|
}
|
|
36
41
|
}
|
|
37
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Extracts project references from tsconfig (composite monorepo support).
|
|
45
|
+
* @returns {string[]} Array of referenced tsconfig paths
|
|
46
|
+
*/
|
|
47
|
+
getProjectReferences(filename = 'tsconfig.json') {
|
|
48
|
+
const configPath = path.join(this.targetDir, filename);
|
|
49
|
+
if (!fs.existsSync(configPath)) return [];
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const content = fs.readFileSync(configPath, 'utf8');
|
|
53
|
+
// Strip comments
|
|
54
|
+
const stripped = content.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
55
|
+
const raw = JSON.parse(stripped);
|
|
56
|
+
const refs = raw.references || [];
|
|
57
|
+
return refs.map(ref => {
|
|
58
|
+
const refPath = path.resolve(this.targetDir, ref.path);
|
|
59
|
+
// If it's a directory, look for tsconfig.json inside
|
|
60
|
+
if (fs.existsSync(refPath) && fs.statSync(refPath).isDirectory()) {
|
|
61
|
+
return path.join(refPath, 'tsconfig.json');
|
|
62
|
+
}
|
|
63
|
+
return refPath;
|
|
64
|
+
});
|
|
65
|
+
} catch (e) {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Extracts the "extends" chain from tsconfig.
|
|
72
|
+
* @returns {string[]} Array of extended tsconfig paths
|
|
73
|
+
*/
|
|
74
|
+
getExtendsChain(filename = 'tsconfig.json') {
|
|
75
|
+
const configPath = path.join(this.targetDir, filename);
|
|
76
|
+
if (!fs.existsSync(configPath)) return [];
|
|
77
|
+
|
|
78
|
+
const chain = [];
|
|
79
|
+
let currentPath = configPath;
|
|
80
|
+
|
|
81
|
+
for (let i = 0; i < 10; i++) { // limit depth to avoid infinite loops
|
|
82
|
+
try {
|
|
83
|
+
const content = fs.readFileSync(currentPath, 'utf8');
|
|
84
|
+
const stripped = content.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
85
|
+
const raw = JSON.parse(stripped);
|
|
86
|
+
if (!raw.extends) break;
|
|
87
|
+
const extendedPath = path.resolve(path.dirname(currentPath), raw.extends);
|
|
88
|
+
const resolvedPath = extendedPath.endsWith('.json') ? extendedPath : extendedPath + '.json';
|
|
89
|
+
if (!fs.existsSync(resolvedPath)) break;
|
|
90
|
+
chain.push(resolvedPath);
|
|
91
|
+
currentPath = resolvedPath;
|
|
92
|
+
} catch (e) {
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return chain;
|
|
98
|
+
}
|
|
99
|
+
|
|
38
100
|
/**
|
|
39
101
|
* Erstellt eine Mapping-Funktion für Aliases aus der tsconfig.
|
|
102
|
+
* Berücksichtigt baseUrl, paths und project references.
|
|
40
103
|
* @param {Object} parsedConfig
|
|
41
104
|
* @returns {Function} Mapper function.
|
|
42
105
|
*/
|
|
43
106
|
getAliasMapper(parsedConfig) {
|
|
44
|
-
if (!parsedConfig || !parsedConfig.options
|
|
107
|
+
if (!parsedConfig || !parsedConfig.options) {
|
|
45
108
|
return (source) => source;
|
|
46
109
|
}
|
|
47
110
|
|
|
@@ -49,19 +112,31 @@ export class TSConfigLoader {
|
|
|
49
112
|
const base = baseUrl ? path.resolve(this.targetDir, baseUrl) : this.targetDir;
|
|
50
113
|
|
|
51
114
|
return (source) => {
|
|
115
|
+
// If no paths configured, still try baseUrl resolution
|
|
116
|
+
if (!paths) {
|
|
117
|
+
if (baseUrl && !source.startsWith('.') && !source.startsWith('/') && !source.startsWith('@')) {
|
|
118
|
+
const candidate = path.resolve(base, source);
|
|
119
|
+
const extensions = ['', '.ts', '.tsx', '.js', '.jsx'];
|
|
120
|
+
for (const ext of extensions) {
|
|
121
|
+
if (fs.existsSync(candidate + ext)) return candidate + ext;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return source;
|
|
125
|
+
}
|
|
126
|
+
|
|
52
127
|
for (const pattern in paths) {
|
|
53
|
-
const regexPattern = pattern.replace(
|
|
128
|
+
const regexPattern = pattern.replace(/\*/g, '(.*)');
|
|
54
129
|
const regex = new RegExp(`^${regexPattern}$`);
|
|
55
130
|
const match = source.match(regex);
|
|
56
131
|
|
|
57
132
|
if (match) {
|
|
58
133
|
const replacements = paths[pattern];
|
|
59
134
|
for (const replacement of replacements) {
|
|
60
|
-
const resolvedReplacement = replacement.replace(
|
|
135
|
+
const resolvedReplacement = replacement.replace(/\*/g, match[1] || '');
|
|
61
136
|
const fullPath = path.resolve(base, resolvedReplacement);
|
|
62
137
|
|
|
63
|
-
//
|
|
64
|
-
const extensions = ['', '.ts', '.tsx', '.js', '.jsx'];
|
|
138
|
+
// Check with common extensions
|
|
139
|
+
const extensions = ['', '.ts', '.tsx', '.js', '.jsx', '/index.ts', '/index.tsx', '/index.js'];
|
|
65
140
|
for (const ext of extensions) {
|
|
66
141
|
if (fs.existsSync(fullPath + ext)) {
|
|
67
142
|
return fullPath + ext;
|
|
@@ -73,4 +148,15 @@ export class TSConfigLoader {
|
|
|
73
148
|
return source;
|
|
74
149
|
};
|
|
75
150
|
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Returns all include/exclude glob patterns from tsconfig.
|
|
154
|
+
*/
|
|
155
|
+
getIncludeExcludePatterns(parsedConfig) {
|
|
156
|
+
if (!parsedConfig || !parsedConfig._rawConfig) return { include: [], exclude: [] };
|
|
157
|
+
return {
|
|
158
|
+
include: parsedConfig._rawConfig.include || [],
|
|
159
|
+
exclude: parsedConfig._rawConfig.exclude || []
|
|
160
|
+
};
|
|
161
|
+
}
|
|
76
162
|
}
|
|
@@ -1,8 +1,183 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
1
4
|
export class WorkspaceGraph {
|
|
2
5
|
constructor(context) {
|
|
3
6
|
this.context = context;
|
|
4
|
-
this.packageManifests = new Map();
|
|
7
|
+
this.packageManifests = new Map(); // dirPath -> manifestData
|
|
8
|
+
this.workspacePackages = new Map(); // packageName -> dirPath
|
|
9
|
+
this.tsconfigPaths = new Map(); // packageName -> tsconfigData
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async initializeWorkspaceMesh() {
|
|
13
|
+
const rootPkgPath = path.join(this.context.cwd, 'package.json');
|
|
14
|
+
try {
|
|
15
|
+
const rootPkg = JSON.parse(await fs.readFile(rootPkgPath, 'utf8'));
|
|
16
|
+
let workspaces = [];
|
|
17
|
+
|
|
18
|
+
// 1. Detect Workspaces (npm/yarn/pnpm/lerna)
|
|
19
|
+
if (rootPkg.workspaces) {
|
|
20
|
+
workspaces = Array.isArray(rootPkg.workspaces) ? rootPkg.workspaces : rootPkg.workspaces.packages || [];
|
|
21
|
+
} else {
|
|
22
|
+
// Fallback for pnpm-workspace.yaml
|
|
23
|
+
const pnpmWorkspacePath = path.join(this.context.cwd, 'pnpm-workspace.yaml');
|
|
24
|
+
try {
|
|
25
|
+
const yaml = await fs.readFile(pnpmWorkspacePath, 'utf8');
|
|
26
|
+
const match = yaml.match(/packages:\n((?:\s+- .+\n?)+)/);
|
|
27
|
+
if (match) {
|
|
28
|
+
workspaces = match[1].split('\n')
|
|
29
|
+
.filter(line => line.trim().startsWith('-'))
|
|
30
|
+
.map(line => line.replace('-', '').trim().replace(/['"]/g, ''));
|
|
31
|
+
}
|
|
32
|
+
} catch (e) {}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (workspaces.length > 0) {
|
|
36
|
+
this.context.isWorkspaceEnabled = true;
|
|
37
|
+
if (this.context.verbose) console.log(`[Workspace] Detected workspaces:`, workspaces);
|
|
38
|
+
|
|
39
|
+
for (const pattern of workspaces) {
|
|
40
|
+
const matches = await this._expandGlob(pattern, this.context.cwd);
|
|
41
|
+
for (const matchDir of matches) {
|
|
42
|
+
const pkgPath = path.join(matchDir, 'package.json');
|
|
43
|
+
try {
|
|
44
|
+
const pkgData = JSON.parse(await fs.readFile(pkgPath, 'utf8'));
|
|
45
|
+
const normalizedDir = matchDir.replace(/\\/g, '/');
|
|
46
|
+
|
|
47
|
+
this.packageManifests.set(normalizedDir, {
|
|
48
|
+
rootDirectory: normalizedDir,
|
|
49
|
+
manifestPath: pkgPath.replace(/\\/g, '/'),
|
|
50
|
+
name: pkgData.name,
|
|
51
|
+
dependencies: pkgData.dependencies || {},
|
|
52
|
+
devDependencies: pkgData.devDependencies || {},
|
|
53
|
+
peerDependencies: pkgData.peerDependencies || {},
|
|
54
|
+
scripts: pkgData.scripts || {},
|
|
55
|
+
entryPoints: this.calculatePackageExportsEntries(pkgData, normalizedDir)
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
if (pkgData.name) {
|
|
59
|
+
this.workspacePackages.set(pkgData.name, normalizedDir);
|
|
60
|
+
this.context.monorepoPackageRoots.add(normalizedDir);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Also try to load tsconfig.json for this workspace
|
|
64
|
+
const tsconfigPath = path.join(matchDir, 'tsconfig.json');
|
|
65
|
+
try {
|
|
66
|
+
const tsconfigData = JSON.parse(await fs.readFile(tsconfigPath, 'utf8'));
|
|
67
|
+
if (pkgData.name) this.tsconfigPaths.set(pkgData.name, tsconfigData);
|
|
68
|
+
} catch(e) {}
|
|
69
|
+
|
|
70
|
+
} catch (e) {}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
} catch (e) {
|
|
75
|
+
if (this.context.verbose) console.log('[Workspace] No root package.json found or invalid.');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
calculatePackageExportsEntries(pkgData, dirPath) {
|
|
80
|
+
const entries = [];
|
|
81
|
+
const addEntry = (p) => {
|
|
82
|
+
if (typeof p === 'string') entries.push(path.resolve(dirPath, p).replace(/\\/g, '/'));
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
if (pkgData.main) addEntry(pkgData.main);
|
|
86
|
+
if (pkgData.module) addEntry(pkgData.module);
|
|
87
|
+
if (pkgData.source) addEntry(pkgData.source);
|
|
88
|
+
if (pkgData.types) addEntry(pkgData.types);
|
|
89
|
+
if (pkgData.typings) addEntry(pkgData.typings);
|
|
90
|
+
|
|
91
|
+
if (pkgData.bin) {
|
|
92
|
+
if (typeof pkgData.bin === 'string') addEntry(pkgData.bin);
|
|
93
|
+
else Object.values(pkgData.bin).forEach(addEntry);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (pkgData.exports) {
|
|
97
|
+
const traverseExports = (obj) => {
|
|
98
|
+
if (typeof obj === 'string') {
|
|
99
|
+
addEntry(obj);
|
|
100
|
+
} else if (typeof obj === 'object' && obj !== null) {
|
|
101
|
+
for (const key in obj) traverseExports(obj[key]);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
traverseExports(pkgData.exports);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return entries;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
isLocalWorkspaceSpecifier(specifier) {
|
|
111
|
+
if (!specifier) return false;
|
|
112
|
+
// Direct match
|
|
113
|
+
if (this.workspacePackages.has(specifier)) return true;
|
|
114
|
+
// Sub-path match (e.g. @my-org/ui/components)
|
|
115
|
+
for (const pkgName of this.workspacePackages.keys()) {
|
|
116
|
+
if (specifier.startsWith(pkgName + '/')) return true;
|
|
117
|
+
}
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
getWorkspacePackageMatch(specifier) {
|
|
122
|
+
if (this.workspacePackages.has(specifier)) {
|
|
123
|
+
const dir = this.workspacePackages.get(specifier);
|
|
124
|
+
return this.packageManifests.get(dir);
|
|
125
|
+
}
|
|
126
|
+
for (const pkgName of this.workspacePackages.keys()) {
|
|
127
|
+
if (specifier.startsWith(pkgName + '/')) {
|
|
128
|
+
const dir = this.workspacePackages.get(pkgName);
|
|
129
|
+
return this.packageManifests.get(dir);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
markWorkspacePackagesAsUsed() {
|
|
136
|
+
for (const [pkgName, dirPath] of this.workspacePackages.entries()) {
|
|
137
|
+
this.context.usedExternalPackages.add(pkgName);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Expands a workspace glob pattern (e.g. 'packages/*') to absolute directory paths.
|
|
143
|
+
* Supports single-level wildcards and direct paths.
|
|
144
|
+
*/
|
|
145
|
+
async _expandGlob(pattern, cwd) {
|
|
146
|
+
const results = [];
|
|
147
|
+
|
|
148
|
+
// Remove trailing slash
|
|
149
|
+
const cleanPattern = pattern.replace(/\/$/, '');
|
|
150
|
+
|
|
151
|
+
// Handle simple wildcard patterns like 'packages/*' or 'apps/*'
|
|
152
|
+
if (cleanPattern.includes('*')) {
|
|
153
|
+
const parts = cleanPattern.split('/');
|
|
154
|
+
const wildcardIndex = parts.findIndex(p => p.includes('*'));
|
|
155
|
+
const baseDir = path.join(cwd, ...parts.slice(0, wildcardIndex));
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
const entries = await fs.readdir(baseDir, { withFileTypes: true });
|
|
159
|
+
for (const entry of entries) {
|
|
160
|
+
if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
|
161
|
+
const fullPath = path.join(baseDir, entry.name);
|
|
162
|
+
// Check if it has a package.json
|
|
163
|
+
try {
|
|
164
|
+
await fs.access(path.join(fullPath, 'package.json'));
|
|
165
|
+
results.push(fullPath.replace(/\\/g, '/'));
|
|
166
|
+
} catch (e) {}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
} catch (e) {}
|
|
170
|
+
} else {
|
|
171
|
+
// Direct path
|
|
172
|
+
const fullPath = path.resolve(cwd, cleanPattern);
|
|
173
|
+
try {
|
|
174
|
+
const stat = await fs.stat(fullPath);
|
|
175
|
+
if (stat.isDirectory()) {
|
|
176
|
+
results.push(fullPath.replace(/\\/g, '/'));
|
|
177
|
+
}
|
|
178
|
+
} catch (e) {}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return results;
|
|
5
182
|
}
|
|
6
|
-
async initializeWorkspaceMesh() {}
|
|
7
|
-
markWorkspacePackagesAsUsed() {}
|
|
8
183
|
}
|