feat(install): create node_modules for workspace members#34970
feat(install): create node_modules for workspace members#34970bartlomieju wants to merge 7 commits into
Conversation
When installing a workspace, `deno install` previously created only a single `node_modules` directory at the workspace root. Native Node.js tooling run from within a workspace member (for example `svelte-check`, `astro`, or `eslint` plugins) expects a local `node_modules` containing the member's dependencies, and npm/pnpm provide exactly that. The shared root `node_modules` also hides missing dependencies, since a member can resolve a package that only a sibling declares (shadow dependencies). This makes the local (isolated) installer create a `node_modules` directory inside each workspace member and symlink that member's direct dependencies into it: npm dependencies point into the shared `node_modules/.deno` store, and sibling workspace members are symlinked to their directories. The workspace root is left untouched and members without dependencies get no directory. Closes #26743 Closes #27550
Extend the previous change to the hoisted linker mode. Each workspace member gets a `node_modules` directory with its direct dependencies symlinked to their actual locations in the hoisted layout (top-level packages, or nested ones in case of version conflicts) and sibling workspace members linked to their directories. The spec test now exercises both the isolated and hoisted linkers.
The per-member node_modules linking skipped the workspace root by comparing member_node_modules to root_node_modules_path. That comparison is unreliable: root_node_modules_path is canonicalized while target_dir is not, so on Windows they differ by 8.3 short names or casing for the same directory. The root member was then re-processed, and in hoisted mode its top-level dependency got symlinked onto itself, producing a self-referential link (FilesystemLoop, os error 1921) that broke the hoisted linker. Mark the root member when building the workspace package list by comparing config folder urls, which are representation-stable, and skip it in both linker modes.
ReviewOverviewBoth linker modes now create a Correctness (verified)
Issues / Risks
Test coverageCovers both linker modes, a member's own npm dep (#26743), and a sibling workspace link including verifying the link target's package.json name (#27550). Gaps: no coverage for the nested/conflicting-version branch of StyleComments are excellent and explain the why (npm/pnpm parity, shadow deps, the Windows path-comparison rationale) rather than restating the code. The member-linking loop is duplicated across both linkers with mode-specific target resolution; given how divergent the target computation is (store path vs hoisted path, cache vs no cache) the duplication is defensible, but a shared helper taking a resolve-target closure would reduce drift risk later. VerdictSolid, well-motivated feature with correct path computation and reasonable tests. The main thing I would want addressed before merge is stale member-symlink pruning, since without it removed deps stay resolvable from members and partially undermine the goal. The missing |
When a workspace member dropped a dependency (or a sibling member was removed), its symlink lingered in the member's node_modules across installs, leaving the dropped dependency resolvable from the member and recreating the shadow-dependency problem the per-member layout is meant to eliminate. Both linkers now prune links whose alias is no longer one of the member's current direct dependencies before re-linking, mirroring the root node_modules pruning. Only symlinks and junctions are removed, so real files a user placed there are left untouched. Also covers a member's devDependencies and re-install-after-removal in the spec test.
|
A couple of optional test-coverage follow-ups worth noting (neither blocking): 1. Assert the workspace root
|
…ped hoisted member dep
When installing a workspace,
deno installpreviously created only asingle
node_modulesdirectory at the workspace root. Native Node.jstooling run from within a workspace member (for example
svelte-check,astro, oreslintplugins) expects a localnode_modulescontainingthe member's dependencies, the way npm and pnpm lay workspaces out, and
fails without it. Sharing only the root
node_modulesalso hides missingdependencies, since a member can resolve a package that only a sibling
declares.
This makes both linker modes create a
node_modulesdirectory insideeach workspace member and symlink that member's direct dependencies into
it: npm dependencies point at their resolved location (the shared
node_modules/.denostore for the isolated linker, or the package'stop-level or nested directory for the hoisted linker) and sibling
workspace members are linked to their directories. The workspace root is
left untouched and members without dependencies get no directory.
Stale links are pruned on reinstall the same way the root
node_modulesprunes them: when a member drops a dependency (or a sibling member is
removed), its link is removed from the member's
node_modulesbeforere-linking so the dropped dependency stops being resolvable. Only
symlinks and junctions are touched, so real files a user placed in the
member's
node_modulesare left alone.Known limitations (left for follow-ups):
node_modules/.binis not populated yet, so tools invokedas
node_modules/.bin/<tool>from within a member still resolve viathe root
.binrather than a local one.not placed anywhere in the hoisted layout (a version-conflict corner
case) is silently skipped rather than linked.
Closes #26743
Closes #27550