Skip to content

Commit f7c3e91

Browse files
Merge pull request #32072 from storybookjs/valentin/add-node-linker-to-telemetry
Telemetry: Add nodeLinker to telemetry
2 parents 628f30c + ec50290 commit f7c3e91

File tree

4 files changed

+264
-15
lines changed

4 files changed

+264
-15
lines changed
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
import { beforeEach, describe, expect, it, vi } from 'vitest';
2+
3+
// eslint-disable-next-line depend/ban-dependencies
4+
import { execaCommand as rawExecaCommand } from 'execa';
5+
import { detect as rawDetect } from 'package-manager-detector';
6+
7+
import { getPackageManagerInfo } from './get-package-manager-info';
8+
9+
vi.mock('execa', async () => {
10+
return {
11+
execaCommand: vi.fn(),
12+
};
13+
});
14+
15+
vi.mock('package-manager-detector', async () => {
16+
return {
17+
detect: vi.fn(),
18+
};
19+
});
20+
21+
vi.mock('../common', async () => {
22+
return {
23+
getProjectRoot: () => '/mock/project/root',
24+
};
25+
});
26+
27+
const execaCommand = vi.mocked(rawExecaCommand);
28+
const detect = vi.mocked(rawDetect);
29+
30+
beforeEach(() => {
31+
execaCommand.mockReset();
32+
detect.mockReset();
33+
});
34+
35+
describe('getPackageManagerInfo', () => {
36+
describe('when no package manager is detected', () => {
37+
it('should return undefined', async () => {
38+
detect.mockResolvedValue(null);
39+
40+
const result = await getPackageManagerInfo();
41+
42+
expect(result).toBeUndefined();
43+
});
44+
});
45+
46+
describe('when yarn is detected', () => {
47+
beforeEach(() => {
48+
detect.mockResolvedValue({
49+
name: 'yarn',
50+
version: '3.6.0',
51+
agent: 'yarn@berry',
52+
});
53+
});
54+
55+
it('should return yarn info with default nodeLinker when command fails', async () => {
56+
execaCommand.mockRejectedValue(new Error('Command failed'));
57+
58+
const result = await getPackageManagerInfo();
59+
60+
expect(result).toEqual({
61+
type: 'yarn',
62+
version: '3.6.0',
63+
agent: 'yarn@berry',
64+
nodeLinker: 'node_modules',
65+
});
66+
});
67+
68+
it('should return yarn info with node_modules nodeLinker', async () => {
69+
execaCommand.mockResolvedValue({
70+
stdout: 'node_modules\n',
71+
} as any);
72+
73+
const result = await getPackageManagerInfo();
74+
75+
expect(result).toEqual({
76+
type: 'yarn',
77+
version: '3.6.0',
78+
agent: 'yarn@berry',
79+
nodeLinker: 'node_modules',
80+
});
81+
});
82+
83+
it('should return yarn info with pnp nodeLinker', async () => {
84+
execaCommand.mockResolvedValue({
85+
stdout: 'pnp\n',
86+
} as any);
87+
88+
const result = await getPackageManagerInfo();
89+
90+
expect(result).toEqual({
91+
type: 'yarn',
92+
version: '3.6.0',
93+
agent: 'yarn@berry',
94+
nodeLinker: 'pnp',
95+
});
96+
});
97+
});
98+
99+
describe('when pnpm is detected', () => {
100+
beforeEach(() => {
101+
detect.mockResolvedValue({
102+
name: 'pnpm',
103+
version: '8.15.0',
104+
agent: 'pnpm',
105+
});
106+
});
107+
108+
it('should return pnpm info with default isolated nodeLinker when command fails', async () => {
109+
execaCommand.mockRejectedValue(new Error('Command failed'));
110+
111+
const result = await getPackageManagerInfo();
112+
113+
expect(result).toEqual({
114+
type: 'pnpm',
115+
version: '8.15.0',
116+
agent: 'pnpm',
117+
nodeLinker: 'node_modules',
118+
});
119+
});
120+
121+
it('should return pnpm info with isolated nodeLinker', async () => {
122+
execaCommand.mockResolvedValue({
123+
stdout: 'isolated\n',
124+
} as any);
125+
126+
const result = await getPackageManagerInfo();
127+
128+
expect(result).toEqual({
129+
type: 'pnpm',
130+
version: '8.15.0',
131+
agent: 'pnpm',
132+
nodeLinker: 'isolated',
133+
});
134+
});
135+
});
136+
137+
describe('when npm is detected', () => {
138+
beforeEach(() => {
139+
detect.mockResolvedValue({
140+
name: 'npm',
141+
version: '9.8.0',
142+
agent: 'npm',
143+
});
144+
});
145+
146+
it('should return npm info with default node_modules nodeLinker', async () => {
147+
const result = await getPackageManagerInfo();
148+
149+
expect(result).toEqual({
150+
type: 'npm',
151+
version: '9.8.0',
152+
agent: 'npm',
153+
nodeLinker: 'node_modules',
154+
});
155+
expect(execaCommand).not.toHaveBeenCalled();
156+
});
157+
});
158+
159+
describe('when bun is detected', () => {
160+
beforeEach(() => {
161+
detect.mockResolvedValue({
162+
name: 'bun',
163+
version: '1.0.0',
164+
agent: 'bun',
165+
});
166+
});
167+
168+
it('should return bun info with default node_modules nodeLinker', async () => {
169+
const result = await getPackageManagerInfo();
170+
171+
expect(result).toEqual({
172+
type: 'bun',
173+
version: '1.0.0',
174+
agent: 'bun',
175+
nodeLinker: 'node_modules',
176+
});
177+
expect(execaCommand).not.toHaveBeenCalled();
178+
});
179+
});
180+
181+
describe('error handling', () => {
182+
beforeEach(() => {
183+
detect.mockResolvedValue({
184+
name: 'yarn',
185+
version: '3.6.0',
186+
agent: 'yarn',
187+
});
188+
});
189+
190+
it('should handle yarn command errors gracefully', async () => {
191+
execaCommand.mockRejectedValue(new Error('yarn command not found'));
192+
193+
const result = await getPackageManagerInfo();
194+
195+
expect(result).toEqual({
196+
type: 'yarn',
197+
version: '3.6.0',
198+
agent: 'yarn',
199+
nodeLinker: 'node_modules',
200+
});
201+
});
202+
203+
it('should handle pnpm command errors gracefully', async () => {
204+
detect.mockResolvedValue({
205+
name: 'pnpm',
206+
version: '8.15.0',
207+
agent: 'pnpm',
208+
});
209+
execaCommand.mockRejectedValue(new Error('pnpm command not found'));
210+
211+
const result = await getPackageManagerInfo();
212+
213+
expect(result).toEqual({
214+
type: 'pnpm',
215+
version: '8.15.0',
216+
agent: 'pnpm',
217+
nodeLinker: 'node_modules',
218+
});
219+
});
220+
});
221+
});
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// eslint-disable-next-line depend/ban-dependencies
2+
import { execaCommand } from 'execa';
3+
import { detect } from 'package-manager-detector';
4+
5+
import { getProjectRoot } from '../common';
6+
7+
export const getPackageManagerInfo = async () => {
8+
const packageManagerType = await detect({ cwd: getProjectRoot() });
9+
10+
if (!packageManagerType) {
11+
return undefined;
12+
}
13+
14+
let nodeLinker: 'node_modules' | 'pnp' | 'pnpm' | 'isolated' | 'hoisted' = 'node_modules';
15+
16+
if (packageManagerType.name === 'yarn') {
17+
try {
18+
const { stdout } = await execaCommand('yarn config get nodeLinker', {
19+
cwd: getProjectRoot(),
20+
});
21+
nodeLinker = stdout.trim() as 'node_modules' | 'pnp' | 'pnpm';
22+
} catch (e) {}
23+
}
24+
25+
if (packageManagerType.name === 'pnpm') {
26+
try {
27+
const { stdout } = await execaCommand('pnpm config get nodeLinker', {
28+
cwd: getProjectRoot(),
29+
});
30+
nodeLinker = (stdout.trim() as 'isolated' | 'hoisted' | 'pnpm') ?? 'isolated';
31+
} catch (e) {}
32+
}
33+
34+
return {
35+
type: packageManagerType.name,
36+
version: packageManagerType.version,
37+
agent: packageManagerType.agent,
38+
nodeLinker,
39+
};
40+
};

