π Search Terms
tsserver hang, Yarn PnP, isInNodeModules, nodeModulesPathPart, watchNodeModulesForPackageJsonChanges, createModuleSpecifierCache, VS Code freeze on save
π Version & Regression Information
Confirmed on TypeScript 5.8.3 and 6.0.2 (latest). This affects TypeScript versions within a Yarn PnP Project.
β― Playground Link
N/A β requires a Yarn PnP project and VS Code.
π» Code
There are two instances of the same class of bug β unchecked indexOf/lastIndexOf returning -1 when a path doesn't contain the literal string /node_modules/, which is always the case in Yarn PnP projects.
Bug 1: src/server/moduleSpecifierCache.ts β createModuleSpecifierCache β set()
https://github.com/microsoft/TypeScript/blob/5f435057/src/server/moduleSpecifierCache.ts#L43
if (p.isInNodeModules) {
// No trailing slash
const nodeModulesPath = p.path.substring(0, p.path.indexOf(nodeModulesPathPart) + nodeModulesPathPart.length - 1);
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// Returns -1 in Yarn PnP (no /node_modules/ in path)
const key = host.toPath(nodeModulesPath);
if (!containedNodeModulesWatchers?.has(key)) {
(containedNodeModulesWatchers ||= new Map()).set(
key,
host.watchNodeModulesForPackageJsonChanges(nodeModulesPath), // β watches wrong dir!
);
}
}
In Yarn PnP, TypeScript's own PnP integration (in src/compiler/moduleSpecifiers.ts) correctly marks cross-package module paths with isInNodeModules = true even when the path doesn't literally contain /node_modules/ (packages live in
.yarn/cache/*.zip paths). However, this code then assumes the path must contain the literal string /node_modules/.
When indexOf returns -1, the arithmetic becomes:
substring(0, -1 + "/node_modules/".length - 1)
= substring(0, -1 + 14 - 1)
= substring(0, 12)
For a project in for example /home/myHome/projects/myProject the substring(0, 12) produces /home/myHome/ which is a valid path. This is then passed to host.watchNodeModulesForPackageJsonChanges(), which calls
watchPackageJsonsInNodeModules() β createNodeModulesWatcher() β watchFactory.watchDirectory(), creating a recursive directory watcher on the user's entire home directory.
This causes tsserver to enumerate and process every file under $HOME, hanging VS Code's extension host for minutes or indefinitely.
Bug 2: src/compiler/moduleNameResolver.ts β readPackageJsonPeerDependencies()
const nodeModules = packageDirectory.substring(
0,
packageDirectory.lastIndexOf("node_modules") + "node_modules".length
) + directorySeparator;
Same pattern: lastIndexOf returns -1, producing substring(0, 11) + / β first 12 chars of the path. Then getPackageJsonInfo(nodeModules + key, ...) tries to resolve peer dependencies under that incorrect directory.
π Actual behavior
Saving any file in VS Code with a Yarn PnP workspace causes tsserver to set up a recursive file watcher on the user's home directory (or an arbitrary prefix of the path, depending on home directory length). The VS Code GUI freezes.
Yarn patches Typescript setting ο»ΏisInNodeModules = true; even though there is no string /node_modules/ in the path. ο»ΏThe Yarn patch doesn't touch the downstream moduleSpecifierCache.ts which assumes isInNodeModules === true means the path contains the literal string /node_modules/. So indexOf at moduleSpecifierCache.ts:42 would never return -1 in an unpatched TypeScript β the bug is latent but unreachable without Yarn PnP.
Without this check the module path matches the firs 12 letters in the path, that in my case happens to be my home directory (e.g. /home/myHome/) making Typescript to watch all files in my $HOME.
π Expected behavior
When indexOf(nodeModulesPathPart) returns -1, the watcher/resolution should be skipped entirely (the path has no node_modules directory to watch).
Suggested fix
Bug 1 β Guard against -1:
if (p.isInNodeModules) {
const nodeModulesIndex = p.path.indexOf(nodeModulesPathPart);
if (nodeModulesIndex !== -1) {
const nodeModulesPath = p.path.substring(0, nodeModulesIndex + nodeModulesPathPart.length - 1);
const key = host.toPath(nodeModulesPath);
if (!containedNodeModulesWatchers?.has(key)) {
(containedNodeModulesWatchers ||= new Map()).set(
key,
host.watchNodeModulesForPackageJsonChanges(nodeModulesPath),
);
}
}
}
Bug 2 β Early return when node_modules not in path:
const nodeModulesIdx = packageDirectory.lastIndexOf("node_modules");
if (nodeModulesIdx === -1) return undefined;
const nodeModules = packageDirectory.substring(0, nodeModulesIdx + "node_modules".length) + directorySeparator;
Additional information about the issue
Environment:
- TypeScript
5.8.3 and 6.0.2
- Yarn
4.x with PnP (nodeLinker: pnp)
- VS Code with Yarn PnP TypeScript SDK (@yarnpkg/sdks)
- Linux (home directory path length happens to match the incorrect substring result)
Workaround:
create a link in some other place pointing to the repo and start VS Code with the yarn PnP SDK and Typescript support from there
cd /tmp
ln -s /home/myHome/myProject
cd myProject
code .
π Search Terms
tsserver hang, Yarn PnP, isInNodeModules, nodeModulesPathPart, watchNodeModulesForPackageJsonChanges, createModuleSpecifierCache, VS Code freeze on save
π Version & Regression Information
Confirmed on TypeScript 5.8.3 and 6.0.2 (latest). This affects TypeScript versions within a Yarn PnP Project.
β― Playground Link
N/A β requires a Yarn PnP project and VS Code.
π» Code
There are two instances of the same class of bug β unchecked indexOf/lastIndexOf returning -1 when a path doesn't contain the literal string /node_modules/, which is always the case in Yarn PnP projects.
Bug 1: src/server/moduleSpecifierCache.ts β createModuleSpecifierCache β set()
https://github.com/microsoft/TypeScript/blob/5f435057/src/server/moduleSpecifierCache.ts#L43
In Yarn PnP, TypeScript's own PnP integration (in src/compiler/moduleSpecifiers.ts) correctly marks cross-package module paths with isInNodeModules = true even when the path doesn't literally contain /node_modules/ (packages live in
.yarn/cache/*.zip paths). However, this code then assumes the path must contain the literal string /node_modules/.
When indexOf returns -1, the arithmetic becomes:
For a project in for example
/home/myHome/projects/myProjectthesubstring(0, 12)produces/home/myHome/which is a valid path. This is then passed to host.watchNodeModulesForPackageJsonChanges(), which callswatchPackageJsonsInNodeModules() β createNodeModulesWatcher() β watchFactory.watchDirectory(), creating a recursive directory watcher on the user's entire home directory.
This causes tsserver to enumerate and process every file under $HOME, hanging VS Code's extension host for minutes or indefinitely.
Bug 2: src/compiler/moduleNameResolver.ts β readPackageJsonPeerDependencies()
Same pattern: lastIndexOf returns -1, producing substring(0, 11) + / β first 12 chars of the path. Then getPackageJsonInfo(nodeModules + key, ...) tries to resolve peer dependencies under that incorrect directory.
π Actual behavior
Saving any file in VS Code with a Yarn PnP workspace causes tsserver to set up a recursive file watcher on the user's home directory (or an arbitrary prefix of the path, depending on home directory length). The VS Code GUI freezes.
Yarn patches Typescript setting
ο»ΏisInNodeModules = true;even though there is no string/node_modules/in the path. ο»ΏThe Yarn patch doesn't touch the downstreammoduleSpecifierCache.tswhich assumesisInNodeModules === truemeans the path contains the literal string/node_modules/. So indexOf at moduleSpecifierCache.ts:42 would never return -1 in an unpatched TypeScript β the bug is latent but unreachable without Yarn PnP.Without this check the module path matches the firs 12 letters in the path, that in my case happens to be my home directory (e.g.
/home/myHome/) making Typescript to watch all files in my $HOME.π Expected behavior
When indexOf(nodeModulesPathPart) returns -1, the watcher/resolution should be skipped entirely (the path has no node_modules directory to watch).
Suggested fix
Bug 1 β Guard against -1:
Bug 2 β Early return when node_modules not in path:
Additional information about the issue
Environment:
5.8.3 and 6.0.2
4.x with PnP (nodeLinker: pnp)
Workaround:
create a link in some other place pointing to the repo and start VS Code with the yarn PnP SDK and Typescript support from there