Skip to content

Commit 38f0134

Browse files
larixermerceyz
andauthored
fix(plugin-nm): Avoid duplicate copies inside aliased dependencies. (#4571)
* Avoids creation of circular symlinks and duplicates inside aliases * Add a test to check that duplicate copies of aliased packages are not created * Stricter ident comparison for hoisted packages * chore: update changelog Co-authored-by: merceyz <[email protected]>
1 parent 1379b71 commit 38f0134

File tree

5 files changed

+77
-6
lines changed

5 files changed

+77
-6
lines changed

.yarn/versions/6da17a68.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
releases:
2+
"@yarnpkg/cli": patch
3+
"@yarnpkg/nm": patch
4+
"@yarnpkg/plugin-nm": patch
5+
6+
declined:
7+
- "@yarnpkg/plugin-compat"
8+
- "@yarnpkg/plugin-constraints"
9+
- "@yarnpkg/plugin-dlx"
10+
- "@yarnpkg/plugin-essentials"
11+
- "@yarnpkg/plugin-init"
12+
- "@yarnpkg/plugin-interactive-tools"
13+
- "@yarnpkg/plugin-npm-cli"
14+
- "@yarnpkg/plugin-pack"
15+
- "@yarnpkg/plugin-patch"
16+
- "@yarnpkg/plugin-pnp"
17+
- "@yarnpkg/plugin-pnpm"
18+
- "@yarnpkg/plugin-stage"
19+
- "@yarnpkg/plugin-typescript"
20+
- "@yarnpkg/plugin-version"
21+
- "@yarnpkg/plugin-workspace-tools"
22+
- "@yarnpkg/builder"
23+
- "@yarnpkg/core"
24+
- "@yarnpkg/doctor"
25+
- "@yarnpkg/pnpify"

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ The following changes only affect people writing Yarn plugins:
6767

6868
- The `pnpm` linker avoids creating symlinks that lead to loops on the file system, by moving them higher up in the directory structure.
6969
- The `pnpm` linker no longer reports duplicate "incompatible virtual" warnings.
70+
- The node-modules linker avoids creation of circular symlinks
71+
- The node-modules linker no longer creates duplicate copies inside of aliased packages
7072

7173
### Bugfixes
7274

packages/acceptance-tests/pkg-tests-specs/sources/dragon.test.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -550,7 +550,6 @@ describe(`Dragon tests`, () => {
550550
for (const [nodeLinker, shouldHaveAccessToTheSameInstance] of [
551551
[`pnp`, true],
552552
[`pnpm`, true],
553-
[`node-modules`, false],
554553
]) {
555554
test(`it should pass the dragon test 11 with "nodeLinker: ${nodeLinker}"`,
556555
makeTemporaryEnv(
@@ -580,9 +579,10 @@ describe(`Dragon tests`, () => {
580579
await expect(run(`install`)).resolves.toBeTruthy();
581580

582581
// Make sure that both the root and dragon-test-11-b have access to the same instance.
583-
// This is only possible with the PnP and pnpm linkers, because the node-modules linker
584-
// can't fulfill the peer dependency promise. For the NM linker we test that it at least
585-
// fulfills the require promise (installing dragon-test-11-a both under the aliased and original name).
582+
// This is only possible with the PnP and pnpm linkers.
583+
// The node-modules linker can't fullfil these requirements for aliased packages,
584+
// without resorting to symlinks and a layout resembling pnpm for aliased dependencies,
585+
// which will be too different from the classic node_modules layout for all the other dependencies.
586586
await expect(source(`
587587
(() => {
588588
const {createRequire} = require(\`module\`);

packages/acceptance-tests/pkg-tests-specs/sources/node-modules.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,25 @@ describe(`Node_Modules`, () => {
347347
),
348348
);
349349

350+
test(`should not create duplicate copies of aliased packages`,
351+
makeTemporaryEnv(
352+
{
353+
private: true,
354+
dependencies: {
355+
[`no-deps2`]: `npm:[email protected]`,
356+
},
357+
},
358+
{
359+
nodeLinker: `node-modules`,
360+
},
361+
async ({path, run, source}) => {
362+
await run(`install`);
363+
364+
expect(await xfs.existsPromise(`${path}/node_modules/no-deps2/node_modules/no-deps` as PortablePath)).toBe(false);
365+
},
366+
),
367+
);
368+
350369
test(`should not hoist package peer dependent on parent if conflict exists`,
351370
// . -> dep -> conflict@2 -> unhoistable --> conflict
352371
// -> conflict@1
@@ -563,6 +582,27 @@ describe(`Node_Modules`, () => {
563582
),
564583
);
565584

585+
test(`should not create circular self-reference symlinks`,
586+
makeTemporaryEnv(
587+
{
588+
workspaces: [`ws`],
589+
},
590+
{
591+
nodeLinker: `node-modules`,
592+
nmHoistingLimits: `workspaces`,
593+
},
594+
async ({path, run}) => {
595+
await writeJson(npath.toPortablePath(`${path}/ws/package.json`), {
596+
name: `ws`,
597+
});
598+
599+
await run(`install`);
600+
601+
expect(await xfs.existsPromise(`${path}/ws/node_modules/ws` as PortablePath)).toEqual(false);
602+
},
603+
),
604+
);
605+
566606
test(`should not hoist multiple packages past workspace hoist border`,
567607
// . -> workspace -> dep1 -> dep2
568608
// should be hoisted to:

packages/yarnpkg-nm/sources/buildNodeModulesTree.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -517,9 +517,11 @@ const populateNodeModulesTree = (pnp: PnpApi, hoistedTree: HoisterResult, option
517517

518518
seenNodes.add(pkg);
519519

520+
const pkgReferences = Array.from(pkg.references).sort().join(`#`);
520521
for (const dep of pkg.dependencies) {
522+
const depReferences = Array.from(dep.references).sort().join(`#`);
521523
// We do not want self-references in node_modules, since they confuse existing tools
522-
if (dep === pkg)
524+
if (dep.identName === pkg.identName && depReferences === pkgReferences)
523525
continue;
524526
const references = Array.from(dep.references).sort();
525527
const locator = {name: dep.identName, reference: references[0]};
@@ -542,7 +544,9 @@ const populateNodeModulesTree = (pnp: PnpApi, hoistedTree: HoisterResult, option
542544
isAnonymousWorkspace = !!(workspace && !workspace.manifest.name);
543545
}
544546

545-
if (!dep.name.endsWith(WORKSPACE_NAME_SUFFIX) && !isAnonymousWorkspace) {
547+
const isCircularSymlink = leafNode.linkType === LinkType.SOFT && nodeModulesLocation.startsWith(leafNode.target);
548+
549+
if (!dep.name.endsWith(WORKSPACE_NAME_SUFFIX) && !isAnonymousWorkspace && !isCircularSymlink) {
546550
const prevNode = tree.get(nodeModulesLocation);
547551
if (prevNode) {
548552
if (prevNode.dirList) {

0 commit comments

Comments
 (0)