diff --git a/.yarn/versions/b764a694.yml b/.yarn/versions/b764a694.yml new file mode 100644 index 000000000000..bd3ff1a3f753 --- /dev/null +++ b/.yarn/versions/b764a694.yml @@ -0,0 +1,23 @@ +releases: + "@yarnpkg/cli": patch + "@yarnpkg/plugin-nm": patch + +declined: + - "@yarnpkg/plugin-compat" + - "@yarnpkg/plugin-constraints" + - "@yarnpkg/plugin-dlx" + - "@yarnpkg/plugin-essentials" + - "@yarnpkg/plugin-init" + - "@yarnpkg/plugin-interactive-tools" + - "@yarnpkg/plugin-npm-cli" + - "@yarnpkg/plugin-pack" + - "@yarnpkg/plugin-patch" + - "@yarnpkg/plugin-pnp" + - "@yarnpkg/plugin-pnpm" + - "@yarnpkg/plugin-stage" + - "@yarnpkg/plugin-typescript" + - "@yarnpkg/plugin-version" + - "@yarnpkg/plugin-workspace-tools" + - "@yarnpkg/builder" + - "@yarnpkg/core" + - "@yarnpkg/doctor" diff --git a/CHANGELOG.md b/CHANGELOG.md index 982e0371c88a..a045c4d73eac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Features in `master` can be tried out by running `yarn set version from sources` ::: - Fixes `preferInteractive` forcing interactive mode in non-TTY environments. +- `node-modules` linker now honors user-defined symlinks for `/node_modules` directories ## 4.1.0 diff --git a/packages/acceptance-tests/pkg-tests-specs/sources/node-modules.test.ts b/packages/acceptance-tests/pkg-tests-specs/sources/node-modules.test.ts index aede1541894c..90e10566c18b 100644 --- a/packages/acceptance-tests/pkg-tests-specs/sources/node-modules.test.ts +++ b/packages/acceptance-tests/pkg-tests-specs/sources/node-modules.test.ts @@ -1855,6 +1855,47 @@ describe(`Node_Modules`, () => { }), ); + it(`should work with user-created /node_modules symlinks`, + makeTemporaryEnv( + { + workspaces: [`ws`], + dependencies: { + }, + }, + { + nodeLinker: `node-modules`, + nmHoistingLimits: `workspaces`, + }, + async ({path, run}) => { + await xfs.mkdirpPromise(ppath.join(path, `ws`)); + const trueInstallDir = ppath.resolve(path, `target`); + await xfs.mkdirPromise(trueInstallDir); + + await xfs.writeJsonPromise(ppath.join(path, `ws/${Filename.manifest}`), { + name: `ws`, + devDependencies: { + [`no-deps`]: `1.0.0`, + }, + }); + + await xfs.symlinkPromise(trueInstallDir, ppath.join(path, `ws/node_modules`)); + + await run(`install`); + + expect(xfs.existsSync(ppath.join(trueInstallDir, `no-deps`))).toBeTruthy(); + expect(xfs.lstatSync(ppath.join(path, `ws/node_modules`)).isSymbolicLink()).toBeTruthy(); + + await xfs.writeJsonPromise(ppath.join(path, `ws/${Filename.manifest}`), { + name: `ws`, + }); + + await run(`install`); + + expect(xfs.existsSync(ppath.join(trueInstallDir, `no-deps`))).toBeFalsy(); + expect(xfs.lstatSync(ppath.join(path, `ws/node_modules`)).isSymbolicLink()).toBeTruthy(); + }), + ); + it(`should support supportedArchitectures`, makeTemporaryEnv( { diff --git a/packages/plugin-nm/sources/NodeModulesLinker.ts b/packages/plugin-nm/sources/NodeModulesLinker.ts index 15d23f5e5e59..4c404cfb28d9 100644 --- a/packages/plugin-nm/sources/NodeModulesLinker.ts +++ b/packages/plugin-nm/sources/NodeModulesLinker.ts @@ -525,15 +525,15 @@ async function findInstallState(project: Project, {unrollAliases = false}: {unro return {locatorMap, binSymlinks, locationTree: buildLocationTree(locatorMap, {skipPrefix: project.cwd}), nmMode, mtimeMs: stats.mtimeMs}; } -const removeDir = async (dir: PortablePath, options: {contentsOnly: boolean, innerLoop?: boolean, allowSymlink?: boolean}): Promise => { +const removeDir = async (dir: PortablePath, options: {contentsOnly: boolean, innerLoop?: boolean, isWorkspaceDir?: boolean}): Promise => { if (dir.split(ppath.sep).indexOf(NODE_MODULES) < 0) throw new Error(`Assertion failed: trying to remove dir that doesn't contain node_modules: ${dir}`); try { + let dirStats; if (!options.innerLoop) { - const stats = options.allowSymlink ? await xfs.statPromise(dir) : await xfs.lstatPromise(dir); - if (options.allowSymlink && !stats.isDirectory() || - (!options.allowSymlink && stats.isSymbolicLink())) { + dirStats = await xfs.lstatPromise(dir); + if ((!dirStats.isDirectory() && !dirStats.isSymbolicLink()) || (dirStats.isSymbolicLink() && !options.isWorkspaceDir)) { await xfs.unlinkPromise(dir); return; } @@ -549,7 +549,9 @@ const removeDir = async (dir: PortablePath, options: {contentsOnly: boolean, inn await xfs.unlinkPromise(targetPath); } } - if (!options.contentsOnly) { + + const isExternalWorkspaceSymlink = !options.innerLoop && options.isWorkspaceDir && dirStats?.isSymbolicLink(); + if (!options.contentsOnly && !isExternalWorkspaceSymlink) { await xfs.rmdirPromise(dir); } } catch (e) { @@ -1133,8 +1135,8 @@ async function persistNodeModules(preinstallState: InstallState, installState: N if (prevNode.children.has(NODE_MODULES)) await removeDir(ppath.join(location, NODE_MODULES), {contentsOnly: false}); - const isRootNmLocation = ppath.basename(location) === NODE_MODULES && locationTree.has(ppath.join(ppath.dirname(location), ppath.sep)); - await removeDir(location, {contentsOnly: location === rootNmDirPath, allowSymlink: isRootNmLocation}); + const isWorkspaceNmLocation = ppath.basename(location) === NODE_MODULES && prevLocationTree.has(ppath.join(ppath.dirname(location))); + await removeDir(location, {contentsOnly: location === rootNmDirPath, isWorkspaceDir: isWorkspaceNmLocation}); } else { for (const [segment, prevChildNode] of prevNode.children) { const childNode = node.children.get(segment); @@ -1164,8 +1166,8 @@ async function persistNodeModules(preinstallState: InstallState, installState: N // 1. If new directory is a symlink, we need to remove it fully // 2. If new directory is a hardlink - we just need to clean it up - const isRootNmLocation = ppath.basename(location) === NODE_MODULES && locationTree.has(ppath.join(ppath.dirname(location), ppath.sep)); - await removeDir(location, {contentsOnly: node.linkType === LinkType.HARD, allowSymlink: isRootNmLocation}); + const isWorkspaceNmLocation = ppath.basename(location) === NODE_MODULES && locationTree.has(ppath.join(ppath.dirname(location))); + await removeDir(location, {contentsOnly: node.linkType === LinkType.HARD, isWorkspaceDir: isWorkspaceNmLocation}); } else { if (!areRealLocatorsEqual(node.locator, prevNode.locator)) await removeDir(location, {contentsOnly: node.linkType === LinkType.HARD});