code/core/src/telemetry/storybook-metadata.ts

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { dirname } from 'node:path';
22

33
import {
4-
getProjectRoot,
54
getStorybookConfiguration,
65
getStorybookInfo,
76
loadMainConfig,
@@ -11,7 +10,6 @@ import { readConfig } from 'storybook/internal/csf-tools';
1110
import type { PackageJson, StorybookConfig } from 'storybook/internal/types';
1211

1312
import { findPackage, findPackagePath } from 'fd-package-json';
14-
import { detect } from 'package-manager-detector';
1513

1614
import { version } from '../../package.json';
1715
import { globalSettings } from '../cli/globalSettings';
@@ -20,6 +18,7 @@ import { getChromaticVersionSpecifier } from './get-chromatic-version';
2018
import { getFrameworkInfo } from './get-framework-info';
2119
import { getHasRouterPackage } from './get-has-router-package';
2220
import { getMonorepoType } from './get-monorepo-type';
21+
import { getPackageManagerInfo } from './get-package-manager-info';
2322
import { getPortableStoriesFileCount } from './get-portable-stories-usage';
2423
import { getActualPackageVersion, getActualPackageVersions } from './package-json';
2524
import { cleanPaths } from './sanitize';
@@ -120,19 +119,7 @@ export const computeStorybookMetadata = async ({
120119
metadata.monorepo = monorepoType;
121120
}
122121

123-
try {
124-
const packageManagerType = await detect({ cwd: getProjectRoot() });
125-
if (packageManagerType) {
126-
metadata.packageManager = {
127-
type: packageManagerType.name,
128-
version: packageManagerType.version,
129-
agent: packageManagerType.agent,
130-
};
131-
}
132-
133-
// Better be safe than sorry, some codebases/paths might end up breaking with something like "spawn pnpm ENOENT"
134-
// so we just set the package manager if the detection is successful
135-
} catch (err) {}
122+
metadata.packageManager = await getPackageManagerInfo();
136123

137124
const language = allDependencies.typescript ? 'typescript' : 'javascript';
138125

code/core/src/telemetry/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ export type StorybookMetadata = {
5858
type: DetectResult['name'];
5959
version: DetectResult['version'];
6060
agent: DetectResult['agent'];
61+
nodeLinker: 'node_modules' | 'pnp' | 'pnpm' | 'isolated' | 'hoisted';
6162
};
6263
typescriptOptions?: Partial<TypescriptOptions>;
6364
addons?: Record<string, StorybookAddon>;

0 commit comments

Comments
 (0)