entkapp 5.3.1 → 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/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
|
@@ -111,6 +111,31 @@ export class PluginRegistry {
|
|
|
111
111
|
return this.plugins.get(name);
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Version 5.4.0: Collect entry points from all plugins for a given file.
|
|
116
|
+
* @param {string} content - The file content
|
|
117
|
+
* @param {string} filePath - The absolute path to the file
|
|
118
|
+
* @returns {Array<string>} List of detected entry points
|
|
119
|
+
*/
|
|
120
|
+
detectEntryPointsFromContent(content, filePath) {
|
|
121
|
+
const entryPoints = [];
|
|
122
|
+
for (const plugin of this.plugins.values()) {
|
|
123
|
+
if (typeof plugin.detectEntryPoints === 'function') {
|
|
124
|
+
try {
|
|
125
|
+
const detected = plugin.detectEntryPoints(content, filePath);
|
|
126
|
+
if (Array.isArray(detected)) {
|
|
127
|
+
entryPoints.push(...detected);
|
|
128
|
+
}
|
|
129
|
+
} catch (e) {
|
|
130
|
+
if (this.context?.verbose) {
|
|
131
|
+
console.warn(`[PluginRegistry] Entry detection failed for plugin "${plugin.name}" on ${filePath}:`, e.message);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return entryPoints;
|
|
137
|
+
}
|
|
138
|
+
|
|
114
139
|
async getActivePlugins(baseDir) {
|
|
115
140
|
const active = [];
|
|
116
141
|
for (const plugin of this.plugins.values()) {
|
|
@@ -121,6 +146,25 @@ export class PluginRegistry {
|
|
|
121
146
|
return active;
|
|
122
147
|
}
|
|
123
148
|
|
|
149
|
+
/**
|
|
150
|
+
* Version 5.3.0: Run content analysis on a node using all active plugins.
|
|
151
|
+
* @param {GraphNode} node - The node to analyze
|
|
152
|
+
* @param {string} filePath - The absolute path to the file
|
|
153
|
+
*/
|
|
154
|
+
async runPluginAnalysis(node, filePath) {
|
|
155
|
+
for (const plugin of this.plugins.values()) {
|
|
156
|
+
if (typeof plugin.analyze === 'function') {
|
|
157
|
+
try {
|
|
158
|
+
await plugin.analyze(node, filePath);
|
|
159
|
+
} catch (e) {
|
|
160
|
+
if (this.context?.verbose) {
|
|
161
|
+
console.warn(`[PluginRegistry] Analysis failed for plugin "${plugin.name}" on ${filePath}:`, e.message);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
124
168
|
/**
|
|
125
169
|
* Version 5.0.0: Run dependency diagnostics across all registered plugins.
|
|
126
170
|
* Returns a consolidated list of missing/misplaced dependencies and orphaned configs.
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import path from 'path';
|
|
9
9
|
import fs from 'fs/promises';
|
|
10
10
|
import { BasePlugin } from '../BasePlugin.js';
|
|
11
|
+
import { FrameworkConfigParser } from '../../resolution/FrameworkConfigParser.js';
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
export class GraphQLPlugin extends BasePlugin {
|
|
@@ -211,6 +212,27 @@ export class NuxtPlugin extends BasePlugin {
|
|
|
211
212
|
}
|
|
212
213
|
return false;
|
|
213
214
|
}
|
|
215
|
+
detectEntryPoints(content, filePath) {
|
|
216
|
+
if (this.getConfigFiles().some(f => filePath.endsWith(f))) {
|
|
217
|
+
const parser = new FrameworkConfigParser(this.context);
|
|
218
|
+
const { entries } = parser.parse(content, filePath);
|
|
219
|
+
return Array.from(entries);
|
|
220
|
+
}
|
|
221
|
+
return [];
|
|
222
|
+
}
|
|
223
|
+
async analyze(node, filePath) {
|
|
224
|
+
if (this.getConfigFiles().some(f => filePath.endsWith(f))) {
|
|
225
|
+
node.isEntry = true;
|
|
226
|
+
if (node.rawCode) {
|
|
227
|
+
const parser = new FrameworkConfigParser(this.context);
|
|
228
|
+
const { aliases, entries } = parser.parse(node.rawCode, filePath);
|
|
229
|
+
if (aliases.size > 0) node.frameworkAliases = aliases;
|
|
230
|
+
if (entries.size > 0) node.frameworkEntries = entries;
|
|
231
|
+
if (node.rawCode.includes('modules:')) node.hasNuxtModules = true;
|
|
232
|
+
if (node.rawCode.includes('components:')) node.hasNuxtComponentsConfig = true;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
214
236
|
}
|
|
215
237
|
|
|
216
238
|
export class RemixPlugin extends BasePlugin {
|
|
@@ -233,6 +255,21 @@ export class RemixPlugin extends BasePlugin {
|
|
|
233
255
|
}
|
|
234
256
|
return false;
|
|
235
257
|
}
|
|
258
|
+
detectEntryPoints(content, filePath) {
|
|
259
|
+
if (filePath.endsWith('remix.config.js') || filePath.includes('vite.config.')) {
|
|
260
|
+
const parser = new FrameworkConfigParser(this.context);
|
|
261
|
+
const { entries } = parser.parse(content, filePath);
|
|
262
|
+
return Array.from(entries);
|
|
263
|
+
}
|
|
264
|
+
return [];
|
|
265
|
+
}
|
|
266
|
+
async analyze(node, filePath) {
|
|
267
|
+
if (filePath.endsWith('remix.config.js')) {
|
|
268
|
+
node.isEntry = true;
|
|
269
|
+
if (node.rawCode?.includes('ignoredRouteFiles:')) node.hasRemixIgnoredRoutes = true;
|
|
270
|
+
if (node.rawCode?.includes('serverBuildPath:')) node.hasRemixServerBuildPath = true;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
236
273
|
}
|
|
237
274
|
|
|
238
275
|
export class SvelteKitPlugin extends BasePlugin {
|
|
@@ -251,6 +288,29 @@ export class SvelteKitPlugin extends BasePlugin {
|
|
|
251
288
|
async isActive(baseDir) {
|
|
252
289
|
try { await fs.access(path.join(baseDir, 'svelte.config.js')); return true; } catch { return false; }
|
|
253
290
|
}
|
|
291
|
+
detectEntryPoints(content, filePath) {
|
|
292
|
+
if (filePath.includes('svelte.config.')) {
|
|
293
|
+
const parser = new FrameworkConfigParser(this.context);
|
|
294
|
+
const { entries } = parser.parse(content, filePath);
|
|
295
|
+
return Array.from(entries);
|
|
296
|
+
}
|
|
297
|
+
return [];
|
|
298
|
+
}
|
|
299
|
+
async analyze(node, filePath) {
|
|
300
|
+
if (filePath.includes('svelte.config.')) {
|
|
301
|
+
node.isEntry = true;
|
|
302
|
+
if (node.rawCode) {
|
|
303
|
+
const parser = new FrameworkConfigParser(this.context);
|
|
304
|
+
const { aliases, entries } = parser.parse(node.rawCode, filePath);
|
|
305
|
+
if (aliases.size > 0) {
|
|
306
|
+
node.frameworkAliases = aliases;
|
|
307
|
+
node.hasSvelteAliases = true;
|
|
308
|
+
}
|
|
309
|
+
if (entries.size > 0) node.frameworkEntries = entries;
|
|
310
|
+
if (node.rawCode.includes('adapter:')) node.hasSvelteAdapter = true;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
254
314
|
}
|
|
255
315
|
|
|
256
316
|
export class AstroPlugin extends BasePlugin {
|
|
@@ -265,12 +325,39 @@ export class AstroPlugin extends BasePlugin {
|
|
|
265
325
|
}
|
|
266
326
|
return false;
|
|
267
327
|
}
|
|
328
|
+
detectEntryPoints(content, filePath) {
|
|
329
|
+
if (this.getConfigFiles().some(f => filePath.endsWith(f))) {
|
|
330
|
+
const parser = new FrameworkConfigParser(this.context);
|
|
331
|
+
const { entries } = parser.parse(content, filePath);
|
|
332
|
+
return Array.from(entries);
|
|
333
|
+
}
|
|
334
|
+
return [];
|
|
335
|
+
}
|
|
336
|
+
async analyze(node, filePath) {
|
|
337
|
+
if (this.getConfigFiles().some(f => filePath.endsWith(f))) {
|
|
338
|
+
node.isEntry = true;
|
|
339
|
+
if (node.rawCode) {
|
|
340
|
+
const parser = new FrameworkConfigParser(this.context);
|
|
341
|
+
const { aliases, entries } = parser.parse(node.rawCode, filePath);
|
|
342
|
+
if (aliases.size > 0) node.frameworkAliases = aliases;
|
|
343
|
+
if (entries.size > 0) node.frameworkEntries = entries;
|
|
344
|
+
if (node.rawCode.includes('integrations:')) node.hasAstroIntegrations = true;
|
|
345
|
+
if (node.rawCode.includes('output:')) node.hasAstroOutputConfig = true;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
268
349
|
}
|
|
269
350
|
|
|
270
351
|
export class VitepressPlugin extends BasePlugin {
|
|
271
352
|
get name() { return 'vitepress'; }
|
|
272
353
|
getConfigFiles() { return ['package.json']; }
|
|
273
354
|
getRequiredPackages() { return [{ name: 'vitepress', dev: true }]; }
|
|
355
|
+
|
|
356
|
+
getRoutePatterns() {
|
|
357
|
+
// Protection: Every file inside a .vitepress folder is treated as an entry point
|
|
358
|
+
return [/\/\.vitepress\//];
|
|
359
|
+
}
|
|
360
|
+
|
|
274
361
|
async isActive(baseDir) {
|
|
275
362
|
try {
|
|
276
363
|
const pkgJson = JSON.parse(await fs.readFile(path.join(baseDir, 'package.json'), 'utf8'));
|
|
@@ -282,6 +369,62 @@ export class VitepressPlugin extends BasePlugin {
|
|
|
282
369
|
return hasDep;
|
|
283
370
|
} catch { return false; }
|
|
284
371
|
}
|
|
372
|
+
|
|
373
|
+
async runDependencyDiagnostics(baseDir) {
|
|
374
|
+
const diagnostics = await super.runDependencyDiagnostics(baseDir);
|
|
375
|
+
|
|
376
|
+
let pkg = null;
|
|
377
|
+
try {
|
|
378
|
+
const raw = await fs.readFile(path.join(baseDir, 'package.json'), 'utf8');
|
|
379
|
+
pkg = JSON.parse(raw);
|
|
380
|
+
} catch { return diagnostics; }
|
|
381
|
+
|
|
382
|
+
const scripts = pkg.scripts || {};
|
|
383
|
+
const allDeps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
384
|
+
const hasVitepressDep = 'vitepress' in allDeps;
|
|
385
|
+
|
|
386
|
+
// Check if any script uses vitepress
|
|
387
|
+
const hasVitepressCommand = Object.values(scripts).some(s => s.includes('vitepress'));
|
|
388
|
+
|
|
389
|
+
// Logic for .vitepress folder existence
|
|
390
|
+
let hasVitepressFolder = false;
|
|
391
|
+
const possibleDirs = ['.vitepress', 'docs/.vitepress', 'docs/docs/.vitepress'];
|
|
392
|
+
for (const d of possibleDirs) {
|
|
393
|
+
try {
|
|
394
|
+
await fs.access(path.join(baseDir, d));
|
|
395
|
+
hasVitepressFolder = true;
|
|
396
|
+
break;
|
|
397
|
+
} catch {}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Scenario 1: build cmd/dependency exists but no folder -> unused dependency
|
|
401
|
+
if ((hasVitepressCommand || hasVitepressDep) && !hasVitepressFolder) {
|
|
402
|
+
diagnostics.push({
|
|
403
|
+
plugin: this.name,
|
|
404
|
+
severity: 'warning',
|
|
405
|
+
message: `Plugin "${this.name}": "vitepress" is defined in package.json or scripts, but no ".vitepress" folder was found. This might be an unused dependency.`
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Scenario 2: folder exists but no dependency -> missing dependency
|
|
410
|
+
if (hasVitepressFolder && !hasVitepressDep) {
|
|
411
|
+
diagnostics.push({
|
|
412
|
+
plugin: this.name,
|
|
413
|
+
severity: 'error',
|
|
414
|
+
message: `Plugin "${this.name}": ".vitepress" folder exists, but "vitepress" package is not installed in package.json.`
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return diagnostics;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async analyze(node, filePath) {
|
|
422
|
+
// If it's a config file inside .vitepress, mark it
|
|
423
|
+
if (filePath.includes('/.vitepress/config.') || filePath.includes('/.vitepress/theme/')) {
|
|
424
|
+
node.isEntry = true;
|
|
425
|
+
node.isVitepressInternal = true;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
285
428
|
}
|
|
286
429
|
|
|
287
430
|
export class GatsbyPlugin extends BasePlugin {
|
|
@@ -321,6 +464,14 @@ export class NextJsPlugin extends BasePlugin {
|
|
|
321
464
|
}
|
|
322
465
|
return false;
|
|
323
466
|
}
|
|
467
|
+
async analyze(node, filePath) {
|
|
468
|
+
if (this.getConfigFiles().some(f => filePath.endsWith(f))) {
|
|
469
|
+
node.isEntry = true;
|
|
470
|
+
if (node.rawCode?.includes('rewrites')) node.hasNextRewrites = true;
|
|
471
|
+
if (node.rawCode?.includes('redirects')) node.hasNextRedirects = true;
|
|
472
|
+
if (node.rawCode?.includes('images:')) node.hasNextImageConfig = true;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
324
475
|
}
|
|
325
476
|
|
|
326
477
|
export class ReactPlugin extends BasePlugin {
|
|
@@ -443,6 +594,42 @@ export class VitePlugin extends BasePlugin {
|
|
|
443
594
|
}
|
|
444
595
|
return false;
|
|
445
596
|
}
|
|
597
|
+
detectEntryPoints(content, filePath) {
|
|
598
|
+
if (filePath.includes('vite.config.')) {
|
|
599
|
+
const parser = new FrameworkConfigParser(this.context);
|
|
600
|
+
const { entries } = parser.parse(content, filePath);
|
|
601
|
+
return Array.from(entries);
|
|
602
|
+
}
|
|
603
|
+
return [];
|
|
604
|
+
}
|
|
605
|
+
async analyze(node, filePath) {
|
|
606
|
+
if (filePath.includes('vite.config.')) {
|
|
607
|
+
// Mark config as an entry so its own dependencies are tracked
|
|
608
|
+
node.isEntry = true;
|
|
609
|
+
|
|
610
|
+
// UPGRADE: Use FrameworkConfigParser for deep structural extraction
|
|
611
|
+
if (node.rawCode) {
|
|
612
|
+
const parser = new FrameworkConfigParser(this.context);
|
|
613
|
+
const { aliases, entries } = parser.parse(node.rawCode, filePath);
|
|
614
|
+
|
|
615
|
+
if (aliases.size > 0) {
|
|
616
|
+
node.frameworkAliases = aliases;
|
|
617
|
+
node.viteAliasesDetected = true;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
if (entries.size > 0) {
|
|
621
|
+
node.frameworkEntries = entries;
|
|
622
|
+
node.hasCustomViteInputs = true;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Detect Plugins (Keep existing simple check)
|
|
626
|
+
const pluginsPattern = /plugins\s*:\s*\[([\s\S]*?)\]/g;
|
|
627
|
+
if (pluginsPattern.test(node.rawCode)) {
|
|
628
|
+
node.hasVitePlugins = true;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
446
633
|
}
|
|
447
634
|
|
|
448
635
|
export class EsbuildPlugin extends BasePlugin {
|
|
@@ -479,6 +666,30 @@ export class WebpackPlugin extends BasePlugin {
|
|
|
479
666
|
}
|
|
480
667
|
return false;
|
|
481
668
|
}
|
|
669
|
+
detectEntryPoints(content, filePath) {
|
|
670
|
+
if (this.getConfigFiles().some(f => filePath.endsWith(f))) {
|
|
671
|
+
const parser = new FrameworkConfigParser(this.context);
|
|
672
|
+
const { entries } = parser.parse(content, filePath);
|
|
673
|
+
return Array.from(entries);
|
|
674
|
+
}
|
|
675
|
+
return [];
|
|
676
|
+
}
|
|
677
|
+
detectEntryPoints(content, filePath) {
|
|
678
|
+
if (this.getConfigFiles().some(f => filePath.endsWith(f))) {
|
|
679
|
+
const parser = new FrameworkConfigParser(this.context);
|
|
680
|
+
const { entries } = parser.parse(content, filePath);
|
|
681
|
+
return Array.from(entries);
|
|
682
|
+
}
|
|
683
|
+
return [];
|
|
684
|
+
}
|
|
685
|
+
async analyze(node, filePath) {
|
|
686
|
+
if (this.getConfigFiles().some(f => filePath.endsWith(f))) {
|
|
687
|
+
node.isEntry = true;
|
|
688
|
+
if (node.rawCode?.includes('entry:')) node.hasWebpackEntries = true;
|
|
689
|
+
if (node.rawCode?.includes('resolve:')) node.hasWebpackResolve = true;
|
|
690
|
+
if (node.rawCode?.includes('plugins:')) node.hasWebpackPlugins = true;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
482
693
|
}
|
|
483
694
|
|
|
484
695
|
export class ParcelPlugin extends BasePlugin {
|
|
@@ -521,6 +732,14 @@ export class TailwindPlugin extends BasePlugin {
|
|
|
521
732
|
}
|
|
522
733
|
return false;
|
|
523
734
|
}
|
|
735
|
+
async analyze(node, filePath) {
|
|
736
|
+
if (this.getConfigFiles().some(f => filePath.endsWith(f))) {
|
|
737
|
+
node.isEntry = true;
|
|
738
|
+
if (node.rawCode?.includes('content:')) node.hasTailwindContent = true;
|
|
739
|
+
if (node.rawCode?.includes('theme:')) node.hasTailwindTheme = true;
|
|
740
|
+
if (node.rawCode?.includes('plugins:')) node.hasTailwindPlugins = true;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
524
743
|
}
|
|
525
744
|
|
|
526
745
|
export class PostcssPlugin extends BasePlugin {
|
|
@@ -665,6 +884,13 @@ export class JestPlugin extends BasePlugin {
|
|
|
665
884
|
async isActive(baseDir) {
|
|
666
885
|
try { await fs.access(path.join(baseDir, 'jest.config.js')); return true; } catch { return false; }
|
|
667
886
|
}
|
|
887
|
+
async analyze(node, filePath) {
|
|
888
|
+
if (filePath.endsWith('jest.config.js')) {
|
|
889
|
+
node.isEntry = true;
|
|
890
|
+
if (node.rawCode?.includes('testEnvironment:')) node.hasJestTestEnv = true;
|
|
891
|
+
if (node.rawCode?.includes('setupFiles:')) node.hasJestSetupFiles = true;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
668
894
|
}
|
|
669
895
|
|
|
670
896
|
export class VitestPlugin extends BasePlugin {
|
|
@@ -677,6 +903,39 @@ export class VitestPlugin extends BasePlugin {
|
|
|
677
903
|
}
|
|
678
904
|
return false;
|
|
679
905
|
}
|
|
906
|
+
detectEntryPoints(content, filePath) {
|
|
907
|
+
if (this.getConfigFiles().some(f => filePath.endsWith(f))) {
|
|
908
|
+
const parser = new FrameworkConfigParser(this.context);
|
|
909
|
+
const { entries } = parser.parse(content, filePath);
|
|
910
|
+
return Array.from(entries);
|
|
911
|
+
}
|
|
912
|
+
return [];
|
|
913
|
+
}
|
|
914
|
+
detectEntryPoints(content, filePath) {
|
|
915
|
+
if (this.getConfigFiles().some(f => filePath.endsWith(f))) {
|
|
916
|
+
const parser = new FrameworkConfigParser(this.context);
|
|
917
|
+
const { entries } = parser.parse(content, filePath);
|
|
918
|
+
return Array.from(entries);
|
|
919
|
+
}
|
|
920
|
+
return [];
|
|
921
|
+
}
|
|
922
|
+
async analyze(node, filePath) {
|
|
923
|
+
if (this.getConfigFiles().some(f => filePath.endsWith(f))) {
|
|
924
|
+
node.isEntry = true;
|
|
925
|
+
if (node.rawCode) {
|
|
926
|
+
const parser = new FrameworkConfigParser(this.context);
|
|
927
|
+
const { aliases, entries } = parser.parse(node.rawCode, filePath);
|
|
928
|
+
if (aliases.size > 0) {
|
|
929
|
+
node.frameworkAliases = aliases;
|
|
930
|
+
node.hasWebpackAliases = true;
|
|
931
|
+
}
|
|
932
|
+
if (entries.size > 0) {
|
|
933
|
+
node.frameworkEntries = entries;
|
|
934
|
+
node.hasWebpackEntries = true;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
}
|
|
680
939
|
}
|
|
681
940
|
|
|
682
941
|
export class PlaywrightPlugin extends BasePlugin {
|
|
@@ -3,73 +3,239 @@ import path from 'path';
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* ConfigLoader
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* - package.json "entkapp" key
|
|
10
|
-
* - CLI flags (passed as overrides)
|
|
6
|
+
*
|
|
7
|
+
* Loads and merges entkapp configuration from multiple sources.
|
|
8
|
+
* Supports Monorepo detection for pnpm, npm/yarn/bun, Lerna, Turbo, and Nx.
|
|
11
9
|
*/
|
|
12
10
|
export class ConfigLoader {
|
|
13
|
-
constructor(cwd) {
|
|
14
|
-
this.cwd = cwd;
|
|
11
|
+
constructor(cwd) {
|
|
12
|
+
this.cwd = cwd;
|
|
15
13
|
}
|
|
16
14
|
|
|
17
15
|
async loadConfig(overrides = {}) {
|
|
18
16
|
let config = this._defaultConfig();
|
|
19
17
|
|
|
20
|
-
// 1.
|
|
18
|
+
// 1. entkapp/config.json
|
|
21
19
|
const jsonConfigPath = path.join(this.cwd, 'entkapp', 'config.json');
|
|
22
20
|
try {
|
|
23
21
|
const raw = await fs.readFile(jsonConfigPath, 'utf8');
|
|
24
|
-
// Strip comments from JSON (JSONC support)
|
|
25
22
|
const stripped = raw.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
26
|
-
|
|
27
|
-
config = this._merge(config, parsed);
|
|
23
|
+
config = this._merge(config, JSON.parse(stripped));
|
|
28
24
|
} catch (e) {}
|
|
29
25
|
|
|
30
|
-
// 2.
|
|
31
|
-
for (const configFile of ['entkapp.config.mjs', 'entkapp.config.js', 'entkapp.config.cjs']) {
|
|
26
|
+
// 2. entkapp.config.ts / .mjs / .js / .cjs
|
|
27
|
+
for (const configFile of ['entkapp.config.ts', 'entkapp.config.mjs', 'entkapp.config.js', 'entkapp.config.cjs']) {
|
|
32
28
|
const jsConfigPath = path.join(this.cwd, configFile);
|
|
33
29
|
try {
|
|
34
30
|
const mod = await import(jsConfigPath);
|
|
35
31
|
const jsConfig = mod.default || mod;
|
|
36
|
-
if (typeof jsConfig === 'object') {
|
|
32
|
+
if (typeof jsConfig === 'object' && jsConfig !== null) {
|
|
37
33
|
config = this._merge(config, jsConfig);
|
|
38
34
|
break;
|
|
39
35
|
}
|
|
40
36
|
} catch (e) {}
|
|
41
37
|
}
|
|
42
38
|
|
|
43
|
-
// 3.
|
|
44
|
-
const pkgPath = path.join(this.cwd, 'package.json');
|
|
39
|
+
// 3. package.json "entkapp" key
|
|
45
40
|
try {
|
|
46
|
-
const pkg = JSON.parse(await fs.readFile(
|
|
41
|
+
const pkg = JSON.parse(await fs.readFile(path.join(this.cwd, 'package.json'), 'utf8'));
|
|
47
42
|
if (pkg.entkapp && typeof pkg.entkapp === 'object') {
|
|
48
43
|
config = this._merge(config, pkg.entkapp);
|
|
49
44
|
}
|
|
50
45
|
} catch (e) {}
|
|
51
46
|
|
|
52
|
-
// 4.
|
|
47
|
+
// 4. Workspace / monorepo packages
|
|
48
|
+
const workspaceData = await this.loadWorkspaceConfigs();
|
|
49
|
+
if (workspaceData.packages.length > 0) {
|
|
50
|
+
config.workspace = true;
|
|
51
|
+
config.workspacePackages = workspaceData.packages;
|
|
52
|
+
config.workspaceTsConfigs = workspaceData.tsConfigs;
|
|
53
|
+
config.workspaceConfigFiles = workspaceData.configFiles;
|
|
54
|
+
config.monorepoConfigs = workspaceData.monorepoConfigs;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 5. CLI overrides
|
|
53
58
|
config = this._merge(config, overrides);
|
|
54
59
|
|
|
60
|
+
// Final sanity check: ensure arrays exist
|
|
61
|
+
if (!Array.isArray(config.entryPoints)) config.entryPoints = [];
|
|
62
|
+
if (!Array.isArray(config.workspacePackages)) config.workspacePackages = [];
|
|
63
|
+
if (!Array.isArray(config.workspaceTsConfigs)) config.workspaceTsConfigs = [];
|
|
64
|
+
if (!Array.isArray(config.workspaceConfigFiles)) config.workspaceConfigFiles = [];
|
|
65
|
+
|
|
55
66
|
return config;
|
|
56
67
|
}
|
|
57
68
|
|
|
69
|
+
async loadWorkspaceConfigs() {
|
|
70
|
+
const result = {
|
|
71
|
+
packages: [],
|
|
72
|
+
tsConfigs: [],
|
|
73
|
+
configFiles: [],
|
|
74
|
+
monorepoConfigs: {}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Load global configs first
|
|
78
|
+
const globalConfigs = ['turbo.json', 'nx.json', 'lerna.json'];
|
|
79
|
+
for (const file of globalConfigs) {
|
|
80
|
+
try {
|
|
81
|
+
const content = JSON.parse(await fs.readFile(path.join(this.cwd, file), 'utf8'));
|
|
82
|
+
result.monorepoConfigs[file] = content;
|
|
83
|
+
} catch (e) {}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const patterns = await this._resolveWorkspacePatterns();
|
|
87
|
+
if (patterns.length === 0) return result;
|
|
88
|
+
|
|
89
|
+
for (const pattern of patterns) {
|
|
90
|
+
const dirs = await this._expandGlob(pattern);
|
|
91
|
+
for (const dir of dirs) {
|
|
92
|
+
await this._readPackageDir(dir, result);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async _resolveWorkspacePatterns() {
|
|
100
|
+
const pnpmPatterns = await this._parseWorkspaceYaml('pnpm-workspace.yaml');
|
|
101
|
+
if (pnpmPatterns.length > 0) return pnpmPatterns;
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const pkg = JSON.parse(await fs.readFile(path.join(this.cwd, 'package.json'), 'utf8'));
|
|
105
|
+
if (pkg.workspaces) {
|
|
106
|
+
return Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces.packages || [];
|
|
107
|
+
}
|
|
108
|
+
} catch (e) {}
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
const lerna = JSON.parse(await fs.readFile(path.join(this.cwd, 'lerna.json'), 'utf8'));
|
|
112
|
+
if (lerna.packages) return lerna.packages;
|
|
113
|
+
} catch (e) {}
|
|
114
|
+
|
|
115
|
+
return this._parseWorkspaceYaml('workspace.yaml');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async _parseWorkspaceYaml(fileName) {
|
|
119
|
+
const filePath = path.join(this.cwd, fileName);
|
|
120
|
+
try {
|
|
121
|
+
const raw = await fs.readFile(filePath, 'utf8');
|
|
122
|
+
const patterns = [];
|
|
123
|
+
const lines = raw.split('\n');
|
|
124
|
+
let inPackages = false;
|
|
125
|
+
for (const line of lines) {
|
|
126
|
+
const trimmed = line.trim();
|
|
127
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
128
|
+
if (/^packages\s*:/.test(trimmed)) {
|
|
129
|
+
inPackages = true;
|
|
130
|
+
const inlineMatch = trimmed.match(/^packages\s*:\s*\[([^\]]*)\]/);
|
|
131
|
+
if (inlineMatch) {
|
|
132
|
+
inPackages = false;
|
|
133
|
+
for (const item of inlineMatch[1].split(',')) {
|
|
134
|
+
const val = item.trim().replace(/^['"]|['"]$/g, '');
|
|
135
|
+
if (val) patterns.push(val);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (inPackages) {
|
|
141
|
+
if (line.match(/^[a-zA-Z0-9_-]/) && trimmed.endsWith(':')) {
|
|
142
|
+
inPackages = false;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (trimmed.startsWith('-')) {
|
|
146
|
+
const val = trimmed.substring(1).trim().replace(/^['"]|['"]$/g, '');
|
|
147
|
+
if (val) patterns.push(val);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return patterns;
|
|
152
|
+
} catch (e) {
|
|
153
|
+
return [];
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async _readPackageDir(dir, result) {
|
|
158
|
+
const normalizedDir = dir.replace(/\\/g, '/');
|
|
159
|
+
let packageName;
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const pkg = JSON.parse(await fs.readFile(path.join(dir, 'package.json'), 'utf8'));
|
|
163
|
+
packageName = pkg.name;
|
|
164
|
+
result.packages.push({
|
|
165
|
+
dir: normalizedDir,
|
|
166
|
+
name: packageName,
|
|
167
|
+
entkappConfig: (pkg.entkapp && typeof pkg.entkapp === 'object') ? pkg.entkapp : {}
|
|
168
|
+
});
|
|
169
|
+
} catch (e) {
|
|
170
|
+
// If no package.json, skip as requested in WorkSpaceGraph logic
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// tsconfig.json
|
|
175
|
+
try {
|
|
176
|
+
const tsconfigRaw = await fs.readFile(path.join(dir, 'tsconfig.json'), 'utf8');
|
|
177
|
+
const stripped = tsconfigRaw.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
178
|
+
result.tsConfigs.push({ dir: normalizedDir, name: packageName, tsconfig: JSON.parse(stripped) });
|
|
179
|
+
} catch (e) {}
|
|
180
|
+
|
|
181
|
+
// *.config.ts / .js
|
|
182
|
+
try {
|
|
183
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
184
|
+
for (const entry of entries) {
|
|
185
|
+
if (!entry.isFile() || !/\.config\.(ts|js|mjs|cjs)$/.test(entry.name)) continue;
|
|
186
|
+
const filePath = path.join(dir, entry.name).replace(/\\/g, '/');
|
|
187
|
+
try {
|
|
188
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
189
|
+
result.configFiles.push({ dir: normalizedDir, name: packageName, fileName: entry.name, filePath, content });
|
|
190
|
+
} catch (e) {}
|
|
191
|
+
}
|
|
192
|
+
} catch (e) {}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async _expandGlob(pattern) {
|
|
196
|
+
const results = [];
|
|
197
|
+
const cleanPattern = pattern.replace(/\/$/, '');
|
|
198
|
+
if (cleanPattern.includes('*')) {
|
|
199
|
+
const parts = cleanPattern.split('/');
|
|
200
|
+
const wildcardIndex = parts.findIndex(p => p.includes('*'));
|
|
201
|
+
const baseDir = path.join(this.cwd, ...parts.slice(0, wildcardIndex));
|
|
202
|
+
try {
|
|
203
|
+
const entries = await fs.readdir(baseDir, { withFileTypes: true });
|
|
204
|
+
for (const entry of entries) {
|
|
205
|
+
if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
|
206
|
+
const fullPath = path.join(baseDir, entry.name);
|
|
207
|
+
try {
|
|
208
|
+
await fs.access(path.join(fullPath, 'package.json'));
|
|
209
|
+
results.push(fullPath.replace(/\\/g, '/'));
|
|
210
|
+
} catch (e) {}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
} catch (e) {}
|
|
214
|
+
} else {
|
|
215
|
+
const fullPath = path.resolve(this.cwd, cleanPattern);
|
|
216
|
+
try {
|
|
217
|
+
const stat = await fs.stat(fullPath);
|
|
218
|
+
if (stat.isDirectory()) results.push(fullPath.replace(/\\/g, '/'));
|
|
219
|
+
} catch (e) {}
|
|
220
|
+
}
|
|
221
|
+
return results;
|
|
222
|
+
}
|
|
223
|
+
|
|
58
224
|
_defaultConfig() {
|
|
59
225
|
return {
|
|
60
226
|
interface: 'CLI',
|
|
61
227
|
useBuiltinPlugins: true,
|
|
62
228
|
useCustomPlugins: true,
|
|
63
|
-
options: {
|
|
64
|
-
verbose: false,
|
|
65
|
-
fastMode: true,
|
|
66
|
-
selfHealing: true
|
|
67
|
-
},
|
|
229
|
+
options: { verbose: false, fastMode: true, selfHealing: true },
|
|
68
230
|
enabledPlugins: [],
|
|
69
231
|
ignoreDependencies: ['entkapp', '@types/*'],
|
|
70
232
|
exclude: ['node_modules', '.git', 'dist', 'build', 'coverage'],
|
|
71
233
|
entryPoints: [],
|
|
72
|
-
workspace: false
|
|
234
|
+
workspace: false,
|
|
235
|
+
workspacePackages: [],
|
|
236
|
+
workspaceTsConfigs: [],
|
|
237
|
+
workspaceConfigFiles: [],
|
|
238
|
+
monorepoConfigs: {}
|
|
73
239
|
};
|
|
74
240
|
}
|
|
75
241
|
|
|
@@ -79,8 +245,6 @@ export class ConfigLoader {
|
|
|
79
245
|
for (const key of Object.keys(override)) {
|
|
80
246
|
if (key === 'options' && typeof override[key] === 'object') {
|
|
81
247
|
result.options = { ...(base.options || {}), ...override[key] };
|
|
82
|
-
} else if (Array.isArray(override[key])) {
|
|
83
|
-
result[key] = override[key];
|
|
84
248
|
} else {
|
|
85
249
|
result[key] = override[key];
|
|
86
250
|
}
|