From 881f7514066898703e87522906a939a825178e0b Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 13 Sep 2026 20:53:52 -0500 Subject: [PATCH] Unify TS 7 project module resolution --- internal/compatibility/dynamic-support.json | 4 +- .../generated/node-v24-backlog.json | 72 ++++++---- .../generated/node-v24-internal.json | 79 +++++++---- internal/compatibility/static-support.json | 4 +- .../src/frontend/lowering/lower-modules.ts | 3 +- .../compiler/src/frontend/lowering/lowerer.ts | 53 ++++--- packages/compiler/src/frontend/program.ts | 130 ++++++++++-------- .../compiler/src/frontend/resolve.test.ts | 5 +- packages/compiler/src/frontend/resolve.ts | 75 +++++++++- .../test/ts7/baselines/order-parity.json | 21 +++ .../compiler/test/ts7/program-adapter.test.ts | 69 +++++++++- .../compiler/test/ts7/resolver-parity.test.ts | 40 ++++++ .../2790-dynamic-import-self-name/dep.ts | 3 + .../2790-dynamic-import-self-name/main.ts | 9 ++ .../package.json | 7 + .../corpus/2830-cjs-require-self-name/lib.cjs | 4 + .../2830-cjs-require-self-name/main.cjs | 5 + .../2830-cjs-require-self-name/package.json | 7 + .../fixtures/strictness/project-paths/main.ts | 4 + .../strictness/project-paths/src/exact.ts | 1 + .../strictness/project-paths/src/message.ts | 1 + .../strictness/project-paths/tsconfig.json | 10 ++ tests/harness/project-config.test.ts | 13 ++ 23 files changed, 464 insertions(+), 155 deletions(-) create mode 100644 tests/corpus/2790-dynamic-import-self-name/dep.ts create mode 100644 tests/corpus/2790-dynamic-import-self-name/main.ts create mode 100644 tests/corpus/2790-dynamic-import-self-name/package.json create mode 100644 tests/corpus/2830-cjs-require-self-name/lib.cjs create mode 100644 tests/corpus/2830-cjs-require-self-name/main.cjs create mode 100644 tests/corpus/2830-cjs-require-self-name/package.json create mode 100644 tests/fixtures/strictness/project-paths/main.ts create mode 100644 tests/fixtures/strictness/project-paths/src/exact.ts create mode 100644 tests/fixtures/strictness/project-paths/src/message.ts create mode 100644 tests/fixtures/strictness/project-paths/tsconfig.json diff --git a/internal/compatibility/dynamic-support.json b/internal/compatibility/dynamic-support.json index 8dcc9cecf..cf7a5096c 100644 --- a/internal/compatibility/dynamic-support.json +++ b/internal/compatibility/dynamic-support.json @@ -25,9 +25,9 @@ { "chapter": "modules", "symbols": ["require.resolve", "require.resolve.paths", "extensions", "main", "module.require", "children", "isPreloading", "paths", "Module"], "status": "not-implemented" }, { "chapter": "modules", "symbols": ["node:", "node_modules"], "status": "partial", "evidence": ["tests/fixtures/npm/cases/builtin-shims/main.ts", "tests/fixtures/npm/cases/create-require/main.ts"] }, { "chapter": "esm", "symbols": ["import", "file:", "node:", "await", "url"], "status": "partial", "evidence": ["tests/fixtures/npm/cases/dynamic-import/main.ts", "tests/corpus/2646-top-level-await.ts"] }, - { "chapter": "esm", "signatures": ["`import()` expressions"], "status": "partial", "evidence": ["tests/fixtures/npm/cases/dynamic-import/main.ts", "tests/corpus/2050-dynamic-import-own-module/main.ts"] }, + { "chapter": "esm", "signatures": ["`import()` expressions"], "status": "partial", "evidence": ["tests/fixtures/npm/cases/dynamic-import/main.ts", "tests/corpus/2050-dynamic-import-own-module/main.ts", "tests/corpus/2790-dynamic-import-self-name/main.ts"] }, { "chapter": "esm", "symbols": ["meta", "import.meta.resolve", "dirname", "filename", "main", "data:", "require", "__filename", "require.main", "require.resolve", "NODE_PATH", "require.extensions", "require.cache"], "status": "not-implemented" }, - { "chapter": "packages", "symbols": ["package.json", "\"name\"", "\"main\"", "\"type\"", "\"exports\"", "\"imports\""], "status": "partial", "evidence": ["tests/fixtures/npm/cases/dual-entry/main.ts", "tests/fixtures/npm/cases/self-name/main.ts", "tests/fixtures/npm/cases/scoped-nested/main.ts"] }, + { "chapter": "packages", "symbols": ["package.json", "\"name\"", "\"main\"", "\"type\"", "\"exports\"", "\"imports\""], "status": "partial", "evidence": ["tests/fixtures/npm/cases/dual-entry/main.ts", "tests/fixtures/npm/cases/self-name/main.ts", "tests/fixtures/npm/cases/scoped-nested/main.ts", "tests/corpus/2790-dynamic-import-self-name/main.ts"] }, { "chapter": "packages", "symbols": ["--input-type"], "status": "not-applicable" }, { "chapter": "typescript", "symbols": ["type"], "status": "not-implemented" } ], diff --git a/internal/compatibility/generated/node-v24-backlog.json b/internal/compatibility/generated/node-v24-backlog.json index 779fef995..3c812e048 100644 --- a/internal/compatibility/generated/node-v24-backlog.json +++ b/internal/compatibility/generated/node-v24-backlog.json @@ -59321,7 +59321,8 @@ "tests": [ "tests/corpus/1612-cjs-module-globals.cjs", "tests/corpus/1596-cjs-modules/main.js", - "tests/corpus/1629-require-main-filename.cjs" + "tests/corpus/1629-require-main-filename.cjs", + "tests/corpus/2830-cjs-require-self-name/main.cjs" ] }, "dynamic": { @@ -59361,7 +59362,8 @@ "tests": [ "tests/corpus/1612-cjs-module-globals.cjs", "tests/corpus/1596-cjs-modules/main.js", - "tests/corpus/1629-require-main-filename.cjs" + "tests/corpus/1629-require-main-filename.cjs", + "tests/corpus/2830-cjs-require-self-name/main.cjs" ] }, "dynamic": { @@ -59401,7 +59403,8 @@ "tests": [ "tests/corpus/1612-cjs-module-globals.cjs", "tests/corpus/1596-cjs-modules/main.js", - "tests/corpus/1629-require-main-filename.cjs" + "tests/corpus/1629-require-main-filename.cjs", + "tests/corpus/2830-cjs-require-self-name/main.cjs" ] }, "dynamic": { @@ -59441,7 +59444,8 @@ "tests": [ "tests/corpus/1612-cjs-module-globals.cjs", "tests/corpus/1596-cjs-modules/main.js", - "tests/corpus/1629-require-main-filename.cjs" + "tests/corpus/1629-require-main-filename.cjs", + "tests/corpus/2830-cjs-require-self-name/main.cjs" ] }, "dynamic": { @@ -59517,7 +59521,8 @@ "tests": [ "tests/corpus/1612-cjs-module-globals.cjs", "tests/corpus/1596-cjs-modules/main.js", - "tests/corpus/1629-require-main-filename.cjs" + "tests/corpus/1629-require-main-filename.cjs", + "tests/corpus/2830-cjs-require-self-name/main.cjs" ] }, "dynamic": { @@ -59692,7 +59697,8 @@ "tests": [ "tests/corpus/1612-cjs-module-globals.cjs", "tests/corpus/1596-cjs-modules/main.js", - "tests/corpus/1629-require-main-filename.cjs" + "tests/corpus/1629-require-main-filename.cjs", + "tests/corpus/2830-cjs-require-self-name/main.cjs" ] }, "dynamic": { @@ -59765,7 +59771,8 @@ "tests": [ "tests/corpus/1612-cjs-module-globals.cjs", "tests/corpus/1596-cjs-modules/main.js", - "tests/corpus/1629-require-main-filename.cjs" + "tests/corpus/1629-require-main-filename.cjs", + "tests/corpus/2830-cjs-require-self-name/main.cjs" ] }, "dynamic": { @@ -59835,7 +59842,8 @@ "tests": [ "tests/corpus/1612-cjs-module-globals.cjs", "tests/corpus/1596-cjs-modules/main.js", - "tests/corpus/1629-require-main-filename.cjs" + "tests/corpus/1629-require-main-filename.cjs", + "tests/corpus/2830-cjs-require-self-name/main.cjs" ] }, "dynamic": { @@ -59875,7 +59883,8 @@ "tests": [ "tests/corpus/1612-cjs-module-globals.cjs", "tests/corpus/1596-cjs-modules/main.js", - "tests/corpus/1629-require-main-filename.cjs" + "tests/corpus/1629-require-main-filename.cjs", + "tests/corpus/2830-cjs-require-self-name/main.cjs" ] }, "dynamic": { @@ -59915,7 +59924,8 @@ "tests": [ "tests/corpus/1612-cjs-module-globals.cjs", "tests/corpus/1596-cjs-modules/main.js", - "tests/corpus/1629-require-main-filename.cjs" + "tests/corpus/1629-require-main-filename.cjs", + "tests/corpus/2830-cjs-require-self-name/main.cjs" ] }, "dynamic": { @@ -61939,7 +61949,8 @@ "tests/corpus/2092-package-imports/main.ts", "tests/corpus/2120-package-self-import/main.ts", "tests/corpus/2124-imports-field-wildcard/main.ts", - "tests/corpus/2390-dot-requires/main.cjs" + "tests/corpus/2390-dot-requires/main.cjs", + "tests/corpus/2830-cjs-require-self-name/main.cjs" ] }, "dynamic": { @@ -61951,7 +61962,8 @@ "tests": [ "tests/fixtures/npm/cases/dual-entry/main.ts", "tests/fixtures/npm/cases/self-name/main.ts", - "tests/fixtures/npm/cases/scoped-nested/main.ts" + "tests/fixtures/npm/cases/scoped-nested/main.ts", + "tests/corpus/2790-dynamic-import-self-name/main.ts" ] } } @@ -61976,7 +61988,8 @@ "tests/corpus/2092-package-imports/main.ts", "tests/corpus/2120-package-self-import/main.ts", "tests/corpus/2124-imports-field-wildcard/main.ts", - "tests/corpus/2390-dot-requires/main.cjs" + "tests/corpus/2390-dot-requires/main.cjs", + "tests/corpus/2830-cjs-require-self-name/main.cjs" ] }, "dynamic": { @@ -61988,7 +62001,8 @@ "tests": [ "tests/fixtures/npm/cases/dual-entry/main.ts", "tests/fixtures/npm/cases/self-name/main.ts", - "tests/fixtures/npm/cases/scoped-nested/main.ts" + "tests/fixtures/npm/cases/scoped-nested/main.ts", + "tests/corpus/2790-dynamic-import-self-name/main.ts" ] } } @@ -62013,7 +62027,8 @@ "tests/corpus/2092-package-imports/main.ts", "tests/corpus/2120-package-self-import/main.ts", "tests/corpus/2124-imports-field-wildcard/main.ts", - "tests/corpus/2390-dot-requires/main.cjs" + "tests/corpus/2390-dot-requires/main.cjs", + "tests/corpus/2830-cjs-require-self-name/main.cjs" ] }, "dynamic": { @@ -62025,7 +62040,8 @@ "tests": [ "tests/fixtures/npm/cases/dual-entry/main.ts", "tests/fixtures/npm/cases/self-name/main.ts", - "tests/fixtures/npm/cases/scoped-nested/main.ts" + "tests/fixtures/npm/cases/scoped-nested/main.ts", + "tests/corpus/2790-dynamic-import-self-name/main.ts" ] } } @@ -62050,7 +62066,8 @@ "tests/corpus/2092-package-imports/main.ts", "tests/corpus/2120-package-self-import/main.ts", "tests/corpus/2124-imports-field-wildcard/main.ts", - "tests/corpus/2390-dot-requires/main.cjs" + "tests/corpus/2390-dot-requires/main.cjs", + "tests/corpus/2830-cjs-require-self-name/main.cjs" ] }, "dynamic": { @@ -62062,7 +62079,8 @@ "tests": [ "tests/fixtures/npm/cases/dual-entry/main.ts", "tests/fixtures/npm/cases/self-name/main.ts", - "tests/fixtures/npm/cases/scoped-nested/main.ts" + "tests/fixtures/npm/cases/scoped-nested/main.ts", + "tests/corpus/2790-dynamic-import-self-name/main.ts" ] } } @@ -62087,7 +62105,8 @@ "tests/corpus/2092-package-imports/main.ts", "tests/corpus/2120-package-self-import/main.ts", "tests/corpus/2124-imports-field-wildcard/main.ts", - "tests/corpus/2390-dot-requires/main.cjs" + "tests/corpus/2390-dot-requires/main.cjs", + "tests/corpus/2830-cjs-require-self-name/main.cjs" ] }, "dynamic": { @@ -62099,7 +62118,8 @@ "tests": [ "tests/fixtures/npm/cases/dual-entry/main.ts", "tests/fixtures/npm/cases/self-name/main.ts", - "tests/fixtures/npm/cases/scoped-nested/main.ts" + "tests/fixtures/npm/cases/scoped-nested/main.ts", + "tests/corpus/2790-dynamic-import-self-name/main.ts" ] } } @@ -62124,7 +62144,8 @@ "tests/corpus/2092-package-imports/main.ts", "tests/corpus/2120-package-self-import/main.ts", "tests/corpus/2124-imports-field-wildcard/main.ts", - "tests/corpus/2390-dot-requires/main.cjs" + "tests/corpus/2390-dot-requires/main.cjs", + "tests/corpus/2830-cjs-require-self-name/main.cjs" ] }, "dynamic": { @@ -62136,7 +62157,8 @@ "tests": [ "tests/fixtures/npm/cases/dual-entry/main.ts", "tests/fixtures/npm/cases/self-name/main.ts", - "tests/fixtures/npm/cases/scoped-nested/main.ts" + "tests/fixtures/npm/cases/scoped-nested/main.ts", + "tests/corpus/2790-dynamic-import-self-name/main.ts" ] } } @@ -62161,7 +62183,8 @@ "tests/corpus/2092-package-imports/main.ts", "tests/corpus/2120-package-self-import/main.ts", "tests/corpus/2124-imports-field-wildcard/main.ts", - "tests/corpus/2390-dot-requires/main.cjs" + "tests/corpus/2390-dot-requires/main.cjs", + "tests/corpus/2830-cjs-require-self-name/main.cjs" ] }, "dynamic": { @@ -62173,7 +62196,8 @@ "tests": [ "tests/fixtures/npm/cases/dual-entry/main.ts", "tests/fixtures/npm/cases/self-name/main.ts", - "tests/fixtures/npm/cases/scoped-nested/main.ts" + "tests/fixtures/npm/cases/scoped-nested/main.ts", + "tests/corpus/2790-dynamic-import-self-name/main.ts" ] } } diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index 3478bddf7..6dd9783e1 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -73681,7 +73681,8 @@ "tests/corpus/1596-cjs-modules/main.js", "tests/corpus/1598-cjs-builtin-require/main.js", "tests/corpus/1612-cjs-module-globals.cjs", - "tests/corpus/1629-require-main-filename.cjs" + "tests/corpus/1629-require-main-filename.cjs", + "tests/corpus/2830-cjs-require-self-name/main.cjs" ] }, "dynamic": { @@ -73718,7 +73719,8 @@ "tests": [ "tests/corpus/1612-cjs-module-globals.cjs", "tests/corpus/1596-cjs-modules/main.js", - "tests/corpus/1629-require-main-filename.cjs" + "tests/corpus/1629-require-main-filename.cjs", + "tests/corpus/2830-cjs-require-self-name/main.cjs" ] }, "dynamic": { @@ -73781,7 +73783,8 @@ "tests": [ "tests/corpus/1612-cjs-module-globals.cjs", "tests/corpus/1596-cjs-modules/main.js", - "tests/corpus/1629-require-main-filename.cjs" + "tests/corpus/1629-require-main-filename.cjs", + "tests/corpus/2830-cjs-require-self-name/main.cjs" ] }, "dynamic": { @@ -73817,7 +73820,8 @@ "tests": [ "tests/corpus/1612-cjs-module-globals.cjs", "tests/corpus/1596-cjs-modules/main.js", - "tests/corpus/1629-require-main-filename.cjs" + "tests/corpus/1629-require-main-filename.cjs", + "tests/corpus/2830-cjs-require-self-name/main.cjs" ] }, "dynamic": { @@ -73853,7 +73857,8 @@ "tests": [ "tests/corpus/1612-cjs-module-globals.cjs", "tests/corpus/1596-cjs-modules/main.js", - "tests/corpus/1629-require-main-filename.cjs" + "tests/corpus/1629-require-main-filename.cjs", + "tests/corpus/2830-cjs-require-self-name/main.cjs" ] }, "dynamic": { @@ -73919,7 +73924,8 @@ "tests": [ "tests/corpus/1612-cjs-module-globals.cjs", "tests/corpus/1596-cjs-modules/main.js", - "tests/corpus/1629-require-main-filename.cjs" + "tests/corpus/1629-require-main-filename.cjs", + "tests/corpus/2830-cjs-require-self-name/main.cjs" ] }, "dynamic": { @@ -74067,7 +74073,8 @@ "tests": [ "tests/corpus/1612-cjs-module-globals.cjs", "tests/corpus/1596-cjs-modules/main.js", - "tests/corpus/1629-require-main-filename.cjs" + "tests/corpus/1629-require-main-filename.cjs", + "tests/corpus/2830-cjs-require-self-name/main.cjs" ] }, "dynamic": { @@ -74129,7 +74136,8 @@ "tests": [ "tests/corpus/1612-cjs-module-globals.cjs", "tests/corpus/1596-cjs-modules/main.js", - "tests/corpus/1629-require-main-filename.cjs" + "tests/corpus/1629-require-main-filename.cjs", + "tests/corpus/2830-cjs-require-self-name/main.cjs" ] }, "dynamic": { @@ -74188,7 +74196,8 @@ "tests": [ "tests/corpus/1612-cjs-module-globals.cjs", "tests/corpus/1596-cjs-modules/main.js", - "tests/corpus/1629-require-main-filename.cjs" + "tests/corpus/1629-require-main-filename.cjs", + "tests/corpus/2830-cjs-require-self-name/main.cjs" ] }, "dynamic": { @@ -74224,7 +74233,8 @@ "tests": [ "tests/corpus/1612-cjs-module-globals.cjs", "tests/corpus/1596-cjs-modules/main.js", - "tests/corpus/1629-require-main-filename.cjs" + "tests/corpus/1629-require-main-filename.cjs", + "tests/corpus/2830-cjs-require-self-name/main.cjs" ] }, "dynamic": { @@ -74260,7 +74270,8 @@ "tests": [ "tests/corpus/1612-cjs-module-globals.cjs", "tests/corpus/1596-cjs-modules/main.js", - "tests/corpus/1629-require-main-filename.cjs" + "tests/corpus/1629-require-main-filename.cjs", + "tests/corpus/2830-cjs-require-self-name/main.cjs" ] }, "dynamic": { @@ -77671,13 +77682,15 @@ "tests/corpus/2092-package-imports/main.ts", "tests/corpus/2120-package-self-import/main.ts", "tests/corpus/2124-imports-field-wildcard/main.ts", - "tests/corpus/2390-dot-requires/main.cjs" + "tests/corpus/2390-dot-requires/main.cjs", + "tests/corpus/2830-cjs-require-self-name/main.cjs" ] }, "dynamic": { "status": "partial", "evidence": "derived:descendants", "tests": [ + "tests/corpus/2790-dynamic-import-self-name/main.ts", "tests/fixtures/npm/cases/dual-entry/main.ts", "tests/fixtures/npm/cases/scoped-nested/main.ts", "tests/fixtures/npm/cases/self-name/main.ts" @@ -77819,7 +77832,8 @@ "tests/corpus/2092-package-imports/main.ts", "tests/corpus/2120-package-self-import/main.ts", "tests/corpus/2124-imports-field-wildcard/main.ts", - "tests/corpus/2390-dot-requires/main.cjs" + "tests/corpus/2390-dot-requires/main.cjs", + "tests/corpus/2830-cjs-require-self-name/main.cjs" ] }, "dynamic": { @@ -77828,7 +77842,8 @@ "tests": [ "tests/fixtures/npm/cases/dual-entry/main.ts", "tests/fixtures/npm/cases/self-name/main.ts", - "tests/fixtures/npm/cases/scoped-nested/main.ts" + "tests/fixtures/npm/cases/scoped-nested/main.ts", + "tests/corpus/2790-dynamic-import-self-name/main.ts" ] }, "anchorSource": "exact" @@ -78226,7 +78241,8 @@ "tests/corpus/2092-package-imports/main.ts", "tests/corpus/2120-package-self-import/main.ts", "tests/corpus/2124-imports-field-wildcard/main.ts", - "tests/corpus/2390-dot-requires/main.cjs" + "tests/corpus/2390-dot-requires/main.cjs", + "tests/corpus/2830-cjs-require-self-name/main.cjs" ] }, "dynamic": { @@ -78235,7 +78251,8 @@ "tests": [ "tests/fixtures/npm/cases/dual-entry/main.ts", "tests/fixtures/npm/cases/self-name/main.ts", - "tests/fixtures/npm/cases/scoped-nested/main.ts" + "tests/fixtures/npm/cases/scoped-nested/main.ts", + "tests/corpus/2790-dynamic-import-self-name/main.ts" ] }, "anchorSource": "exact" @@ -78259,7 +78276,8 @@ "tests/corpus/2092-package-imports/main.ts", "tests/corpus/2120-package-self-import/main.ts", "tests/corpus/2124-imports-field-wildcard/main.ts", - "tests/corpus/2390-dot-requires/main.cjs" + "tests/corpus/2390-dot-requires/main.cjs", + "tests/corpus/2830-cjs-require-self-name/main.cjs" ] }, "dynamic": { @@ -78268,7 +78286,8 @@ "tests": [ "tests/fixtures/npm/cases/dual-entry/main.ts", "tests/fixtures/npm/cases/self-name/main.ts", - "tests/fixtures/npm/cases/scoped-nested/main.ts" + "tests/fixtures/npm/cases/scoped-nested/main.ts", + "tests/corpus/2790-dynamic-import-self-name/main.ts" ] }, "anchorSource": "exact" @@ -78292,7 +78311,8 @@ "tests/corpus/2092-package-imports/main.ts", "tests/corpus/2120-package-self-import/main.ts", "tests/corpus/2124-imports-field-wildcard/main.ts", - "tests/corpus/2390-dot-requires/main.cjs" + "tests/corpus/2390-dot-requires/main.cjs", + "tests/corpus/2830-cjs-require-self-name/main.cjs" ] }, "dynamic": { @@ -78301,7 +78321,8 @@ "tests": [ "tests/fixtures/npm/cases/dual-entry/main.ts", "tests/fixtures/npm/cases/self-name/main.ts", - "tests/fixtures/npm/cases/scoped-nested/main.ts" + "tests/fixtures/npm/cases/scoped-nested/main.ts", + "tests/corpus/2790-dynamic-import-self-name/main.ts" ] }, "anchorSource": "exact" @@ -78325,7 +78346,8 @@ "tests/corpus/2092-package-imports/main.ts", "tests/corpus/2120-package-self-import/main.ts", "tests/corpus/2124-imports-field-wildcard/main.ts", - "tests/corpus/2390-dot-requires/main.cjs" + "tests/corpus/2390-dot-requires/main.cjs", + "tests/corpus/2830-cjs-require-self-name/main.cjs" ] }, "dynamic": { @@ -78334,7 +78356,8 @@ "tests": [ "tests/fixtures/npm/cases/dual-entry/main.ts", "tests/fixtures/npm/cases/self-name/main.ts", - "tests/fixtures/npm/cases/scoped-nested/main.ts" + "tests/fixtures/npm/cases/scoped-nested/main.ts", + "tests/corpus/2790-dynamic-import-self-name/main.ts" ] }, "anchorSource": "exact" @@ -78358,7 +78381,8 @@ "tests/corpus/2092-package-imports/main.ts", "tests/corpus/2120-package-self-import/main.ts", "tests/corpus/2124-imports-field-wildcard/main.ts", - "tests/corpus/2390-dot-requires/main.cjs" + "tests/corpus/2390-dot-requires/main.cjs", + "tests/corpus/2830-cjs-require-self-name/main.cjs" ] }, "dynamic": { @@ -78367,7 +78391,8 @@ "tests": [ "tests/fixtures/npm/cases/dual-entry/main.ts", "tests/fixtures/npm/cases/self-name/main.ts", - "tests/fixtures/npm/cases/scoped-nested/main.ts" + "tests/fixtures/npm/cases/scoped-nested/main.ts", + "tests/corpus/2790-dynamic-import-self-name/main.ts" ] }, "anchorSource": "exact" @@ -78391,7 +78416,8 @@ "tests/corpus/2092-package-imports/main.ts", "tests/corpus/2120-package-self-import/main.ts", "tests/corpus/2124-imports-field-wildcard/main.ts", - "tests/corpus/2390-dot-requires/main.cjs" + "tests/corpus/2390-dot-requires/main.cjs", + "tests/corpus/2830-cjs-require-self-name/main.cjs" ] }, "dynamic": { @@ -78400,7 +78426,8 @@ "tests": [ "tests/fixtures/npm/cases/dual-entry/main.ts", "tests/fixtures/npm/cases/self-name/main.ts", - "tests/fixtures/npm/cases/scoped-nested/main.ts" + "tests/fixtures/npm/cases/scoped-nested/main.ts", + "tests/corpus/2790-dynamic-import-self-name/main.ts" ] }, "anchorSource": "exact" diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index baccf0463..8fcd70f24 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -70,13 +70,13 @@ "features": [ { "chapter": "environment_variables", "symbols": ["export"], "status": "not-applicable" }, { "chapter": "globals", "symbols": ["fetch"], "status": "partial", "evidence": ["tests/harness/fetch-conformance.test.ts"] }, - { "chapter": "modules", "symbols": ["__dirname", "__filename", "exports", "require", "module.require", "main", "filename"], "status": "partial", "evidence": ["tests/corpus/1612-cjs-module-globals.cjs", "tests/corpus/1596-cjs-modules/main.js", "tests/corpus/1629-require-main-filename.cjs"] }, + { "chapter": "modules", "symbols": ["__dirname", "__filename", "exports", "require", "module.require", "main", "filename"], "status": "partial", "evidence": ["tests/corpus/1612-cjs-module-globals.cjs", "tests/corpus/1596-cjs-modules/main.js", "tests/corpus/1629-require-main-filename.cjs", "tests/corpus/2830-cjs-require-self-name/main.cjs"] }, { "chapter": "modules", "symbols": ["require.resolve", "require.resolve.paths", "cache", "extensions", "children", "id", "isPreloading", "loaded", "parent", "paths", "Module"], "status": "not-implemented" }, { "chapter": "modules", "symbols": ["node:", "node_modules"], "status": "partial", "evidence": ["tests/corpus/1598-cjs-builtin-require/main.js", "tests/corpus/1596-cjs-modules/main.js"] }, { "chapter": "esm", "symbols": ["import", "file:", "node:", "await"], "status": "partial", "evidence": ["tests/corpus/950-modules-basic/main.ts", "tests/corpus/2646-top-level-await.ts"] }, { "chapter": "esm", "symbols": ["url", "dirname", "filename", "main"], "status": "partial", "evidence": ["tests/corpus/2701-import-meta/main.mjs"] }, { "chapter": "esm", "symbols": ["meta", "import.meta.resolve", "data:", "require", "__filename", "require.main", "require.resolve", "NODE_PATH", "require.extensions", "require.cache"], "status": "not-implemented" }, - { "chapter": "packages", "symbols": ["package.json", "\"name\"", "\"main\"", "\"type\"", "\"exports\"", "\"imports\""], "status": "partial", "evidence": ["tests/corpus/2092-package-imports/main.ts", "tests/corpus/2120-package-self-import/main.ts", "tests/corpus/2124-imports-field-wildcard/main.ts", "tests/corpus/2390-dot-requires/main.cjs"] }, + { "chapter": "packages", "symbols": ["package.json", "\"name\"", "\"main\"", "\"type\"", "\"exports\"", "\"imports\""], "status": "partial", "evidence": ["tests/corpus/2092-package-imports/main.ts", "tests/corpus/2120-package-self-import/main.ts", "tests/corpus/2124-imports-field-wildcard/main.ts", "tests/corpus/2390-dot-requires/main.cjs", "tests/corpus/2830-cjs-require-self-name/main.cjs"] }, { "chapter": "packages", "symbols": ["--input-type"], "status": "not-applicable" }, { "chapter": "typescript", "symbols": ["type"], "status": "not-implemented" } ], diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index 5cb3a40f8..7224007cb 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -67,7 +67,7 @@ export interface FileParts { }); } -/** A source file dynamic `import("")` can host as a COMPILED +/** A source file dynamic `import("")` can host as a COMPILED * module namespace (lowerOwnModuleImport): a non-declaration program file * that is not JSON and not CommonJS-flavored (a CJS namespace is built * from module.exports through Node's lexer — a different surface with no @@ -77,7 +77,6 @@ export interface FileParts { sf: ts.SourceFile, spec: string, ): ts.SourceFile | null { - if (!isRelativeSpecifier(spec) && !spec.startsWith("/")) return null; const dep = resolveImport(program, sf, spec); if (!dep || dep.isDeclarationFile) return null; if (dep.fileName.endsWith(".json") || dep.fileName.endsWith(".cts")) return null; diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 55f728e1d..02e98b20f 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -15,7 +15,6 @@ import { InternalCompilerError } from "../../errors.js"; */ import { resolve } from "node:path"; import { tsgoPath } from "../dts-paths.js"; -import { isRelativeSpecifier } from "../workspace-registry.js"; import * as ts from "../ts7/adapter.js"; import type { ScrDiagnostic } from "../../diagnostics/diagnostic.js"; import { @@ -1893,12 +1892,10 @@ export class Lowerer { const recvDecl = recvDecls.find(ts.isImportClause) ?? recvDecls[0]; if (recvDecl && ts.isVariableDeclaration(recvDecl) && recvDecl.initializer) { const spec = requireSpecOf(recvDecl.initializer); - const dep = - spec === null - ? null - : isRelativeSpecifier(spec) - ? resolveImport(this.program, recvDecl.getSourceFile(), spec) - : npmStaticDepSf7(this.program, recvDecl.getSourceFile(), spec); + const dep = spec === null + ? null + : resolveImport(this.program, recvDecl.getSourceFile(), spec) ?? + npmStaticDepSf7(this.program, recvDecl.getSourceFile(), spec); if (dep) symbol = this.cjsModuleExportSymbol(dep, ident.text); } else if (recvDecl && ts.isImportClause(recvDecl)) { // The DEFAULT-import spelling of the same binding: the dep is @@ -2028,14 +2025,14 @@ export class Lowerer { } /** The source file a checker-resolved module-specifier node names. The - * checker path covers package.json aliases as well as relative imports; - * resolveImport is the fallback for the latter. */ + * checker path is preferred; resolveImport is the canonical fallback for + * every project-module spelling. */ private moduleSourceFileOf(from: ts.SourceFile, spec: ts.StringLiteral): ts.SourceFile | null { const moduleSymbol = this.checker.getSymbolAtLocation(spec); for (const decl of moduleSymbol ? this.checker.declarationsOf(moduleSymbol) : []) { if (ts.isSourceFile(decl)) return decl; } - return isRelativeSpecifier(spec.text) ? resolveImport(this.program, from, spec.text) : null; + return resolveImport(this.program, from, spec.text); } /** Follow one project-module export through the checker's resolved export @@ -2262,22 +2259,19 @@ export class Lowerer { return null; } const spec = importDecl.moduleSpecifier.text; - // Bare specifiers resolve only for opted-in --npm-static packages — - // their CJS entries take the same default-binding interop as a - // relative require; every other bare import answers null and keeps - // its own machinery. - const dep = isRelativeSpecifier(spec) - ? resolveImport(this.program, importDecl.getSourceFile(), spec) - : npmStaticDepSf7(this.program, importDecl.getSourceFile(), spec); + // Project aliases/self-references resolve through the same entry point as + // relative imports; an opted-in --npm-static package is the fallback. + const dep = resolveImport(this.program, importDecl.getSourceFile(), spec) ?? + npmStaticDepSf7(this.program, importDecl.getSourceFile(), spec); if (!dep || !isJsSourceFile(dep) || isNodeEsmFile(dep)) return null; return dep; } /** True when `expr` is an identifier bound by a top-level - * `const x = require("./local")` of a RELATIVE module — the CommonJS - * namespace binding — or of a bare specifier naming an opted-in - * --npm-static package (its CJS entry is a program module, so the - * binding is the same namespace over the same export table), or by a + * `const x = require("./local")` of a project module — relative, + * tsconfig-aliased, or package.json-mediated — or of a bare specifier + * naming an opted-in --npm-static package (its CJS entry is a program + * module, so the binding is the same namespace over the same export table), or by a * DEFAULT import of a CommonJS JS module (`import d from "./lib.cjs"`: * Node binds d to module.exports, the same value require answers). * Member accesses on it resolve through the export table (property @@ -2298,7 +2292,7 @@ export class Lowerer { const spec = requireSpecOf(decl.initializer); if (spec === null) return false; if ( - !isRelativeSpecifier(spec) && + resolveImport(this.program, decl.getSourceFile(), spec) === null && npmStaticDepSf7(this.program, decl.getSourceFile(), spec) === null ) { return false; @@ -2513,16 +2507,15 @@ export class Lowerer { * their external dependencies by name). Null for everything else * (builtins load nothing; the rest kept its preflight fence). */ requireInitStmt(spec: string, node: ts.Node): IrStmt | null { - // Relative requires resolve within the program; a BARE require can be - // a program-module edge too when it names an opted-in --npm-static - // package (one package requiring another — the resolution answered - // its shipped JS, the file is in the module order, and the reads - // alias its globals). Without the guarded %init call at this position + // Project requires resolve within the program; another bare require can + // be a program-module edge when it names an opted-in --npm-static package + // (one package requiring another — the resolution answered its shipped + // JS, the file is in the module order, and the reads alias its globals). + // Without the guarded %init call at this position // those globals stay uninitialized: the dep's module body would never // run. - const dep = isRelativeSpecifier(spec) - ? resolveImport(this.program, node.getSourceFile(), spec) - : npmStaticDepSf7(this.program, node.getSourceFile(), spec); + const dep = resolveImport(this.program, node.getSourceFile(), spec) ?? + npmStaticDepSf7(this.program, node.getSourceFile(), spec); if (!dep || dep.fileName.endsWith(".json")) return null; if (this.asyncInitFiles.has(dep)) { this.unsupported( diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index 5bc47cbb6..c72f36686 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -40,7 +40,7 @@ * that path (no snapshot pins it). */ import { builtinModules } from "node:module"; -import { dirname, resolve } from "node:path"; +import { dirname, isAbsolute, resolve } from "node:path"; import * as ts from "./ts7/adapter.js"; import type { ScrDiagnostic } from "../diagnostics/diagnostic.js"; import { @@ -50,7 +50,7 @@ import { tscPassthroughDiag, unsupportedDiag, } from "../diagnostics/diagnostic.js"; -import { isNodeModulesPath, nearestInvalidPackageJsonPath, nearestPackageType, nearestPkgJsonPath, projectDtsRuntimeSibling, resolveBareModule, resolveProjectImport, resolveRelativeModule, resolveTypeDirective, setProjectRealm } from "./resolve.js"; +import { isNodeModulesPath, nearestInvalidPackageJsonPath, nearestPackageType, nearestPkgJsonPath, projectDtsRuntimeSibling, resolveBareModule, resolveProjectModule, resolveTypeDirective, setProjectPathMappings, setProjectRealm } from "./resolve.js"; import { probeNodeImportRefusal, probeNodeRequireRefusal } from "./npm.js"; import { isNpmStaticPackage, npmStaticActive, npmStaticFsShadow, npmStaticPackageOfPath, reportNpmStaticOffender, setNpmStaticPackages } from "./npm-static.js"; import { provenanceEntryFor, provenancePaths } from "./provenance-registry.js"; @@ -174,6 +174,27 @@ function adoptProjectConfig7( const value = parsed.options[key]; if (value !== undefined) adopted[key] = value; } + // TS 7 accepts `paths`, but no longer accepts `baseUrl` as a compiler + // option. Translate the established baseUrl+paths spelling into absolute + // targets: the synthesized config lives beside the entry rather than the + // real config, so preserving relative targets would silently change their + // meaning. The absolute map is also the own resolver's configuration. + const rawPaths = parsed.options["paths"]; + if (rawPaths !== undefined && typeof rawPaths === "object" && rawPaths !== null) { + const configDir = dirname(configFile); + const rawBaseUrl = parsed.options["baseUrl"]; + const base = typeof rawBaseUrl === "string" + ? (isAbsolute(rawBaseUrl) ? rawBaseUrl : resolve(configDir, rawBaseUrl)) + : configDir; + const paths: Record = {}; + for (const [key, targets] of Object.entries(rawPaths as Record)) { + if (!Array.isArray(targets)) continue; + paths[key] = targets + .filter((target): target is string => typeof target === "string") + .map((target) => isAbsolute(target) ? target : resolve(base, target)); + } + adopted["paths"] = paths; + } const nullChecks = adopted["strictNullChecks"] ?? adopted["strict"] ?? false; if (nullChecks !== true) { diags.push(strictNullChecksFloorDiag(configFile)); @@ -327,6 +348,12 @@ function loadProgram7( externalTypes: ReadonlyMap = new Map(), ): LoadResult & { disposeAll: () => void } { const config = adoptProjectConfig7(host, entryPath); + const configuredPaths = config.options["paths"]; + setProjectPathMappings( + configuredPaths !== undefined && typeof configuredPaths === "object" && configuredPaths !== null + ? configuredPaths as Record + : null, + ); const nodeTypes = config.configFile ? resolveNodeTypes7(entryPath) : null; // skipLibCheck is FORCED with @types/node in the program: checking a // third-party lib's internals against OUR lib choice (es2025, no dyn) is @@ -345,7 +372,10 @@ function loadProgram7( // driver against the package's real TypeScript, not its shipped .d.ts. const provenance = provenancePaths(); if (provenance !== null || externalTypes.size > 0) { - const paths: Record = { ...(provenance ?? {}) }; + const paths: Record = { + ...(configuredPaths as Record | undefined), + ...(provenance ?? {}), + }; for (const [specifier, declarationPath] of externalTypes) { paths[specifier] = [declarationPath]; } @@ -1487,7 +1517,7 @@ export function makeCycleAdmission( * resolveImport) for the lowering: CommonJS require statements lower to * guarded %init calls of exactly the module preflight resolved here. */ function resolveImport7(program: ts.Program, from: ts.SourceFile, specifier: string): ts.SourceFile | null { - const resolved = resolveRelativeModule(from.fileName, specifier); + const resolved = resolveProjectModule(from.fileName, specifier); if (resolved === null) return null; return program.getSourceFile(resolved) ?? null; } @@ -1505,7 +1535,7 @@ function resolveNpmImport7( return null; } // --provenance-sources: a registered specifier is NOT an npm import — - // its attested source compiles as program modules (resolveProjectImport + // its attested source compiles as program modules (resolveProjectModule // answers the entry), so no island embed and no .d.ts type surface. if (provenanceEntryFor(specifier) !== null) return null; const resolved = resolveBareModule(fromFileName, specifier); @@ -2078,9 +2108,15 @@ function preflight7(load: LoadResult): { pos: stmt.getStart(sf), }); }; + // Project modules win before npm fallback: this is the source graph + // tsgo type-checked for tsconfig paths, package imports/self-references, + // and provenance entries. Ordinary packages still answer null here. + const projDep = isBare ? resolveImport7(program, sf, spec) : null; // "#" specifiers can never name an npm package — they are the - // imports-field family, resolved below. - const npm = isBare && !spec.startsWith("#") ? resolveNpmImport7(sf.fileName, spec) : null; + // imports-field family, resolved by the project arm above. + const npm = isBare && projDep === null && !spec.startsWith("#") + ? resolveNpmImport7(sf.fileName, spec) + : null; if (npm && isNodeTypesPath(npm.typesFile)) { diags.push(unsupportedDiag("SC1010", locOf7(stmt), unsupportedModuleFeatureOf(spec))); continue; @@ -2110,19 +2146,13 @@ function preflight7(load: LoadResult): { continue; } } - // PROJECT imports: package.json-mediated specifiers that resolve to - // the program's own sources — `#alias` (the imports field) and - // self-name references (the nearest package.json's name through its - // exports). A source answer is an ordinary user-module edge, exactly - // like a relative import (the checker resolved the bindings the same - // way); a refused resolution is Node's startup crash with Node's - // exact message; what keeps a compile fence: the unsupported - // builtin and the types-only resolution (Node-hostable or ambiguous - // — scriptc's own limitations, named as such). - let projDep: ts.SourceFile | null = null; + // PROJECT imports: tsconfig paths and package.json-mediated specifiers + // that resolve to the program's own sources. A source answer is an + // ordinary user-module edge, exactly like a relative import (the + // checker resolved the bindings the same way); a refused resolution is + // Node's startup crash with Node's exact message; what keeps a compile + // fence is unsupported builtin or types-only surface. if (isBare && npmStaticDep === null) { - const resolved = resolveProjectImport(sf.fileName, spec); - projDep = resolved !== null ? (program.getSourceFile(resolved) ?? null) : null; if (projDep !== null && projDep.isDeclarationFile) { diags.push( unsupportedDiag( @@ -2296,8 +2326,8 @@ function preflight7(load: LoadResult): { } } const isRelative = isRelativeSpecifier(req.spec); - let dep: ts.SourceFile | null = null; - if (!isRelative) { + let dep = resolveImport7(program, sf, req.spec); + if (dep === null && !isRelative) { // --npm-static: a require() of an OPTED-IN package is a // program-module edge exactly like the import-declaration // form above (bundle dists require their workspace siblings — @@ -2324,8 +2354,6 @@ function preflight7(load: LoadResult): { } continue; } - } else { - dep = resolveImport7(program, sf, req.spec); } if (dep && dep.fileName.endsWith(".json")) { diags.push(unsupportedDiag("SC1012", loc, "require() of JSON modules")); @@ -2365,6 +2393,15 @@ function preflight7(load: LoadResult): { if (load.externalTypes.has(spec)) { continue; } + const projectDep = resolveImport7(program, sf, spec); + if (projectDep !== null) { + if (projectDep.fileName.endsWith(".json")) { + diags.push(unsupportedDiag("SC1012", loc, "require() of JSON modules")); + continue; + } + deps.push({ dep: projectDep }); + continue; + } if (!isRelativeSpecifier(spec)) { // --npm-static: opted-in packages ride the program-module edge // (the statement-level require branch above). @@ -2383,12 +2420,6 @@ function preflight7(load: LoadResult): { } continue; } - const dep = resolveImport7(program, sf, spec); - if (dep && dep.fileName.endsWith(".json")) { - diags.push(unsupportedDiag("SC1012", loc, "require() of JSON modules")); - continue; - } - if (dep) deps.push({ dep }); } } } @@ -2533,19 +2564,17 @@ function cjsNamedImportLinkCheck( diags: ScrDiagnostic[], ): StartupCrash | null { const lexMemo = new Map>(); - const isRelative = isRelativeSpecifier; - // Relative specifiers resolve as ever; PROJECT imports (#alias/self-name - // — the same package.json-mediated edges preflight admits) join them so + // Project specifiers resolve as ever; aliases and package.json-mediated + // edges (#alias/self-name) join relative imports so // a named import THROUGH one of a CommonJS module keeps Node's lexer // check. Builtin/npm targets contribute nothing here — except opted-in // --npm-static packages, whose CJS entries face Node's lexer exactly // like program CJS files (their JS IS the program now). const resolveEdge = (from: ts.SourceFile, spec: string): ts.SourceFile | null => { - if (isRelative(spec)) return resolveImport7(program, from, spec); + const project = resolveImport7(program, from, spec); + if (project !== null) return project; const npmStatic = npmStaticDepSf7(program, from, spec); - if (npmStatic !== null) return npmStatic; - const p = resolveProjectImport(from.fileName, spec); - return p !== null ? (program.getSourceFile(p) ?? null) : null; + return npmStatic; }; // Reexport targets union in only when they resolve to CommonJS program // files (Node's cjsPreparseModuleExports rule). @@ -2793,16 +2822,11 @@ export function orderedImportsOf( if (ts.isImportDeclaration(stmt) && erasedTypeOnlyImport(stmt)) continue; if (!stmt.moduleSpecifier || !ts.isStringLiteral(stmt.moduleSpecifier)) continue; const spec = stmt.moduleSpecifier.text; - const isRelative = isRelativeSpecifier(spec); - // Relative edges as ever; PROJECT imports (#alias/self-name — the - // package.json-mediated specifiers preflight admits as user-module - // edges) resolve to the same dep so the importer's %init header calls - // theirs. Builtin and npm bare specifiers stay null (resolveProjectImport - // answers only inside the project) — EXCEPT opted-in --npm-static + // Every project edge (relative, tsconfig alias, #alias, or self-name) + // resolves to the same dep so the importer's %init header calls theirs. + // Builtin and npm bare specifiers stay null — EXCEPT opted-in --npm-static // packages, whose entries are program modules the header must init. - const dep = isRelative - ? resolveImport7(program, sf, spec) - : (npmStaticDepSf7(program, sf, spec) ?? resolveProjectImportSf7(program, sf, spec)); + const dep = resolveImport7(program, sf, spec) ?? npmStaticDepSf7(program, sf, spec); const isJson = dep !== null && dep.fileName.endsWith(".json"); out.push({ stmt, dep: isJson ? null : dep }); } @@ -2823,22 +2847,6 @@ export function npmStaticDepSf7(program: ts.Program, sf: ts.SourceFile, spec: st return program.getSourceFile(npm.typesFile) ?? null; } -/** resolveProjectImport lifted to SourceFile answers: the program's file - * for a `#alias`/self-name resolution, or null (unresolved, or resolved - * outside the program — declaration files included; callers that admit - * d.ts answers probe resolveProjectImport directly). */ -function resolveProjectImportSf7( - program: ts.Program, - sf: ts.SourceFile, - spec: string, -): ts.SourceFile | null { - if (spec.startsWith("node:")) return null; - const p = resolveProjectImport(sf.fileName, spec); - if (p === null) return null; - const dep = program.getSourceFile(p) ?? null; - return dep !== null && !dep.isDeclarationFile ? dep : null; -} - /** True when `expr` is the `module.exports` property access. */ export function isModuleExportsAccess(expr: ts.Expression): expr is ts.PropertyAccessExpression { return ( diff --git a/packages/compiler/src/frontend/resolve.test.ts b/packages/compiler/src/frontend/resolve.test.ts index 52b24c04f..26bf685dd 100644 --- a/packages/compiler/src/frontend/resolve.test.ts +++ b/packages/compiler/src/frontend/resolve.test.ts @@ -2,7 +2,7 @@ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { expect, test } from "vitest"; -import { clearResolveCaches, projectDtsRuntimeSibling, setProjectRealm } from "./resolve.js"; +import { clearResolveCaches, projectDtsRuntimeSibling, resolveProjectModule, setProjectPathMappings, setProjectRealm } from "./resolve.js"; test("resolver reset clears the active project package realm", async () => { const dir = await mkdtemp(join(tmpdir(), "scriptc-resolve-reset-")); @@ -20,9 +20,12 @@ test("resolver reset clears the active project package realm", async () => { clearResolveCaches(); setProjectRealm(join(src, "main.ts")); expect(projectDtsRuntimeSibling(declaration)).toBe(runtime); + setProjectPathMappings({ "@app/value": [join(src, "value")] }); + expect(resolveProjectModule(join(src, "main.ts"), "@app/value")).toBe(runtime); clearResolveCaches(); expect(projectDtsRuntimeSibling(declaration)).toBeNull(); + expect(resolveProjectModule(join(src, "main.ts"), "@app/value")).toBeNull(); } finally { clearResolveCaches(); await rm(dir, { recursive: true, force: true }); diff --git a/packages/compiler/src/frontend/resolve.ts b/packages/compiler/src/frontend/resolve.ts index 7eca7c471..381db68a1 100644 --- a/packages/compiler/src/frontend/resolve.ts +++ b/packages/compiler/src/frontend/resolve.ts @@ -5,10 +5,11 @@ * TypeScript world cross this module, so both lanes may share it. * * The contract is PARITY with typescript@5.9.3's answers under scriptc's - * fixed resolution options (moduleResolution bundler, allowJs, checkJs, - * resolveJsonModule, allowImportingTsExtensions — program.ts's - * COMPILER_OPTIONS): candidate lists below are transcribed from probed - * failedLookupLocations of ts.resolveModuleName / the resolved answers on + * fixed resolution options plus the active project's normalized `paths` + * mappings (moduleResolution bundler, allowJs, checkJs, resolveJsonModule, + * allowImportingTsExtensions — program.ts's COMPILER_OPTIONS): candidate + * lists below are transcribed from probed failedLookupLocations of + * ts.resolveModuleName / the resolved answers on * the fixture tree, and the resolver parity suite (test/ts7/resolver- * parity.test.ts) sweeps every specifier of the whole corpus through BOTH * implementations and requires identical answers. Change 5.9.3's options @@ -18,7 +19,7 @@ import { dirname, isAbsolute, join, resolve } from "node:path"; import { isNpmStaticPackage, npmStaticPackageOfPath, npmStaticTransformPkgJson } from "./npm-static.js"; import { provenanceEntryFor } from "./provenance-registry.js"; import { trackedAccessibleEntries, trackedDirectoryExists, trackedExists, trackedFileExists, trackedReadFile, trackedRealpath } from "./input-tracker.js"; -import { packageNameOfSpecifier } from "./workspace-registry.js"; +import { isRelativeSpecifier, packageNameOfSpecifier } from "./workspace-registry.js"; function isFile(path: string): boolean { return trackedFileExists(path); @@ -268,6 +269,55 @@ export function projectDtsRuntimeSibling(path: string): string | null { return isFile(sibling) ? sibling : null; } +/** Active tsconfig `paths` mappings for the program being compiled. The + * targets are absolute: program.ts resolves them against the real config's + * baseUrl/config directory before handing the same map to tsgo's synthesized + * config and this resolver. Keeping the normalized map here prevents the + * checker and lowering from resolving the same bare project specifier in + * different coordinate systems. */ +let projectPathMappings: Readonly> | null = null; + +export function setProjectPathMappings( + paths: Readonly> | null, +): void { + projectPathMappings = paths; +} + +function resolveViaProjectPaths(specifier: string): string | null { + if (projectPathMappings === null) return null; + let match: { key: string; targets: readonly string[]; prefix: string; suffix: string } | null = null; + for (const [key, targets] of Object.entries(projectPathMappings)) { + const star = key.indexOf("*"); + if (star < 0) { + if (key === specifier) { + match = { key, targets, prefix: key, suffix: "" }; + break; + } + continue; + } + const prefix = key.slice(0, star); + const suffix = key.slice(star + 1); + if ( + specifier.startsWith(prefix) && + specifier.length >= prefix.length + suffix.length && + specifier.endsWith(suffix) && + (match === null || (match.key.includes("*") && prefix.length > match.prefix.length)) + ) { + match = { key, targets, prefix, suffix }; + } + } + if (match === null) return null; + const wildcard = match.key.includes("*") + ? specifier.slice(match.prefix.length, specifier.length - match.suffix.length) + : ""; + for (const target of match.targets) { + const candidate = target.includes("*") ? target.split("*").join(wildcard) : target; + const answer = loadAsFile(candidate) ?? loadAsDirectory(candidate) ?? (isFile(candidate) ? candidate : null); + if (answer !== null) return projectDtsRuntimeSibling(answer) ?? answer; + } + return null; +} + /** Resolves a RELATIVE import specifier from `fromFile` the way 5.9.3's * ts.resolveModuleName does under scriptc's options. Returns the resolved * absolute path (not realpath'd — matching 5.9.3, which keeps in-project @@ -511,6 +561,8 @@ export function resolveProjectImport(fromFile: string, specifier: string): strin // program modules instead of island-embedding its published dist. const provenance = provenanceEntryFor(specifier); if (provenance !== null) return provenance; + const viaPaths = resolveViaProjectPaths(specifier); + if (viaPaths !== null) return viaPaths; const pkgDir = nearestPkgDir(dirname(resolve(fromFile))); if (pkgDir === null) return null; const pkg = pkgJsonOf(pkgDir) as (PkgJson & { imports?: unknown; exports?: unknown; type?: string }) | null; @@ -545,6 +597,18 @@ export function resolveProjectImport(fromFile: string, specifier: string): strin return loadAsFile(path) ?? (isFile(path) ? path : null); } +/** The one resolver entry point for source modules that compile into the + * current program. Relative paths, tsconfig aliases, package imports, package + * self-references, and provenance entries all meet here; builtins and ordinary + * npm packages deliberately answer null for their dedicated callers. */ +export function resolveProjectModule(fromFile: string, specifier: string): string | null { + if (isRelativeSpecifier(specifier) || isAbsolute(specifier)) { + return resolveRelativeModule(fromFile, specifier); + } + if (specifier.startsWith("node:")) return null; + return resolveProjectImport(fromFile, specifier); +} + /* 5.9.3 with allowJs resolves node_modules in TWO FULL PASSES (probed): the * whole node_modules walk-up first admits only TypeScript/declaration * answers (a .js exports target or main maps to its .ts/.d.ts twins, JS @@ -825,6 +889,7 @@ export function clearResolveCaches(): void { pkgJsonCache.clear(); workspaceMembersCache.clear(); projectRealmPkgJson = null; + projectPathMappings = null; } /** True when `path` is under a node_modules directory (the diff --git a/packages/compiler/test/ts7/baselines/order-parity.json b/packages/compiler/test/ts7/baselines/order-parity.json index 730dbd228..bc4403686 100644 --- a/packages/compiler/test/ts7/baselines/order-parity.json +++ b/packages/compiler/test/ts7/baselines/order-parity.json @@ -5857,6 +5857,12 @@ ], "diags": [] }, + "/tests/corpus/2790-dynamic-import-self-name/main.ts": { + "order": [ + "/tests/corpus/2790-dynamic-import-self-name/main.ts" + ], + "diags": [] + }, "/tests/corpus/2791-record-optional-keys.ts": { "order": [ "/tests/corpus/2791-record-optional-keys.ts" @@ -5917,6 +5923,13 @@ ], "diags": [] }, + "/tests/corpus/2830-cjs-require-self-name/main.cjs": { + "order": [ + "/tests/corpus/2830-cjs-require-self-name/lib.cjs", + "/tests/corpus/2830-cjs-require-self-name/main.cjs" + ], + "diags": [] + }, "/tests/corpus/300-if-else.ts": { "order": [ "/tests/corpus/300-if-else.ts" @@ -8604,6 +8617,14 @@ } ] }, + "/tests/fixtures/strictness/project-paths/main.ts": { + "order": [ + "/tests/fixtures/strictness/project-paths/src/exact.ts", + "/tests/fixtures/strictness/project-paths/src/message.ts", + "/tests/fixtures/strictness/project-paths/main.ts" + ], + "diags": [] + }, "/tests/fixtures/node-types/argv-env.ts": { "order": [ "/tests/fixtures/node-types/argv-env.ts" diff --git a/packages/compiler/test/ts7/program-adapter.test.ts b/packages/compiler/test/ts7/program-adapter.test.ts index e3a83007c..e775dd285 100644 --- a/packages/compiler/test/ts7/program-adapter.test.ts +++ b/packages/compiler/test/ts7/program-adapter.test.ts @@ -1,6 +1,6 @@ -import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; -import { join } from "node:path"; +import { basename, join } from "node:path"; import { describe, expect, test } from "vitest"; import { checkPreflight, loadProgram } from "../../src/frontend/program.js"; import { tsgoPath } from "../../src/frontend/dts-paths.js"; @@ -67,3 +67,68 @@ console.log(required.value); rmSync(dir, { recursive: true, force: true }); } }); + +test("project paths resolve identically in the TS 7 program and scriptc module graph", () => { + const tempRoot = process.platform === "win32" ? tmpdir() : "/tmp"; + const dir = mkdtempSync(join(tempRoot, "scriptc-project-paths-")); + const src = join(dir, "src"); + mkdirSync(src); + writeFileSync( + join(dir, "tsconfig.json"), + JSON.stringify({ + compilerOptions: { + strictNullChecks: true, + baseUrl: ".", + paths: { "@app/*": ["src/*"] }, + }, + }), + ); + writeFileSync(join(src, "message.ts"), 'export const message = "paths agree";\n'); + const entry = join(dir, "main.ts"); + writeFileSync(entry, 'import { message } from "@app/message";\nconsole.log(message);\n'); + + const load = loadProgram(entry); + try { + const diagnostics = checkPreflight(load); + expect(diagnostics, diagnostics.map((diag) => `${diag.code}: ${diag.message}`).join("\n")) + .toEqual([]); + expect(load.moduleOrder.map((file) => basename(file.fileName))).toEqual(["message.ts", "main.ts"]); + } finally { + load.dispose(); + rmSync(dir, { recursive: true, force: true }); + } +}); + +test("inherited project paths stay relative to their declaring config", () => { + const tempRoot = process.platform === "win32" ? tmpdir() : "/tmp"; + const dir = mkdtempSync(join(tempRoot, "scriptc-inherited-paths-")); + const base = join(dir, "base"); + const app = join(dir, "app"); + mkdirSync(join(base, "src"), { recursive: true }); + mkdirSync(app); + writeFileSync( + join(base, "tsconfig.json"), + JSON.stringify({ + compilerOptions: { + strictNullChecks: true, + baseUrl: ".", + paths: { "@base/*": ["src/*"] }, + }, + }), + ); + writeFileSync(join(base, "src/message.ts"), 'export const message = "inherited paths agree";\n'); + writeFileSync(join(app, "tsconfig.json"), JSON.stringify({ extends: "../base/tsconfig.json" })); + const entry = join(app, "main.ts"); + writeFileSync(entry, 'import { message } from "@base/message";\nconsole.log(message);\n'); + + const load = loadProgram(entry); + try { + const diagnostics = checkPreflight(load); + expect(diagnostics, diagnostics.map((diag) => `${diag.code}: ${diag.message}`).join("\n")) + .toEqual([]); + expect(load.moduleOrder.map((file) => basename(file.fileName))).toEqual(["message.ts", "main.ts"]); + } finally { + load.dispose(); + rmSync(dir, { recursive: true, force: true }); + } +}); diff --git a/packages/compiler/test/ts7/resolver-parity.test.ts b/packages/compiler/test/ts7/resolver-parity.test.ts index 0f89ce827..13993ebab 100644 --- a/packages/compiler/test/ts7/resolver-parity.test.ts +++ b/packages/compiler/test/ts7/resolver-parity.test.ts @@ -17,8 +17,10 @@ import { projectDtsRuntimeSibling, resolveBareModule, resolveProjectImport, + resolveProjectModule, resolveRelativeModule, resolveTypeDirective, + setProjectPathMappings, } from "../../src/frontend/resolve.js"; import { isRelativeSpecifier } from "../../src/frontend/workspace-registry.js"; import { options5 } from "./harness.js"; @@ -329,6 +331,44 @@ test("synthetic project imports: the imports field and self-name exports", () => expect(failures, failures.join("\n")).toEqual([]); }); +test("synthetic project paths use TypeScript's exact and best-pattern resolution", () => { + const dir = mkdtempSync(join(tmpdir(), "scriptc-resolve-paths-")); + const write = (rel: string, text = "export const x = 1;\n"): void => { + mkdirSync(dirname(join(dir, rel)), { recursive: true }); + writeFileSync(join(dir, rel), text); + }; + write("main.ts"); + write("src/exact.ts"); + write("src/general/value.ts"); + write("src/special/value.ts"); + const configured = { + exact: ["src/exact.ts"], + "@app/*": ["missing/*", "src/general/*"], + "@app/special/*": ["src/special/*"], + }; + setProjectPathMappings( + Object.fromEntries( + Object.entries(configured).map(([key, targets]) => [ + key, + targets.map((target) => join(dir, target)), + ]), + ), + ); + const from = join(dir, "main.ts"); + expect(resolveProjectModule(from, join(dir, "src/exact.ts"))).toBe(join(dir, "src/exact.ts")); + for (const spec of ["exact", "@app/value", "@app/special/value", "@app/missing"]) { + const reference = ts5.resolveModuleName( + spec, + from, + { ...OPTS, baseUrl: dir, paths: configured }, + ts5.sys, + ).resolvedModule; + expect(resolveProjectImport(from, spec), spec).toBe(reference?.resolvedFileName ?? null); + } + clearResolveCaches(); + rmSync(dir, { recursive: true, force: true }); +}); + test("the 'node' type directive resolves identically from every fixture anchor", () => { const anchors = [ join(repoRoot, "tests/fixtures/node-types/argv-env.ts"), diff --git a/tests/corpus/2790-dynamic-import-self-name/dep.ts b/tests/corpus/2790-dynamic-import-self-name/dep.ts new file mode 100644 index 000000000..f3670a35d --- /dev/null +++ b/tests/corpus/2790-dynamic-import-self-name/dep.ts @@ -0,0 +1,3 @@ +console.log("dep evaluated"); + +export const answer = 42; diff --git a/tests/corpus/2790-dynamic-import-self-name/main.ts b/tests/corpus/2790-dynamic-import-self-name/main.ts new file mode 100644 index 000000000..a831fc645 --- /dev/null +++ b/tests/corpus/2790-dynamic-import-self-name/main.ts @@ -0,0 +1,9 @@ +// @dynamic +async function main(): Promise { + console.log("before import"); + const module = await import("scriptc-dynamic-self/dep"); + console.log("answer", module.answer); +} + +main(); +console.log("after call"); diff --git a/tests/corpus/2790-dynamic-import-self-name/package.json b/tests/corpus/2790-dynamic-import-self-name/package.json new file mode 100644 index 000000000..42277b1c9 --- /dev/null +++ b/tests/corpus/2790-dynamic-import-self-name/package.json @@ -0,0 +1,7 @@ +{ + "name": "scriptc-dynamic-self", + "type": "module", + "exports": { + "./dep": "./dep.ts" + } +} diff --git a/tests/corpus/2830-cjs-require-self-name/lib.cjs b/tests/corpus/2830-cjs-require-self-name/lib.cjs new file mode 100644 index 000000000..d0ca8de99 --- /dev/null +++ b/tests/corpus/2830-cjs-require-self-name/lib.cjs @@ -0,0 +1,4 @@ +'use strict'; + +exports.double = (value) => value * 2; +exports.label = 'self'; diff --git a/tests/corpus/2830-cjs-require-self-name/main.cjs b/tests/corpus/2830-cjs-require-self-name/main.cjs new file mode 100644 index 000000000..3eabead9b --- /dev/null +++ b/tests/corpus/2830-cjs-require-self-name/main.cjs @@ -0,0 +1,5 @@ +'use strict'; + +const lib = require('scriptc-cjs-self/lib'); + +console.log(lib.double(21), lib.label); diff --git a/tests/corpus/2830-cjs-require-self-name/package.json b/tests/corpus/2830-cjs-require-self-name/package.json new file mode 100644 index 000000000..a9b34fe7f --- /dev/null +++ b/tests/corpus/2830-cjs-require-self-name/package.json @@ -0,0 +1,7 @@ +{ + "name": "scriptc-cjs-self", + "type": "commonjs", + "exports": { + "./lib": "./lib.cjs" + } +} diff --git a/tests/fixtures/strictness/project-paths/main.ts b/tests/fixtures/strictness/project-paths/main.ts new file mode 100644 index 000000000..20f05a487 --- /dev/null +++ b/tests/fixtures/strictness/project-paths/main.ts @@ -0,0 +1,4 @@ +import { exact } from "@exact"; +import { message } from "@app/message"; + +console.log(message, exact); diff --git a/tests/fixtures/strictness/project-paths/src/exact.ts b/tests/fixtures/strictness/project-paths/src/exact.ts new file mode 100644 index 000000000..ed7265696 --- /dev/null +++ b/tests/fixtures/strictness/project-paths/src/exact.ts @@ -0,0 +1 @@ +export const exact = 42; diff --git a/tests/fixtures/strictness/project-paths/src/message.ts b/tests/fixtures/strictness/project-paths/src/message.ts new file mode 100644 index 000000000..5ab3c2101 --- /dev/null +++ b/tests/fixtures/strictness/project-paths/src/message.ts @@ -0,0 +1 @@ +export const message = "paths agree"; diff --git a/tests/fixtures/strictness/project-paths/tsconfig.json b/tests/fixtures/strictness/project-paths/tsconfig.json new file mode 100644 index 000000000..166e1d6eb --- /dev/null +++ b/tests/fixtures/strictness/project-paths/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "strict": true, + "baseUrl": ".", + "paths": { + "@app/*": ["missing/*", "src/*"], + "@exact": ["src/exact.ts"] + } + } +} diff --git a/tests/harness/project-config.test.ts b/tests/harness/project-config.test.ts index 4604c6747..921b57b34 100644 --- a/tests/harness/project-config.test.ts +++ b/tests/harness/project-config.test.ts @@ -58,6 +58,19 @@ test("indexed-strict: the project's EXTRA strictness is honored — preflight fa expect(result.diagnostics.some((d) => d.message.includes("possibly 'undefined'"))).toBe(true); }); +test("project paths: TS 7 checking and native module lowering share the configured aliases", async () => { + const outDir = outDirFor("project-paths"); + const result = await compile(join(fixture("project-paths"), "main.ts"), { + outPath: join(outDir, "main"), + outDir, + sanitize, + }); + expect(result.ok, !result.ok ? JSON.stringify(result.diagnostics, null, 2) : "").toBe(true); + if (!result.ok) return; + const { stdout } = await execFileAsync(result.binaryPath); + expect(stdout).toBe("paths agree 42\n"); +}); + test("node-types: the supported process surface lowers statically under @types/node", async () => { const outDir = outDirFor("node-types"); const result = await compile(join(nodeTypesDir, "argv-env.ts"), {