1142 字
6 分鐘

ERR_PACKAGE_PATH_NOT_EXPORTED:檔案存在,為什麼 Node.js 不能匯入?

node_modules 裡明明有檔案,匯入 some-package/dist/internal.js 卻收到 ERR_PACKAGE_PATH_NOT_EXPORTED,先看套件的 exports。檔案存在不代表它是公開匯入路徑;套件可以只開放列出的入口。 重新安裝同一版本,不會自動把內部路徑變成公開 API。

Node.js 官方 Packages 文件說明,定義 exports 後,未公開的套件子路徑會受到限制;同時存在時,exports 優先於 main。因此排錯重點是「誰匯入哪個 specifier、目前版本公開了什麼」,而不是先刪掉 lockfile。

ERR_PACKAGE_PATH_NOT_EXPORTED 要先找呼叫端#

錯誤中的 package subpath 例如 ./dist/internal.js,對應你寫的 some-package/dist/internal.js。保留完整 stack,在自己的程式碼或錯誤指出的依賴位置找出那個 import/require。

  • 呼叫端是自己的程式:對照目前安裝版本的官方 API,改用公開入口。IDE 自動補上的 /dist/、/lib/ 不一定能用。
  • 呼叫端是另一個套件:查那個依賴與目標套件的相容版本。只改自己頂層的 import,無法修掉依賴內部仍存在的深層匯入。
  • 只在某個環境出現:比較實際 Node.js、安裝版本、lockfile 與啟動方式。條件式 exports 可以依 import/require 等條件選不同入口,不能只拿一份目錄截圖判定兩邊等價。

錯誤碼要分清楚:ERR_PACKAGE_PATH_NOT_EXPORTED指向 export 邊界;找不到套件或公開入口指向不存在的檔案,則是另一類問題。不要把所有 module loading 錯誤都歸成 ESM/CommonJS 不相容。

零下載實驗:公開子路徑成功,實體檔案路徑失敗#

以下程式自行在 OS 暫存目錄建立一個假套件,沒有執行 npm install、沒有寫入目前專案的 node_modules。存成 exports-check.mjs,用 node exports-check.mjs 執行;fixture 留在最後輸出的目錄。

import assert from 'node:assert/strict';
import { mkdtemp, mkdir, writeFile, access } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { pathToFileURL } from 'node:url';
const root = await mkdtemp(join(tmpdir(), 'exports-check-'));
const pkg = join(root, 'node_modules', 'demo-exports');
await mkdir(join(pkg, 'dist'), { recursive: true });
await writeFile(join(pkg, 'package.json'), JSON.stringify({
name: 'demo-exports',
version: '1.0.0',
type: 'module',
main: './dist/internal.js',
exports: {
'.': './dist/index.js',
'./feature': './dist/feature.js',
'./missing': './dist/absent.js',
},
}));
await writeFile(join(pkg, 'dist', 'index.js'), 'export const value = 1;');
await writeFile(join(pkg, 'dist', 'feature.js'), 'export const value = 2;');
await writeFile(join(pkg, 'dist', 'internal.js'), 'export const value = 99;');
await access(join(pkg, 'dist', 'internal.js')); // 實體檔案確實存在
const consumer = join(root, 'consumer.mjs');
await writeFile(consumer, `
import assert from 'node:assert/strict';
import { value as rootValue } from 'demo-exports';
import { value as featureValue } from 'demo-exports/feature';
assert.equal(rootValue, 1); // exports 優先,沒有走 main 的 99
assert.equal(featureValue, 2);
for (const specifier of [
'demo-exports/dist/internal.js',
'demo-exports/dist/feature.js',
'demo-exports/package.json',
]) {
await assert.rejects(import(specifier), {
code: 'ERR_PACKAGE_PATH_NOT_EXPORTED',
});
}
await assert.rejects(import('demo-exports/missing'), {
code: 'ERR_MODULE_NOT_FOUND',
});
`);
await import(pathToFileURL(consumer).href);
console.log('公開入口、深層路徑、manifest 與缺檔錯誤檢查通過');
console.log('Fixture:', root);

這個套件公開 demo-exports/feature,但沒有公開 demo-exports/dist/feature.js;兩者指向相關檔案,也不代表兩個名稱能互換。package.json 同樣沒有自動公開,因此 require('pkg/package.json') 或對應的 import 不能當成永遠可用的版本查詢方法。

./missing 已公開卻指向缺檔,fixture 因而收到 ERR_MODULE_NOT_FOUND。這個對照能幫你區分 export 邊界與發行內容缺失,避免對完全不同的錯誤套用同一個修復。

刊登範例由內容維護流程於 2026-10-27 在 Node.js v24.13.0、macOS 執行。它驗證 Node 原生 ESM resolver,沒有測特定第三方套件、bundler、TypeScript、CommonJS 條件分支或瀏覽器。

修正 import,而不是繞過套件契約#

應用程式使用者先找文件列出的公開入口;若沒有等價 API,向套件維護者確認支援方式,或選擇已確認相容的依賴組合。直接修改 node_modules 裡的 exports 不會成為可重現的安裝結果,也可能在下次安裝被覆蓋。

套件維護者則要檢查:預定公開的 subpath 有沒有列出、目標檔案有沒有進入發行包,以及條件分支是否覆蓋承諾的使用方式。fixture 示範的 ./feature 是公開名稱;是否保留副檔名,應在契約內保持一致,不要讓讀者猜內部目錄。

Node 官方也指出,exports 不是強制安全隔離;取得檔案絕對路徑後,仍可能從其他路徑存取。用絕對路徑繞過限制既不能證明 API 受支援,也不會修復依賴相容性。這裡要維持的是公開介面與可重現的解析結果。

若程式是透過 Vite 執行,而且修改依賴後仍出現舊結果,再區分 Vite 依賴預打包與快取問題。先確認 export 路徑有效,再處理快取;快取清除不能替套件增加公開入口。

參考資料:

Node.js:Package entry points

Node.js:Subpath exports

Node.js:ERR_PACKAGE_PATH_NOT_EXPORTED

ERR_PACKAGE_PATH_NOT_EXPORTED:檔案存在,為什麼 Node.js 不能匯入?
https://laplusda.com/posts/node-package-path-not-exported/
作者
Zero
發佈於
2026-10-27
許可協議
CC BY-NC-SA 4.0