entkapp 5.3.0 → 5.4.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/README.md +1 -7
- package/bin/cli.js +58 -117
- package/package.json +1 -1
- package/src/EngineContext.js +6 -0
- package/src/ast/ASTAnalyzer.js +94 -6
- package/src/ast/MagicDetector.js +23 -43
- package/src/ast/OxcAnalyzer.js +190 -27
- package/src/ast/SecretScanner.js +112 -50
- package/src/index.js +87 -4
- package/src/performance/GraphCache.js +27 -0
- package/src/performance/WorkerPool.js +22 -4
- package/src/performance/WorkerTaskRunner.js +26 -0
- package/src/plugins/BasePlugin.js +16 -27
- package/src/plugins/PluginRegistry.js +44 -0
- package/src/plugins/ecosystems/UltimateBundle.js +259 -0
- package/src/resolution/ConfigLoader.js +190 -26
- package/src/resolution/FrameworkConfigParser.js +119 -0
- package/src/resolution/GraphAnalyzer.js +65 -1
- package/src/resolution/PathMapper.js +81 -0
- package/src/resolution/TSConfigLoader.js +3 -1
- package/src/resolution/WorkSpaceGraph.js +260 -89
|
@@ -1,82 +1,208 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* WorkspaceGraph
|
|
6
|
+
*
|
|
7
|
+
* Discovers and maps all packages in a monorepo/workspace setup.
|
|
8
|
+
* Supported workspace manifests (in priority order):
|
|
9
|
+
* 1. pnpm-workspace.yaml (pnpm)
|
|
10
|
+
* 2. package.json "workspaces" field (npm / yarn / bun)
|
|
11
|
+
* 3. lerna.json (Lerna)
|
|
12
|
+
* 4. workspace.yaml (generic / custom toolchains)
|
|
13
|
+
*/
|
|
4
14
|
export class WorkspaceGraph {
|
|
5
|
-
constructor(context) {
|
|
6
|
-
this.context = context;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
this.
|
|
15
|
+
constructor(context) {
|
|
16
|
+
this.context = context;
|
|
17
|
+
|
|
18
|
+
/** @type {Map<string, Object>} dirPath -> manifest data */
|
|
19
|
+
this.packageManifests = new Map();
|
|
20
|
+
|
|
21
|
+
/** @type {Map<string, string>} packageName -> dirPath */
|
|
22
|
+
this.workspacePackages = new Map();
|
|
23
|
+
|
|
24
|
+
/** @type {Map<string, Object>} packageName -> parsed tsconfig data */
|
|
25
|
+
this.tsconfigPaths = new Map();
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Per-package framework / tool config files.
|
|
29
|
+
* @type {Map<string, Array<{fileName: string, filePath: string, content: string}>>}
|
|
30
|
+
*/
|
|
31
|
+
this.packageConfigFiles = new Map();
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Global pipeline / monorepo configs (turbo.json, nx.json, etc.)
|
|
35
|
+
* @type {Map<string, Object>} fileName -> parsed content
|
|
36
|
+
*/
|
|
37
|
+
this.monorepoConfigs = new Map();
|
|
10
38
|
}
|
|
11
39
|
|
|
12
40
|
async initializeWorkspaceMesh() {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
41
|
+
let workspaces = [];
|
|
42
|
+
|
|
43
|
+
// 1. pnpm-workspace.yaml
|
|
44
|
+
workspaces = await this._parseWorkspaceYaml('pnpm-workspace.yaml');
|
|
45
|
+
|
|
46
|
+
// 2. package.json "workspaces"
|
|
47
|
+
if (workspaces.length === 0) {
|
|
48
|
+
try {
|
|
49
|
+
const rootPkgPath = path.join(this.context.cwd, 'package.json');
|
|
50
|
+
const rootPkg = JSON.parse(await fs.readFile(rootPkgPath, 'utf8'));
|
|
51
|
+
if (rootPkg.workspaces) {
|
|
52
|
+
workspaces = Array.isArray(rootPkg.workspaces)
|
|
53
|
+
? rootPkg.workspaces
|
|
54
|
+
: rootPkg.workspaces.packages || [];
|
|
55
|
+
}
|
|
56
|
+
} catch (e) {}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 3. lerna.json
|
|
60
|
+
if (workspaces.length === 0) {
|
|
61
|
+
try {
|
|
62
|
+
const lernaPath = path.join(this.context.cwd, 'lerna.json');
|
|
63
|
+
const lerna = JSON.parse(await fs.readFile(lernaPath, 'utf8'));
|
|
64
|
+
workspaces = lerna.packages || [];
|
|
65
|
+
} catch (e) {}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 4. workspace.yaml
|
|
69
|
+
if (workspaces.length === 0) {
|
|
70
|
+
workspaces = await this._parseWorkspaceYaml('workspace.yaml');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
await this._loadGlobalMonorepoConfigs();
|
|
74
|
+
|
|
75
|
+
if (workspaces.length === 0) return;
|
|
76
|
+
|
|
77
|
+
this.context.isWorkspaceEnabled = true;
|
|
78
|
+
if (this.context.verbose) {
|
|
79
|
+
console.log('[Workspace] Detected workspace patterns:', workspaces);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
for (const pattern of workspaces) {
|
|
83
|
+
const matches = await this._expandGlob(pattern, this.context.cwd);
|
|
84
|
+
for (const matchDir of matches) {
|
|
85
|
+
await this._loadPackageDir(matchDir);
|
|
33
86
|
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
34
89
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
90
|
+
async _loadGlobalMonorepoConfigs() {
|
|
91
|
+
const configFiles = ['turbo.json', 'nx.json'];
|
|
92
|
+
for (const file of configFiles) {
|
|
93
|
+
try {
|
|
94
|
+
const filePath = path.join(this.context.cwd, file);
|
|
95
|
+
const content = JSON.parse(await fs.readFile(filePath, 'utf8'));
|
|
96
|
+
this.monorepoConfigs.set(file, content);
|
|
97
|
+
} catch (e) {}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async _parseWorkspaceYaml(fileName) {
|
|
102
|
+
const yamlPath = path.join(this.context.cwd, fileName);
|
|
103
|
+
try {
|
|
104
|
+
const raw = await fs.readFile(yamlPath, 'utf8');
|
|
105
|
+
const patterns = [];
|
|
106
|
+
const lines = raw.split('\n');
|
|
107
|
+
let inPackages = false;
|
|
108
|
+
for (const line of lines) {
|
|
109
|
+
const trimmed = line.trim();
|
|
110
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
111
|
+
if (/^packages\s*:/.test(trimmed)) {
|
|
112
|
+
inPackages = true;
|
|
113
|
+
const inlineMatch = trimmed.match(/^packages\s*:\s*\[([^\]]*)\]/);
|
|
114
|
+
if (inlineMatch) {
|
|
115
|
+
inPackages = false;
|
|
116
|
+
for (const item of inlineMatch[1].split(',')) {
|
|
117
|
+
const val = item.trim().replace(/^['"]|['"]$/g, '');
|
|
118
|
+
if (val) patterns.push(val);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (inPackages) {
|
|
124
|
+
if (line.match(/^[a-zA-Z0-9_-]/) && trimmed.endsWith(':')) {
|
|
125
|
+
inPackages = false;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (trimmed.startsWith('-')) {
|
|
129
|
+
const val = trimmed.substring(1).trim().replace(/^['"]|['"]$/g, '');
|
|
130
|
+
if (val) patterns.push(val);
|
|
71
131
|
}
|
|
72
132
|
}
|
|
73
133
|
}
|
|
134
|
+
return patterns;
|
|
74
135
|
} catch (e) {
|
|
75
|
-
|
|
136
|
+
return [];
|
|
76
137
|
}
|
|
77
138
|
}
|
|
78
139
|
|
|
79
|
-
|
|
140
|
+
async _loadPackageDir(matchDir) {
|
|
141
|
+
const normalizedDir = matchDir.replace(/\\/g, '/');
|
|
142
|
+
const pkgPath = path.join(matchDir, 'package.json');
|
|
143
|
+
let pkgData = null;
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
pkgData = JSON.parse(await fs.readFile(pkgPath, 'utf8'));
|
|
147
|
+
} catch (e) {
|
|
148
|
+
// UPGRADE: If no package.json, it's not a package – skip silently
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const packageName = pkgData.name || path.basename(matchDir);
|
|
153
|
+
|
|
154
|
+
this.packageManifests.set(normalizedDir, {
|
|
155
|
+
rootDirectory: normalizedDir,
|
|
156
|
+
manifestPath: pkgPath.replace(/\\/g, '/'),
|
|
157
|
+
name: packageName,
|
|
158
|
+
version: pkgData.version,
|
|
159
|
+
dependencies: pkgData.dependencies || {},
|
|
160
|
+
devDependencies: pkgData.devDependencies || {},
|
|
161
|
+
peerDependencies: pkgData.peerDependencies || {},
|
|
162
|
+
scripts: pkgData.scripts || {},
|
|
163
|
+
entryPoints: await this.calculatePackageExportsEntries(pkgData, normalizedDir)
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
this.workspacePackages.set(packageName, normalizedDir);
|
|
167
|
+
this.context.monorepoPackageRoots.add(normalizedDir);
|
|
168
|
+
|
|
169
|
+
// tsconfig.json
|
|
170
|
+
const tsconfigPath = path.join(matchDir, 'tsconfig.json');
|
|
171
|
+
try {
|
|
172
|
+
const tsconfigRaw = await fs.readFile(tsconfigPath, 'utf8');
|
|
173
|
+
const stripped = tsconfigRaw.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
174
|
+
const tsconfigData = JSON.parse(stripped);
|
|
175
|
+
this.tsconfigPaths.set(packageName, tsconfigData);
|
|
176
|
+
this.tsconfigPaths.set(normalizedDir, tsconfigData);
|
|
177
|
+
} catch (e) {}
|
|
178
|
+
|
|
179
|
+
await this._loadPackageConfigFiles(matchDir, normalizedDir, packageName);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async _loadPackageConfigFiles(packageDir, normalizedDir, packageName) {
|
|
183
|
+
const configEntries = [];
|
|
184
|
+
let dirEntries;
|
|
185
|
+
try {
|
|
186
|
+
dirEntries = await fs.readdir(packageDir, { withFileTypes: true });
|
|
187
|
+
} catch (e) { return; }
|
|
188
|
+
|
|
189
|
+
for (const entry of dirEntries) {
|
|
190
|
+
if (!entry.isFile()) continue;
|
|
191
|
+
const isConfig = /\.config\.(ts|js|mjs|cjs)$/.test(entry.name);
|
|
192
|
+
const isJson = entry.name === 'package.json' || entry.name === 'tsconfig.json';
|
|
193
|
+
|
|
194
|
+
if (!isConfig && !isJson) continue;
|
|
195
|
+
|
|
196
|
+
const filePath = path.join(packageDir, entry.name).replace(/\\/g, '/');
|
|
197
|
+
try {
|
|
198
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
199
|
+
configEntries.push({ fileName: entry.name, filePath, content });
|
|
200
|
+
} catch (e) {}
|
|
201
|
+
}
|
|
202
|
+
if (configEntries.length > 0) this.packageConfigFiles.set(normalizedDir, configEntries);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async calculatePackageExportsEntries(pkgData, dirPath) {
|
|
80
206
|
const entries = [];
|
|
81
207
|
const addEntry = (p) => {
|
|
82
208
|
if (typeof p === 'string') entries.push(path.resolve(dirPath, p).replace(/\\/g, '/'));
|
|
@@ -87,7 +213,7 @@ export class WorkspaceGraph {
|
|
|
87
213
|
if (pkgData.source) addEntry(pkgData.source);
|
|
88
214
|
if (pkgData.types) addEntry(pkgData.types);
|
|
89
215
|
if (pkgData.typings) addEntry(pkgData.typings);
|
|
90
|
-
|
|
216
|
+
|
|
91
217
|
if (pkgData.bin) {
|
|
92
218
|
if (typeof pkgData.bin === 'string') addEntry(pkgData.bin);
|
|
93
219
|
else Object.values(pkgData.bin).forEach(addEntry);
|
|
@@ -95,23 +221,33 @@ export class WorkspaceGraph {
|
|
|
95
221
|
|
|
96
222
|
if (pkgData.exports) {
|
|
97
223
|
const traverseExports = (obj) => {
|
|
98
|
-
if (typeof obj === 'string')
|
|
99
|
-
|
|
100
|
-
} else if (typeof obj === 'object' && obj !== null) {
|
|
224
|
+
if (typeof obj === 'string') addEntry(obj);
|
|
225
|
+
else if (typeof obj === 'object' && obj !== null) {
|
|
101
226
|
for (const key in obj) traverseExports(obj[key]);
|
|
102
227
|
}
|
|
103
228
|
};
|
|
104
229
|
traverseExports(pkgData.exports);
|
|
105
230
|
}
|
|
106
231
|
|
|
232
|
+
// UPGRADE: If no explicit entry points found, look for standard index files (poly-extension)
|
|
233
|
+
if (entries.length === 0) {
|
|
234
|
+
const standardIndexFiles = ['index.js', 'index.ts', 'index.jsx', 'index.tsx', 'index.mjs', 'index.cjs'];
|
|
235
|
+
for (const fileName of standardIndexFiles) {
|
|
236
|
+
const fullPath = path.join(dirPath, fileName);
|
|
237
|
+
try {
|
|
238
|
+
await fs.access(fullPath);
|
|
239
|
+
addEntry(fileName);
|
|
240
|
+
break; // Take the first one found
|
|
241
|
+
} catch {}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
107
245
|
return entries;
|
|
108
246
|
}
|
|
109
247
|
|
|
110
248
|
isLocalWorkspaceSpecifier(specifier) {
|
|
111
249
|
if (!specifier) return false;
|
|
112
|
-
// Direct match
|
|
113
250
|
if (this.workspacePackages.has(specifier)) return true;
|
|
114
|
-
// Sub-path match (e.g. @my-org/ui/components)
|
|
115
251
|
for (const pkgName of this.workspacePackages.keys()) {
|
|
116
252
|
if (specifier.startsWith(pkgName + '/')) return true;
|
|
117
253
|
}
|
|
@@ -133,51 +269,86 @@ export class WorkspaceGraph {
|
|
|
133
269
|
}
|
|
134
270
|
|
|
135
271
|
markWorkspacePackagesAsUsed() {
|
|
136
|
-
for (const [pkgName
|
|
272
|
+
for (const [pkgName] of this.workspacePackages.entries()) {
|
|
137
273
|
this.context.usedExternalPackages.add(pkgName);
|
|
138
274
|
}
|
|
139
275
|
}
|
|
140
276
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
277
|
+
async findFrameworkConfigs() {
|
|
278
|
+
const configFiles = new Set();
|
|
279
|
+
await this._scanDirForConfigs(this.context.cwd, configFiles);
|
|
280
|
+
for (const [dir, entries] of this.packageConfigFiles.entries()) {
|
|
281
|
+
for (const { fileName, filePath } of entries) {
|
|
282
|
+
if (/\.config\.(ts|js|mjs|cjs)$/.test(fileName)) configFiles.add(filePath);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
for (const root of this.context.monorepoPackageRoots) {
|
|
286
|
+
const normalizedRoot = root.replace(/\\/g, '/');
|
|
287
|
+
if (!this.packageConfigFiles.has(normalizedRoot)) await this._scanDirForConfigs(root, configFiles);
|
|
288
|
+
}
|
|
289
|
+
return Array.from(configFiles);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async _scanDirForConfigs(dir, configFiles) {
|
|
293
|
+
try {
|
|
294
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
295
|
+
for (const entry of entries) {
|
|
296
|
+
if (entry.isFile() && entry.name.includes('.config.') && /\.(ts|js|mjs|cjs)$/.test(entry.name)) {
|
|
297
|
+
configFiles.add(path.join(dir, entry.name).replace(/\\/g, '/'));
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
} catch (e) {}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
getPackageConfigFiles(dirOrName) {
|
|
304
|
+
const normalized = dirOrName.replace(/\\/g, '/');
|
|
305
|
+
if (this.packageConfigFiles.has(normalized)) return this.packageConfigFiles.get(normalized);
|
|
306
|
+
const dir = this.workspacePackages.get(dirOrName);
|
|
307
|
+
if (dir) return this.packageConfigFiles.get(dir) || [];
|
|
308
|
+
return [];
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
getPackageTsConfig(dirOrName) {
|
|
312
|
+
const normalized = dirOrName.replace(/\\/g, '/');
|
|
313
|
+
if (this.tsconfigPaths.has(normalized)) return this.tsconfigPaths.get(normalized);
|
|
314
|
+
const dir = this.workspacePackages.get(dirOrName);
|
|
315
|
+
if (dir) return this.tsconfigPaths.get(dir);
|
|
316
|
+
return undefined;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
getSummary() {
|
|
320
|
+
return {
|
|
321
|
+
packages: this.packageManifests.size,
|
|
322
|
+
tsconfigsLoaded: this.tsconfigPaths.size,
|
|
323
|
+
configFilesLoaded: Array.from(this.packageConfigFiles.values()).reduce((sum, arr) => sum + arr.length, 0),
|
|
324
|
+
monorepoConfigs: Array.from(this.monorepoConfigs.keys()),
|
|
325
|
+
packageNames: Array.from(this.workspacePackages.keys())
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
145
329
|
async _expandGlob(pattern, cwd) {
|
|
146
330
|
const results = [];
|
|
147
|
-
|
|
148
|
-
// Remove trailing slash
|
|
149
331
|
const cleanPattern = pattern.replace(/\/$/, '');
|
|
150
|
-
|
|
151
|
-
// Handle simple wildcard patterns like 'packages/*' or 'apps/*'
|
|
152
332
|
if (cleanPattern.includes('*')) {
|
|
153
333
|
const parts = cleanPattern.split('/');
|
|
154
334
|
const wildcardIndex = parts.findIndex(p => p.includes('*'));
|
|
155
335
|
const baseDir = path.join(cwd, ...parts.slice(0, wildcardIndex));
|
|
156
|
-
|
|
157
336
|
try {
|
|
158
337
|
const entries = await fs.readdir(baseDir, { withFileTypes: true });
|
|
159
338
|
for (const entry of entries) {
|
|
160
339
|
if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
|
161
340
|
const fullPath = path.join(baseDir, entry.name);
|
|
162
|
-
|
|
163
|
-
try {
|
|
164
|
-
await fs.access(path.join(fullPath, 'package.json'));
|
|
165
|
-
results.push(fullPath.replace(/\\/g, '/'));
|
|
166
|
-
} catch (e) {}
|
|
341
|
+
results.push(fullPath.replace(/\\/g, '/'));
|
|
167
342
|
}
|
|
168
343
|
}
|
|
169
344
|
} catch (e) {}
|
|
170
345
|
} else {
|
|
171
|
-
// Direct path
|
|
172
346
|
const fullPath = path.resolve(cwd, cleanPattern);
|
|
173
347
|
try {
|
|
174
348
|
const stat = await fs.stat(fullPath);
|
|
175
|
-
if (stat.isDirectory())
|
|
176
|
-
results.push(fullPath.replace(/\\/g, '/'));
|
|
177
|
-
}
|
|
349
|
+
if (stat.isDirectory()) results.push(fullPath.replace(/\\/g, '/'));
|
|
178
350
|
} catch (e) {}
|
|
179
351
|
}
|
|
180
|
-
|
|
181
352
|
return results;
|
|
182
353
|
}
|
|
183
354
|
}
|