Skip to content

Subpath imports (#) fail on Windows when package.json path >= 260 chars (MAX_PATH) #62043

@wonu

Description

@wonu

Version

v24.12.0

Platform

Microsoft Windows NT 10.0.26200.0 x64

Subsystem

No response

What steps will reproduce the bug?

I ran into this while running pnpm dev in a project with a deep node_modules path:

TypeError: Package import specifier "#module-sync-enabled" is not defined imported from
C:\Users\...\node_modules\.pnpm\@react-router+dev@7.13.0_...\@react-router\dev\module-sync-enabled\index.mjs

Minimal repro — save as test.mjs and run with node test.mjs on Windows (with LongPathsEnabled):

import fs from "node:fs";
import { createRequire } from "node:module";
import path from "node:path";

// \\?\ prefix bypasses MAX_PATH limit for Win32 file APIs
const lp = (p) => "\\\\?\\" + p;
const ok = (fn) => {
	try { fn(); return "OK"; }
	catch (e) { console.error(e); return "FAIL"; }
};

const MAX = 259; // MAX_PATH (260) minus null terminator
const cwd = process.cwd();
const fixedLen = cwd.length + 2 + "package.json".length;
const results = {};

for (const target of [MAX, MAX + 1]) {
	const dir = path.join(cwd, "a".repeat(target - fixedLen));
	const dep = path.join(dir, "node_modules", "dep");

	fs.mkdirSync(lp(dep), { recursive: true });
	fs.writeFileSync(
		lp(path.join(dir, "package.json")),
		JSON.stringify({ imports: { "#foo": "./foo.mjs" } }),
	);
	fs.writeFileSync(lp(path.join(dir, "foo.mjs")), "export default 1\n");
	fs.writeFileSync(
		lp(path.join(dep, "package.json")),
		JSON.stringify({ name: "dep", exports: { ".": "./index.mjs" } }),
	);
	fs.writeFileSync(lp(path.join(dep, "index.mjs")), "export default 1\n");

	const { resolve } = createRequire(path.join(dir, "_.mjs"));
	results[`${target} chars`] = {
		"fs.access": ok(() => fs.accessSync(lp(path.join(dir, "package.json")))),
		'resolve("dep")': ok(() => resolve("dep")),
		'resolve("#foo")': ok(() => resolve("#foo")),
	};

	fs.rmSync(lp(dir), { recursive: true, force: true });
}

console.table(results);

How often does it reproduce? Is there a required condition?

Always reproduces when the package.json path is >= 260 characters. Requires Windows with LongPathsEnabled set (so that other file operations work fine past MAX_PATH).

Common with deeply nested node_modules (e.g. pnpm .pnpm layout + tools that create deep directories like .claude/worktrees).

What is the expected behavior? Why is that the expected behavior?

All three operations should succeed at both path lengths:

┌───────────┬───────────┬────────────────┬─────────────────┐
│ (index)   │ fs.access │ resolve("dep") │ resolve("#foo") │
├───────────┼───────────┼────────────────┼─────────────────┤
│ 259 chars │ 'OK'      │ 'OK'           │ 'OK'            │
│ 260 chars │ 'OK'      │ 'OK'           │ 'OK'            │
└───────────┴───────────┴────────────────┴─────────────────┘

LongPathsEnabled is set and both fs operations and subpath exports (which also resolve through package.json) work fine past MAX_PATH.

What do you see instead?

┌───────────┬───────────┬────────────────┬─────────────────┐
│ (index)   │ fs.access │ resolve("dep") │ resolve("#foo") │
├───────────┼───────────┼────────────────┼─────────────────┤
│ 259 chars │ 'OK'      │ 'OK'           │ 'OK'            │
│ 260 chars │ 'OK'      │ 'OK'           │ 'FAIL'          │
└───────────┴───────────┴────────────────┴─────────────────┘
TypeError [ERR_PACKAGE_IMPORT_NOT_DEFINED]: Package import specifier "#foo" is not defined imported from ...\_.mjs

Only subpath imports (#) fail. I suspect GetPackageScopeConfig in src/node_modules.cc is missing the ToNamespacedPath call that was added to other functions in #53294 (later refactored into NormalizePath in #60425).

Additional information

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions