diff --git a/AGENTS.md b/AGENTS.md
index 55f3c0752f22..c805a77786ba 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -288,6 +288,18 @@ Avoid `console.log`, `console.warn`, and `console.error` unless the file is isol
These usually start long-running development servers and are the wrong default for agents.
+## Code Authoring Principles
+
+These are recurring failure modes in agent-authored changes to this repo. Apply them when writing or reviewing code, not just when asked.
+
+- **Comments are maintenance docs, not an investigation transcript.** Explain *why* for the next maintainer. Do not commit internal ticket / acceptance-criteria codes (`AC-X2`, `Probe B`, `R6`), the narrative of how you figured something out, "verified byte-identical" provenance prose, or cross-file line references (`L125→L131`) — they are noise and they rot. One or two sentences of rationale beats a paragraph of evidence.
+- **Verify environment assumptions empirically before encoding them.** If a design rests on "the bundler strips X" or "this metadata is empty here", prove it with a throwaway probe before building on it (and before writing it into a comment as fact). A 10-line experiment is cheaper than a wrong architecture.
+- **Encode assumptions with static checks first.** If an assumption is expected to always hold, prefer making it impossible via TypeScript types and existing lint rules. When static checks are not practical, add a cheap runtime assertion close to the boundary so violations fail loudly at the source.
+- **Avoid redundant tests already covered elsewhere.** Do not add tests for code patterns already guaranteed by TypeScript or linting, and do not duplicate coverage that already exists in Storybook `play` functions or Playwright tests.
+- **Test contracts (including side effects), not private implementation details.** It is valid to assert side effects when they are part of the public contract. Avoid assertions about internals that are not part of an exported contract, user-visible DOM output, or externally observable behavior.
+- **Bias toward broader coverage for security and migrations.** For security-sensitive code paths and legacy data migration logic, prefer handling more edge cases and documenting evidence for the chosen safeguards. Migration compatibility code should be explicitly version-scoped so it can be removed once the support window ends.
+- **Prefer deletion and simplicity over speculative generality.** No abstraction, fallback, or "flexibility" for a consumer or scenario that does not exist in this codebase today. If a change adds many lines, check whether the right change removes them.
+
## Maintenance Rules For Agents
- Use this file as the canonical instruction source
diff --git a/CHANGELOG.prerelease.md b/CHANGELOG.prerelease.md
index 853fe614ccd9..72a099b0ede4 100644
--- a/CHANGELOG.prerelease.md
+++ b/CHANGELOG.prerelease.md
@@ -1,3 +1,23 @@
+## 10.5.0-alpha.2
+
+- A11y-Addon: Preserve disabled a11y rules with runOnly - [#34649](https://github.com/storybookjs/storybook/pull/34649), thanks @cyphercodes!
+- A11y: Fix MDX heading anchors not keyboard accessible - [#34368](https://github.com/storybookjs/storybook/pull/34368), thanks @TheSeydiCharyyev!
+- Add an optional TypeScript peer to react-vite - [#34627](https://github.com/storybookjs/storybook/pull/34627), thanks @wojtekmaj!
+- Addon-Docs: Resolve CSF4 module exports without a default export - [#34834](https://github.com/storybookjs/storybook/pull/34834), thanks @TheSeydiCharyyev!
+- Angular: Detect model() signal outputs (type inference + compodoc autodocs + runtime binding) - [#34833](https://github.com/storybookjs/storybook/pull/34833), thanks @valentinpalkovic!
+- Build: Upgrade type-fest to latest version 5.6.0 - [#34791](https://github.com/storybookjs/storybook/pull/34791), thanks @tobiasdiez!
+- CLI: Support `peerDependencies` in framework detection for component libraries - [#34516](https://github.com/storybookjs/storybook/pull/34516), thanks @zhyd1997!
+- CSF-Next: Add tags type support for - [#34819](https://github.com/storybookjs/storybook/pull/34819), thanks @unional!
+- Core: Add runtime instance registry - [#34863](https://github.com/storybookjs/storybook/pull/34863), thanks @kasperpeulen!
+- Core: Categorize UniversalStore follower timeout error - [#34592](https://github.com/storybookjs/storybook/pull/34592), thanks @justismailmemon!
+- Docs: Add ariaLabel support to ActionItem interface - [#34749](https://github.com/storybookjs/storybook/pull/34749), thanks @TheSeydiCharyyev!
+- Docs: Scope control input ids to each block instance - [#34793](https://github.com/storybookjs/storybook/pull/34793), thanks @TheSeydiCharyyev!
+- Docs: Support explicit id prop on for standalone MDX - [#34808](https://github.com/storybookjs/storybook/pull/34808), thanks @TheSeydiCharyyev!
+- Maintenance: Replace `resolve` and `resolve.exports` with `oxc-resolver` - [#34692](https://github.com/storybookjs/storybook/pull/34692), thanks @valentinpalkovic!
+- Next.js-Vite: Bump vite-plugin-storybook-nextjs to ^3.3.0 - [#34838](https://github.com/storybookjs/storybook/pull/34838), thanks @yatishgoel!
+- Vitest: Reset playwright cursor position to avoid hover bug - [#34765](https://github.com/storybookjs/storybook/pull/34765), thanks @Sidnioulz!
+- Vue3: Specify a specific version for non-dev dependency - [#34794](https://github.com/storybookjs/storybook/pull/34794), thanks @ScopeyNZ!
+
## 10.5.0-alpha.1
- Angular: Fix custom paths for stats.json on angular - [#34551](https://github.com/storybookjs/storybook/pull/34551), thanks @mrginglymus!
diff --git a/code/addons/a11y/src/a11yRunner.test.ts b/code/addons/a11y/src/a11yRunner.test.ts
index 9018e7417c6e..3edeaa1977e7 100644
--- a/code/addons/a11y/src/a11yRunner.test.ts
+++ b/code/addons/a11y/src/a11yRunner.test.ts
@@ -1,3 +1,4 @@
+import type { AxeResults } from 'axe-core';
import type { Mock } from 'vitest';
import { beforeEach, describe, expect, it, vi } from 'vitest';
@@ -5,17 +6,57 @@ import { addons } from 'storybook/preview-api';
import { EVENTS } from './constants.ts';
+const { axeMock, documentMock } = vi.hoisted(() => {
+ const documentMock = {
+ body: {},
+ getElementById: vi.fn(),
+ location: { pathname: '/iframe.html' },
+ };
+
+ return {
+ documentMock,
+ axeMock: {
+ reset: vi.fn(),
+ configure: vi.fn(),
+ run: vi.fn(),
+ },
+ };
+});
+
+vi.mock('@storybook/global', () => ({
+ global: {
+ document: documentMock,
+ },
+}));
+
+vi.mock('axe-core', () => ({
+ default: axeMock,
+}));
+
vi.mock('storybook/preview-api');
const mockedAddons = vi.mocked(addons);
+const axeResults = {
+ violations: [],
+ passes: [],
+ incomplete: [],
+ inapplicable: [],
+} as Partial as AxeResults;
+
describe('a11yRunner', () => {
let mockChannel: { on: Mock; emit?: Mock };
beforeEach(() => {
- mockedAddons.getChannel.mockReset();
+ vi.resetModules();
+ vi.clearAllMocks();
+
+ documentMock.getElementById.mockReturnValue(null);
+ axeMock.run.mockResolvedValue(axeResults);
mockChannel = { on: vi.fn(), emit: vi.fn() };
- mockedAddons.getChannel.mockReturnValue(mockChannel as any);
+ mockedAddons.getChannel.mockReturnValue(
+ mockChannel as unknown as ReturnType
+ );
});
it('should listen to events', async () => {
@@ -24,4 +65,67 @@ describe('a11yRunner', () => {
expect(mockedAddons.getChannel).toHaveBeenCalled();
expect(mockChannel.on).toHaveBeenCalledWith(EVENTS.MANUAL, expect.any(Function));
});
+
+ it('passes disabled configured rules to axe.run when runOnly is present', async () => {
+ const { run } = await import('./a11yRunner.ts');
+ const input = {
+ config: {
+ rules: [
+ { id: 'target-size', enabled: false },
+ { id: 'color-contrast', enabled: true },
+ ],
+ },
+ options: {
+ runOnly: ['wcag2a'],
+ rules: {
+ 'button-name': { enabled: false },
+ },
+ },
+ };
+
+ await run(input, 'example-story');
+
+ expect(axeMock.configure).toHaveBeenCalledWith({
+ rules: [
+ { id: 'region', enabled: false },
+ { id: 'target-size', enabled: false },
+ { id: 'color-contrast', enabled: true },
+ ],
+ });
+ expect(axeMock.run).toHaveBeenCalledWith(expect.any(Object), {
+ runOnly: ['wcag2a'],
+ rules: {
+ region: { enabled: false },
+ 'target-size': { enabled: false },
+ 'button-name': { enabled: false },
+ },
+ });
+ expect(axeMock.run.mock.calls[0][1]).not.toBe(input.options);
+ expect(input.options).toEqual({
+ runOnly: ['wcag2a'],
+ rules: {
+ 'button-name': { enabled: false },
+ },
+ });
+ });
+
+ it('respects configured rule overrides when collecting disabled rules', async () => {
+ const { run } = await import('./a11yRunner.ts');
+
+ await run(
+ {
+ config: {
+ rules: [{ id: 'region', enabled: true }],
+ },
+ options: {
+ runOnly: ['wcag2a'],
+ },
+ },
+ 'example-story'
+ );
+
+ expect(axeMock.run).toHaveBeenCalledWith(expect.any(Object), {
+ runOnly: ['wcag2a'],
+ });
+ });
});
diff --git a/code/addons/a11y/src/a11yRunner.ts b/code/addons/a11y/src/a11yRunner.ts
index 00fb4540f8a7..b5aa244c6a84 100644
--- a/code/addons/a11y/src/a11yRunner.ts
+++ b/code/addons/a11y/src/a11yRunner.ts
@@ -2,7 +2,7 @@ import { ElementA11yParameterError } from 'storybook/internal/preview-errors';
import { global } from '@storybook/global';
-import type { AxeResults, ContextProp, ContextSpec } from 'axe-core';
+import type { AxeResults, ContextProp, ContextSpec, RunOptions, Spec } from 'axe-core';
import { addons, waitForAnimations } from 'storybook/preview-api';
import { withLinkPaths } from './a11yRunnerUtils.ts';
@@ -21,6 +21,47 @@ const DISABLED_RULES = [
'region',
] as const;
+const getDisabledRules = (rules: Spec['rules'] = []) => {
+ const disabledRules: NonNullable = {};
+
+ // Rules are applied in order, so a later entry overrides an earlier one for the same id.
+ for (const { id, enabled } of rules) {
+ if (!id || typeof enabled !== 'boolean') {
+ continue;
+ }
+
+ if (enabled) {
+ delete disabledRules[id];
+ } else {
+ disabledRules[id] = { enabled: false };
+ }
+ }
+
+ return disabledRules;
+};
+
+const mergeDisabledRulesIntoRunOptions = (options: RunOptions, config: Spec): RunOptions => {
+ // axe.run({ runOnly }) can re-enable tagged rules, so mirror configured disables into
+ // the same run options object without mutating the user's parameters.
+ if (!options.runOnly) {
+ return options;
+ }
+
+ const disabledRules = getDisabledRules(config.rules);
+
+ if (Object.keys(disabledRules).length === 0) {
+ return options;
+ }
+
+ return {
+ ...options,
+ rules: {
+ ...disabledRules,
+ ...options.rules,
+ },
+ };
+};
+
// A simple queue to run axe-core in sequence
// This is necessary because axe-core is not designed to run in parallel
const queue: (() => Promise)[] = [];
@@ -92,6 +133,8 @@ export const run = async (input: A11yParameters = DEFAULT_PARAMETERS, storyId: s
axe.configure(configWithDefault);
+ const optionsWithDisabledRules = mergeDisabledRulesIntoRunOptions(options, configWithDefault);
+
return new Promise((resolve, reject) => {
const highlightsRoot = document?.getElementById('storybook-highlights-root');
if (highlightsRoot) {
@@ -100,7 +143,7 @@ export const run = async (input: A11yParameters = DEFAULT_PARAMETERS, storyId: s
const task = async () => {
try {
- const result = await axe.run(context, options);
+ const result = await axe.run(context, optionsWithDisabledRules);
const resultWithLinks = withLinkPaths(result, storyId);
resolve(resultWithLinks);
} catch (error) {
diff --git a/code/core/package.json b/code/core/package.json
index 153083bfe561..20e047c0fa75 100644
--- a/code/core/package.json
+++ b/code/core/package.json
@@ -380,7 +380,8 @@
"tinyspy": "^3.0.2",
"ts-dedent": "^2.0.0",
"tsconfig-paths": "^4.2.0",
- "type-fest": "^4.18.1",
+ "type-fest": "^5.6.0",
+ "type-plus": "^8.0.0-beta.8",
"typescript": "^5.8.3",
"unique-string": "^3.0.0",
"use-resize-observer": "^9.1.0",
diff --git a/code/core/src/cli/dev.ts b/code/core/src/cli/dev.ts
index 1b59ba9e4223..7127cd82df3b 100644
--- a/code/core/src/cli/dev.ts
+++ b/code/core/src/cli/dev.ts
@@ -49,7 +49,7 @@ export const dev = async (cliOptions: CLIOptions) => {
configType: 'DEVELOPMENT',
ignorePreview: !!cliOptions.previewUrl && !cliOptions.forceBuildPreview,
cache: cache as any,
- packageJson: packageJson as unknown as PackageJson, // type-fest types are wrong here because we're on an outdated version of the package
+ packageJson: packageJson,
} as Options;
await withTelemetry(
diff --git a/code/core/src/cli/eslintPlugin.ts b/code/core/src/cli/eslintPlugin.ts
index 95163eb619cb..573fb360e811 100644
--- a/code/core/src/cli/eslintPlugin.ts
+++ b/code/core/src/cli/eslintPlugin.ts
@@ -315,16 +315,15 @@ export async function configureEslintPlugin({
}
} else {
logger.debug('No ESLint config file found, configuring in package.json instead');
- const { packageJson } = packageManager.primaryPackageJson;
+ const { packageJson, operationDir } = packageManager.primaryPackageJson;
const existingExtends = normalizeExtends(packageJson.eslintConfig?.extends).filter(Boolean);
- packageManager.writePackageJson({
- ...packageJson,
- eslintConfig: {
- ...packageJson.eslintConfig,
- extends: [...existingExtends, 'plugin:storybook/recommended'],
- },
- });
+ packageJson.eslintConfig = {
+ ...packageJson.eslintConfig,
+ extends: [...existingExtends, 'plugin:storybook/recommended'],
+ };
+
+ packageManager.writePackageJson(packageJson, operationDir);
}
}
diff --git a/code/core/src/common/js-package-manager/JsPackageManager.ts b/code/core/src/common/js-package-manager/JsPackageManager.ts
index 8b8322e77e27..0c2b70f72d6b 100644
--- a/code/core/src/common/js-package-manager/JsPackageManager.ts
+++ b/code/core/src/common/js-package-manager/JsPackageManager.ts
@@ -271,12 +271,10 @@ export abstract class JsPackageManager {
// Update cache with the written content
// Ensure dependencies and devDependencies exist (even if empty) to match PackageJsonWithDepsAndDevDeps type
- const cachedPackageJson: PackageJsonWithIndent = {
- ...packageJsonToWrite,
- dependencies: { ...(packageJsonToWrite.dependencies || {}) },
- devDependencies: { ...(packageJsonToWrite.devDependencies || {}) },
- peerDependencies: { ...(packageJsonToWrite.peerDependencies || {}) },
- };
+ const cachedPackageJson = packageJsonToWrite as PackageJsonWithIndent;
+ cachedPackageJson.dependencies = { ...(packageJsonToWrite.dependencies || {}) };
+ cachedPackageJson.devDependencies = { ...(packageJsonToWrite.devDependencies || {}) };
+ cachedPackageJson.peerDependencies = { ...(packageJsonToWrite.peerDependencies || {}) };
cachedPackageJson[indentSymbol] = indent;
JsPackageManager.packageJsonCache.set(packageJsonPath, cachedPackageJson);
}
@@ -612,16 +610,11 @@ export abstract class JsPackageManager {
public addScripts(scripts: Record) {
const { operationDir, packageJson } = this.#getPrimaryPackageJson();
- this.writePackageJson(
- {
- ...packageJson,
- scripts: {
- ...packageJson.scripts,
- ...scripts,
- },
- },
- operationDir
- );
+ packageJson.scripts = {
+ ...packageJson.scripts,
+ ...scripts,
+ };
+ this.writePackageJson(packageJson, operationDir);
}
public addPackageResolutions(versions: Record) {
diff --git a/code/core/src/core-server/build-dev.ts b/code/core/src/core-server/build-dev.ts
index 1c9431b3a71d..9ba3aeafac96 100644
--- a/code/core/src/core-server/build-dev.ts
+++ b/code/core/src/core-server/build-dev.ts
@@ -35,6 +35,11 @@ import { getManagerBuilder, getPreviewBuilder } from './utils/get-builders.ts';
import { getServerChannel } from './utils/get-server-channel.ts';
import { outputStartupInformation } from './utils/output-startup-information.ts';
import { outputStats } from './utils/output-stats.ts';
+import {
+ getMcpMetadataFromMainConfig,
+ type RuntimeInstanceRecord,
+ writeStorybookRuntimeInstanceRecord,
+} from './utils/runtime-instance-registry.ts';
import { getServerAddresses, getServerChannelUrl, getServerPort } from './utils/server-address.ts';
import { getServer } from './utils/server-init.ts';
import { stripCommentsAndStrings } from './utils/strip-comments-and-strings.ts';
@@ -303,6 +308,18 @@ export async function buildDevStandalone(
storybookDevServer(fullOptions, server)
);
+ const mcp: RuntimeInstanceRecord['mcp'] = getMcpMetadataFromMainConfig(config);
+
+ await writeStorybookRuntimeInstanceRecord({
+ address: localAddress,
+ mcp,
+ port,
+ storybookVersion,
+ }).catch((error: unknown) => {
+ logger.warn('Storybook failed to write its runtime instance registry record.');
+ logger.debug(error instanceof Error ? (error.stack ?? error.message) : String(error));
+ });
+
const previewTotalTime = previewResult?.totalTime;
const managerTotalTime = managerResult?.totalTime;
const previewStats = previewResult?.stats;
diff --git a/code/core/src/core-server/utils/runtime-instance-registry.test.ts b/code/core/src/core-server/utils/runtime-instance-registry.test.ts
new file mode 100644
index 000000000000..2dcc1f02a4b3
--- /dev/null
+++ b/code/core/src/core-server/utils/runtime-instance-registry.test.ts
@@ -0,0 +1,151 @@
+import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync } from 'node:fs';
+import { tmpdir } from 'node:os';
+
+import { join, resolve } from 'pathe';
+import { afterEach, describe, expect, it } from 'vitest';
+
+import {
+ createRuntimeInstanceRecord,
+ getMcpMetadataFromMainConfig,
+ writeRuntimeInstanceRecord,
+ writeStorybookRuntimeInstanceRecord,
+} from './runtime-instance-registry.ts';
+
+const tempDirs: string[] = [];
+
+function makeTempDir() {
+ const dir = mkdtempSync(join(tmpdir(), 'storybook-runtime-registry-'));
+ tempDirs.push(dir);
+ return dir;
+}
+
+afterEach(() => {
+ while (tempDirs.length > 0) {
+ rmSync(tempDirs.pop()!, { force: true, recursive: true });
+ }
+});
+
+describe('getMcpMetadataFromMainConfig', () => {
+ it('marks MCP as not-installed when addon-mcp is not configured', () => {
+ expect(getMcpMetadataFromMainConfig({ addons: ['@storybook/addon-docs'] })).toEqual({
+ status: 'not-installed',
+ });
+ });
+
+ it('uses the default endpoint when addon-mcp is configured as a string', () => {
+ expect(getMcpMetadataFromMainConfig({ addons: ['@storybook/addon-mcp'] })).toEqual({
+ status: 'ready',
+ endpoint: '/mcp',
+ });
+ });
+
+ it('uses endpoint from addon-mcp options', () => {
+ expect(
+ getMcpMetadataFromMainConfig({
+ addons: [{ name: '@storybook/addon-mcp', options: { endpoint: '/custom-mcp' } }],
+ })
+ ).toEqual({
+ status: 'ready',
+ endpoint: '/custom-mcp',
+ });
+ });
+});
+
+describe('createRuntimeInstanceRecord', () => {
+ const baseOptions = {
+ address: 'http://localhost:6006/?path=/docs/button--primary',
+ instanceId: '00000000-0000-4000-8000-000000000000',
+ now: new Date('2026-05-18T12:00:00.000Z'),
+ pid: 12345,
+ port: 6006,
+ storybookVersion: '10.5.0-alpha.0',
+ };
+
+ it('creates a schemaVersion 1 runtime instance record', () => {
+ const cwd = join(tmpdir(), 'storybook-project', '..', 'storybook-project');
+
+ expect(createRuntimeInstanceRecord({ ...baseOptions, cwd })).toEqual({
+ schemaVersion: 1,
+ instanceId: '00000000-0000-4000-8000-000000000000',
+ pid: 12345,
+ cwd: resolve(cwd),
+ url: 'http://localhost:6006',
+ port: 6006,
+ storybookVersion: '10.5.0-alpha.0',
+ startedAt: '2026-05-18T12:00:00.000Z',
+ updatedAt: '2026-05-18T12:00:00.000Z',
+ mcp: { status: 'not-installed' },
+ });
+ });
+
+ it('marks MCP as not-installed by default', () => {
+ const record = createRuntimeInstanceRecord(baseOptions);
+
+ expect(record.mcp).toEqual({ status: 'not-installed' });
+ expect(record.mcp).not.toHaveProperty('endpoint');
+ });
+
+ it('uses provided MCP state', () => {
+ const record = createRuntimeInstanceRecord({
+ ...baseOptions,
+ address: 'http://localhost:7007/?path=/story/example--primary',
+ mcp: { status: 'ready', endpoint: '/storybook-mcp' },
+ port: 7007,
+ });
+
+ expect(record.mcp).toEqual({
+ status: 'ready',
+ endpoint: '/storybook-mcp',
+ });
+ });
+
+ it('stores only the Storybook origin in url and excludes initial path query params', () => {
+ const record = createRuntimeInstanceRecord({
+ ...baseOptions,
+ address: 'https://localhost:8008/?path=/story/example--primary',
+ port: 8008,
+ });
+
+ expect(record.url).toBe('https://localhost:8008');
+ expect(record.mcp).toEqual({ status: 'not-installed' });
+ });
+});
+
+describe('writeRuntimeInstanceRecord', () => {
+ it('writes via a temporary file in the registry directory and renames to the final JSON path', async () => {
+ const registryDir = makeTempDir();
+ const record = createRuntimeInstanceRecord({
+ address: 'http://localhost:6006/',
+ instanceId: '00000000-0000-4000-8000-000000000001',
+ now: new Date('2026-05-18T12:00:00.000Z'),
+ pid: 12345,
+ port: 6006,
+ storybookVersion: '10.5.0-alpha.0',
+ });
+
+ const recordPath = await writeRuntimeInstanceRecord(record, registryDir);
+
+ expect(recordPath).toBe(join(registryDir, `${record.instanceId}.json`));
+ expect(readdirSync(registryDir)).toEqual([`${record.instanceId}.json`]);
+ expect(JSON.parse(readFileSync(recordPath, 'utf-8'))).toEqual(record);
+ });
+});
+
+describe('writeStorybookRuntimeInstanceRecord', () => {
+ it('cleans up the written instance record', async () => {
+ const registryDir = makeTempDir();
+ const registration = await writeStorybookRuntimeInstanceRecord({
+ address: 'http://localhost:6006/',
+ port: 6006,
+ registerCleanup: false,
+ registryDir,
+ storybookVersion: '10.5.0-alpha.0',
+ });
+
+ expect(existsSync(registration.recordPath)).toBe(true);
+
+ await registration.cleanup();
+
+ expect(existsSync(registration.recordPath)).toBe(false);
+ });
+});
diff --git a/code/core/src/core-server/utils/runtime-instance-registry.ts b/code/core/src/core-server/utils/runtime-instance-registry.ts
new file mode 100644
index 000000000000..0b6b19d2bc97
--- /dev/null
+++ b/code/core/src/core-server/utils/runtime-instance-registry.ts
@@ -0,0 +1,178 @@
+import { existsSync, rmSync } from 'node:fs';
+import { mkdir, rename, rm, writeFile } from 'node:fs/promises';
+import { randomUUID } from 'node:crypto';
+import { homedir } from 'node:os';
+
+import type { StorybookConfig } from 'storybook/internal/types';
+
+import { join, resolve } from 'pathe';
+
+const STORYBOOK_MCP_ADDON = '@storybook/addon-mcp';
+const DEFAULT_MCP_ENDPOINT = '/mcp';
+
+export type RuntimeInstanceRecord = {
+ schemaVersion: 1;
+ instanceId: string;
+ pid: number;
+ cwd: string;
+ url: string;
+ port: number;
+ storybookVersion: string;
+ startedAt: string;
+ updatedAt: string;
+ mcp: { status: 'not-installed' } | { status: 'ready'; endpoint: string };
+};
+
+export type RuntimeInstanceRegistration = {
+ record: RuntimeInstanceRecord;
+ recordPath: string;
+ cleanup: () => Promise;
+ unregisterProcessCleanup: () => void;
+};
+
+export function getDefaultRuntimeInstanceRegistryDir() {
+ return join(homedir(), '.storybook', 'instances');
+}
+
+export function getOrigin(address: string) {
+ return new URL(address).origin;
+}
+
+export function getMcpMetadataFromMainConfig(
+ mainConfig: Pick
+): RuntimeInstanceRecord['mcp'] {
+ const addon = mainConfig.addons?.find(
+ (entry) =>
+ entry === STORYBOOK_MCP_ADDON ||
+ (typeof entry === 'object' && entry.name === STORYBOOK_MCP_ADDON)
+ );
+
+ if (!addon) {
+ return { status: 'not-installed' };
+ }
+
+ const endpoint =
+ typeof addon === 'object' && typeof addon.options?.endpoint === 'string'
+ ? addon.options.endpoint
+ : DEFAULT_MCP_ENDPOINT;
+
+ return { status: 'ready', endpoint };
+}
+
+export function createRuntimeInstanceRecord({
+ address,
+ cwd = process.cwd(),
+ instanceId = randomUUID(),
+ mcp = { status: 'not-installed' },
+ now = new Date(),
+ pid = process.pid,
+ port,
+ storybookVersion,
+}: {
+ address: string;
+ cwd?: string;
+ instanceId?: string;
+ mcp?: RuntimeInstanceRecord['mcp'];
+ now?: Date;
+ pid?: number;
+ port: number;
+ storybookVersion: string;
+}): RuntimeInstanceRecord {
+ const origin = getOrigin(address);
+ const timestamp = now.toISOString();
+
+ return {
+ schemaVersion: 1,
+ instanceId,
+ pid,
+ cwd: resolve(cwd),
+ url: origin,
+ port,
+ storybookVersion,
+ startedAt: timestamp,
+ updatedAt: timestamp,
+ mcp,
+ };
+}
+
+export async function writeRuntimeInstanceRecord(
+ record: RuntimeInstanceRecord,
+ registryDir = getDefaultRuntimeInstanceRegistryDir()
+) {
+ await mkdir(registryDir, { recursive: true });
+
+ const recordPath = join(registryDir, `${record.instanceId}.json`);
+ const tempPath = join(
+ registryDir,
+ `${record.instanceId}.${record.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`
+ );
+
+ try {
+ await writeFile(tempPath, `${JSON.stringify(record, null, 2)}\n`, 'utf-8');
+ await rename(tempPath, recordPath);
+ } catch (error) {
+ await rm(tempPath, { force: true }).catch(() => undefined);
+ throw error;
+ }
+
+ return recordPath;
+}
+
+function registerProcessCleanup(recordPath: string) {
+ const cleanupSync = () => {
+ if (existsSync(recordPath)) {
+ rmSync(recordPath, { force: true });
+ }
+ };
+
+ process.once('exit', cleanupSync);
+ process.prependOnceListener('SIGINT', cleanupSync);
+ process.prependOnceListener('SIGTERM', cleanupSync);
+
+ return () => {
+ process.off('exit', cleanupSync);
+ process.off('SIGINT', cleanupSync);
+ process.off('SIGTERM', cleanupSync);
+ };
+}
+
+export async function writeStorybookRuntimeInstanceRecord({
+ address,
+ cwd,
+ mcp,
+ pid,
+ port,
+ registryDir,
+ registerCleanup = true,
+ storybookVersion,
+}: {
+ address: string;
+ cwd?: string;
+ mcp?: RuntimeInstanceRecord['mcp'];
+ pid?: number;
+ port: number;
+ registryDir?: string;
+ registerCleanup?: boolean;
+ storybookVersion: string;
+}): Promise {
+ const record = createRuntimeInstanceRecord({
+ address,
+ cwd,
+ mcp,
+ pid,
+ port,
+ storybookVersion,
+ });
+ const recordPath = await writeRuntimeInstanceRecord(record, registryDir);
+ const unregisterProcessCleanup = registerCleanup ? registerProcessCleanup(recordPath) : () => {};
+
+ return {
+ record,
+ recordPath,
+ unregisterProcessCleanup,
+ cleanup: async () => {
+ unregisterProcessCleanup();
+ await rm(recordPath, { force: true });
+ },
+ };
+}
diff --git a/code/core/src/csf/csf-factories.test.ts b/code/core/src/csf/csf-factories.test.ts
index b3c146c8f6dd..c80a6f6b3ecb 100644
--- a/code/core/src/csf/csf-factories.test.ts
+++ b/code/core/src/csf/csf-factories.test.ts
@@ -1,7 +1,9 @@
//* @vitest-environment happy-dom */
-import { describe, expect, test, vi } from 'vitest';
+import { describe, expect, expectTypeOf, test, vi } from 'vitest';
+import { testType } from 'type-plus';
import { definePreview, definePreviewAddon, getStoryChildren } from './csf-factories.ts';
+import type { Tag } from './story.ts';
interface Addon1Types {
parameters: { foo?: { value: string } };
@@ -76,3 +78,60 @@ describe('test function', () => {
expect(testFn).toHaveBeenCalled();
});
});
+
+describe('customize tags type', () => {
+ // Customizing tags type enables autocompletion of tags.
+ test('with addon', () => {
+ const addon = definePreviewAddon<{ tags: Array<'foo' | 'bar' | (string & {})> }>({});
+ const preview = definePreview({ addons: [addon] });
+ const meta = preview.meta({
+ tags: ['foo', 'something-else'],
+ });
+ meta.story({
+ tags: ['foo', 'something-else'],
+ });
+ testType.canAssign<
+ Parameters[0]['tags'],
+ Array<'foo' | 'bar' | (string & {})>
+ >(true);
+ testType.canAssign<
+ Parameters[0] extends Object
+ ? Parameters[0]['tags']
+ : never,
+ Array<'foo' | 'bar' | (string & {})>
+ >(true);
+ testType.canAssign<
+ Parameters[0] extends Object
+ ? Parameters[0]['tags']
+ : never,
+ Tag[]
+ >(true);
+ });
+ test('with type method', () => {
+ const preview = definePreview({ addons: [] }).type<{
+ tags: Array<'foo' | 'bar' | (string & {})>;
+ }>();
+ const meta = preview.meta({
+ tags: ['foo', 'something-else'],
+ });
+ meta.story({
+ tags: ['foo', 'something-else'],
+ });
+ testType.canAssign<
+ Parameters[0]['tags'],
+ Array<'foo' | 'bar' | (string & {})>
+ >(true);
+ testType.canAssign<
+ Parameters[0] extends Object
+ ? Parameters[0]['tags']
+ : never,
+ Array<'foo' | 'bar' | (string & {})>
+ >(true);
+ testType.canAssign<
+ Parameters[0] extends Object
+ ? Parameters[0]['tags']
+ : never,
+ Tag[]
+ >(true);
+ });
+});
diff --git a/code/core/src/csf/story.ts b/code/core/src/csf/story.ts
index e2c8c4f57df5..5d8a3d27704d 100644
--- a/code/core/src/csf/story.ts
+++ b/code/core/src/csf/story.ts
@@ -1,4 +1,4 @@
-import type { RemoveIndexSignature, Simplify, UnionToIntersection } from 'type-fest';
+import type { OmitIndexSignature, Simplify, UnionToIntersection } from 'type-fest';
import type { ToolbarArgType } from '../toolbar/index.ts';
import type { SBScalarType, SBType } from './SBType.ts';
@@ -185,6 +185,7 @@ export interface GlobalTypes {
* type-checked across all stories.
*/
export interface AddonTypes {
+ tags?: Tag[] | undefined;
args?: unknown;
parameters?: Record;
globals?: Record;
@@ -413,7 +414,7 @@ export interface BaseAnnotations;
/** Named tags for a story, used to filter stories in different contexts. */
- tags?: Tag[];
+ tags?: (TRenderer['tags'] extends Tag[] ? TRenderer['tags'] : Tag[]) | undefined;
mount?: (context: StoryContext) => TRenderer['mount'];
}
@@ -588,7 +589,7 @@ export type ArgsFromMeta = Meta extends {
decorators?: (infer Decorators)[] | (infer Decorators);
}
? Simplify<
- RemoveIndexSignature<
+ OmitIndexSignature<
RArgs & DecoratorsArgs & LoaderArgs
>
>
diff --git a/code/core/src/manager-errors.ts b/code/core/src/manager-errors.ts
index b33843655ddd..0767c0685782 100644
--- a/code/core/src/manager-errors.ts
+++ b/code/core/src/manager-errors.ts
@@ -18,6 +18,7 @@ export enum Category {
MANAGER_CORE_EVENTS = 'MANAGER_CORE-EVENTS',
MANAGER_ROUTER = 'MANAGER_ROUTER',
MANAGER_THEMING = 'MANAGER_THEMING',
+ MANAGER_UNIVERSAL_STORE = 'MANAGER_UNIVERSAL-STORE',
}
export class ProviderDoesNotExtendBaseProviderError extends StorybookError {
@@ -66,3 +67,14 @@ export class StatusTypeIdMismatchError extends StorybookError {
});
}
}
+
+export class UniversalStoreFollowerTimeoutError extends StorybookError {
+ constructor(followerId: string) {
+ super({
+ name: 'UniversalStoreFollowerTimeoutError',
+ category: Category.MANAGER_UNIVERSAL_STORE,
+ code: 1,
+ message: `Timed out waiting for leader state for UniversalStore follower with id: '${followerId}'. Ensure a leader with the same id exists and is reachable before creating a follower.`,
+ });
+ }
+}
diff --git a/code/core/src/preview-api/modules/preview-web/docs-context/DocsContext.test.ts b/code/core/src/preview-api/modules/preview-web/docs-context/DocsContext.test.ts
index b26c3d165d1a..585aab1f44d5 100644
--- a/code/core/src/preview-api/modules/preview-web/docs-context/DocsContext.test.ts
+++ b/code/core/src/preview-api/modules/preview-web/docs-context/DocsContext.test.ts
@@ -112,6 +112,50 @@ describe('referenceMeta', () => {
' must reference a CSF file module export or meta export. Did you mistakenly reference your component instead of your CSF file?'
);
});
+
+ it('works with different module namespace objects when there is no default export', () => {
+ // Simulates CSF4 modules (no `export default meta`) split into a chunk:
+ // the MDX-imported namespace differs by identity from the one Storybook registered.
+ // Resolution should fall back to looking up the CSF file via any story export.
+ const { story, csfFile, storyExport } = csfFileParts('meta--story', 'meta', {
+ includeDefaultExport: false,
+ });
+ const store = {
+ componentStoriesFromCSFFile: () => [story],
+ } as unknown as StoryStore;
+ const context = new DocsContext(channel, store, renderStoryToElement, [csfFile]);
+
+ const differentModuleExports = { story: storyExport };
+
+ expect(() => context.referenceMeta(differentModuleExports, true)).not.toThrow();
+ expect(context.storyById()).toEqual(story);
+ });
+
+ it('throws for module objects whose story exports span multiple CSF files', () => {
+ const firstParts = csfFileParts('first-meta--first-story', 'first-meta', {
+ includeDefaultExport: false,
+ });
+ const secondParts = csfFileParts('second-meta--second-story', 'second-meta', {
+ includeDefaultExport: false,
+ });
+ const store = {
+ componentStoriesFromCSFFile: ({ csfFile }: { csfFile: CSFFile }) =>
+ csfFile === firstParts.csfFile ? [firstParts.story] : [secondParts.story],
+ } as unknown as StoryStore;
+ const context = new DocsContext(channel, store, renderStoryToElement, [
+ firstParts.csfFile,
+ secondParts.csfFile,
+ ]);
+
+ const mixedModuleExports = {
+ first: firstParts.storyExport,
+ second: secondParts.storyExport,
+ };
+
+ expect(() => context.referenceMeta(mixedModuleExports, true)).toThrow(
+ ' must reference a CSF file module export or meta export. Did you mistakenly reference your component instead of your CSF file?'
+ );
+ });
});
describe('resolveOf', () => {
@@ -157,6 +201,38 @@ describe('resolveOf', () => {
});
});
+ it('works for CSF4 module exports with different object identity and no default export', () => {
+ // CSF4 modules (no `export default meta`) may be split into a separate chunk,
+ // producing a namespace object whose identity differs from Storybook's record.
+ // Resolution falls back to identifying the CSF file via the story exports.
+ const noDefaultParts = csfFileParts('meta--no-default-story', 'meta-no-default', {
+ includeDefaultExport: false,
+ });
+ const noDefaultStore = {
+ componentStoriesFromCSFFile: () => [noDefaultParts.story],
+ preparedMetaFromCSFFile: () => ({ prepareMeta: 'preparedMeta' }),
+ projectAnnotations,
+ } as unknown as StoryStore;
+ const noDefaultContext = new DocsContext(channel, noDefaultStore, renderStoryToElement, [
+ noDefaultParts.csfFile,
+ ]);
+ noDefaultContext.attachCSFFile(noDefaultParts.csfFile);
+
+ expect(noDefaultContext.resolveOf({ story: noDefaultParts.storyExport })).toEqual({
+ type: 'meta',
+ csfFile: noDefaultParts.csfFile,
+ preparedMeta: expect.any(Object),
+ });
+ });
+
+ it('resolves a CSF4 Story object to its story, not its containing CSF file', () => {
+ // A CSF4 Story (detected via `_tag === 'Story'`) is an individual export,
+ // not a namespace. The namespace fallback must skip it so that
+ // still resolves to the Primary story.
+ const csf4Story = { _tag: 'Story', input: storyExport };
+ expect(context.resolveOf(csf4Story, ['story'])).toEqual({ type: 'story', story });
+ });
+
it('works for components', () => {
expect(context.resolveOf(component)).toEqual({
type: 'component',
diff --git a/code/core/src/preview-api/modules/preview-web/docs-context/DocsContext.ts b/code/core/src/preview-api/modules/preview-web/docs-context/DocsContext.ts
index 803b9aca0fdd..00a7b87b7532 100644
--- a/code/core/src/preview-api/modules/preview-web/docs-context/DocsContext.ts
+++ b/code/core/src/preview-api/modules/preview-web/docs-context/DocsContext.ts
@@ -57,8 +57,11 @@ export class DocsContext implements DocsContextProps
referenceCSFFile(csfFile: CSFFile) {
this.exportsToCSFFile.set(csfFile.moduleExports, csfFile);
// Also set the default export as the component's exports,
- // to allow `import ButtonStories from './Button.stories'`
- this.exportsToCSFFile.set(csfFile.moduleExports.default, csfFile);
+ // to allow `import ButtonStories from './Button.stories'`.
+ // CSF4 modules may not have a default export, so guard against it.
+ if ('default' in csfFile.moduleExports) {
+ this.exportsToCSFFile.set(csfFile.moduleExports.default, csfFile);
+ }
const stories = this.store.componentStoriesFromCSFFile({ csfFile });
@@ -171,6 +174,40 @@ export class DocsContext implements DocsContextProps
csfFile = this.exportsToCSFFile.get((moduleExportOrType as ModuleExports).default);
}
+ // CSF4 modules don't have a default export, and when a bundler splits the
+ // story module into a separate chunk the namespace passed to
+ // may differ by object identity from the one Storybook registered. Fall back
+ // to resolving the CSF file via any of its story exports.
+ // Skip individual story objects (handled by the story lookup below).
+ if (
+ !csfFile &&
+ moduleExportOrType &&
+ typeof moduleExportOrType === 'object' &&
+ !isStory(moduleExportOrType)
+ ) {
+ let matchedCSFFile: CSFFile | undefined;
+ for (const exportValue of Object.values(moduleExportOrType as ModuleExports)) {
+ const story = this.exportToStory.get(
+ isStory(exportValue) ? exportValue.input : exportValue
+ );
+ if (!story) {
+ continue;
+ }
+ const storyCSFFile = this.storyIdToCSFFile.get(story.id);
+ if (!storyCSFFile) {
+ continue;
+ }
+ if (!matchedCSFFile) {
+ matchedCSFFile = storyCSFFile;
+ } else if (matchedCSFFile !== storyCSFFile) {
+ // Story exports span multiple CSF files — ambiguous, reject.
+ matchedCSFFile = undefined;
+ break;
+ }
+ }
+ csfFile = matchedCSFFile;
+ }
+
if (csfFile) {
return { type: 'meta', csfFile } as TResolvedExport;
}
diff --git a/code/core/src/preview-api/modules/preview-web/docs-context/test-utils.ts b/code/core/src/preview-api/modules/preview-web/docs-context/test-utils.ts
index af0a6836388e..74727c3cdec6 100644
--- a/code/core/src/preview-api/modules/preview-web/docs-context/test-utils.ts
+++ b/code/core/src/preview-api/modules/preview-web/docs-context/test-utils.ts
@@ -1,11 +1,17 @@
import type { CSFFile, PreparedStory } from 'storybook/internal/types';
-export function csfFileParts(storyId = 'meta--story', metaId = 'meta') {
+export function csfFileParts(
+ storyId = 'meta--story',
+ metaId = 'meta',
+ { includeDefaultExport = true }: { includeDefaultExport?: boolean } = {}
+) {
// These compose the raw exports of the CSF file
const component = {};
const metaExport = { component };
const storyExport = {};
- const moduleExports = { default: metaExport, story: storyExport };
+ const moduleExports = includeDefaultExport
+ ? { default: metaExport, story: storyExport }
+ : { story: storyExport };
// This is the prepared story + CSF file after SB has processed them
const storyAnnotations = {
diff --git a/code/core/src/shared/universal-store/index.test.ts b/code/core/src/shared/universal-store/index.test.ts
index c02c6aa6dbac..3a9ecd44e21d 100644
--- a/code/core/src/shared/universal-store/index.test.ts
+++ b/code/core/src/shared/universal-store/index.test.ts
@@ -6,6 +6,7 @@ import { UniversalStore } from './index.ts';
import { instances as mockedInstances } from './__mocks__/instances.ts';
import { MockUniversalStore } from './mock.ts';
import type { ChannelEvent } from './types.ts';
+import { UniversalStoreFollowerTimeoutError } from '../../manager-errors.ts';
vi.mock('./instances');
@@ -690,8 +691,8 @@ You should reuse the existing instance instead of trying to create a new one.`);
// Assert - eventually the follower.untilReady() promise should throw an error when the timeout is reached
vi.advanceTimersToNextTimer();
- await expect(follower.untilReady()).rejects.toThrowErrorMatchingInlineSnapshot(
- `[TypeError: No existing state found for follower with id: 'env1:test'. Make sure a leader with the same id exists before creating a follower.]`
+ await expect(follower.untilReady()).rejects.toBeInstanceOf(
+ UniversalStoreFollowerTimeoutError
);
expect(follower.status).toBe(UniversalStore.Status.ERROR);
});
diff --git a/code/core/src/shared/universal-store/index.ts b/code/core/src/shared/universal-store/index.ts
index 83e1c9d0a99b..b22912409b2d 100644
--- a/code/core/src/shared/universal-store/index.ts
+++ b/code/core/src/shared/universal-store/index.ts
@@ -16,6 +16,7 @@ import type {
StatusType,
StoreOptions,
} from './types.ts';
+import { UniversalStoreFollowerTimeoutError } from '../../manager-errors.ts';
const CHANNEL_EVENT_PREFIX = 'UNIVERSAL_STORE:' as const;
@@ -542,13 +543,7 @@ export class UniversalStore<
);
// 2. Wait 1 sec for a response, then reject the syncing promise if not already resolved
setTimeout(() => {
- // if the state is already resolved by a response before this timeout,
- // rejecting it doesn't do anything, it will be ignored
- this.syncing!.reject!(
- new TypeError(
- `No existing state found for follower with id: '${this.id}'. Make sure a leader with the same id exists before creating a follower.`
- )
- );
+ this.syncing!.reject!(new UniversalStoreFollowerTimeoutError(this.id));
}, 1000);
}
}
diff --git a/code/frameworks/angular/src/client/preview.ts b/code/frameworks/angular/src/client/preview.ts
index d8190b90256f..630c75d32a6d 100644
--- a/code/frameworks/angular/src/client/preview.ts
+++ b/code/frameworks/angular/src/client/preview.ts
@@ -16,7 +16,7 @@ import type {
StoryAnnotations,
} from 'storybook/internal/types';
-import type { RemoveIndexSignature, SetOptional, Simplify, UnionToIntersection } from 'type-fest';
+import type { OmitIndexSignature, SetOptional, Simplify, UnionToIntersection } from 'type-fest';
import * as angularAnnotations from './config.ts';
import * as angularDocsAnnotations from './docs/config.ts';
@@ -54,7 +54,7 @@ export function __definePreview[]>(
}
type InferArgs = Simplify<
- TArgs & Simplify>>
+ TArgs & Simplify>>
>;
type InferComponentArgs any> = Partial<
diff --git a/code/frameworks/react-vite/package.json b/code/frameworks/react-vite/package.json
index 7df5cde5817f..5d4c7a7b3416 100644
--- a/code/frameworks/react-vite/package.json
+++ b/code/frameworks/react-vite/package.json
@@ -71,8 +71,14 @@
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"storybook": "workspace:^",
+ "typescript": ">= 4.9.x",
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
},
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ },
"publishConfig": {
"access": "public"
},
diff --git a/code/frameworks/tanstack-react/src/index.ts b/code/frameworks/tanstack-react/src/index.ts
index 8d1be2f4436f..ceeb49365c6f 100644
--- a/code/frameworks/tanstack-react/src/index.ts
+++ b/code/frameworks/tanstack-react/src/index.ts
@@ -11,7 +11,7 @@ import type {
Renderer,
StoryAnnotations,
} from 'storybook/internal/types';
-import type { RemoveIndexSignature, Simplify, UnionToIntersection } from 'type-fest';
+import type { OmitIndexSignature, Simplify, UnionToIntersection } from 'type-fest';
import type { AnyRoute, FileRoutesByPath } from '@tanstack/react-router';
import type { ReactMeta, ReactPreview } from '@storybook/react';
@@ -44,7 +44,7 @@ type DecoratorsArgs = UnionToIntersectio
type InferCombinedTypes = ReactTypes &
T & {
args: Simplify<
- TArgs & Simplify>>
+ TArgs & Simplify>>
>;
};
diff --git a/code/lib/cli-storybook/src/codemod/csf-factories.ts b/code/lib/cli-storybook/src/codemod/csf-factories.ts
index a82e95e2677b..ffb1302d912b 100644
--- a/code/lib/cli-storybook/src/codemod/csf-factories.ts
+++ b/code/lib/cli-storybook/src/codemod/csf-factories.ts
@@ -103,7 +103,6 @@ export const csfFactories: CommandFix = {
);
packageJson.imports = {
...packageJson.imports,
- // @ts-expect-error we need to upgrade type-fest
'#*': ['./*', './*.ts', './*.tsx', './*.js', './*.jsx'],
};
packageManager.writePackageJson(packageJson);
diff --git a/code/package.json b/code/package.json
index da3023da6a60..a05546971076 100644
--- a/code/package.json
+++ b/code/package.json
@@ -196,5 +196,6 @@
"Dependency Upgrades"
]
]
- }
+ },
+ "deferredNextVersion": "10.5.0-alpha.2"
}
diff --git a/code/renderers/react/package.json b/code/renderers/react/package.json
index 252028606b34..5dc6b7b3f955 100644
--- a/code/renderers/react/package.json
+++ b/code/renderers/react/package.json
@@ -78,7 +78,7 @@
"react-element-to-jsx-string": "npm:@7rulnik/react-element-to-jsx-string@15.0.1",
"require-from-string": "^2.0.2",
"ts-dedent": "^2.0.0",
- "type-fest": "~2.19"
+ "type-fest": "^5.6.0"
},
"peerDependencies": {
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
diff --git a/code/renderers/react/src/preview.tsx b/code/renderers/react/src/preview.tsx
index 20662495a2f1..66fd92c21261 100644
--- a/code/renderers/react/src/preview.tsx
+++ b/code/renderers/react/src/preview.tsx
@@ -13,7 +13,7 @@ import type {
StoryAnnotations,
} from 'storybook/internal/types';
-import type { RemoveIndexSignature, SetOptional, Simplify, UnionToIntersection } from 'type-fest';
+import type { OmitIndexSignature, SetOptional, Simplify, UnionToIntersection } from 'type-fest';
import * as reactAnnotations from './entry-preview.tsx';
import * as reactArgTypesAnnotations from './entry-preview-argtypes.ts';
@@ -27,7 +27,7 @@ type DecoratorsArgs = UnionToIntersectio
>;
type InferArgs = Simplify<
- TArgs & Simplify>>
+ TArgs & Simplify>>
>;
type InferReactTypes = ReactTypes &
diff --git a/code/renderers/svelte/package.json b/code/renderers/svelte/package.json
index f17afa8e1478..a9616000154a 100644
--- a/code/renderers/svelte/package.json
+++ b/code/renderers/svelte/package.json
@@ -54,7 +54,7 @@
],
"dependencies": {
"ts-dedent": "^2.0.0",
- "type-fest": "~2.19"
+ "type-fest": "^5.6.0"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^6.2.0",
diff --git a/code/renderers/vue3/package.json b/code/renderers/vue3/package.json
index fbc58e7bf05a..e286f2652fc4 100644
--- a/code/renderers/vue3/package.json
+++ b/code/renderers/vue3/package.json
@@ -50,7 +50,7 @@
],
"dependencies": {
"@storybook/global": "^5.0.0",
- "type-fest": "~2.19",
+ "type-fest": "^5.6.0",
"vue-component-type-helpers": "^3.2.9"
},
"devDependencies": {
diff --git a/code/renderers/vue3/src/preview.ts b/code/renderers/vue3/src/preview.ts
index 4fc84bf4af2f..cdcd033af04d 100644
--- a/code/renderers/vue3/src/preview.ts
+++ b/code/renderers/vue3/src/preview.ts
@@ -16,7 +16,7 @@ import type {
StoryAnnotations,
} from 'storybook/internal/types';
-import type { RemoveIndexSignature, SetOptional, Simplify, UnionToIntersection } from 'type-fest';
+import type { OmitIndexSignature, SetOptional, Simplify, UnionToIntersection } from 'type-fest';
import * as vueAnnotations from './entry-preview.ts';
import * as vueDocsAnnotations from './entry-preview-docs.ts';
@@ -54,7 +54,7 @@ export function __definePreview[]>(
}
type InferArgs = Simplify<
- TArgs & Simplify>>
+ TArgs & Simplify>>
>;
type InferVueTypes = VueTypes &
diff --git a/code/renderers/vue3/src/public-types.ts b/code/renderers/vue3/src/public-types.ts
index 642345ca1084..1f57b5211aec 100644
--- a/code/renderers/vue3/src/public-types.ts
+++ b/code/renderers/vue3/src/public-types.ts
@@ -12,7 +12,7 @@ import type {
StrictArgs,
} from 'storybook/internal/types';
-import type { Constructor, RemoveIndexSignature, SetOptional, Simplify } from 'type-fest';
+import type { Constructor, OmitIndexSignature, SetOptional, Simplify } from 'type-fest';
import type { FunctionalComponent, VNodeChild } from 'vue';
import type { ComponentProps, ComponentSlots } from 'vue-component-type-helpers';
@@ -62,7 +62,7 @@ export type StoryObj = TMetaOrCmpOrArgs extends {
: never
: StoryAnnotations>;
-type ExtractSlots = AllowNonFunctionSlots>>>;
+type ExtractSlots = AllowNonFunctionSlots>>>;
type AllowNonFunctionSlots = {
[K in keyof Slots]: Slots[K] | VNodeChild;
diff --git a/code/renderers/web-components/src/preview.ts b/code/renderers/web-components/src/preview.ts
index 54a5753cad01..fc9a6e469003 100644
--- a/code/renderers/web-components/src/preview.ts
+++ b/code/renderers/web-components/src/preview.ts
@@ -16,7 +16,7 @@ import type {
StoryAnnotations,
} from 'storybook/internal/types';
-import type { RemoveIndexSignature, SetOptional, Simplify, UnionToIntersection } from 'type-fest';
+import type { OmitIndexSignature, SetOptional, Simplify, UnionToIntersection } from 'type-fest';
import * as webComponentsAnnotations from './entry-preview.ts';
import * as webComponentsDocsAnnotations from './entry-preview-docs.ts';
@@ -53,7 +53,7 @@ export function __definePreview[]>(
}
type InferArgs = Simplify<
- TArgs & Simplify>>
+ TArgs & Simplify>>
>;
type InferWebComponentsTypes = WebComponentsTypes &
diff --git a/package.json b/package.json
index 50daec9ca445..0528cdbf352c 100644
--- a/package.json
+++ b/package.json
@@ -58,7 +58,7 @@
"playwright": "1.58.2",
"playwright-core": "1.58.2",
"react": "^18.2.0",
- "type-fest": "~2.19",
+ "react-joyride/type-fest": "~2.19",
"typescript": "^5.9.3"
},
"devDependencies": {
diff --git a/scripts/ecosystem-ci/existing-resolutions.js b/scripts/ecosystem-ci/existing-resolutions.js
index 730972ec8982..d4c15b1d476b 100644
--- a/scripts/ecosystem-ci/existing-resolutions.js
+++ b/scripts/ecosystem-ci/existing-resolutions.js
@@ -23,6 +23,6 @@ export const EXISTING_RESOLUTIONS = new Set([
'playwright',
'playwright-core',
'react',
- 'type-fest',
+ 'react-joyride/type-fest',
'typescript',
]);
diff --git a/scripts/package.json b/scripts/package.json
index c92d8e5ccd31..ef90cf9c1a72 100644
--- a/scripts/package.json
+++ b/scripts/package.json
@@ -158,7 +158,7 @@
"tinyglobby": "^0.2.15",
"trash": "^7.2.0",
"ts-dedent": "^2.2.0",
- "type-fest": "~2.19",
+ "type-fest": "^5.6.0",
"typescript": "^5.9.3",
"uuid": "^9.0.1",
"vitest": "^4.1.5",
diff --git a/yarn.lock b/yarn.lock
index 09c81a9324ff..dfaecc1d88bd 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -9015,7 +9015,11 @@ __metadata:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
storybook: "workspace:^"
+ typescript: ">= 4.9.x"
vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0
+ peerDependenciesMeta:
+ typescript:
+ optional: true
languageName: unknown
linkType: soft
@@ -9066,7 +9070,7 @@ __metadata:
react-element-to-jsx-string: "npm:@7rulnik/react-element-to-jsx-string@15.0.1"
require-from-string: "npm:^2.0.2"
ts-dedent: "npm:^2.0.0"
- type-fest: "npm:~2.19"
+ type-fest: "npm:^5.6.0"
peerDependencies:
"@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
"@types/react-dom": ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
@@ -9222,7 +9226,7 @@ __metadata:
tinyglobby: "npm:^0.2.15"
trash: "npm:^7.2.0"
ts-dedent: "npm:^2.2.0"
- type-fest: "npm:~2.19"
+ type-fest: "npm:^5.6.0"
typescript: "npm:^5.9.3"
uuid: "npm:^9.0.1"
verdaccio: "npm:^5.33.0"
@@ -9301,7 +9305,7 @@ __metadata:
svelte-check: "npm:^4.3.2"
sveltedoc-parser: "npm:^4.2.1"
ts-dedent: "npm:^2.0.0"
- type-fest: "npm:~2.19"
+ type-fest: "npm:^5.6.0"
typescript: "npm:^5.8.3"
vite: "npm:^7.0.4"
peerDependencies:
@@ -9383,7 +9387,7 @@ __metadata:
"@storybook/global": "npm:^5.0.0"
"@testing-library/vue": "npm:^8.0.0"
"@vitejs/plugin-vue": "npm:^4.6.2"
- type-fest: "npm:~2.19"
+ type-fest: "npm:^5.6.0"
typescript: "npm:^5.8.3"
vue: "npm:^3.2.47"
vue-component-type-helpers: "npm:^3.2.9"
@@ -20594,6 +20598,13 @@ __metadata:
languageName: node
linkType: hard
+"is-buffer@npm:^2.0.5":
+ version: 2.0.5
+ resolution: "is-buffer@npm:2.0.5"
+ checksum: 10c0/e603f6fced83cf94c53399cff3bda1a9f08e391b872b64a73793b0928be3e5f047f2bcece230edb7632eaea2acdbfcb56c23b33d8a20c820023b230f1485679a
+ languageName: node
+ linkType: hard
+
"is-bun-module@npm:^2.0.0":
version: 2.0.0
resolution: "is-bun-module@npm:2.0.0"
@@ -29692,7 +29703,8 @@ __metadata:
tinyspy: "npm:^3.0.2"
ts-dedent: "npm:^2.0.0"
tsconfig-paths: "npm:^4.2.0"
- type-fest: "npm:^4.18.1"
+ type-fest: "npm:^5.6.0"
+ type-plus: "npm:^8.0.0-beta.8"
typescript: "npm:^5.8.3"
unique-string: "npm:^3.0.0"
use-resize-observer: "npm:^9.1.0"
@@ -30259,6 +30271,13 @@ __metadata:
languageName: node
linkType: hard
+"tagged-tag@npm:^1.0.0":
+ version: 1.0.0
+ resolution: "tagged-tag@npm:1.0.0"
+ checksum: 10c0/91d25c9ffb86a91f20522cefb2cbec9b64caa1febe27ad0df52f08993ff60888022d771e868e6416cf2e72dab68449d2139e8709ba009b74c6c7ecd4000048d1
+ languageName: node
+ linkType: hard
+
"tapable@npm:^2.0.0, tapable@npm:^2.1.1, tapable@npm:^2.2.0, tapable@npm:^2.2.1, tapable@npm:^2.3.0":
version: 2.3.0
resolution: "tapable@npm:2.3.0"
@@ -30423,6 +30442,17 @@ __metadata:
languageName: node
linkType: hard
+"tersify@npm:^3.11.1":
+ version: 3.12.1
+ resolution: "tersify@npm:3.12.1"
+ dependencies:
+ acorn: "npm:^8.8.2"
+ is-buffer: "npm:^2.0.5"
+ unpartial: "npm:^1.0.4"
+ checksum: 10c0/b2701bacb9c8f1e063ca2c1c1d1559cfac491230b95d1b33a273343aa9ad8c62130a9f54a64f381a221d56869df74f938c3a284187e769697db49bc8cc7e7e4a
+ languageName: node
+ linkType: hard
+
"test-exclude@npm:^6.0.0":
version: 6.0.0
resolution: "test-exclude@npm:6.0.0"
@@ -31016,13 +31046,36 @@ __metadata:
languageName: node
linkType: hard
-"type-fest@npm:~2.19":
+"type-fest@npm:^0.20.2":
+ version: 0.20.2
+ resolution: "type-fest@npm:0.20.2"
+ checksum: 10c0/dea9df45ea1f0aaa4e2d3bed3f9a0bfe9e5b2592bddb92eb1bf06e50bcf98dbb78189668cd8bc31a0511d3fc25539b4cd5c704497e53e93e2d40ca764b10bfc3
+ languageName: node
+ linkType: hard
+
+"type-fest@npm:^1.0.1":
+ version: 1.4.0
+ resolution: "type-fest@npm:1.4.0"
+ checksum: 10c0/a3c0f4ee28ff6ddf800d769eafafcdeab32efa38763c1a1b8daeae681920f6e345d7920bf277245235561d8117dab765cb5f829c76b713b4c9de0998a5397141
+ languageName: node
+ linkType: hard
+
+"type-fest@npm:^2.14.0, type-fest@npm:~2.19":
version: 2.19.0
resolution: "type-fest@npm:2.19.0"
checksum: 10c0/a5a7ecf2e654251613218c215c7493574594951c08e52ab9881c9df6a6da0aeca7528c213c622bc374b4e0cb5c443aa3ab758da4e3c959783ce884c3194e12cb
languageName: node
linkType: hard
+"type-fest@npm:^5.6.0":
+ version: 5.6.0
+ resolution: "type-fest@npm:5.6.0"
+ dependencies:
+ tagged-tag: "npm:^1.0.0"
+ checksum: 10c0/5468a8ffda7f3904e6f7bbd8069eb8b6dd4bd9156e206df7a01d09a73e28cd1afedf74ead9d0fc12841c8c90074194859feca240511c50800962fde1bd9ddcbc
+ languageName: node
+ linkType: hard
+
"type-is@npm:~1.6.18":
version: 1.6.18
resolution: "type-is@npm:1.6.18"
@@ -31033,6 +31086,18 @@ __metadata:
languageName: node
linkType: hard
+"type-plus@npm:^8.0.0-beta.8":
+ version: 8.0.0-beta.8
+ resolution: "type-plus@npm:8.0.0-beta.8"
+ dependencies:
+ tersify: "npm:^3.11.1"
+ unpartial: "npm:^1.0.4"
+ peerDependencies:
+ typescript: ">= 5.6.0"
+ checksum: 10c0/cbd44a56f6264f0909f23ffcfe04e0f57bdde1de9763343258b8e9ed121877dc47c4947623c515bd4ae37a01eac3e5ec974743a694897574e5efab59a0fb94c6
+ languageName: node
+ linkType: hard
+
"typed-array-buffer@npm:^1.0.3":
version: 1.0.3
resolution: "typed-array-buffer@npm:1.0.3"
@@ -31399,6 +31464,13 @@ __metadata:
languageName: node
linkType: hard
+"unpartial@npm:^1.0.4":
+ version: 1.0.5
+ resolution: "unpartial@npm:1.0.5"
+ checksum: 10c0/bdfe89d0712679e22eee654a645493a2aad2428bf9cc0d27d5b56e986001e40c1c70b41407708f33d5ccffe1ced71ee3245e99bb62559fef3d6960feac625a07
+ languageName: node
+ linkType: hard
+
"unpipe@npm:1.0.0, unpipe@npm:~1.0.0":
version: 1.0.0
resolution: "unpipe@npm:1.0.0"