microsoft / microsoft/TypeScript

Module resolution: tsc unexpectedly rewrites the import path when exports field contains multiple entries resolving to the same file

Offen
#56,290 4 Kommentare 0 Reaktionen 0 zugewiesene Personen Auf GitHub ansehen

Dieses Issue hat noch niemand übernommen.

Needs Proposal Suggestion
Vorherrschende Sprache
Go
Sterne
111k
Forks
14.4k
Ø Merge
1 T. 19 Std.
Gemergte PRs (30 T.)
117

Beschreibung

Demo Repo

https://github.com/sodatea/ts-exports-rewrite

Which of the following problems are you reporting?

Something else more complicated which I'll explain in more detail

Demonstrate the defect described above with a code sample.

In the reproduction repo, the parse.ts file indirectly depends on the lru-cache module's type by importing the cache.ts file:

import { createCache } from './cache'
export const parseCache = createCache<{}>()

After compilation, in parse.d.ts, we would normally expect a import('lru-cache').LRUCache expression.
However, it actually becomes import("lru-cache/min").LRUCache.
On the other hand, the compiled cache.d.ts file still imports from lru-cache, not lru-cache/min.

This seems to be related to lru-cache@10.0.1's exports field: https://unpkg.com/browse/lru-cache@10.0.1/package.json#L34
It contains 2 entries, ./min and ., both pointing to the same file. TypeScript picks the first entry.

If I modify the exports field to make . appear before ./min, then the compiled declaration file would use import("lru-cache") as expected.

While this isn't a very severe bug and has easy workarounds, this is quite unexpected.
Because lru-cache/min is an entry only available in exports field, this compilation result breaks the compatibility with older build tools. And it is quite hard to debug, because no documentation ever mentioned that the order of keys in the exports field would make a difference.

