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 路徑有效,再處理快取;快取清除不能替套件增加公開入口。
參考資料: