Skip to content

Commit 6cca59f

Browse files
tgriesserbestander
authored andcommitted
Fix for #1214, linked scoped dependencies should not be overwritten (#1970)
* Fix for #1214 Scoped dependencies are nested one folder deeper than traditional dependencies, the possiblyExtraneous needed updating to work with the actual dependencies, not the entire scoped folder. * Fix typo/variable renaming
1 parent 776359e commit 6cca59f

File tree

8 files changed

+68
-5
lines changed

8 files changed

+68
-5
lines changed

__tests__/commands/install/integration.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -695,3 +695,21 @@ test.concurrent('install a module with incompatible optional dependency should s
695695
assert.ok(!(await fs.exists(path.join(config.cwd, 'node_modules', 'dep-a'))));
696696
});
697697
});
698+
699+
test.concurrent('install will not overwrite files in symlinked scoped directories', async (): Promise<void> => {
700+
await runInstall({}, 'install-dont-overwrite-linked-scoped', async (config): Promise<void> => {
701+
const dependencyPath = path.join(config.cwd, 'node_modules', '@fakescope', 'fake-dependency');
702+
assert.equal(
703+
'Symlinked scoped package test',
704+
(await fs.readJson(path.join(dependencyPath, 'package.json'))).description,
705+
);
706+
assert.ok(!(await fs.exists(path.join(dependencyPath, 'index.js'))));
707+
}, async (cwd) => {
708+
const dirToLink = path.join(cwd, 'dir-to-link');
709+
await fs.mkdirp(path.join(cwd, '.yarn-link', '@fakescope'));
710+
await fs.symlink(dirToLink, path.join(cwd, '.yarn-link', '@fakescope', 'fake-dependency'));
711+
await fs.mkdirp(path.join(cwd, 'node_modules', '@fakescope'));
712+
await fs.symlink(dirToLink, path.join(cwd, 'node_modules', '@fakescope', 'fake-dependency'));
713+
});
714+
});
715+
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
yarn-offline-mirror=./mirror-for-offline
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "@fakescope/fake-dependency",
3+
"description": "Symlinked scoped package test",
4+
"version": "1.0.1",
5+
"dependencies": {},
6+
"license": "MIT"
7+
}
576 Bytes
Binary file not shown.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"dependencies": {
3+
"@fakescope/fake-dependency": "1.0.1"
4+
}
5+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2+
# yarn lockfile v1
3+
"@fakescope/[email protected]":
4+
version "1.0.1"
5+
resolved "@fakescope-fake-dependency-1.0.1.tgz#477dafd486d856af0b3faf5a5f1c895001221609"

src/config.js

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,21 @@ export default class Config {
177177

178178
await fs.mkdirp(this.globalFolder);
179179
await fs.mkdirp(this.linkFolder);
180-
this.linkedModules = await fs.readdir(this.linkFolder);
180+
181+
this.linkedModules = [];
182+
183+
const linkedModules = await fs.readdir(this.linkFolder);
184+
185+
for (const dir of linkedModules) {
186+
const linkedPath = path.join(this.linkFolder, dir);
187+
188+
if (dir[0] === '@') { // it's a scope, not a package
189+
const scopedLinked = await fs.readdir(linkedPath);
190+
this.linkedModules.push(...scopedLinked.map((scopedDir) => path.join(dir, scopedDir)));
191+
} else {
192+
this.linkedModules.push(dir);
193+
}
194+
}
181195

182196
for (const key of Object.keys(registries)) {
183197
const Registry = registries[key];
@@ -372,7 +386,7 @@ export default class Config {
372386

373387
/**
374388
* Read normalized package info according yarn-metadata.json
375-
* throw an error if package.json was not found
389+
* throw an error if package.json was not found
376390
*/
377391

378392
async readManifest(dir: string, priorityRegistry?: RegistryNames, isRoot?: boolean = false): Promise<Manifest> {
@@ -386,8 +400,8 @@ export default class Config {
386400
}
387401

388402
/**
389-
* try get the manifest file by looking
390-
* 1. mainfest fiel in cache
403+
* try get the manifest file by looking
404+
* 1. mainfest file in cache
391405
* 2. manifest file in registry
392406
*/
393407
maybeReadManifest(dir: string, priorityRegistry?: RegistryNames, isRoot?: boolean = false): Promise<?Manifest> {

src/package-linker.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,9 @@ export default class PackageLinker {
148148
});
149149
}
150150

151+
// keep track of all scoped paths to remove empty scopes after copy
152+
const scopedPaths = new Set();
153+
151154
// register root & scoped packages as being possibly extraneous
152155
const possibleExtraneous: Set<string> = new Set();
153156
for (const folder of this.config.registryFolders) {
@@ -158,12 +161,14 @@ export default class PackageLinker {
158161
let filepath;
159162
for (const file of files) {
160163
filepath = path.join(loc, file);
161-
possibleExtraneous.add(filepath);
162164
if (file[0] === '@') { // it's a scope, not a package
165+
scopedPaths.add(filepath);
163166
const subfiles = await fs.readdir(filepath);
164167
for (const subfile of subfiles) {
165168
possibleExtraneous.add(path.join(filepath, subfile));
166169
}
170+
} else {
171+
possibleExtraneous.add(filepath);
167172
}
168173
}
169174
}
@@ -200,6 +205,14 @@ export default class PackageLinker {
200205
},
201206
});
202207

208+
// remove any empty scoped directories
209+
for (const scopedPath of scopedPaths) {
210+
const files = await fs.readdir(scopedPath);
211+
if (files.length === 0) {
212+
await fs.unlink(scopedPath);
213+
}
214+
}
215+
203216
//
204217
if (this.config.binLinks) {
205218
const tickBin = this.reporter.progress(flatTree.length);

0 commit comments

Comments
 (0)