diff --git a/graphify/extract.py b/graphify/extract.py index 3005657..96eed61 100644 --- a/graphify/extract.py +++ b/graphify/extract.py @@ -225,15 +225,6 @@ def _resolve_js_module_path(p: Path) -> Path: """ if p.is_file(): return p - # Directory imports must be handled before any suffix logic, otherwise - # `from './queue'` (where queue/ is a real directory) would short-circuit - # on .is_file() = False and never reach the index lookup. - if p.is_dir(): - for idx in _JS_INDEX_FILES: - c = p / idx - if c.is_file(): - return c - return p # TS ESM convention: import path written with .js but the real file is .ts. # Apply BEFORE the generic append loop so we don't accidentally match # foo.js → foo.js.ts when the real file is foo.ts. @@ -245,17 +236,24 @@ def _resolve_js_module_path(p: Path) -> Path: c = p.with_suffix(".tsx") if c.is_file(): return c - # Try appending extensions to the FULL filename. Covers bare paths, - # multi-dot helper files, Svelte 5 rune files, config files, etc. + # Try appending extensions to the FULL filename BEFORE checking for a + # directory import. Both TypeScript and Vite resolvers prefer a file + # match over a directory match — projects routinely have a `foo.ts` + # file living alongside a `foo/` directory of sub-modules (e.g. + # `auth.ts` next to `auth/`). If we checked the directory first, those + # file imports would silently lose to a directory with no `index.*`. for ext in _JS_RESOLVE_EXTS: c = p.parent / (p.name + ext) if c.is_file(): return c - # Treat as a not-yet-existing directory import: .//index.{ts,…} - for idx in _JS_INDEX_FILES: - c = p / idx - if c.is_file(): - return c + # Directory imports: try .//index.{ts,tsx,js,jsx}. Reached only + # after every file-extension candidate has been ruled out, matching the + # resolver fallback chain. + if p.is_dir(): + for idx in _JS_INDEX_FILES: + c = p / idx + if c.is_file(): + return c return p diff --git a/tests/test_import_extension_resolution.py b/tests/test_import_extension_resolution.py index c57122e..39051c9 100644 --- a/tests/test_import_extension_resolution.py +++ b/tests/test_import_extension_resolution.py @@ -63,6 +63,20 @@ def test_resolve_prefers_ts_over_svelte_when_both_exist(tmp_path): assert _resolve_js_module_path(bare) == ts_target +def test_resolve_file_wins_over_sibling_directory(tmp_path): + """Real-world repro: a project has both `auth.ts` (file) and `auth/` + (directory of sub-modules) at the same path. Both TypeScript and Vite + prefer the file match. If the resolver checks the directory first and + falls back on a missing index, every `from './auth'` import silently + drops because the directory has no index.{ts,…}.""" + file_target = _write(tmp_path / "auth.ts", "export const x = 1") + sibling_dir = tmp_path / "auth" + sibling_dir.mkdir() + _write(sibling_dir / "helpers.ts", "export const y = 2") + bare = tmp_path / "auth" + assert _resolve_js_module_path(bare) == file_target + + def test_resolve_directory_to_index_ts(tmp_path): pkg = tmp_path / "queue" target = _write(pkg / "index.ts", "export const x = 1")