返回 Skill 列表
extension
分类: 开发与工程无需 API Key

nodejs-subpath-imports

在调试子路径导入错误(ERR_PACKAGE_IMPORT_NOT_DEFINED,“仅允许./”)、配置package.json的imports字段、从tsconfig paths迁移,或使用条件构建跨平台代码时使用。涵盖了所有打包工具强制执行的Node.js规范约束。

person作者: jakexiaohubgithub

Node.js Subpath Imports

CRITICAL

Path Restrictions Are Node.js Spec, Not Bundler Limitations

When you see errors about path formats in imports field:

| Error pattern | This is... | NOT... | | ------------------- | ------------ | ------------------ | | "only allows ./" | Node.js spec | Metro limitation | | "rejects ../ paths" | Node.js spec | Webpack limitation | | "must be relative" | Node.js spec | Bundler bug |

Every compliant resolver enforces these rules. See Spec: Restrictions below.


Operations

Debug Subpath Import Errors

  1. Check error message for pattern (see CRITICAL table above)
  2. Verify targets start with ./ (required for local files)
  3. Check for forbidden segments: .., ., node_modules
  4. Verify extensions are explicit (no auto-resolution)
  5. Check condition order (first match wins)

Common errors:

  • ERR_PACKAGE_PATH_NOT_EXPORTED → subpath not in exports
  • ERR_PACKAGE_IMPORT_NOT_DEFINED → key not in imports
  • MODULE_NOT_FOUND → missing extension or wrong path

Debug Project Config

  1. Check moduleResolution in tsconfig (bundler, node16, nodenext)
  2. Verify resolvePackageJsonImports: true (default with above modes)
  3. Check bundler's condition configuration matches your targets
  4. Verify type: "module" if using ESM

Migrate from tsconfig Paths

Before (compile-time only):

// tsconfig.json
{
  "compilerOptions": {
    "paths": { "@utils/*": ["./src/utils/*"] }
  }
}

After (runtime + compile-time):

// package.json
{
  "imports": {
    "#utils/*": "./src/utils/*.js"
  }
}
// tsconfig.json
{
  "compilerOptions": {
    "moduleResolution": "bundler",
    "resolvePackageJsonImports": true
  }
}

Key changes:

  • @ prefix → # prefix (required by spec)
  • Add explicit .js extensions
  • Works at runtime without bundler

Build Cross-Platform Code

See reference/cross-platform.md for complete guide with:

  • Platform abstraction patterns
  • External package substitution (unique to imports)
  • Conditions by runtime (Node, Deno, Bun)
  • Conditions by bundler (Webpack, esbuild, Vite, Metro, etc.)

Quick pattern:

{
  "imports": {
    "#platform": {
      "react-native": "./src/platform/native.js",
      "browser": "./src/platform/browser.js",
      "node": "./src/platform/node.js",
      "default": "./src/platform/node.js"
    },
    "#fetch": {
      "node": "node-fetch",
      "default": "./src/fetch-native.js"
    }
  }
}

What This Enables

Runtime aliases - unlike tsconfig.json paths (compile-time only), subpath imports resolve at runtime:

import { helper } from "#utils/helper.js"; // Works in Node, Deno, Bun natively

Cross-platform code - same import, different implementation per environment:

{
  "imports": {
    "#fetch": {
      "node": "node-fetch",
      "browser": "./fetch-browser.js",
      "default": "./fetch-polyfill.js"
    }
  }
}

Spec: Syntax

Source: Node.js Packages - Subpath imports

Keys Must Start with #

{ "imports": { "#dep": "./dep.js", "#internal/*": "./src/internal/*.js" } }

Future: Node.js v26+ allows #/ prefix (PR #60864).

Targets: Strings, Objects, or Patterns

{
  "imports": {
    "#string": "./path/to/file.js",
    "#object": { "node": "pkg", "default": "./polyfill.js" },
    "#pattern/*": "./src/*.js"
  }
}

Relative Targets Must Start with ./

{ "imports": { "#good": "./src/file.js", "#bad": "src/file.js" } }

External packages (bare specifiers) don't need ./.

External Packages Allowed

