diff --git a/gitnexus/src/core/ingestion/import-resolvers/standard.ts b/gitnexus/src/core/ingestion/import-resolvers/standard.ts index 47e2dabb2e..4ee6e1440b 100644 --- a/gitnexus/src/core/ingestion/import-resolvers/standard.ts +++ b/gitnexus/src/core/ingestion/import-resolvers/standard.ts @@ -72,6 +72,13 @@ export const resolveImportPath = ( const resolved = tryResolveWithExtensions(rewritten, allFiles); if (resolved) return cache(resolved); + // ESM fallback: strip .js/.jsx/.mjs/.cjs and retry with TS equivalents + const strippedAlias = stripJsExtension(rewritten); + if (strippedAlias !== null) { + const esmResolved = tryResolveWithExtensions(strippedAlias, allFiles); + if (esmResolved) return cache(esmResolved); + } + // Try suffix matching as fallback const parts = rewritten.split('/').filter(Boolean); const suffixResult = suffixResolve(parts, normalizedFileList, allFileList, index); @@ -132,9 +139,6 @@ export const resolveImportPath = ( // TypeScript ESM: imports use .js/.jsx/.mjs/.cjs but source files are // .ts/.tsx/.mts/.cts. Strip the JS-family extension and re-resolve. - // NOTE: This fallback only applies to relative imports. Path alias imports - // (e.g. @/utils.js via tsconfig paths) do not yet strip .js extensions — - // that is a known limitation tracked for follow-up. if (language === SupportedLanguages.TypeScript || language === SupportedLanguages.JavaScript) { const stripped = stripJsExtension(basePath); if (stripped !== null) { diff --git a/gitnexus/test/unit/esm-extension-resolution.test.ts b/gitnexus/test/unit/esm-extension-resolution.test.ts index 69dc652cf3..7948880498 100644 --- a/gitnexus/test/unit/esm-extension-resolution.test.ts +++ b/gitnexus/test/unit/esm-extension-resolution.test.ts @@ -151,3 +151,76 @@ describe('stripJsExtension', () => { it('returns null for .ts', () => expect(stripJsExtension('foo/bar.ts')).toBeNull()); it('returns null for no extension', () => expect(stripJsExtension('foo/bar')).toBeNull()); }); + +describe('ESM extension resolution — path aliases with .js extensions', () => { + const aliasAtToSrc = new Map([['@/', 'src/']]); + const aliasTildeToSrc = new Map([['~/', 'src/']]); + + function resolveWithAlias( + currentFile: string, + importPath: string, + ctx: ReturnType, + aliases: Map, + baseUrl = '.', + ): string | null { + return resolveImportPath( + currentFile, + importPath, + ctx.allFilesSet, + ctx.files, + ctx.normalized, + ctx.cache, + SupportedLanguages.TypeScript, + { aliases, baseUrl }, + ctx.index, + ); + } + + it('resolves @/utils.js to src/utils.ts via alias', () => { + const ctx = makeCtx(['src/index.ts', 'src/utils.ts']); + const result = resolveWithAlias('src/index.ts', '@/utils.js', ctx, aliasAtToSrc, '.'); + expect(result).toBe('src/utils.ts'); + }); + + it('resolves @/component.jsx to src/component.tsx via alias', () => { + const ctx = makeCtx(['src/index.ts', 'src/component.tsx']); + const result = resolveWithAlias('src/index.ts', '@/component.jsx', ctx, aliasAtToSrc, '.'); + expect(result).toBe('src/component.tsx'); + }); + + it('resolves @/config.mjs to src/config.mts via alias', () => { + const ctx = makeCtx(['src/index.ts', 'src/config.mts']); + const result = resolveWithAlias('src/index.ts', '@/config.mjs', ctx, aliasAtToSrc, '.'); + expect(result).toBe('src/config.mts'); + }); + + it('resolves @/legacy.cjs to src/legacy.cts via alias', () => { + const ctx = makeCtx(['src/index.ts', 'src/legacy.cts']); + const result = resolveWithAlias('src/index.ts', '@/legacy.cjs', ctx, aliasAtToSrc, '.'); + expect(result).toBe('src/legacy.cts'); + }); + + it('prefers actual .js file over TS fallback in alias resolution', () => { + const ctx = makeCtx(['src/index.ts', 'src/utils.js', 'src/utils.ts']); + const result = resolveWithAlias('src/index.ts', '@/utils.js', ctx, aliasAtToSrc, '.'); + expect(result).toBe('src/utils.js'); + }); + + it('resolves alias with baseUrl prefix', () => { + const ctx = makeCtx(['app/src/index.ts', 'app/src/helpers/token.ts']); + const result = resolveWithAlias( + 'app/src/index.ts', + '~/helpers/token.js', + ctx, + aliasTildeToSrc, + 'app', + ); + expect(result).toBe('app/src/helpers/token.ts'); + }); + + it('returns null when alias .js import has no matching source', () => { + const ctx = makeCtx(['src/index.ts']); + const result = resolveWithAlias('src/index.ts', '@/missing.js', ctx, aliasAtToSrc, '.'); + expect(result).toBeNull(); + }); +});