---
title: "文件明明在包里，Node.js 为什么报 ERR_PACKAGE_PATH_NOT_EXPORTED？"
date: 2026-11-13
slug: node-package-exports-subpaths
canonical: https://kernelranch.com/blog/2026/11/node-package-exports-subpaths
markdown_url: https://kernelranch.com/blog/2026/11/node-package-exports-subpaths.md
tags: ["nodejs", "npm", "modules"]
---碰到 `ERR_PACKAGE_PATH_NOT_EXPORTED`，先看消费方实际装到的 `package.json`，检查 `exports` 有没有声明报错里的子路径。`exports` 定义了哪些包名入口能被解析。文件存在只能说明它被安装到了磁盘。

## 1. 同一个文件，公开前后怎么解析

保存为 `package_exports_lab.py`，运行时需 Python 和 Node.js。脚本只写临时目录，不使用 npm registry。

```python
import json
from pathlib import Path
import subprocess
from tempfile import TemporaryDirectory

with TemporaryDirectory() as tmp:
    root = Path(tmp)
    package = root / "node_modules" / "demo-exports"
    package.mkdir(parents=True)
    config = {"name": "demo-exports", "type": "commonjs",
              "exports": {".": "./index.cjs"}}
    manifest = package / "package.json"
    (package / "index.cjs").write_text("module.exports = 'root';", encoding="utf-8")
    (package / "internal.cjs").write_text("module.exports = 'helper';", encoding="utf-8")
    manifest.write_text(json.dumps(config), encoding="utf-8")

    def node(mode, script):
        result = subprocess.run(
            ["node", "--input-type=" + mode, "-e", script],
            cwd=root, capture_output=True, text=True, check=True,
        )
        return result.stdout.strip()

    assert node("commonjs", "console.log(require('demo-exports'))") == "root"
    assert node("commonjs", "console.log(require('./node_modules/demo-exports/internal.cjs'))") == "helper"
    for mode, operation in (
        ("commonjs", "require('demo-exports/internal.cjs')"),
        ("module", "await import('demo-exports/internal.cjs')"),
    ):
        result = node(mode, f"try {{ {operation} }} catch (err) {{ console.log(err.code) }}")
        assert result == "ERR_PACKAGE_PATH_NOT_EXPORTED"
        print(mode, result)

    config["exports"]["./helper"] = "./internal.cjs"
    manifest.write_text(json.dumps(config), encoding="utf-8")
    assert node("commonjs", "console.log(require('demo-exports/helper'))") == "helper"
    assert node("module", "import h from 'demo-exports/helper'; console.log(h)") == "helper"
    print("demo-exports/helper works in both loaders")
```

```text
commonjs ERR_PACKAGE_PATH_NOT_EXPORTED
module ERR_PACKAGE_PATH_NOT_EXPORTED
demo-exports/helper works in both loaders
```

## 2. 按维护者和消费方各自的入口修复

消费方先找包的文档，确认同一功能是否从根入口或其他公开子路径导出。代码依赖旧版深层路径时，查清版本边界，再把调用迁到受支持的入口。直接拼安装路径虽然在样本中可行，但会依赖目录布局，升级或切换包管理器后容易失效。

维护者给旧包增加 `exports` 时，要盘点已有的公开入口。旧版曾承诺 `pkg/feature` 和 `pkg/feature.js` 的话，需要明确保留对应键，或在迁移说明里指出路径变更。发布前用打包产物建一个干净的消费方项目，分别运行实际支持的 `import` 与 `require`。两种加载器可能因条件导出命中不同文件，本例特意让它们共用一个 CJS 目标来隔离子路径因素。

## 小结

把包名和子路径当作发布接口。复现时应从消费方导入，直接执行包内文件无法验证公开入口。

## 数据来源与检索时间

| 来源 | URL | 检索日期 | 对应内容 |
|---|---|---|---|
| Node.js 24.19.0 Packages | https://nodejs.org/download/release/v24.19.0/docs/api/packages.html#package-entry-points | 2026-11-13 | exports 的入口优先级、封装及兼容性 |
| Node.js 24.19.0 Subpath exports | https://nodejs.org/download/release/v24.19.0/docs/api/packages.html#subpath-exports | 2026-11-13 | 显式公开子路径及相关错误 |

## 核验说明

- Windows、Python 3.11.9、Node.js 24.19.0 实测 CommonJS 和 ESM。实验使用本地临时包，未测试 bundler 或发布流程。