{
  "imports": {
    "#dep": { "node": "dep-node-native", "default": "./dep-polyfill.js" }
  },
  "dependencies": { "dep-node-native": "^1.0.0" }
}

Spec: Resolution Algorithm

Source: Node.js ESM - Resolution Algorithm

  1. Import specifier must start with #
  2. Find matching key in nearest package.json imports field
  3. If value is object → apply condition matching (first match wins)
  4. If key contains * → perform string substitution on target
  5. Resolve resulting path/specifier

Pattern Substitution

* is literal string replacement, not glob:

{ "imports": { "#internal/*.js": "./src/internal/*.js" } }
import z from "#internal/z.js"; // → ./src/internal/z.js
import deep from "#internal/nested/x.js"; // → ./src/internal/nested/x.js

Spec: Conditions

Source: Node.js Packages - Conditional exports

First matching condition wins. Order from most specific to least.

Built-in Conditions

| Condition | When Matched | | ------------- | -------------------------- | | node-addons | Node.js with native addons | | node | Any Node.js environment | | import | Loaded via import | | require | Loaded via require() | | default | Fallback (must be last) |

Community Conditions

| Condition | When Matched | | ---------------------------- | ---------------------------- | | types | TypeScript (should be first) | | browser | Web browser | | react-native | React Native | | react-server | React Server Components | | development / production | Build mode |

Nested Conditions

{
  "imports": {
    "#utils": {
      "node": { "import": "./utils-node.mjs", "require": "./utils-node.cjs" },
      "browser": "./utils-browser.js",
      "default": "./utils.mjs"
    }
  }
}

Spec: Restrictions

Source: Node.js Packages - Subpath patterns

Forbidden Path Segments

After ./, these are disallowed:

| Segment | Why | | -------------- | -------------- | | .. | Path traversal | | . | Ambiguous | | node_modules | Injection |

Invalid Examples

{
  "imports": {
    "#bad1": "../outside/file.js",
    "#bad2": "./dist/../dist/file.js",
    "#bad3": "././file.js",
    "#bad4": "./node_modules/pkg/file.js"
  }
}

Spec: Scope

Source: Node.js Packages - Subpath imports

  • Private to package - only code within the package can use # imports
  • Cannot be consumed externally - import x from 'pkg/#internal' is invalid
  • Isolated mappings - no inheritance between packages

Edge Cases & Gotchas

For full details with sources, see reference/edge-cases.md.

| Gotcha | Summary | | ------------------------- | ---------------------------------------------- | | PATTERN_KEY_COMPARE | Longer base index wins, then total length | | No extension guessing | Must use explicit .js in CJS and ESM | | Arrays as fallbacks | Only for validation errors, NOT file-not-found | | Trailing slash (DEP0155) | Use #lib/* pattern instead of #lib/ | | Pattern keys need one * | Multiple * = exact match only | | Exact before patterns | Non-pattern keys checked first |


Tool Support

All major runtimes and bundlers support subpath imports. For versions, conditions config, and examples, see reference/tool-support.md.

Runtimes: Node.js v14.6+, Deno v1.25+, Bun v1.0+

Bundlers: esbuild, Webpack 5+, Rspack, Rollup, Rolldown, Metro v0.82+

Frameworks: Vite, Next.js, Astro, Expo


Complete Example

{
  "name": "my-app",
  "type": "module",
  "imports": {
    "#db": "./src/database/index.js",
    "#db/*": "./src/database/*.js",
    "#utils/*": "./src/utils/*.js",
    "#platform": {
      "node": "./src/platform/node.js",
      "browser": "./src/platform/browser.js",
      "react-native": "./src/platform/native.js",
      "default": "./src/platform/node.js"
    }
  }
}
import { connect } from "#db"; // exact match → ./src/database/index.js
import { query } from "#db/queries.js"; // pattern → ./src/database/queries.js
import platform from "#platform"; // condition-based

Monorepo Note

Subpath imports cannot reference outside the package - ./ requirement and .. prohibition are fundamental.

Alternatives for shared code:

  1. Workspace dependencies - proper package
  2. Symlinks - link into package
  3. tsconfig paths - build-time fallback