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.
@@ -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
- this.packageManifests = new Map(); // dirPath -> manifestData
8
- this.workspacePackages = new Map(); // packageName -> dirPath
9
- this.tsconfigPaths = new Map(); // packageName -> tsconfigData
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
- 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) {}
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
- 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) {}
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
- if (this.context.verbose) console.log('[Workspace] No root package.json found or invalid.');
136
+ return [];
76
137
  }
77
138
  }
78
139
 
79
- calculatePackageExportsEntries(pkgData, dirPath) {
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
- addEntry(obj);
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, dirPath] of this.workspacePackages.entries()) {
272
+ for (const [pkgName] of this.workspacePackages.entries()) {
137
273
  this.context.usedExternalPackages.add(pkgName);
138
274
  }
139
275
  }
140
276
 
141
- /**
142
- * Expands a workspace glob pattern (e.g. 'packages/*') to absolute directory paths.
143
- * Supports single-level wildcards and direct paths.
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
- // 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) {}
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
  }