Run tsc --showConfig and paste its output here
{
    "compilerOptions": {
        "outDir": "./temp",
        "target": "esnext",
        "module": "esnext",
        "moduleResolution": "bundler",
        "esModuleInterop": true,
        "declaration": true,
        "emitDeclarationOnly": true,
        "types": []
    },
    "files": [
        "./cache.ts",
        "./parse.ts"
    ],
    "include": [
        "cache.ts",
        "parse.ts"
    ],
    "exclude": [
        "temp"
    ]
}
Run tsc --traceResolution and paste its output here
======== Resolving module 'lru-cache' from '/Users/haoqun/Reproductions/ts-exports-rewrite/cache.ts'. ========
Explicitly specified module resolution kind: 'Bundler'.
Resolving in CJS mode with conditions 'import', 'types'.
Found 'package.json' at '/Users/haoqun/Reproductions/ts-exports-rewrite/package.json'.
Loading module 'lru-cache' from 'node_modules' folder, target file types: TypeScript, JavaScript, Declaration, JSON.
Searching all ancestor node_modules directories for preferred extensions: TypeScript, Declaration.
Found 'package.json' at '/Users/haoqun/Reproductions/ts-exports-rewrite/node_modules/lru-cache/package.json'.
Entering conditional exports.
Matched 'exports' condition 'import'.
Entering conditional exports.
Matched 'exports' condition 'types'.
Using 'exports' subpath '.' with target './dist/mjs/index.d.ts'.
File '/Users/haoqun/Reproductions/ts-exports-rewrite/node_modules/lru-cache/dist/mjs/index.d.ts' exists - use it as a name resolution result.
Resolved under condition 'types'.
Exiting conditional exports.
Resolved under condition 'import'.
Exiting conditional exports.
Resolving real path for '/Users/haoqun/Reproductions/ts-exports-rewrite/node_modules/lru-cache/dist/mjs/index.d.ts', result '/Users/haoqun/Reproductions/ts-exports-rewrite/node_modules/lru-cache/dist/mjs/index.d.ts'.
======== Module name 'lru-cache' was successfully resolved to '/Users/haoqun/Reproductions/ts-exports-rewrite/node_modules/lru-cache/dist/mjs/index.d.ts' with Package ID 'lru-cache/dist/mjs/index.d.ts@10.0.1'. ========
======== Resolving module './cache' from '/Users/haoqun/Reproductions/ts-exports-rewrite/parse.ts'. ========
Explicitly specified module resolution kind: 'Bundler'.
Resolving in CJS mode with conditions 'import', 'types'.
Loading module as file / folder, candidate module location '/Users/haoqun/Reproductions/ts-exports-rewrite/cache', target file types: TypeScript, JavaScript, Declaration, JSON.
File '/Users/haoqun/Reproductions/ts-exports-rewrite/cache.ts' exists - use it as a name resolution result.
======== Module name './cache' was successfully resolved to '/Users/haoqun/Reproductions/ts-exports-rewrite/cache.ts'. ========
Paste the package.json of the importing module, if it exists
{
  "name": "ts-exports-rewrite",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "tsc"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "lru-cache": "10.0.1",
    "typescript": "^5.2.2"
  }
}
Paste the package.json of the target module, if it exists
{
  "name": "lru-cache",
  "description": "A cache object that deletes the least-recently-used items.",
  "version": "10.0.1",
  "author": "Isaac Z. Schlueter <i@izs.me>",
  "keywords": [
    "mru",
    "lru",
    "cache"
  ],
  "sideEffects": false,
  "scripts": {
    "build": "npm run prepare",
    "preprepare": "rm -rf dist",
    "prepare": "tsc -p tsconfig.json && tsc -p tsconfig-esm.json",
    "postprepare": "bash fixup.sh",
    "pretest": "npm run prepare",
    "presnap": "npm run prepare",
    "test": "c8 tap",
    "snap": "c8 tap",
    "preversion": "npm test",
    "postversion": "npm publish",
    "prepublishOnly": "git push origin --follow-tags",
    "format": "prettier --write .",
    "typedoc": "typedoc --tsconfig tsconfig-esm.json ./src/*.ts",
    "benchmark-results-typedoc": "bash scripts/benchmark-results-typedoc.sh",
    "prebenchmark": "npm run prepare",
    "benchmark": "make -C benchmark",
    "preprofile": "npm run prepare",
    "profile": "make -C benchmark profile"
  },
  "main": "./dist/cjs/index.js",
  "module": "./dist/mjs/index.js",
  "exports": {
    "./min": {
      "import": {
        "types": "./dist/mjs/index.d.ts",
        "default": "./dist/mjs/index.min.js"
      },
      "require": {
        "types": "./dist/cjs/index.d.ts",
        "default": "./dist/cjs/index.min.js"
      }
    },
    ".": {
      "import": {
        "types": "./dist/mjs/index.d.ts",
        "default": "./dist/mjs/index.js"
      },
      "require": {
        "types": "./dist/cjs/index.d.ts",
        "default": "./dist/cjs/index.js"
      }
    }
  },
  "repository": "git://github.com/isaacs/node-lru-cache.git",
  "devDependencies": {
    "@size-limit/preset-small-lib": "^7.0.8",
    "@types/node": "^20.2.5",
    "@types/tap": "^15.0.6",
    "benchmark": "^2.1.4",
    "c8": "^7.11.2",
    "clock-mock": "^1.0.6",
    "esbuild": "^0.17.11",
    "eslint-config-prettier": "^8.5.0",
    "marked": "^4.2.12",
    "mkdirp": "^2.1.5",
    "prettier": "^2.6.2",
    "size-limit": "^7.0.8",
    "tap": "^16.3.4",
    "ts-node": "^10.9.1",
    "tslib": "^2.4.0",
    "typedoc": "^0.24.6",
    "typescript": "^5.0.4"
  },
  "license": "ISC",
  "files": [
    "dist"
  ],
  "engines": {
    "node": "14 || >=16.14"
  },
  "prettier": {
    "semi": false,
    "printWidth": 70,
    "tabWidth": 2,
    "useTabs": false,
    "singleQuote": true,
    "jsxSingleQuote": false,
    "bracketSameLine": true,
    "arrowParens": "avoid",
    "endOfLine": "lf"
  },
  "tap": {
    "coverage": false,
    "node-arg": [
      "--expose-gc",
      "-r",
      "ts-node/register"
    ],
    "ts": false
  },
  "size-limit": [
    {
      "path": "./dist/mjs/index.js"
    }
  ]
}
Any other comments can go here

Have a nice day!

Beitragsleitfaden

Beitragsleitfaden öffnen

Erste Schritte

  1. Lies das ganze Issue und danach den Beitragsleitfaden des Projekts.
  2. Schreib ins Issue, dass du es übernimmst — das erspart doppelte Arbeit.
  3. Forke das Repository und arbeite in einem Branch.
  4. Öffne einen Pull Request, der die Issue-Nummer nennt.

Rechercherichtung

Beginne mit der verknüpften ts-exports-rewrite-Reproduktion, insbesondere mit parse.ts und cache.ts, und führe deren tsc-Build mit aktivierter Ausgabe von Deklarationen aus. Vergleiche parse.d.ts und cache.d.ts und verfolge anschließend die Modulauflösung für lru-cache und dessen Exporteinträge. Als abgeschlossen gilt die Aufgabe, wenn die Ausgabe der Deklarationen mit dem erwarteten Paketeinstiegspunkt kompatibel bleibt, während mehrere Exporte in dieselbe Datei aufgelöst werden.

Vom Indexierungsmodell aus dem Issue-Text verfasst.

Bewertung

Tech-Stack
typescript
Bereich
compilers
Issue-Typ
Bug
Schwierigkeit
4/5
Geschätzter Aufwand
3-5 Tage
Aktivitätsstatus
Veraltet
Klarheit
Größtenteils klar
Anfängerfreundlichkeit
35/100

Neue Issues direkt in Ihr Postfach

Eine kurze Übersicht über anfängerfreundliche GitHub-Issues.