From 6adef6060039deaa16d4409ebfc4187bbc003e46 Mon Sep 17 00:00:00 2001 From: carlos-alm Date: Tue, 18 Aug 2026 01:28:12 -0600 Subject: [PATCH 1/2] fix(resolve): probe .mts/.cts extensions for extension-less imports Discovered while fixing #2299 (the .mjs->.mts/.cjs->.cts emitted-extension remap). A separate, distinct code path -- resolving an extension-less relative specifier like `import './foo'` by probing a fixed list of candidate extensions -- never tried .mts/.cts in either engine, even though both engines otherwise recognize and parse .mts/.cts files (#2073). Adds .mts/.cts to the extension-probing list in resolveImportPathJS (src/domain/graph/resolve.ts) and probe_known_extensions's EXTENSIONS constant (crates/codegraph-core/src/domain/graph/resolve.rs), keeping the two lists in sync per the dual-engine parity requirement. Placed alongside .ts/.tsx to keep TypeScript-family extensions grouped, matching the existing ordering convention. docs check acknowledged -- bug fix only, no new language support, feature, or architectural change to document. Impact: 1 functions changed, 19 affected --- .../src/domain/graph/resolve.rs | 48 +++++++++++++++++++ src/domain/graph/resolve.ts | 2 + tests/unit/resolve.test.ts | 15 ++++++ 3 files changed, 65 insertions(+) diff --git a/crates/codegraph-core/src/domain/graph/resolve.rs b/crates/codegraph-core/src/domain/graph/resolve.rs index 93a6a99fe..38707b4ce 100644 --- a/crates/codegraph-core/src/domain/graph/resolve.rs +++ b/crates/codegraph-core/src/domain/graph/resolve.rs @@ -1503,6 +1503,8 @@ fn probe_known_extensions( const EXTENSIONS: &[&str] = &[ ".ts", ".tsx", + ".mts", + ".cts", ".js", ".jsx", ".mjs", @@ -1950,6 +1952,52 @@ mod tests { assert_eq!(result, "src/legacy.cts"); } + #[test] + fn probe_known_extensions_resolves_an_extension_less_specifier_to_mts() { + // #2464: distinct from resolve_mjs_to_mts_remap_with_known_files above + // — `./utils` here carries no extension at all, unlike `./utils.mjs`, + // so it exercises probe_known_extensions's EXTENSIONS list directly + // instead of EMIT_EXTENSION_REMAPS. + let mut known = HashSet::new(); + known.insert("src/utils.mts".to_string()); + + let aliases = PathAliases { + base_url: None, + paths: vec![], + }; + + let result = resolve_import_path_inner( + "/project/src/index.mts", + "./utils", + "/project", + &aliases, + Some(&known), + None, + ); + assert_eq!(result, "src/utils.mts"); + } + + #[test] + fn probe_known_extensions_resolves_an_extension_less_specifier_to_cts() { + let mut known = HashSet::new(); + known.insert("src/legacy.cts".to_string()); + + let aliases = PathAliases { + base_url: None, + paths: vec![], + }; + + let result = resolve_import_path_inner( + "/project/src/index.cts", + "./legacy", + "/project", + &aliases, + Some(&known), + None, + ); + assert_eq!(result, "src/legacy.cts"); + } + #[test] fn probe_js_to_ts_remap_does_not_corrupt_a_path_with_an_earlier_js_substring() { // Regression guard: the previous implementation used diff --git a/src/domain/graph/resolve.ts b/src/domain/graph/resolve.ts index 66d45fa1a..c919e38ee 100644 --- a/src/domain/graph/resolve.ts +++ b/src/domain/graph/resolve.ts @@ -1259,6 +1259,8 @@ function resolveImportPathJS( for (const ext of [ '.ts', '.tsx', + '.mts', + '.cts', '.js', '.jsx', '.mjs', diff --git a/tests/unit/resolve.test.ts b/tests/unit/resolve.test.ts index 8935eb68c..db0ae6cd2 100644 --- a/tests/unit/resolve.test.ts +++ b/tests/unit/resolve.test.ts @@ -112,6 +112,21 @@ describe('resolveImportPathJS', () => { expect(result).toMatch(/legacy\.cts$/); }); + it('resolves an extension-less specifier to a .mts file (#2464)', () => { + // Distinct from the #2299 remap tests above: `./util` here carries no + // extension at all, unlike `./util.mjs`, so it exercises the + // extension-probing loop directly instead of EMIT_EXTENSION_REMAPS. + const fromFile = path.join(tmpDir, 'src', 'esm', 'index.mts'); + const result = resolveImportPathJS(fromFile, './util', tmpDir, null); + expect(result).toMatch(/util\.mts$/); + }); + + it('resolves an extension-less specifier to a .cts file (#2464)', () => { + const fromFile = path.join(tmpDir, 'src', 'cjs', 'index.cts'); + const result = resolveImportPathJS(fromFile, './legacy', tmpDir, null); + expect(result).toMatch(/legacy\.cts$/); + }); + it('resolves directory to index.js', () => { const fromFile = path.join(tmpDir, 'src', 'index.js'); const result = resolveImportPathJS(fromFile, './lib', tmpDir, null); From f06c9d72d286f6f64b14ea8caff587c68cd6a983 Mon Sep 17 00:00:00 2001 From: carlos-alm Date: Tue, 18 Aug 2026 01:45:22 -0600 Subject: [PATCH 2/2] fix(resolve): also probe /index.mts and /index.cts for directory imports Follow-up to Greptile review on this same PR: the direct .mts/.cts candidates alone don't cover the directory-index convention (import './dir' -> dir/index.mts), which every other extension already in this list (.ts, .tsx, .js) supports via its own /index. entry. Adds /index.mts and /index.cts to both engines' extension-probing lists, alongside the existing /index.ts and /index.tsx entries. docs check acknowledged -- bug fix only, no new language support, feature, or architectural change to document. Impact: 1 functions changed, 19 affected --- .../src/domain/graph/resolve.rs | 47 +++++++++++++++++++ src/domain/graph/resolve.ts | 2 + tests/unit/resolve.test.ts | 24 ++++++++++ 3 files changed, 73 insertions(+) diff --git a/crates/codegraph-core/src/domain/graph/resolve.rs b/crates/codegraph-core/src/domain/graph/resolve.rs index 38707b4ce..4253fef6c 100644 --- a/crates/codegraph-core/src/domain/graph/resolve.rs +++ b/crates/codegraph-core/src/domain/graph/resolve.rs @@ -1512,6 +1512,8 @@ fn probe_known_extensions( ".pyi", "/index.ts", "/index.tsx", + "/index.mts", + "/index.cts", "/index.js", "/__init__.py", ]; @@ -1998,6 +2000,51 @@ mod tests { assert_eq!(result, "src/legacy.cts"); } + #[test] + fn probe_known_extensions_resolves_a_directory_specifier_to_its_index_mts() { + // Greptile follow-up on #2464: the direct .mts/.cts candidates alone + // don't cover the directory-index convention (`./dir` -> `dir/index.mts`), + // which every other extension in this list already supports. + let mut known = HashSet::new(); + known.insert("src/esm-dir/index.mts".to_string()); + + let aliases = PathAliases { + base_url: None, + paths: vec![], + }; + + let result = resolve_import_path_inner( + "/project/src/index.mts", + "./esm-dir", + "/project", + &aliases, + Some(&known), + None, + ); + assert_eq!(result, "src/esm-dir/index.mts"); + } + + #[test] + fn probe_known_extensions_resolves_a_directory_specifier_to_its_index_cts() { + let mut known = HashSet::new(); + known.insert("src/cjs-dir/index.cts".to_string()); + + let aliases = PathAliases { + base_url: None, + paths: vec![], + }; + + let result = resolve_import_path_inner( + "/project/src/index.cts", + "./cjs-dir", + "/project", + &aliases, + Some(&known), + None, + ); + assert_eq!(result, "src/cjs-dir/index.cts"); + } + #[test] fn probe_js_to_ts_remap_does_not_corrupt_a_path_with_an_earlier_js_substring() { // Regression guard: the previous implementation used diff --git a/src/domain/graph/resolve.ts b/src/domain/graph/resolve.ts index c919e38ee..168a56139 100644 --- a/src/domain/graph/resolve.ts +++ b/src/domain/graph/resolve.ts @@ -1268,6 +1268,8 @@ function resolveImportPathJS( '.pyi', '/index.ts', '/index.tsx', + '/index.mts', + '/index.cts', '/index.js', '/__init__.py', ]) { diff --git a/tests/unit/resolve.test.ts b/tests/unit/resolve.test.ts index db0ae6cd2..d79799692 100644 --- a/tests/unit/resolve.test.ts +++ b/tests/unit/resolve.test.ts @@ -46,9 +46,13 @@ beforeAll(() => { // shared/core.ts (for alias resolution) // src/esm/util.mts (for .mjs -> .mts remap, #2299) // src/cjs/legacy.cts (for .cjs -> .cts remap, #2299) + // src/esm-dir/index.mts (for /index.mts directory resolution, #2464) + // src/cjs-dir/index.cts (for /index.cts directory resolution, #2464) fs.mkdirSync(path.join(tmpDir, 'src', 'lib'), { recursive: true }); fs.mkdirSync(path.join(tmpDir, 'src', 'esm'), { recursive: true }); fs.mkdirSync(path.join(tmpDir, 'src', 'cjs'), { recursive: true }); + fs.mkdirSync(path.join(tmpDir, 'src', 'esm-dir'), { recursive: true }); + fs.mkdirSync(path.join(tmpDir, 'src', 'cjs-dir'), { recursive: true }); fs.mkdirSync(path.join(tmpDir, 'shared'), { recursive: true }); fs.writeFileSync(path.join(tmpDir, 'src', 'math.js'), 'export const add = (a, b) => a + b;'); @@ -65,6 +69,11 @@ beforeAll(() => { fs.writeFileSync(path.join(tmpDir, 'shared', 'core.ts'), 'export const x = 1;'); fs.writeFileSync(path.join(tmpDir, 'src', 'esm', 'util.mts'), 'export const greet = () => "hi";'); fs.writeFileSync(path.join(tmpDir, 'src', 'cjs', 'legacy.cts'), 'export const x = 1;'); + fs.writeFileSync( + path.join(tmpDir, 'src', 'esm-dir', 'index.mts'), + 'export const greet = () => "hi";', + ); + fs.writeFileSync(path.join(tmpDir, 'src', 'cjs-dir', 'index.cts'), 'export const x = 1;'); }); afterAll(() => { @@ -127,6 +136,21 @@ describe('resolveImportPathJS', () => { expect(result).toMatch(/legacy\.cts$/); }); + it('resolves a directory specifier to its index.mts file (#2464, Greptile follow-up)', () => { + // Caught by review: the direct .mts/.cts candidates alone don't cover + // the directory-index convention (`import './dir'` -> `dir/index.mts`), + // which every other extension in this list already supports. + const fromFile = path.join(tmpDir, 'src', 'index.mts'); + const result = resolveImportPathJS(fromFile, './esm-dir', tmpDir, null); + expect(result).toMatch(/esm-dir\/index\.mts$/); + }); + + it('resolves a directory specifier to its index.cts file (#2464, Greptile follow-up)', () => { + const fromFile = path.join(tmpDir, 'src', 'index.cts'); + const result = resolveImportPathJS(fromFile, './cjs-dir', tmpDir, null); + expect(result).toMatch(/cjs-dir\/index\.cts$/); + }); + it('resolves directory to index.js', () => { const fromFile = path.join(tmpDir, 'src', 'index.js'); const result = resolveImportPathJS(fromFile, './lib', tmpDir, null);