Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 9.0.18

- CLI: Fix Storybook doctor compatibility checks - [#32077](https://github.com/storybookjs/storybook/pull/32077), thanks @yannbf!
- Svelte: Fix union types generating invalid labels in argTypes - [#31980](https://github.com/storybookjs/storybook/pull/31980), thanks @grantralls!
- Telemetry: Add nodeLinker to telemetry - [#32072](https://github.com/storybookjs/storybook/pull/32072), thanks @valentinpalkovic!

## 9.0.17

- Addon Vitest: Fix support for plain `stories.tsx` files - [#32041](https://github.com/storybookjs/storybook/pull/32041), thanks @ghengeveld!
Expand Down
221 changes: 221 additions & 0 deletions code/core/src/telemetry/get-package-manager-info.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';

// eslint-disable-next-line depend/ban-dependencies
import { execaCommand as rawExecaCommand } from 'execa';
import { detect as rawDetect } from 'package-manager-detector';

import { getPackageManagerInfo } from './get-package-manager-info';

vi.mock('execa', async () => {
return {
execaCommand: vi.fn(),
};
});

vi.mock('package-manager-detector', async () => {
return {
detect: vi.fn(),
};
});

vi.mock('../common', async () => {
return {
getProjectRoot: () => '/mock/project/root',
};
});

const execaCommand = vi.mocked(rawExecaCommand);
const detect = vi.mocked(rawDetect);

beforeEach(() => {
execaCommand.mockReset();
detect.mockReset();
});

describe('getPackageManagerInfo', () => {
describe('when no package manager is detected', () => {
it('should return undefined', async () => {
detect.mockResolvedValue(null);

const result = await getPackageManagerInfo();

expect(result).toBeUndefined();
});
});

describe('when yarn is detected', () => {
beforeEach(() => {
detect.mockResolvedValue({
name: 'yarn',
version: '3.6.0',
agent: 'yarn@berry',
});
});

it('should return yarn info with default nodeLinker when command fails', async () => {
execaCommand.mockRejectedValue(new Error('Command failed'));

const result = await getPackageManagerInfo();

expect(result).toEqual({
type: 'yarn',
version: '3.6.0',
agent: 'yarn@berry',
nodeLinker: 'node_modules',
});
});

it('should return yarn info with node_modules nodeLinker', async () => {
execaCommand.mockResolvedValue({
stdout: 'node_modules\n',
} as any);

const result = await getPackageManagerInfo();

expect(result).toEqual({
type: 'yarn',
version: '3.6.0',
agent: 'yarn@berry',
nodeLinker: 'node_modules',
});
});

it('should return yarn info with pnp nodeLinker', async () => {
execaCommand.mockResolvedValue({
stdout: 'pnp\n',
} as any);

const result = await getPackageManagerInfo();

expect(result).toEqual({
type: 'yarn',
version: '3.6.0',
agent: 'yarn@berry',
nodeLinker: 'pnp',
});
});
});

describe('when pnpm is detected', () => {
beforeEach(() => {
detect.mockResolvedValue({
name: 'pnpm',
version: '8.15.0',
agent: 'pnpm',
});
});

it('should return pnpm info with default isolated nodeLinker when command fails', async () => {
execaCommand.mockRejectedValue(new Error('Command failed'));

const result = await getPackageManagerInfo();

expect(result).toEqual({
type: 'pnpm',
version: '8.15.0',
agent: 'pnpm',
nodeLinker: 'node_modules',
});
});

it('should return pnpm info with isolated nodeLinker', async () => {
execaCommand.mockResolvedValue({
stdout: 'isolated\n',
} as any);

const result = await getPackageManagerInfo();

expect(result).toEqual({
type: 'pnpm',
version: '8.15.0',
agent: 'pnpm',
nodeLinker: 'isolated',
});
});
});

describe('when npm is detected', () => {
beforeEach(() => {
detect.mockResolvedValue({
name: 'npm',
version: '9.8.0',
agent: 'npm',
});
});

it('should return npm info with default node_modules nodeLinker', async () => {
const result = await getPackageManagerInfo();

expect(result).toEqual({
type: 'npm',
version: '9.8.0',
agent: 'npm',
nodeLinker: 'node_modules',
});
expect(execaCommand).not.toHaveBeenCalled();
});
});

describe('when bun is detected', () => {
beforeEach(() => {
detect.mockResolvedValue({
name: 'bun',
version: '1.0.0',
agent: 'bun',
});
});

it('should return bun info with default node_modules nodeLinker', async () => {
const result = await getPackageManagerInfo();

expect(result).toEqual({
type: 'bun',
version: '1.0.0',
agent: 'bun',
nodeLinker: 'node_modules',
});
expect(execaCommand).not.toHaveBeenCalled();
});
});

describe('error handling', () => {
beforeEach(() => {
detect.mockResolvedValue({
name: 'yarn',
version: '3.6.0',
agent: 'yarn',
});
});

it('should handle yarn command errors gracefully', async () => {
execaCommand.mockRejectedValue(new Error('yarn command not found'));

const result = await getPackageManagerInfo();

expect(result).toEqual({
type: 'yarn',
version: '3.6.0',
agent: 'yarn',
nodeLinker: 'node_modules',
});
});

it('should handle pnpm command errors gracefully', async () => {
detect.mockResolvedValue({
name: 'pnpm',
version: '8.15.0',
agent: 'pnpm',
});
execaCommand.mockRejectedValue(new Error('pnpm command not found'));

const result = await getPackageManagerInfo();

expect(result).toEqual({
type: 'pnpm',
version: '8.15.0',
agent: 'pnpm',
nodeLinker: 'node_modules',
});
});
});
});
40 changes: 40 additions & 0 deletions code/core/src/telemetry/get-package-manager-info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// eslint-disable-next-line depend/ban-dependencies
import { execaCommand } from 'execa';
import { detect } from 'package-manager-detector';

import { getProjectRoot } from '../common';

export const getPackageManagerInfo = async () => {
const packageManagerType = await detect({ cwd: getProjectRoot() });

if (!packageManagerType) {
return undefined;
}

let nodeLinker: 'node_modules' | 'pnp' | 'pnpm' | 'isolated' | 'hoisted' = 'node_modules';

if (packageManagerType.name === 'yarn') {
try {
const { stdout } = await execaCommand('yarn config get nodeLinker', {
cwd: getProjectRoot(),
});
nodeLinker = stdout.trim() as 'node_modules' | 'pnp' | 'pnpm';
} catch (e) {}
}

if (packageManagerType.name === 'pnpm') {
try {
const { stdout } = await execaCommand('pnpm config get nodeLinker', {
cwd: getProjectRoot(),
});
nodeLinker = (stdout.trim() as 'isolated' | 'hoisted' | 'pnpm') ?? 'isolated';
} catch (e) {}
}

return {
type: packageManagerType.name,
version: packageManagerType.version,
agent: packageManagerType.agent,
nodeLinker,
};
};
17 changes: 2 additions & 15 deletions code/core/src/telemetry/storybook-metadata.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { dirname } from 'node:path';

import {
getProjectRoot,
getStorybookConfiguration,
getStorybookInfo,
loadMainConfig,
Expand All @@ -11,7 +10,6 @@ import { readConfig } from 'storybook/internal/csf-tools';
import type { PackageJson, StorybookConfig } from 'storybook/internal/types';

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

import { version } from '../../package.json';
import { globalSettings } from '../cli/globalSettings';
Expand All @@ -20,6 +18,7 @@ import { getChromaticVersionSpecifier } from './get-chromatic-version';
import { getFrameworkInfo } from './get-framework-info';
import { getHasRouterPackage } from './get-has-router-package';
import { getMonorepoType } from './get-monorepo-type';
import { getPackageManagerInfo } from './get-package-manager-info';
import { getPortableStoriesFileCount } from './get-portable-stories-usage';
import { getActualPackageVersion, getActualPackageVersions } from './package-json';
import { cleanPaths } from './sanitize';
Expand Down Expand Up @@ -120,19 +119,7 @@ export const computeStorybookMetadata = async ({
metadata.monorepo = monorepoType;
}

try {
const packageManagerType = await detect({ cwd: getProjectRoot() });
if (packageManagerType) {
metadata.packageManager = {
type: packageManagerType.name,
version: packageManagerType.version,
agent: packageManagerType.agent,
};
}

// Better be safe than sorry, some codebases/paths might end up breaking with something like "spawn pnpm ENOENT"
// so we just set the package manager if the detection is successful
} catch (err) {}
metadata.packageManager = await getPackageManagerInfo();

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

Expand Down
1 change: 1 addition & 0 deletions code/core/src/telemetry/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export type StorybookMetadata = {
type: DetectResult['name'];
version: DetectResult['version'];
agent: DetectResult['agent'];
nodeLinker: 'node_modules' | 'pnp' | 'pnpm' | 'isolated' | 'hoisted';
};
typescriptOptions?: Partial<TypescriptOptions>;
addons?: Record<string, StorybookAddon>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ export const checkPackageCompatibility = async (
// prevent issues with "tag" based versions e.g. "latest" or "next" instead of actual numbers
return (
versionRange &&
// We can't check compatibility for 0.x packages, so we skip them
!/^[~^]?0\./.test(versionRange) &&
semver.validRange(versionRange) &&
!semver.satisfies(currentStorybookVersion, versionRange)
);
Expand Down Expand Up @@ -101,6 +103,11 @@ export const checkPackageCompatibility = async (
export const getIncompatibleStorybookPackages = async (
context: Context
): Promise<AnalysedPackage[]> => {
if (context.currentStorybookVersion.includes('0.0.0')) {
// We can't know if a Storybook canary version is compatible with other packages, so we skip it
return [];
}

const allDeps = context.packageManager.getAllDependencies();
const storybookLikeDeps = Object.keys(allDeps).filter((dep) => dep.includes('storybook'));
if (storybookLikeDeps.length === 0 && !context.skipErrors) {
Expand Down
3 changes: 2 additions & 1 deletion code/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -283,5 +283,6 @@
"Dependency Upgrades"
]
]
}
},
"deferredNextVersion": "9.0.18"
}
1 change: 0 additions & 1 deletion code/renderers/svelte/src/extractArgTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,6 @@ const parseTypeToControl = (type: JSDocType | undefined): any => {
return {
control: {
type: 'radio',
labels: options.map(String),
},
options,
};
Expand Down
4 changes: 2 additions & 2 deletions docs/addons/addon-migration-guide.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ sidebar:
We sincerely appreciate the dedication and effort addon creators put into keeping the Storybook ecosystem vibrant and up-to-date. As Storybook evolves to version 9.0, bringing new features and improvements, this guide is here to assist you in migrating your addons from 8.x to 9.0. If you need to migrate your addon from an earlier version of Storybook, please first refer to the [Addon migration guide for Storybook 8.0](../../../release-8-6/docs/addons/addon-migration-guide.mdx).

<Callout variant="info">
As we gather feedback from the community, we'll update this page. We also have a general [Storybook migration guide](../migration-guide/index.mdx) if you're looking for that.
As we gather feedback from the community, we'll update this page. We also have a general [Storybook migration guide](../releases/migration-guide.mdx) if you're looking for that.
</Callout>

## Replacing dependencies

Many previously-published packages have [moved to be part of Storybook's core](../migration-guide/index.mdx#package-structure-changes). If your addon depends on any of these packages, you should remove them from your `package.json` and update your addon to import from the new location. If your addon does not already depend on the `storybook` package, you should add it to your `package.json` as a dependency.
Many previously-published packages have [moved to be part of Storybook's core](../releases/migration-guide.mdx#package-structure-changes). If your addon depends on any of these packages, you should remove them from your `package.json` and update your addon to import from the new location. If your addon does not already depend on the `storybook` package, you should add it to your `package.json` as a dependency.

```diff title="package.json"
{
Expand Down
Loading
Loading