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"