Skip to content

Commit 845c8fd

Browse files
committed
fix(install): call a version-manager global install global, not project-local
1 parent 9d4f3ff commit 845c8fd

2 files changed

Lines changed: 54 additions & 3 deletions

File tree

‎src/__tests__/selfupdate/detect.test.ts‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,41 @@ describe('how_installed', ()=>{
3030
expect(info.kind).toBe('npm-local');
3131
});
3232

33+
it('calls it local when the owning project is an ancestor of the working directory', ()=>{
34+
// node resolves upward, so a copy in /home/artem/app/node_modules is
35+
// this project's whether you stand in app or in app/src.
36+
const project = at('home', 'artem', 'app');
37+
const info = how_installed({
38+
module_dir: path.join(project, 'node_modules', 'reply-cli'),
39+
cwd: path.join(project, 'src', 'deep'),
40+
read_package: pkg('reply-cli'),
41+
});
42+
expect(info.kind).toBe('npm-local');
43+
});
44+
45+
it('still calls a version-manager install global from the home directory', ()=>{
46+
// The regression that matters: ~/.nvm/... is under $HOME, so asking
47+
// whether the module sits under cwd would call this project-local and
48+
// refuse to update the one install we can actually drive.
49+
const home = at('home', 'artem');
50+
const info = how_installed({
51+
module_dir: path.join(home, '.nvm', 'versions', 'node', 'v22.17.1', 'lib', 'node_modules', 'reply-cli'),
52+
cwd: home,
53+
read_package: pkg('reply-cli'),
54+
});
55+
expect(info.kind).toBe('npm-global');
56+
});
57+
58+
it('attributes a nested copy to the project that owns the tree', ()=>{
59+
const project = at('home', 'artem', 'app');
60+
const info = how_installed({
61+
module_dir: path.join(project, 'node_modules', 'some-tool', 'node_modules', 'reply-cli'),
62+
cwd: project,
63+
read_package: pkg('reply-cli'),
64+
});
65+
expect(info.kind).toBe('npm-local');
66+
});
67+
3368
it('recognises npx before node_modules, since an npx cache has both', ()=>{
3469
const info = how_installed({
3570
module_dir: at('home', 'artem', '.npm', '_npx', 'a1b2', 'node_modules', 'reply-cli'),

‎src/selfupdate/detect.ts‎

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,18 +44,34 @@ const inside = (parent: string, child: string): boolean=>{
4444
return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
4545
};
4646

47+
// The directory whose node_modules holds this package — its first one, so a
48+
// transitive copy under a nested node_modules is still attributed to the
49+
// project that owns the tree.
50+
const owning_project = (module_dir: string, first_node_modules: number): string=>{
51+
const depth = module_dir.split(/[\\/]+/).length - first_node_modules;
52+
return path.resolve(module_dir, ...Array<string>(depth).fill('..'));
53+
};
54+
4755
const classify = (module_dir: string, cwd: string): Install_kind=>{
4856
const parts = module_dir.split(/[\\/]+/);
4957
// Checked before node_modules on purpose: an npx cache contains both.
5058
if (parts.includes('_npx'))
5159
{
5260
return 'npx';
5361
}
54-
if (parts.includes('node_modules'))
62+
const at = parts.indexOf('node_modules');
63+
if (at === -1)
5564
{
56-
return inside(cwd, module_dir) ? 'npm-local' : 'npm-global';
65+
return 'source';
5766
}
58-
return 'source';
67+
// Local means node resolves it by walking up from the working directory,
68+
// so the owning project must BE the working directory or an ancestor of
69+
// it. Asking only whether module_dir sits under cwd gets this backwards
70+
// for a version-manager install — ~/.nvm/.../node_modules is under $HOME,
71+
// and a user standing in $HOME would see their global install called
72+
// project-local, with the update refused for a project that isn't there.
73+
const project = owning_project(module_dir, at);
74+
return project === cwd || inside(project, cwd) ? 'npm-local' : 'npm-global';
5975
};
6076

6177
const how_installed = (deps: Detect_deps = {}): Install_info=>{

0 commit comments

Comments
 (0)