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
- Check error message for pattern (see CRITICAL table above)
- Verify targets start with
./(required for local files) - Check for forbidden segments:
..,.,node_modules - Verify extensions are explicit (no auto-resolution)
- Check condition order (first match wins)
Common errors:
ERR_PACKAGE_PATH_NOT_EXPORTED→ subpath not in exportsERR_PACKAGE_IMPORT_NOT_DEFINED→ key not in importsMODULE_NOT_FOUND→ missing extension or wrong path
Debug Project Config
- Check
moduleResolutionin tsconfig (bundler,node16,nodenext) - Verify
resolvePackageJsonImports: true(default with above modes) - Check bundler's condition configuration matches your targets
- 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
.jsextensions - 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
- Import specifier must start with
# - Find matching key in nearest
package.jsonimportsfield - If value is object → apply condition matching (first match wins)
- If key contains
*→ perform string substitution on target - 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:
- Workspace dependencies - proper package
- Symlinks - link into package
- tsconfig paths - build-time fallback
微信扫一扫