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.
@@ -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) { this.cwd = cwd; }
3
- async loadConfig() { return {}; }
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(projectRoot, 'package.json');
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 githubWorkflows = path.join(projectRoot, '.github/workflows');
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
- // 4. Identify Unused Binaries
199
- // Scan node_modules/.bin for all available binaries
220
+ async _identifyUnusedBinaries(dir, usedBinaries) {
200
221
  try {
201
- const binDir = path.join(projectRoot, 'node_modules', '.bin');
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
- // Tsconfig paths can be loaded here later
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 (p.endsWith('.js')) {
23
- const tsPath = p.slice(0, -3) + '.ts';
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 (p.endsWith('.jsx')) {
29
- const tsxPath = p.slice(0, -4) + '.tsx';
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(p);
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(p, `index${ext}`);
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 p;
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, 'tsconfig.json');
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 result = ts.parseConfigFileTextWithComments(configPath, content);
22
+ const readResult = ts.readConfigFile(configPath, ts.sys.readFile);
21
23
 
22
- if (result.error) {
24
+ if (readResult.error) {
23
25
  return null;
24
26
  }
25
27
 
26
28
  const parsed = ts.parseJsonConfigFileContent(
27
- result.config,
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 || !parsedConfig.options.paths) {
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(/\*/, match[1]);
135
+ const resolvedReplacement = replacement.replace(/\*/g, match[1] || '');
61
136
  const fullPath = path.resolve(base, resolvedReplacement);
62
137
 
63
- // Prüfe ob Datei existiert
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
  }