TypeScript 模块解析:当导出字段包含多个条目,且都解析到同一个文件时,tsc意外地重写导入路径,

zz2j4svz  于 6个月前  发布在  TypeScript
关注(0)|答案(4)|浏览(44)

演示仓库

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

你报告的问题是以下哪一个?

更复杂一些的问题,我将在后面详细解释

用代码示例展示上述缺陷。

在复现仓库中, parse.ts 文件 间接依赖于lru-cache 模块的类型,通过导入 cache.ts 文件:

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

编译后,在 parse.d.ts 中,我们通常期望得到一个 import('lru-cache').LRUCache 表达式。
然而,实际上变成了 import("lru-cache/min").LRUCache
另一方面,编译后的 cache.d.ts 文件仍然从 lru-cache 导入,而不是 lru-cache/min
这似乎与 lru-cache@10.0.1 的 exports 字段有关:https://unpkg.com/browse/lru-cache@10.0.1/package.json#L34
它包含两个条目, ./min.,都指向同一个文件。TypeScript 选择了第一个条目。
如果我修改 exports 字段,使 ../min 之前出现,那么编译声明文件将按预期使用 import("lru-cache")
虽然这不是一个非常严重的错误,有简单的解决方法,但这是相当意外的。
因为 lru-cache/min 仅在 exports 字段中可用,因此此编译结果破坏了与旧构建工具的兼容性。而且调试起来相当困难,因为从未有人提到 exports 字段中的键顺序会有所不同。

运行 tsc --showConfig 并粘贴输出结果在这里

{
    "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"
    ]
}

运行 tsc --traceResolution 并粘贴输出结果在这里

======== 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'. ========

如果存在,粘贴导入模块的 package.json

{
  "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"
  }
}

如果存在,粘贴目标模块的 package.json

{
  "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"
    }
  ]
}

可以在这里添加其他评论

祝你有个美好的一天!

vpfxa7rd

vpfxa7rd1#

这是#56261#56265中的相同代码。在夜间构建中,这种情况是否仍然发生?你没有填写模板哦,这是模块解析模板...

lfapxunr

lfapxunr2#

哦,我应该注意到这个拉取请求!感谢修复。
但不幸的是,它仍然有问题。
我已经使用 typescript@5.4.0-dev.20231103 更新了我的复现仓库,它仍然发出相同的 parse.d.ts

r7s23pms

r7s23pms3#

当我调查 #56261 时,我注意到了这个问题,并怀疑这种情况会发生。我不认为这是一个bug,但我理解为什么在这种情况下这是不理想的。但是我不知道你能制定什么规则来捕获这个问题。我认为你期望推断出的模块说明符是 "lru-cache" 的主要原因是,一个不同的文件以这种方式解析了问题中的符号,但通常情况下,两个文件不能使用相同的模块说明符(即使它是一个裸露的说明符)来引用同一个文件。这对于同一目录下的两个相同模块格式的文件是正确的,但如果文件位于不同的目录中,我们必须看看它们是否有不同的 package.jsons 在范围内或不同的 node_modules 目录在范围内,在这一点上,我们基本上是从头开始看看我们可以使用哪个模块说明符来解析目标文件。如果我们没有明确依赖于告诉我们可以使用 "lru-cache" 的缓存条目,那么似乎没有好的理由为什么不应该推断出 "lru-cache/min"

imzjd6km

imzjd6km4#

感谢您的详细解释!
是的,我同意这是一个难以优化的边缘情况,考虑到实现限制。
当前的行为并不非常不合理 - 只要它是一致的。
我会将此回复作为参考,并向vuelru-cache提交拉取请求。谢谢!

相关问题