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.
@@ -3,73 +3,239 @@ import path from 'path';
3
3
 
4
4
  /**
5
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)
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. Try entkapp/config.json
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
- const parsed = JSON.parse(stripped);
27
- config = this._merge(config, parsed);
23
+ config = this._merge(config, JSON.parse(stripped));
28
24
  } catch (e) {}
29
25
 
30
- // 2. Try entkapp.config.js / entkapp.config.mjs
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. Try package.json "entkapp" key
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(pkgPath, 'utf8'));
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. Apply CLI overrides
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
  }
@@ -0,0 +1,119 @@
1
+ import path from 'path';
2
+ import fs from 'fs';
3
+
4
+ /**
5
+ * Lightweight Static Analysis Parser for Vite / Vitest / Framework Configurations.
6
+ * Extracts 'resolve.alias' and 'build.lib.entry' without executing the file.
7
+ * Version 5.4.0: Added poly-extension path resolution.
8
+ */
9
+ export class FrameworkConfigParser {
10
+ constructor(context) {
11
+ this.context = context;
12
+ }
13
+
14
+ /**
15
+ * Parses a Framework configuration file and extracts structural metadata.
16
+ * @param {string} content - Raw source code of the config file
17
+ * @param {string} filePath - Absolute path to the config file
18
+ * @returns {Object} { aliases: Map<string, string>, entries: Set<string> }
19
+ */
20
+ parse(content, filePath) {
21
+ const results = {
22
+ aliases: new Map(),
23
+ entries: new Set()
24
+ };
25
+
26
+ if (!content) return results;
27
+
28
+ const configDir = path.dirname(filePath);
29
+
30
+ // 1. Extract Aliases from resolve: { alias: { ... } } or alias: [ ... ]
31
+ this._extractAliases(content, configDir, results.aliases);
32
+
33
+ // 2. Extract Entry Points from build: { lib: { entry: '...' } } or rollupOptions: { input: '...' }
34
+ this._extractEntries(content, configDir, results.entries);
35
+
36
+ return results;
37
+ }
38
+
39
+ _extractAliases(content, configDir, aliasMap) {
40
+ const objAliasPatterns = [
41
+ /alias\s*:\s*\{([\s\S]*?)\}/g,
42
+ /resolve\s*:\s*\{[\s\S]*?alias\s*:\s*\{([\s\S]*?)\}/g
43
+ ];
44
+
45
+ for (const pattern of objAliasPatterns) {
46
+ let match;
47
+ while ((match = pattern.exec(content)) !== null) {
48
+ const inner = match[1];
49
+ const pairPattern = /(?:['"]?)(@?[a-zA-Z0-9_\-\/*]+)(?:['"]?)\s*:\s*(?:['"]([^'"]+)['"]|(path\.(?:resolve|join)\([\s\S]*?\)))/g;
50
+ let pair;
51
+ while ((pair = pairPattern.exec(inner)) !== null) {
52
+ const [_, key, stringVal, callVal] = pair;
53
+ aliasMap.set(key, this._resolveValue(stringVal || callVal, configDir));
54
+ }
55
+ }
56
+ }
57
+
58
+ const arrAliasPattern = /alias\s*:\s*\[([\s\S]*?)\]/;
59
+ const arrMatch = arrAliasPattern.exec(content);
60
+ if (arrMatch) {
61
+ const inner = arrMatch[1];
62
+ const findPattern = /\{\s*find\s*:\s*['"]([^'"]+)['"]\s*,\s*replacement\s*:\s*['"]([^'"]+)['"]\s*\}/g;
63
+ let findMatch;
64
+ while ((findMatch = findPattern.exec(inner)) !== null) {
65
+ const [_, key, value] = findMatch;
66
+ aliasMap.set(key, this._resolveValue(value, configDir));
67
+ }
68
+ }
69
+ }
70
+
71
+ _extractEntries(content, configDir, entrySet) {
72
+ const libEntryPattern = /entry\s*:\s*['"]([^'"]+)['"]/;
73
+ const libMatch = libEntryPattern.exec(content);
74
+ if (libMatch) {
75
+ entrySet.add(this._resolveValue(libMatch[1], configDir));
76
+ }
77
+
78
+ const inputPattern = /input\s*:\s*(?:['"]([^'"]+)['"]|\[([\s\S]*?)\])/;
79
+ const inputMatch = inputPattern.exec(content);
80
+ if (inputMatch) {
81
+ if (inputMatch[1]) {
82
+ entrySet.add(this._resolveValue(inputMatch[1], configDir));
83
+ } else if (inputMatch[2]) {
84
+ const paths = inputMatch[2].match(/['"]([^'"]+)['"]/g);
85
+ if (paths) {
86
+ paths.forEach(p => entrySet.add(this._resolveValue(p.replace(/['"]/g, ''), configDir)));
87
+ }
88
+ }
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Resolves a path value and applies poly-extension fallback if the file doesn't exist.
94
+ */
95
+ _resolveValue(val, configDir) {
96
+ const pathCallPattern = /path\.(?:resolve|join)\s*\(\s*(?:__dirname\s*,\s*)?['"]([^'"]+)['"]\s*\)/;
97
+ const pathMatch = pathCallPattern.exec(val);
98
+ if (pathMatch) {
99
+ val = pathMatch[1];
100
+ }
101
+
102
+ if (val.startsWith('.') || val.startsWith('/') || /^[a-zA-Z]:/.test(val)) {
103
+ let resolved = path.resolve(configDir, val).replace(/\\/g, '/');
104
+
105
+ // UPGRADE: Poly-extension fallback
106
+ if (!fs.existsSync(resolved)) {
107
+ const base = resolved.replace(/\.[a-zA-Z0-9]+$/, '');
108
+ const extensions = ['.ts', '.js', '.tsx', '.jsx', '.mjs', '.cjs'];
109
+ for (const ext of extensions) {
110
+ if (fs.existsSync(base + ext)) {
111
+ return (base + ext).replace(/\\/g, '/');
112
+ }
113
+ }
114
+ }
115
+ return resolved;
116
+ }
117
+ return val;
118
+ }
119
+ }
@@ -10,6 +10,36 @@ export class GraphAnalyzer {
10
10
  constructor(context) {
11
11
  this.context = context;
12
12
  this.cwd = context.cwd;
13
+ // UPGRADE 5.4.3: Default Boundary Rules
14
+ this.boundaryRules = context.config?.boundaries || [
15
+ { from: 'packages/shared-*', to: 'apps/*', allow: false, message: 'Shared packages must not import from applications.' },
16
+ { from: '*', to: '**/internal/**', allow: false, message: 'Private internal utilities must not be imported externally.' }
17
+ ];
18
+ }
19
+
20
+ /**
21
+ * UPGRADE 5.4.3: Boundary Enforcement
22
+ * Checks if an import violates defined architectural boundaries.
23
+ */
24
+ checkBoundaries(fromPath, toPath) {
25
+ const relFrom = path.relative(this.cwd, fromPath).replace(/\\/g, '/');
26
+ const relTo = path.relative(this.cwd, toPath).replace(/\\/g, '/');
27
+
28
+ for (const rule of this.boundaryRules) {
29
+ const fromMatch = this._globMatch(relFrom, rule.from);
30
+ const toMatch = this._globMatch(relTo, rule.to);
31
+
32
+ if (fromMatch && toMatch && !rule.allow) {
33
+ return { violated: true, message: rule.message };
34
+ }
35
+ }
36
+ return { violated: false };
37
+ }
38
+
39
+ _globMatch(str, pattern) {
40
+ if (pattern === '*') return true;
41
+ const regex = new RegExp('^' + pattern.replace(/\*\*/g, '.*').replace(/\*/g, '[^/]*') + '$');
42
+ return regex.test(str);
13
43
  }
14
44
 
15
45
  /**
@@ -63,7 +93,41 @@ export class GraphAnalyzer {
63
93
  }
64
94
  }
65
95
 
66
- return { deadFiles, zombieExports };
96
+ const unusedImports = [];
97
+ const boundaryViolations = [];
98
+
99
+ for (const [filePath, node] of graph.entries()) {
100
+ if (!reachable.has(filePath)) continue;
101
+
102
+ // UPGRADE 5.4.3: Check boundaries for every import
103
+ for (const impPath of node.explicitImports) {
104
+ if (graph.has(impPath)) {
105
+ const violation = this.checkBoundaries(filePath, impPath);
106
+ if (violation.violated) {
107
+ boundaryViolations.push({
108
+ file: path.relative(this.cwd, filePath).replace(/\\/g, '/'),
109
+ target: path.relative(this.cwd, impPath).replace(/\\/g, '/'),
110
+ message: violation.message
111
+ });
112
+ }
113
+ }
114
+ }
115
+
116
+ if (node.localImportBindings) {
117
+ for (const [localName, meta] of node.localImportBindings.entries()) {
118
+ // Check if this local binding is used anywhere in the file
119
+ if (!node.instantiatedIdentifiers.has(localName)) {
120
+ unusedImports.push({
121
+ file: path.relative(this.cwd, filePath).replace(/\\/g, '/'),
122
+ specifier: meta.specifier,
123
+ symbol: meta.originalName === '*' ? 'Namespace' : meta.originalName
124
+ });
125
+ }
126
+ }
127
+ }
128
+ }
129
+
130
+ return { deadFiles, zombieExports, unusedImports, boundaryViolations };
67
131
  }
68
132
 
69
133
  _walk(filePath, reachable) {
@@ -6,15 +6,22 @@ export class PathMapper {
6
6
  constructor(context) {
7
7
  this.context = context;
8
8
  this.aliasMappers = []; // list of alias mapping functions
9
+ // UPGRADE: Raw alias patterns for fast "is this an alias?" checks without file-existence tests
10
+ this._aliasPatterns = []; // Array of { regex }
9
11
  }
10
12
 
11
13
  async loadMappings(tsconfigFilename = 'tsconfig.json') {
14
+ // UPGRADE: Reset on reload to avoid duplicate entries when called twice (workspace re-load)
15
+ this.aliasMappers = [];
16
+ this._aliasPatterns = [];
17
+
12
18
  // Load root tsconfig
13
19
  const loader = new TSConfigLoader(this.context.cwd);
14
20
  const config = loader.load();
15
21
  if (config) {
16
22
  const mapper = loader.getAliasMapper(config);
17
23
  this.aliasMappers.push(mapper);
24
+ this._collectAliasPatterns(config, loader);
18
25
  if (this.context.verbose) console.log(`[PathMapper] Loaded root tsconfig aliases`);
19
26
  }
20
27
 
@@ -26,12 +33,86 @@ export class PathMapper {
26
33
  if (wsConfig) {
27
34
  const wsMapper = wsLoader.getAliasMapper(wsConfig);
28
35
  this.aliasMappers.push(wsMapper);
36
+ this._collectAliasPatterns(wsConfig, wsLoader);
29
37
  if (this.context.verbose) console.log(`[PathMapper] Loaded workspace tsconfig aliases from ${root}`);
30
38
  }
31
39
  }
32
40
  }
33
41
  }
34
42
 
43
+ /**
44
+ * UPGRADE: Collect raw alias patterns from a parsed tsconfig so we can answer
45
+ * "is this specifier a tsconfig alias?" without needing the target file to exist on disk.
46
+ * Handles patterns like "@shared/*", "@idk/*", "~/*", etc.
47
+ * @param {Object} parsedConfig - Result of TSConfigLoader.load()
48
+ * @param {TSConfigLoader} loader
49
+ */
50
+ _collectAliasPatterns(parsedConfig, loader) {
51
+ if (!parsedConfig || !parsedConfig.options) return;
52
+ const { paths, baseUrl } = parsedConfig.options;
53
+
54
+ if (paths) {
55
+ for (const pattern in paths) {
56
+ // Convert tsconfig glob pattern to a regex
57
+ // e.g. "@shared/*" -> /^@shared\// or "@shared" -> /^@shared$/
58
+ const escaped = pattern
59
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&') // escape regex special chars except *
60
+ .replace(/\\\*/g, '.*'); // replace escaped \* with .*
61
+ try {
62
+ this._aliasPatterns.push({ regex: new RegExp('^' + escaped + '$') });
63
+ // Also add a prefix variant so "@shared/foo/bar" matches "@shared/*"
64
+ const prefixEscaped = pattern.replace(/\*.*$/, '').replace(/[.+^${}()|[\]\\]/g, '\\$&');
65
+ if (prefixEscaped && prefixEscaped !== escaped) {
66
+ this._aliasPatterns.push({ regex: new RegExp('^' + prefixEscaped) });
67
+ }
68
+ } catch (e) { /* ignore invalid regex */ }
69
+ }
70
+ }
71
+ }
72
+
73
+ /**
74
+ * UPGRADE: Returns true if the given specifier matches any tsconfig path alias pattern.
75
+ * This check does NOT require the target file to exist on disk.
76
+ * Used by the unlisted-dependency audit to skip tsconfig-aliased imports.
77
+ * @param {string} specifier
78
+ * @returns {boolean}
79
+ */
80
+ isTsconfigAlias(specifier) {
81
+ if (!specifier || typeof specifier !== 'string') return false;
82
+ for (const { regex } of this._aliasPatterns) {
83
+ if (regex.test(specifier)) return true;
84
+ }
85
+ return false;
86
+ }
87
+
88
+ /**
89
+ * UPGRADE: Manually register a dynamic alias (e.g. from vite.config.js).
90
+ * @param {string} key - Alias key (e.g. "@shared")
91
+ * @param {string} target - Absolute path or relative path to resolve
92
+ */
93
+ addAlias(key, target) {
94
+ if (!key || !target) return;
95
+
96
+ // 1. Add to aliasMappers for resolvePath()
97
+ const mapper = (p) => {
98
+ if (p === key) return target;
99
+ if (p.startsWith(key + '/')) {
100
+ return path.join(target, p.substring(key.length + 1)).replace(/\\/g, '/');
101
+ }
102
+ return p;
103
+ };
104
+ this.aliasMappers.push(mapper);
105
+
106
+ // 2. Add to _aliasPatterns for isTsconfigAlias() checks
107
+ const escaped = key.replace(/[.+^${}()|[\]\\]/g, '\\$&');
108
+ try {
109
+ this._aliasPatterns.push({ regex: new RegExp('^' + escaped + '$') });
110
+ this._aliasPatterns.push({ regex: new RegExp('^' + escaped + '/') });
111
+ } catch (e) {}
112
+
113
+ if (this.context.verbose) console.log(`[PathMapper] Added dynamic alias: ${key} -> ${target}`);
114
+ }
115
+
35
116
  /**
36
117
  * Resolves physical module paths on disk, translating modern .js imports
37
118
  * back to their actual TypeScript source files.
@@ -109,7 +109,9 @@ export class TSConfigLoader {
109
109
  }
110
110
 
111
111
  const { paths, baseUrl } = parsedConfig.options;
112
- const base = baseUrl ? path.resolve(this.targetDir, baseUrl) : this.targetDir;
112
+ // In ts.parseJsonConfigFileContent, baseUrl is already resolved to an absolute path if it was present
113
+ // If not, we fall back to targetDir
114
+ const base = baseUrl ? baseUrl : this.targetDir;
113
115
 
114
116
  return (source) => {
115
117
  // If no paths configured, still try baseUrl resolution