Fix: support array form of tsconfig extends (TypeScript 5.0) (#1017)

TS 5.0 allows `"extends": ["./a", "./b"]`. _read_tsconfig_aliases called
`extends.startswith("@")` directly, so an array raised
`AttributeError: 'list' object has no attribute 'startswith'`. _safe_extract
caught it and skipped the WHOLE file — and since alias resolution fires on any
path-aliased import, every TS file using `@/`-style imports in a repo with
array-extends tsconfig was dropped, yielding an empty graph.

Normalize `extends` to a list (str -> [str], list kept, else []), process
entries in order (later overrides earlier, current config's paths override all
parents per TS semantics), and skip scoped npm configs (`@...`) per entry as
before.

Adds regression test test_tsconfig_array_extends_alias_resolves_existing_ts_file.
This commit is contained in:
msotnikov
2026-05-26 11:17:14 +03:00
committed by GitHub
parent 4e80d8621f
commit 336652779b
2 changed files with 43 additions and 2 deletions
+17 -2
View File
@@ -190,9 +190,24 @@ def _read_tsconfig_aliases(tsconfig: Path, base_dir: Path, seen: set) -> dict[st
return {}
aliases: dict[str, str] = {}
# `extends` may be a string or, since TypeScript 5.0, an array of paths.
# For an array, parents are processed in order with later entries
# overriding earlier ones; the extending config (paths below) overrides
# all parents. Without the list branch, an array `extends` raised
# `AttributeError: 'list' object has no attribute 'startswith'`, which
# _safe_extract turned into a skip of the whole file.
extends = data.get("extends")
if extends and not extends.startswith("@"):
extended_path = (base_dir / extends).resolve()
if isinstance(extends, str):
extends_list = [extends]
elif isinstance(extends, list):
extends_list = [e for e in extends if isinstance(e, str)]
else:
extends_list = []
for ext in extends_list:
# Skip scoped npm package configs (e.g. @tsconfig/svelte) — not on disk.
if not ext or ext.startswith("@"):
continue
extended_path = (base_dir / ext).resolve()
if not extended_path.suffix:
extended_path = extended_path.with_suffix(".json")
if extended_path.exists():
+26
View File
@@ -334,6 +334,32 @@ def test_tsconfig_alias_import_resolves_existing_ts_file(tmp_path: Path):
assert _has_edge(result, "src/routes/page.ts", "src/lib/types/type-helpers.ts")
def test_tsconfig_array_extends_alias_resolves_existing_ts_file(tmp_path: Path):
# TypeScript 5.0 allows `extends` as an array; later entries override
# earlier ones. The `paths` alias is inherited from the second parent.
# Regression: an array `extends` previously raised
# `AttributeError: 'list' object has no attribute 'startswith'`, which
# _safe_extract turned into a skip of every file using the alias.
_write(tmp_path / "tsconfig.base.json", json.dumps({"compilerOptions": {"strict": True}}))
_write(
tmp_path / "tsconfig.paths.json",
json.dumps({"compilerOptions": {"baseUrl": ".", "paths": {"$lib/*": ["src/lib/*"]}}}),
)
_write(
tmp_path / "tsconfig.json",
json.dumps({"extends": ["./tsconfig.base.json", "./tsconfig.paths.json"]}),
)
target = _write(tmp_path / "src/lib/types/type-helpers.ts", "export type Helper = string\n")
importer = _write(
tmp_path / "src/routes/page.ts",
"import type { Helper } from '$lib/types/type-helpers'\nconst value: Helper = 'x'\n",
)
result = _extract_for([target, importer], tmp_path)
assert _has_edge(result, "src/routes/page.ts", "src/lib/types/type-helpers.ts")
def test_pnpm_workspace_package_import_resolves_package_entry(tmp_path: Path):
_write(
tmp_path / "pnpm-workspace.yaml",