From 23e007670c946a7be6397e8e2be57ae60626bb7f Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 18 Mar 2026 11:18:38 +0000
Subject: [PATCH 01/14] Initial plan
From 6e408d5d8fbbd40ad3356dc3e1b06f61d3aa00c7 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 18 Mar 2026 11:31:50 +0000
Subject: [PATCH 02/14] fix: scope localStorage persistence per Storybook
instance and purge stale tag filters
- Add STORYBOOK_INSTANCE_ID global derived from configDir hash to isolate
localStorage keys per Storybook project
- Include migration from old shared storage key on first load
- Automatically purge tag filters that don't match any entries in the
current index after loading (prevents stale filters from other instances)
- Keep built-in filters (_docs, _play, _test) regardless of index content
Co-authored-by: Sidnioulz <5108577+Sidnioulz@users.noreply.github.com>
---
.../src/builder-manager/utils/framework.ts | 11 ++
code/core/src/manager-api/modules/stories.ts | 46 +++++++
code/core/src/manager-api/store.ts | 35 ++++-
code/core/src/manager-api/tests/store.test.js | 4 +
.../src/manager-api/tests/stories.test.ts | 123 ++++++++++++++++++
code/core/src/manager-api/typings.d.ts | 1 +
code/core/src/manager/typings.d.ts | 5 +
7 files changed, 223 insertions(+), 2 deletions(-)
diff --git a/code/core/src/builder-manager/utils/framework.ts b/code/core/src/builder-manager/utils/framework.ts
index df89e68a6336..50538c26cdfe 100644
--- a/code/core/src/builder-manager/utils/framework.ts
+++ b/code/core/src/builder-manager/utils/framework.ts
@@ -1,3 +1,5 @@
+import { createHash } from 'node:crypto';
+
import {
extractFrameworkPackageName,
frameworkPackages,
@@ -27,5 +29,14 @@ export const buildFrameworkGlobalsFromOptions = async (options: Options) => {
globals.STORYBOOK_RENDERER = renderer;
globals.STORYBOOK_NETWORK_ADDRESS = options.networkAddress;
+ // Compute a unique instance ID from the configDir so the manager can scope localStorage
+ // keys per Storybook instance. This prevents shared state across multiple projects.
+ if (options.configDir) {
+ globals.STORYBOOK_INSTANCE_ID = createHash('sha256')
+ .update(options.configDir)
+ .digest('hex')
+ .slice(0, 12);
+ }
+
return globals;
};
diff --git a/code/core/src/manager-api/modules/stories.ts b/code/core/src/manager-api/modules/stories.ts
index 15803f447faa..f368a00fb64b 100644
--- a/code/core/src/manager-api/modules/stories.ts
+++ b/code/core/src/manager-api/modules/stories.ts
@@ -888,6 +888,46 @@ export const init: ModuleFn = ({
);
};
+ /**
+ * Purge tag filters that don't match any entries in the current unfiltered index. Built-in
+ * filters (like _docs, _play, _test) are always kept because they filter on entry metadata, not
+ * tags.
+ */
+ const purgeStaleTagFilters = async () => {
+ const state = store.getState();
+ const { internal_index, includedTagFilters, excludedTagFilters } = state;
+
+ if (!internal_index || !includedTagFilters || !excludedTagFilters) {
+ return;
+ }
+
+ // Collect all tags present across every entry in the unfiltered index
+ const allTags = new Set();
+ for (const entry of Object.values(internal_index.entries)) {
+ entry.tags?.forEach((tag: string) => allTags.add(tag));
+ }
+
+ // Keep a tag if it is a built-in filter key or actually appears in the index
+ const isValidTag = (tag: Tag) => Object.hasOwn(BUILT_IN_FILTERS, tag) || allTags.has(tag);
+
+ const validIncluded = includedTagFilters.filter(isValidTag);
+ const validExcluded = excludedTagFilters.filter(isValidTag);
+
+ const includedChanged = validIncluded.length !== includedTagFilters.length;
+ const excludedChanged = validExcluded.length !== excludedTagFilters.length;
+
+ if (includedChanged || excludedChanged) {
+ await store.setState(
+ {
+ includedTagFilters: validIncluded,
+ excludedTagFilters: validExcluded,
+ },
+ { persistence: 'permanent' }
+ );
+ recomputeFilters();
+ }
+ };
+
// On initial load, the local iframe will select the first story (or other "selection specifier")
// and emit STORY_SPECIFIED with the id. We need to ensure we respond to this change.
provider.channel?.on(
@@ -1158,6 +1198,12 @@ export const init: ModuleFn = ({
provider.channel?.on(STORY_INDEX_INVALIDATED, () => api.fetchIndex());
await api.fetchIndex();
+
+ // After the first index load, purge any stale tag filters that don't match
+ // any entries in the unfiltered index. This prevents leftover filters from
+ // another Storybook instance (or a previous project state) from hiding all
+ // sidebar entries.
+ purgeStaleTagFilters();
},
};
};
diff --git a/code/core/src/manager-api/store.ts b/code/core/src/manager-api/store.ts
index b1071e47002b..f9c2e456b774 100644
--- a/code/core/src/manager-api/store.ts
+++ b/code/core/src/manager-api/store.ts
@@ -1,3 +1,5 @@
+import { global } from '@storybook/global';
+
import type { StoreAPI } from 'store2';
import store from 'store2';
@@ -7,10 +9,39 @@ import type { State } from './root';
// setting up the store, overriding set and get to use telejson
storeSetup(store._);
-export const STORAGE_KEY = '@storybook/manager/store';
+const STORAGE_KEY_BASE = '@storybook/manager/store';
+
+/**
+ * Build the storage key, scoped per Storybook instance when possible. The STORYBOOK_INSTANCE_ID
+ * global is injected by the builder from a hash of configDir, which ensures each Storybook project
+ * uses its own localStorage namespace.
+ */
+function buildStorageKey(): string {
+ try {
+ const instanceId = global.STORYBOOK_INSTANCE_ID;
+ if (instanceId) {
+ return `${STORAGE_KEY_BASE}/${instanceId}`;
+ }
+ } catch {
+ // Ignore - fall back to the base key
+ }
+ return STORAGE_KEY_BASE;
+}
+
+export const STORAGE_KEY = buildStorageKey();
function get(storage: StoreAPI) {
- const data = storage.get(STORAGE_KEY);
+ let data = storage.get(STORAGE_KEY);
+
+ // Migration: if no data exists under the instance-specific key, try the old shared key.
+ // This is a one-time migration that preserves existing settings when upgrading.
+ if (!data && STORAGE_KEY !== STORAGE_KEY_BASE) {
+ data = storage.get(STORAGE_KEY_BASE);
+ if (data) {
+ storage.set(STORAGE_KEY, data);
+ }
+ }
+
return data || {};
}
diff --git a/code/core/src/manager-api/tests/store.test.js b/code/core/src/manager-api/tests/store.test.js
index e77bc1cd289e..105c50a5faa4 100644
--- a/code/core/src/manager-api/tests/store.test.js
+++ b/code/core/src/manager-api/tests/store.test.js
@@ -20,6 +20,10 @@ vi.mock('store2', () => ({
}));
describe('store', () => {
+ it('uses the base storage key when STORYBOOK_INSTANCE_ID is not set', () => {
+ expect(STORAGE_KEY).toBe('@storybook/manager/store');
+ });
+
it('sensibly combines local+session storage for initial state', () => {
store2.session.get.mockReturnValueOnce({ foo: 'bar', combined: { a: 'b' } });
store2.local.get.mockReturnValueOnce({ foo: 'baz', another: 'value', combined: { c: 'd' } });
diff --git a/code/core/src/manager-api/tests/stories.test.ts b/code/core/src/manager-api/tests/stories.test.ts
index 3bdbeec046fd..85a789f0cad7 100644
--- a/code/core/src/manager-api/tests/stories.test.ts
+++ b/code/core/src/manager-api/tests/stories.test.ts
@@ -1672,4 +1672,127 @@ describe('stories API', () => {
`);
});
});
+
+ describe('purgeStaleTagFilters', () => {
+ it('removes user tag filters that do not match any entries in the index', async () => {
+ const entriesWithTags = {
+ 'a--1': {
+ type: 'story' as const,
+ subtype: 'story' as const,
+ title: 'a',
+ name: '1',
+ id: 'a--1',
+ importPath: './a.ts',
+ tags: ['dev', 'existing-tag'],
+ },
+ };
+
+ fetch.mockReturnValue(
+ Promise.resolve({
+ status: 200,
+ ok: true,
+ json: () => ({ v: 5, entries: entriesWithTags }),
+ } as any as Response)
+ );
+
+ const moduleArgs = createMockModuleArgs({
+ initialState: {
+ // Simulate leftover stale tag from another SB instance
+ includedTagFilters: ['existing-tag', 'stale-tag-from-other-project'],
+ excludedTagFilters: ['another-stale-tag'],
+ } as any,
+ });
+ const { init } = initStories(moduleArgs as unknown as ModuleArgs);
+ const { store } = moduleArgs;
+
+ await init!();
+ await wait(16);
+
+ const { includedTagFilters, excludedTagFilters } = store.getState();
+ expect(includedTagFilters).toEqual(['existing-tag']);
+ expect(excludedTagFilters).toEqual([]);
+ });
+
+ it('keeps built-in filter tags even if they do not appear in entry tags', async () => {
+ const entriesWithTags = {
+ 'a--1': {
+ type: 'story' as const,
+ subtype: 'story' as const,
+ title: 'a',
+ name: '1',
+ id: 'a--1',
+ importPath: './a.ts',
+ tags: ['dev'],
+ },
+ };
+
+ fetch.mockReturnValue(
+ Promise.resolve({
+ status: 200,
+ ok: true,
+ json: () => ({ v: 5, entries: entriesWithTags }),
+ } as any as Response)
+ );
+
+ const moduleArgs = createMockModuleArgs({
+ initialState: {
+ // _docs and _play are built-in filters and should be kept
+ includedTagFilters: ['_docs', '_play'],
+ excludedTagFilters: ['_test', 'stale-tag'],
+ } as any,
+ });
+ const { init } = initStories(moduleArgs as unknown as ModuleArgs);
+ const { store } = moduleArgs;
+
+ await init!();
+ await wait(16);
+
+ const { includedTagFilters, excludedTagFilters } = store.getState();
+ expect(includedTagFilters).toEqual(['_docs', '_play']);
+ expect(excludedTagFilters).toEqual(['_test']);
+ });
+
+ it('does not update state if all tag filters are valid', async () => {
+ const entriesWithTags = {
+ 'a--1': {
+ type: 'story' as const,
+ subtype: 'story' as const,
+ title: 'a',
+ name: '1',
+ id: 'a--1',
+ importPath: './a.ts',
+ tags: ['dev', 'my-tag'],
+ },
+ };
+
+ fetch.mockReturnValue(
+ Promise.resolve({
+ status: 200,
+ ok: true,
+ json: () => ({ v: 5, entries: entriesWithTags }),
+ } as any as Response)
+ );
+
+ const moduleArgs = createMockModuleArgs({
+ initialState: {
+ includedTagFilters: ['my-tag'],
+ excludedTagFilters: ['_docs'],
+ } as any,
+ });
+ const { init } = initStories(moduleArgs as unknown as ModuleArgs);
+ const { store } = moduleArgs;
+
+ const stateBeforeInit = store.getState();
+ await init!();
+ await wait(16);
+
+ // setState should have been called for setIndex but not for purging tag filters
+ const setStateCalls = (store.setState as any).mock.calls;
+ const tagFilterUpdates = setStateCalls.filter(
+ (call: any[]) =>
+ call[0] && 'includedTagFilters' in call[0] && 'excludedTagFilters' in call[0]
+ );
+ expect(tagFilterUpdates).toHaveLength(0);
+ });
+ });
});
diff --git a/code/core/src/manager-api/typings.d.ts b/code/core/src/manager-api/typings.d.ts
index 7bf67c97fc33..a4043e9265f9 100644
--- a/code/core/src/manager-api/typings.d.ts
+++ b/code/core/src/manager-api/typings.d.ts
@@ -10,3 +10,4 @@ declare var STORYBOOK_ADDON_STATE: Record;
declare var STORYBOOK_FRAMEWORK: import('storybook/internal/types').SupportedFramework | undefined;
declare var STORYBOOK_RENDERER: import('storybook/internal/types').SupportedRenderer | undefined;
declare var STORYBOOK_BUILDER: import('storybook/internal/types').SupportedBuilder | undefined;
+declare var STORYBOOK_INSTANCE_ID: string | undefined;
diff --git a/code/core/src/manager/typings.d.ts b/code/core/src/manager/typings.d.ts
index fb3fa90c7557..13e99af96c5a 100644
--- a/code/core/src/manager/typings.d.ts
+++ b/code/core/src/manager/typings.d.ts
@@ -34,4 +34,9 @@ declare var __STORYBOOK_CLIENT_LOGGER__: any;
declare var __STORYBOOK_ADDONS_CHANNEL__: any;
declare var __STORYBOOK_TYPES__: any;
declare var STORYBOOK_RENDERER: string | undefined;
+/**
+ * A short hash derived from the Storybook config directory path. Used to scope localStorage keys
+ * per Storybook instance so that persistence is not shared across different projects.
+ */
+declare var STORYBOOK_INSTANCE_ID: string | undefined;
declare var sendTelemetryError: (error: any) => void;
From 0aa1bbce807fd0c4f7e1634373a360370d2aab39 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 18 Mar 2026 11:35:11 +0000
Subject: [PATCH 03/14] fix: address code review feedback - await purge,
clarify catch block and hash length
Co-authored-by: Sidnioulz <5108577+Sidnioulz@users.noreply.github.com>
---
code/core/src/builder-manager/utils/framework.ts | 1 +
code/core/src/manager-api/modules/stories.ts | 2 +-
code/core/src/manager-api/store.ts | 2 +-
3 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/code/core/src/builder-manager/utils/framework.ts b/code/core/src/builder-manager/utils/framework.ts
index 50538c26cdfe..10d6e1c0f5c7 100644
--- a/code/core/src/builder-manager/utils/framework.ts
+++ b/code/core/src/builder-manager/utils/framework.ts
@@ -31,6 +31,7 @@ export const buildFrameworkGlobalsFromOptions = async (options: Options) => {
// Compute a unique instance ID from the configDir so the manager can scope localStorage
// keys per Storybook instance. This prevents shared state across multiple projects.
+ // 12 hex characters (48 bits of entropy) is more than sufficient for uniqueness.
if (options.configDir) {
globals.STORYBOOK_INSTANCE_ID = createHash('sha256')
.update(options.configDir)
diff --git a/code/core/src/manager-api/modules/stories.ts b/code/core/src/manager-api/modules/stories.ts
index f368a00fb64b..586870aa889e 100644
--- a/code/core/src/manager-api/modules/stories.ts
+++ b/code/core/src/manager-api/modules/stories.ts
@@ -1203,7 +1203,7 @@ export const init: ModuleFn = ({
// any entries in the unfiltered index. This prevents leftover filters from
// another Storybook instance (or a previous project state) from hiding all
// sidebar entries.
- purgeStaleTagFilters();
+ await purgeStaleTagFilters();
},
};
};
diff --git a/code/core/src/manager-api/store.ts b/code/core/src/manager-api/store.ts
index f9c2e456b774..fa099e1779d7 100644
--- a/code/core/src/manager-api/store.ts
+++ b/code/core/src/manager-api/store.ts
@@ -23,7 +23,7 @@ function buildStorageKey(): string {
return `${STORAGE_KEY_BASE}/${instanceId}`;
}
} catch {
- // Ignore - fall back to the base key
+ // In edge cases (e.g. restricted globals), fall back to the shared base key
}
return STORAGE_KEY_BASE;
}
From 95c10fbf6a852174a47adfd0d7234c04793b6c4d Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 18 Mar 2026 18:14:20 +0000
Subject: [PATCH 04/14] fix: address all PR review feedback
- Use getAnonymousProjectId() from telemetry instead of configDir hash
- Use 'anonymous' as fallback instead of shared base key
- Store current version in localStorage for future migration logic
- Remove migration logic from get() that ran on every call
- Replace wait(16) timers in tests with vi.waitFor
- Export getAnonymousProjectId from storybook/internal/telemetry
Co-authored-by: Sidnioulz <5108577+Sidnioulz@users.noreply.github.com>
---
.../src/builder-manager/utils/framework.ts | 16 +++-----
code/core/src/manager-api/store.ts | 38 +++++++++++--------
code/core/src/manager-api/tests/store.test.js | 16 +++++++-
.../src/manager-api/tests/stories.test.ts | 20 +++++-----
code/core/src/manager/typings.d.ts | 5 ++-
code/core/src/telemetry/index.ts | 2 +
6 files changed, 56 insertions(+), 41 deletions(-)
diff --git a/code/core/src/builder-manager/utils/framework.ts b/code/core/src/builder-manager/utils/framework.ts
index 10d6e1c0f5c7..726c117220e0 100644
--- a/code/core/src/builder-manager/utils/framework.ts
+++ b/code/core/src/builder-manager/utils/framework.ts
@@ -1,11 +1,10 @@
-import { createHash } from 'node:crypto';
-
import {
extractFrameworkPackageName,
frameworkPackages,
frameworkToRenderer,
getFrameworkName,
} from 'storybook/internal/common';
+import { getAnonymousProjectId } from 'storybook/internal/telemetry';
import { type Options, SupportedBuilder } from 'storybook/internal/types';
export const buildFrameworkGlobalsFromOptions = async (options: Options) => {
@@ -29,15 +28,10 @@ export const buildFrameworkGlobalsFromOptions = async (options: Options) => {
globals.STORYBOOK_RENDERER = renderer;
globals.STORYBOOK_NETWORK_ADDRESS = options.networkAddress;
- // Compute a unique instance ID from the configDir so the manager can scope localStorage
- // keys per Storybook instance. This prevents shared state across multiple projects.
- // 12 hex characters (48 bits of entropy) is more than sufficient for uniqueness.
- if (options.configDir) {
- globals.STORYBOOK_INSTANCE_ID = createHash('sha256')
- .update(options.configDir)
- .digest('hex')
- .slice(0, 12);
- }
+ // Derive a stable, anonymous project ID from Git info (remote URL + working directory) so the
+ // manager can scope localStorage keys per Storybook instance. Falls back to 'anonymous' when
+ // Git info is unavailable (e.g. in non-Git projects or CI without Git).
+ globals.STORYBOOK_INSTANCE_ID = getAnonymousProjectId() ?? 'anonymous';
return globals;
};
diff --git a/code/core/src/manager-api/store.ts b/code/core/src/manager-api/store.ts
index fa099e1779d7..127b735f66c3 100644
--- a/code/core/src/manager-api/store.ts
+++ b/code/core/src/manager-api/store.ts
@@ -5,6 +5,7 @@ import store from 'store2';
import storeSetup from './lib/store-setup';
import type { State } from './root';
+import { version as currentVersion } from './version';
// setting up the store, overriding set and get to use telejson
storeSetup(store._);
@@ -13,8 +14,8 @@ const STORAGE_KEY_BASE = '@storybook/manager/store';
/**
* Build the storage key, scoped per Storybook instance when possible. The STORYBOOK_INSTANCE_ID
- * global is injected by the builder from a hash of configDir, which ensures each Storybook project
- * uses its own localStorage namespace.
+ * global is injected by the builder from getAnonymousProjectId() (a Git-derived project hash). When
+ * unavailable, we use 'anonymous' to keep new data separate from the legacy shared key.
*/
function buildStorageKey(): string {
try {
@@ -23,25 +24,25 @@ function buildStorageKey(): string {
return `${STORAGE_KEY_BASE}/${instanceId}`;
}
} catch {
- // In edge cases (e.g. restricted globals), fall back to the shared base key
+ // In edge cases (e.g. restricted globals), fall back to anonymous
}
- return STORAGE_KEY_BASE;
+ return `${STORAGE_KEY_BASE}/anonymous`;
}
export const STORAGE_KEY = buildStorageKey();
-function get(storage: StoreAPI) {
- let data = storage.get(STORAGE_KEY);
-
- // Migration: if no data exists under the instance-specific key, try the old shared key.
- // This is a one-time migration that preserves existing settings when upgrading.
- if (!data && STORAGE_KEY !== STORAGE_KEY_BASE) {
- data = storage.get(STORAGE_KEY_BASE);
- if (data) {
- storage.set(STORAGE_KEY, data);
- }
- }
+/**
+ * Key used to store the Storybook version alongside persisted data. This allows future migration
+ * logic to be version-aware when the storage format changes.
+ */
+const VERSION_KEY = `${STORAGE_KEY}/__version__`;
+function persistVersion(storage: StoreAPI) {
+ storage.set(VERSION_KEY, currentVersion);
+}
+
+function get(storage: StoreAPI) {
+ const data = storage.get(STORAGE_KEY);
return data || {};
}
@@ -98,7 +99,12 @@ export default class Store {
// We don't only merge at the very top level (the same way as React setState)
// when you set keys, so it makes sense to do the same in combining the two storage modes
// Really, you shouldn't store the same key in both places
- return { ...base, ...get(store.local), ...get(store.session) };
+ const initialState = { ...base, ...get(store.local), ...get(store.session) };
+
+ // Record the current version in localStorage so future migrations can be version-aware
+ persistVersion(store.local);
+
+ return initialState;
}
getState() {
diff --git a/code/core/src/manager-api/tests/store.test.js b/code/core/src/manager-api/tests/store.test.js
index 105c50a5faa4..7635521dfb5e 100644
--- a/code/core/src/manager-api/tests/store.test.js
+++ b/code/core/src/manager-api/tests/store.test.js
@@ -19,9 +19,21 @@ vi.mock('store2', () => ({
},
}));
+vi.mock('../version', () => ({ version: '0.0.0-test' }));
+
describe('store', () => {
- it('uses the base storage key when STORYBOOK_INSTANCE_ID is not set', () => {
- expect(STORAGE_KEY).toBe('@storybook/manager/store');
+ it('falls back to anonymous storage key when STORYBOOK_INSTANCE_ID is not set', () => {
+ expect(STORAGE_KEY).toBe('@storybook/manager/store/anonymous');
+ });
+
+ it('persists the current version in localStorage on getInitialState', () => {
+ store2.session.get.mockReturnValueOnce({});
+ store2.local.get.mockReturnValueOnce({});
+
+ const s = new Store({});
+ s.getInitialState();
+
+ expect(store2.local.set).toHaveBeenCalledWith(`${STORAGE_KEY}/__version__`, '0.0.0-test');
});
it('sensibly combines local+session storage for initial state', () => {
diff --git a/code/core/src/manager-api/tests/stories.test.ts b/code/core/src/manager-api/tests/stories.test.ts
index 85a789f0cad7..9c66f891d074 100644
--- a/code/core/src/manager-api/tests/stories.test.ts
+++ b/code/core/src/manager-api/tests/stories.test.ts
@@ -1706,11 +1706,12 @@ describe('stories API', () => {
const { store } = moduleArgs;
await init!();
- await wait(16);
- const { includedTagFilters, excludedTagFilters } = store.getState();
- expect(includedTagFilters).toEqual(['existing-tag']);
- expect(excludedTagFilters).toEqual([]);
+ await vi.waitFor(() => {
+ const { includedTagFilters, excludedTagFilters } = store.getState();
+ expect(includedTagFilters).toEqual(['existing-tag']);
+ expect(excludedTagFilters).toEqual([]);
+ });
});
it('keeps built-in filter tags even if they do not appear in entry tags', async () => {
@@ -1745,11 +1746,12 @@ describe('stories API', () => {
const { store } = moduleArgs;
await init!();
- await wait(16);
- const { includedTagFilters, excludedTagFilters } = store.getState();
- expect(includedTagFilters).toEqual(['_docs', '_play']);
- expect(excludedTagFilters).toEqual(['_test']);
+ await vi.waitFor(() => {
+ const { includedTagFilters, excludedTagFilters } = store.getState();
+ expect(includedTagFilters).toEqual(['_docs', '_play']);
+ expect(excludedTagFilters).toEqual(['_test']);
+ });
});
it('does not update state if all tag filters are valid', async () => {
@@ -1782,9 +1784,7 @@ describe('stories API', () => {
const { init } = initStories(moduleArgs as unknown as ModuleArgs);
const { store } = moduleArgs;
- const stateBeforeInit = store.getState();
await init!();
- await wait(16);
// setState should have been called for setIndex but not for purging tag filters
const setStateCalls = (store.setState as any).mock.calls;
diff --git a/code/core/src/manager/typings.d.ts b/code/core/src/manager/typings.d.ts
index 13e99af96c5a..4b67ef96c38e 100644
--- a/code/core/src/manager/typings.d.ts
+++ b/code/core/src/manager/typings.d.ts
@@ -35,8 +35,9 @@ declare var __STORYBOOK_ADDONS_CHANNEL__: any;
declare var __STORYBOOK_TYPES__: any;
declare var STORYBOOK_RENDERER: string | undefined;
/**
- * A short hash derived from the Storybook config directory path. Used to scope localStorage keys
- * per Storybook instance so that persistence is not shared across different projects.
+ * A stable, anonymous project identifier derived from Git info (remote URL + working directory
+ * path). Used to scope localStorage keys per Storybook instance so that persistence is not shared
+ * across different projects. Falls back to 'anonymous' when Git info is unavailable.
*/
declare var STORYBOOK_INSTANCE_ID: string | undefined;
declare var sendTelemetryError: (error: any) => void;
diff --git a/code/core/src/telemetry/index.ts b/code/core/src/telemetry/index.ts
index bfa5840eb1bd..ae9229c3ccaa 100644
--- a/code/core/src/telemetry/index.ts
+++ b/code/core/src/telemetry/index.ts
@@ -8,6 +8,8 @@ import type { EventType, Options, Payload, TelemetryData } from './types';
export { oneWayHash } from './one-way-hash';
+export { getAnonymousProjectId } from './anonymous-id';
+
export * from './storybook-metadata';
export * from './types';
From 39d15de65d79ea9fcc398e88f0aed600b43c9b4a Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 18 Mar 2026 18:27:13 +0000
Subject: [PATCH 05/14] refactor: address review feedback - JSDoc comment,
better variable names, simplify storage key
Co-authored-by: Sidnioulz <5108577+Sidnioulz@users.noreply.github.com>
---
.../src/builder-manager/utils/framework.ts | 7 ++++---
code/core/src/manager-api/modules/stories.ts | 13 ++++++-------
code/core/src/manager-api/store.ts | 19 +++----------------
3 files changed, 13 insertions(+), 26 deletions(-)
diff --git a/code/core/src/builder-manager/utils/framework.ts b/code/core/src/builder-manager/utils/framework.ts
index 726c117220e0..392faf743507 100644
--- a/code/core/src/builder-manager/utils/framework.ts
+++ b/code/core/src/builder-manager/utils/framework.ts
@@ -28,9 +28,10 @@ export const buildFrameworkGlobalsFromOptions = async (options: Options) => {
globals.STORYBOOK_RENDERER = renderer;
globals.STORYBOOK_NETWORK_ADDRESS = options.networkAddress;
- // Derive a stable, anonymous project ID from Git info (remote URL + working directory) so the
- // manager can scope localStorage keys per Storybook instance. Falls back to 'anonymous' when
- // Git info is unavailable (e.g. in non-Git projects or CI without Git).
+ /**
+ * Anonymous project ID derived from Git info, falls back to `anonymous` when it can't be
+ * computed.
+ */
globals.STORYBOOK_INSTANCE_ID = getAnonymousProjectId() ?? 'anonymous';
return globals;
diff --git a/code/core/src/manager-api/modules/stories.ts b/code/core/src/manager-api/modules/stories.ts
index 586870aa889e..10e6d543e0bf 100644
--- a/code/core/src/manager-api/modules/stories.ts
+++ b/code/core/src/manager-api/modules/stories.ts
@@ -901,17 +901,16 @@ export const init: ModuleFn = ({
return;
}
- // Collect all tags present across every entry in the unfiltered index
- const allTags = new Set();
+ const allExistingTags = new Set();
for (const entry of Object.values(internal_index.entries)) {
- entry.tags?.forEach((tag: string) => allTags.add(tag));
+ entry.tags?.forEach((tag: string) => allExistingTags.add(tag));
}
- // Keep a tag if it is a built-in filter key or actually appears in the index
- const isValidTag = (tag: Tag) => Object.hasOwn(BUILT_IN_FILTERS, tag) || allTags.has(tag);
+ const isExistingTag = (tag: Tag) =>
+ Object.hasOwn(BUILT_IN_FILTERS, tag) || allExistingTags.has(tag);
- const validIncluded = includedTagFilters.filter(isValidTag);
- const validExcluded = excludedTagFilters.filter(isValidTag);
+ const validIncluded = includedTagFilters.filter(isExistingTag);
+ const validExcluded = excludedTagFilters.filter(isExistingTag);
const includedChanged = validIncluded.length !== includedTagFilters.length;
const excludedChanged = validExcluded.length !== excludedTagFilters.length;
diff --git a/code/core/src/manager-api/store.ts b/code/core/src/manager-api/store.ts
index 127b735f66c3..d9a1c45e6506 100644
--- a/code/core/src/manager-api/store.ts
+++ b/code/core/src/manager-api/store.ts
@@ -13,23 +13,10 @@ storeSetup(store._);
const STORAGE_KEY_BASE = '@storybook/manager/store';
/**
- * Build the storage key, scoped per Storybook instance when possible. The STORYBOOK_INSTANCE_ID
- * global is injected by the builder from getAnonymousProjectId() (a Git-derived project hash). When
- * unavailable, we use 'anonymous' to keep new data separate from the legacy shared key.
+ * Storage key scoped per Storybook instance via `STORYBOOK_INSTANCE_ID` (a Git-derived project
+ * hash).
*/
-function buildStorageKey(): string {
- try {
- const instanceId = global.STORYBOOK_INSTANCE_ID;
- if (instanceId) {
- return `${STORAGE_KEY_BASE}/${instanceId}`;
- }
- } catch {
- // In edge cases (e.g. restricted globals), fall back to anonymous
- }
- return `${STORAGE_KEY_BASE}/anonymous`;
-}
-
-export const STORAGE_KEY = buildStorageKey();
+export const STORAGE_KEY = `${STORAGE_KEY_BASE}/${global.STORYBOOK_INSTANCE_ID || 'anonymous'}`;
/**
* Key used to store the Storybook version alongside persisted data. This allows future migration
From 98d01ed7ce9dc00a898eec929d0e4363d235e8d6 Mon Sep 17 00:00:00 2001
From: Steve Dodier-Lazaro
Date: Fri, 20 Mar 2026 18:04:58 +0100
Subject: [PATCH 06/14] Dedupe project id logic and avoid globals
---
.../src/builder-manager/utils/framework.ts | 23 +--
code/core/src/manager-api/store.ts | 6 +-
code/core/src/telemetry/anonymous-id.test.ts | 186 ++++++++++--------
3 files changed, 113 insertions(+), 102 deletions(-)
diff --git a/code/core/src/builder-manager/utils/framework.ts b/code/core/src/builder-manager/utils/framework.ts
index 392faf743507..e5e47e33c01f 100644
--- a/code/core/src/builder-manager/utils/framework.ts
+++ b/code/core/src/builder-manager/utils/framework.ts
@@ -3,23 +3,26 @@ import {
frameworkPackages,
frameworkToRenderer,
getFrameworkName,
-} from 'storybook/internal/common';
-import { getAnonymousProjectId } from 'storybook/internal/telemetry';
-import { type Options, SupportedBuilder } from 'storybook/internal/types';
+} from "storybook/internal/common";
+import { type Options, SupportedBuilder } from "storybook/internal/types";
export const buildFrameworkGlobalsFromOptions = async (options: Options) => {
const globals: Record = {};
- const { builder: builderConfig, channelOptions } = await options.presets.apply('core');
- const builderName = typeof builderConfig === 'string' ? builderConfig : builderConfig?.name;
- const builder = Object.values(SupportedBuilder).find((builder) => builderName?.includes(builder));
+ const { builder: builderConfig, channelOptions } =
+ await options.presets.apply("core");
+ const builderName =
+ typeof builderConfig === "string" ? builderConfig : builderConfig?.name;
+ const builder = Object.values(SupportedBuilder).find((builder) =>
+ builderName?.includes(builder),
+ );
const frameworkName = await getFrameworkName(options);
const frameworkPackageName = extractFrameworkPackageName(frameworkName);
const framework = frameworkPackages[frameworkPackageName];
const renderer = frameworkToRenderer[framework];
- if (options.configType === 'DEVELOPMENT') {
+ if (options.configType === "DEVELOPMENT") {
// Manager only needs the token currently, so we don't pass any other channel options.
globals.CHANNEL_OPTIONS = { wsToken: channelOptions?.wsToken };
}
@@ -28,11 +31,5 @@ export const buildFrameworkGlobalsFromOptions = async (options: Options) => {
globals.STORYBOOK_RENDERER = renderer;
globals.STORYBOOK_NETWORK_ADDRESS = options.networkAddress;
- /**
- * Anonymous project ID derived from Git info, falls back to `anonymous` when it can't be
- * computed.
- */
- globals.STORYBOOK_INSTANCE_ID = getAnonymousProjectId() ?? 'anonymous';
-
return globals;
};
diff --git a/code/core/src/manager-api/store.ts b/code/core/src/manager-api/store.ts
index d9a1c45e6506..33742796bbec 100644
--- a/code/core/src/manager-api/store.ts
+++ b/code/core/src/manager-api/store.ts
@@ -1,11 +1,11 @@
-import { global } from '@storybook/global';
-
import type { StoreAPI } from 'store2';
import store from 'store2';
import storeSetup from './lib/store-setup';
import type { State } from './root';
import { version as currentVersion } from './version';
+import { getAnonymousProjectId } from 'storybook/internal/telemetry';
+
// setting up the store, overriding set and get to use telejson
storeSetup(store._);
@@ -16,7 +16,7 @@ const STORAGE_KEY_BASE = '@storybook/manager/store';
* Storage key scoped per Storybook instance via `STORYBOOK_INSTANCE_ID` (a Git-derived project
* hash).
*/
-export const STORAGE_KEY = `${STORAGE_KEY_BASE}/${global.STORYBOOK_INSTANCE_ID || 'anonymous'}`;
+export const STORAGE_KEY = `${STORAGE_KEY_BASE}/${getAnonymousProjectId() || 'anonymous'}`;
/**
* Key used to store the Storybook version alongside persisted data. This allows future migration
diff --git a/code/core/src/telemetry/anonymous-id.test.ts b/code/core/src/telemetry/anonymous-id.test.ts
index cdee088797ab..148046633b40 100644
--- a/code/core/src/telemetry/anonymous-id.test.ts
+++ b/code/core/src/telemetry/anonymous-id.test.ts
@@ -1,21 +1,21 @@
-import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { beforeEach, describe, expect, it, vi } from "vitest";
-import { executeCommandSync } from 'storybook/internal/common';
+import { executeCommandSync } from "storybook/internal/common";
import {
getAnonymousProjectId,
getProjectSince,
normalizeGitUrl,
unhashedProjectId,
-} from './anonymous-id';
+} from "./anonymous-id";
-vi.mock(import('storybook/internal/common'), async (actualModule) => {
+vi.mock(import("storybook/internal/common"), async (actualModule) => {
const actual = await actualModule();
return {
...actual,
executeCommandSync: vi.fn(actual.executeCommandSync),
- getProjectRoot: () => '/path/to/project/root',
+ getProjectRoot: () => "/path/to/project/root",
};
});
@@ -23,163 +23,177 @@ beforeEach(() => {
vi.mocked(executeCommandSync).mockReset();
});
-describe('normalizeGitUrl', () => {
- it('trims off https://', () => {
- expect(normalizeGitUrl('https://github.com/storybookjs/storybook.git')).toEqual(
- 'github.com/storybookjs/storybook.git'
- );
+describe("normalizeGitUrl", () => {
+ it("trims off https://", () => {
+ expect(
+ normalizeGitUrl("https://github.com/storybookjs/storybook.git"),
+ ).toEqual("github.com/storybookjs/storybook.git");
});
- it('trims off http://', () => {
- expect(normalizeGitUrl('http://github.com/storybookjs/storybook.git')).toEqual(
- 'github.com/storybookjs/storybook.git'
- );
+ it("trims off http://", () => {
+ expect(
+ normalizeGitUrl("http://github.com/storybookjs/storybook.git"),
+ ).toEqual("github.com/storybookjs/storybook.git");
});
- it('trims off git+https://', () => {
- expect(normalizeGitUrl('git+https://github.com/storybookjs/storybook.git')).toEqual(
- 'github.com/storybookjs/storybook.git'
- );
+ it("trims off git+https://", () => {
+ expect(
+ normalizeGitUrl("git+https://github.com/storybookjs/storybook.git"),
+ ).toEqual("github.com/storybookjs/storybook.git");
});
- it('trims off https://username@', () => {
- expect(normalizeGitUrl('https://username@github.com/storybookjs/storybook.git')).toEqual(
- 'github.com/storybookjs/storybook.git'
- );
+ it("trims off https://username@", () => {
+ expect(
+ normalizeGitUrl("https://username@github.com/storybookjs/storybook.git"),
+ ).toEqual("github.com/storybookjs/storybook.git");
});
- it('trims off http://username@', () => {
- expect(normalizeGitUrl('http://username@github.com/storybookjs/storybook.git')).toEqual(
- 'github.com/storybookjs/storybook.git'
- );
+ it("trims off http://username@", () => {
+ expect(
+ normalizeGitUrl("http://username@github.com/storybookjs/storybook.git"),
+ ).toEqual("github.com/storybookjs/storybook.git");
});
- it('trims off https://username:password@', () => {
+ it("trims off https://username:password@", () => {
expect(
- normalizeGitUrl('https://username:password@github.com/storybookjs/storybook.git')
- ).toEqual('github.com/storybookjs/storybook.git');
+ normalizeGitUrl(
+ "https://username:password@github.com/storybookjs/storybook.git",
+ ),
+ ).toEqual("github.com/storybookjs/storybook.git");
});
- it('trims off http://username:password@', () => {
+ it("trims off http://username:password@", () => {
expect(
- normalizeGitUrl('http://username:password@github.com/storybookjs/storybook.git')
- ).toEqual('github.com/storybookjs/storybook.git');
+ normalizeGitUrl(
+ "http://username:password@github.com/storybookjs/storybook.git",
+ ),
+ ).toEqual("github.com/storybookjs/storybook.git");
});
- it('trims off git://', () => {
- expect(normalizeGitUrl('git://github.com/storybookjs/storybook.git')).toEqual(
- 'github.com/storybookjs/storybook.git'
- );
+ it("trims off git://", () => {
+ expect(
+ normalizeGitUrl("git://github.com/storybookjs/storybook.git"),
+ ).toEqual("github.com/storybookjs/storybook.git");
});
- it('trims off git@', () => {
- expect(normalizeGitUrl('git@github.com:storybookjs/storybook.git')).toEqual(
- 'github.com/storybookjs/storybook.git'
+ it("trims off git@", () => {
+ expect(normalizeGitUrl("git@github.com:storybookjs/storybook.git")).toEqual(
+ "github.com/storybookjs/storybook.git",
);
});
- it('trims off git+ssh://git@', () => {
- expect(normalizeGitUrl('git+ssh://git@github.com:storybookjs/storybook.git')).toEqual(
- 'github.com/storybookjs/storybook.git'
- );
+ it("trims off git+ssh://git@", () => {
+ expect(
+ normalizeGitUrl("git+ssh://git@github.com:storybookjs/storybook.git"),
+ ).toEqual("github.com/storybookjs/storybook.git");
});
- it('trims off ssh://git@', () => {
- expect(normalizeGitUrl('ssh://git@github.com:storybookjs/storybook.git')).toEqual(
- 'github.com/storybookjs/storybook.git'
- );
+ it("trims off ssh://git@", () => {
+ expect(
+ normalizeGitUrl("ssh://git@github.com:storybookjs/storybook.git"),
+ ).toEqual("github.com/storybookjs/storybook.git");
});
- it('adds .git if missing', () => {
- expect(normalizeGitUrl('https://github.com/storybookjs/storybook')).toEqual(
- 'github.com/storybookjs/storybook.git'
+ it("adds .git if missing", () => {
+ expect(normalizeGitUrl("https://github.com/storybookjs/storybook")).toEqual(
+ "github.com/storybookjs/storybook.git",
);
});
- it('trims off #hash', () => {
- expect(normalizeGitUrl('https://github.com/storybookjs/storybook.git#next')).toEqual(
- 'github.com/storybookjs/storybook.git'
- );
+ it("trims off #hash", () => {
+ expect(
+ normalizeGitUrl("https://github.com/storybookjs/storybook.git#next"),
+ ).toEqual("github.com/storybookjs/storybook.git");
});
- it('trims off extra whitespace', () => {
- expect(normalizeGitUrl('https://github.com/storybookjs/storybook.git#next\n')).toEqual(
- 'github.com/storybookjs/storybook.git'
- );
+ it("trims off extra whitespace", () => {
+ expect(
+ normalizeGitUrl("https://github.com/storybookjs/storybook.git#next\n"),
+ ).toEqual("github.com/storybookjs/storybook.git");
- expect(normalizeGitUrl('https://github.com/storybookjs/storybook.git\n')).toEqual(
- 'github.com/storybookjs/storybook.git'
- );
+ expect(
+ normalizeGitUrl("https://github.com/storybookjs/storybook.git\n"),
+ ).toEqual("github.com/storybookjs/storybook.git");
});
});
-describe('unhashedProjectId', () => {
- it('does not touch unix paths', () => {
+describe("unhashedProjectId", () => {
+ it("does not touch unix paths", () => {
expect(
- unhashedProjectId('https://github.com/storybookjs/storybook.git\n', 'path/to/storybook')
- ).toBe('github.com/storybookjs/storybook.gitpath/to/storybook');
+ unhashedProjectId(
+ "https://github.com/storybookjs/storybook.git\n",
+ "path/to/storybook",
+ ),
+ ).toBe("github.com/storybookjs/storybook.gitpath/to/storybook");
});
- it('normalizes windows paths', () => {
+ it("normalizes windows paths", () => {
expect(
- unhashedProjectId('https://github.com/storybookjs/storybook.git\n', 'path\\to\\storybook')
- ).toBe('github.com/storybookjs/storybook.gitpath/to/storybook');
+ unhashedProjectId(
+ "https://github.com/storybookjs/storybook.git\n",
+ "path\\to\\storybook",
+ ),
+ ).toBe("github.com/storybookjs/storybook.gitpath/to/storybook");
});
});
-describe('getProjectSince', () => {
+describe("getProjectSince", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.resetModules();
});
- it('returns the Storybook creation date from git log output', () => {
+ it("returns the Storybook creation date from git log output", () => {
vi.mocked(executeCommandSync).mockReturnValue(
- '2025-12-11 16:24:01 +0530\n' + '2014-12-11 19:09:10 +0530'
+ "2025-12-11 16:24:01 +0530\n" + "2014-12-11 19:09:10 +0530",
);
- expect(getProjectSince()).toEqual(new Date('2025-12-11T10:54:01.000Z'));
+ expect(getProjectSince()).toEqual(new Date("2025-12-11T10:54:01.000Z"));
});
- it('returns undefined if git log output is empty', async () => {
- vi.mocked(executeCommandSync).mockReturnValue('');
+ it("returns undefined if git log output is empty", async () => {
+ vi.mocked(executeCommandSync).mockReturnValue("");
- const { getProjectSince: getProjSince } = await import('./anonymous-id');
+ const { getProjectSince: getProjSince } = await import("./anonymous-id");
expect(getProjSince()).toBeUndefined();
});
- it('returns undefined if git log fails', async () => {
+ it("returns undefined if git log fails", async () => {
vi.mocked(executeCommandSync).mockImplementation(() => {
- throw new Error('git not available');
+ throw new Error("git not available");
});
- const { getProjectSince: getProjSince } = await import('./anonymous-id');
+ const { getProjectSince: getProjSince } = await import("./anonymous-id");
expect(getProjSince()).toBeUndefined();
});
});
-describe('getAnonymousProjectId', () => {
+describe("getAnonymousProjectId", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.resetModules();
- vi.spyOn(process, 'cwd').mockReturnValue('/path/to/project/root');
+ vi.spyOn(process, "cwd").mockReturnValue("/path/to/project/root");
});
- it('returns hashed project id for Storybook repo when git command succeeds', async () => {
- vi.mocked(executeCommandSync).mockReturnValue('git@github.com:storybookjs/storybook.git');
+ it("returns hashed project id for Storybook repo when git command succeeds", async () => {
+ vi.mocked(executeCommandSync).mockReturnValue(
+ "git@github.com:storybookjs/storybook.git",
+ );
const result = getAnonymousProjectId();
- expect(result).toMatch('061e4ee22a1f7c079849d97234b3be94d016fb1f24ba11878c41f8b48c0213bf');
+ expect(result).toMatch(
+ "061e4ee22a1f7c079849d97234b3be94d016fb1f24ba11878c41f8b48c0213bf",
+ );
});
- it('returns undefined when git command fails', async () => {
- const { getAnonymousProjectId: getAnonId } = await import('./anonymous-id');
+ it("returns undefined when git command fails", async () => {
+ const { getAnonymousProjectId: getAnonId } = await import("./anonymous-id");
vi.mocked(executeCommandSync).mockImplementation(() => {
- throw new Error('git not available');
+ throw new Error("git not available");
});
const result = getAnonId();
From e9bde7cd0ff83de9a330b85f9e942da5672f1a45 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 20 Mar 2026 17:20:13 +0000
Subject: [PATCH 07/14] Split STORAGE_KEY test into two: mock telemetry to test
project ID vs anonymous fallback
---
code/core/src/manager-api/tests/store.test.js | 28 +++++++++++++++++--
1 file changed, 25 insertions(+), 3 deletions(-)
diff --git a/code/core/src/manager-api/tests/store.test.js b/code/core/src/manager-api/tests/store.test.js
index 7635521dfb5e..0e23b613c524 100644
--- a/code/core/src/manager-api/tests/store.test.js
+++ b/code/core/src/manager-api/tests/store.test.js
@@ -1,4 +1,4 @@
-import { describe, expect, it, vi } from 'vitest';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
import flushPromises from 'flush-promises';
import store2 from 'store2';
@@ -21,9 +21,31 @@ vi.mock('store2', () => ({
vi.mock('../version', () => ({ version: '0.0.0-test' }));
+vi.mock('storybook/internal/telemetry', () => ({
+ getAnonymousProjectId: vi.fn(),
+}));
+
describe('store', () => {
- it('falls back to anonymous storage key when STORYBOOK_INSTANCE_ID is not set', () => {
- expect(STORAGE_KEY).toBe('@storybook/manager/store/anonymous');
+ describe('STORAGE_KEY', () => {
+ beforeEach(() => {
+ vi.resetModules();
+ });
+
+ it('scopes storage key to the project ID when available', async () => {
+ vi.doMock('storybook/internal/telemetry', () => ({
+ getAnonymousProjectId: () => 'test-project-id',
+ }));
+ const { STORAGE_KEY: key } = await import('../store');
+ expect(key).toBe('@storybook/manager/store/test-project-id');
+ });
+
+ it('falls back to anonymous when project ID is unavailable', async () => {
+ vi.doMock('storybook/internal/telemetry', () => ({
+ getAnonymousProjectId: () => undefined,
+ }));
+ const { STORAGE_KEY: key } = await import('../store');
+ expect(key).toBe('@storybook/manager/store/anonymous');
+ });
});
it('persists the current version in localStorage on getInitialState', () => {
From a381d845f221c1f9a2987974a430dcdd4dc51014 Mon Sep 17 00:00:00 2001
From: Steve Dodier-Lazaro
Date: Mon, 23 Mar 2026 20:26:01 +0100
Subject: [PATCH 08/14] style: format files
---
.../src/builder-manager/utils/framework.ts | 16 +-
code/core/src/manager-api/store.ts | 1 -
code/core/src/telemetry/anonymous-id.test.ts | 186 ++++++++----------
3 files changed, 92 insertions(+), 111 deletions(-)
diff --git a/code/core/src/builder-manager/utils/framework.ts b/code/core/src/builder-manager/utils/framework.ts
index e5e47e33c01f..df89e68a6336 100644
--- a/code/core/src/builder-manager/utils/framework.ts
+++ b/code/core/src/builder-manager/utils/framework.ts
@@ -3,26 +3,22 @@ import {
frameworkPackages,
frameworkToRenderer,
getFrameworkName,
-} from "storybook/internal/common";
-import { type Options, SupportedBuilder } from "storybook/internal/types";
+} from 'storybook/internal/common';
+import { type Options, SupportedBuilder } from 'storybook/internal/types';
export const buildFrameworkGlobalsFromOptions = async (options: Options) => {
const globals: Record = {};
- const { builder: builderConfig, channelOptions } =
- await options.presets.apply("core");
- const builderName =
- typeof builderConfig === "string" ? builderConfig : builderConfig?.name;
- const builder = Object.values(SupportedBuilder).find((builder) =>
- builderName?.includes(builder),
- );
+ const { builder: builderConfig, channelOptions } = await options.presets.apply('core');
+ const builderName = typeof builderConfig === 'string' ? builderConfig : builderConfig?.name;
+ const builder = Object.values(SupportedBuilder).find((builder) => builderName?.includes(builder));
const frameworkName = await getFrameworkName(options);
const frameworkPackageName = extractFrameworkPackageName(frameworkName);
const framework = frameworkPackages[frameworkPackageName];
const renderer = frameworkToRenderer[framework];
- if (options.configType === "DEVELOPMENT") {
+ if (options.configType === 'DEVELOPMENT') {
// Manager only needs the token currently, so we don't pass any other channel options.
globals.CHANNEL_OPTIONS = { wsToken: channelOptions?.wsToken };
}
diff --git a/code/core/src/manager-api/store.ts b/code/core/src/manager-api/store.ts
index 33742796bbec..a92748e67705 100644
--- a/code/core/src/manager-api/store.ts
+++ b/code/core/src/manager-api/store.ts
@@ -6,7 +6,6 @@ import type { State } from './root';
import { version as currentVersion } from './version';
import { getAnonymousProjectId } from 'storybook/internal/telemetry';
-
// setting up the store, overriding set and get to use telejson
storeSetup(store._);
diff --git a/code/core/src/telemetry/anonymous-id.test.ts b/code/core/src/telemetry/anonymous-id.test.ts
index 148046633b40..cdee088797ab 100644
--- a/code/core/src/telemetry/anonymous-id.test.ts
+++ b/code/core/src/telemetry/anonymous-id.test.ts
@@ -1,21 +1,21 @@
-import { beforeEach, describe, expect, it, vi } from "vitest";
+import { beforeEach, describe, expect, it, vi } from 'vitest';
-import { executeCommandSync } from "storybook/internal/common";
+import { executeCommandSync } from 'storybook/internal/common';
import {
getAnonymousProjectId,
getProjectSince,
normalizeGitUrl,
unhashedProjectId,
-} from "./anonymous-id";
+} from './anonymous-id';
-vi.mock(import("storybook/internal/common"), async (actualModule) => {
+vi.mock(import('storybook/internal/common'), async (actualModule) => {
const actual = await actualModule();
return {
...actual,
executeCommandSync: vi.fn(actual.executeCommandSync),
- getProjectRoot: () => "/path/to/project/root",
+ getProjectRoot: () => '/path/to/project/root',
};
});
@@ -23,177 +23,163 @@ beforeEach(() => {
vi.mocked(executeCommandSync).mockReset();
});
-describe("normalizeGitUrl", () => {
- it("trims off https://", () => {
- expect(
- normalizeGitUrl("https://github.com/storybookjs/storybook.git"),
- ).toEqual("github.com/storybookjs/storybook.git");
+describe('normalizeGitUrl', () => {
+ it('trims off https://', () => {
+ expect(normalizeGitUrl('https://github.com/storybookjs/storybook.git')).toEqual(
+ 'github.com/storybookjs/storybook.git'
+ );
});
- it("trims off http://", () => {
- expect(
- normalizeGitUrl("http://github.com/storybookjs/storybook.git"),
- ).toEqual("github.com/storybookjs/storybook.git");
+ it('trims off http://', () => {
+ expect(normalizeGitUrl('http://github.com/storybookjs/storybook.git')).toEqual(
+ 'github.com/storybookjs/storybook.git'
+ );
});
- it("trims off git+https://", () => {
- expect(
- normalizeGitUrl("git+https://github.com/storybookjs/storybook.git"),
- ).toEqual("github.com/storybookjs/storybook.git");
+ it('trims off git+https://', () => {
+ expect(normalizeGitUrl('git+https://github.com/storybookjs/storybook.git')).toEqual(
+ 'github.com/storybookjs/storybook.git'
+ );
});
- it("trims off https://username@", () => {
- expect(
- normalizeGitUrl("https://username@github.com/storybookjs/storybook.git"),
- ).toEqual("github.com/storybookjs/storybook.git");
+ it('trims off https://username@', () => {
+ expect(normalizeGitUrl('https://username@github.com/storybookjs/storybook.git')).toEqual(
+ 'github.com/storybookjs/storybook.git'
+ );
});
- it("trims off http://username@", () => {
- expect(
- normalizeGitUrl("http://username@github.com/storybookjs/storybook.git"),
- ).toEqual("github.com/storybookjs/storybook.git");
+ it('trims off http://username@', () => {
+ expect(normalizeGitUrl('http://username@github.com/storybookjs/storybook.git')).toEqual(
+ 'github.com/storybookjs/storybook.git'
+ );
});
- it("trims off https://username:password@", () => {
+ it('trims off https://username:password@', () => {
expect(
- normalizeGitUrl(
- "https://username:password@github.com/storybookjs/storybook.git",
- ),
- ).toEqual("github.com/storybookjs/storybook.git");
+ normalizeGitUrl('https://username:password@github.com/storybookjs/storybook.git')
+ ).toEqual('github.com/storybookjs/storybook.git');
});
- it("trims off http://username:password@", () => {
+ it('trims off http://username:password@', () => {
expect(
- normalizeGitUrl(
- "http://username:password@github.com/storybookjs/storybook.git",
- ),
- ).toEqual("github.com/storybookjs/storybook.git");
+ normalizeGitUrl('http://username:password@github.com/storybookjs/storybook.git')
+ ).toEqual('github.com/storybookjs/storybook.git');
});
- it("trims off git://", () => {
- expect(
- normalizeGitUrl("git://github.com/storybookjs/storybook.git"),
- ).toEqual("github.com/storybookjs/storybook.git");
+ it('trims off git://', () => {
+ expect(normalizeGitUrl('git://github.com/storybookjs/storybook.git')).toEqual(
+ 'github.com/storybookjs/storybook.git'
+ );
});
- it("trims off git@", () => {
- expect(normalizeGitUrl("git@github.com:storybookjs/storybook.git")).toEqual(
- "github.com/storybookjs/storybook.git",
+ it('trims off git@', () => {
+ expect(normalizeGitUrl('git@github.com:storybookjs/storybook.git')).toEqual(
+ 'github.com/storybookjs/storybook.git'
);
});
- it("trims off git+ssh://git@", () => {
- expect(
- normalizeGitUrl("git+ssh://git@github.com:storybookjs/storybook.git"),
- ).toEqual("github.com/storybookjs/storybook.git");
+ it('trims off git+ssh://git@', () => {
+ expect(normalizeGitUrl('git+ssh://git@github.com:storybookjs/storybook.git')).toEqual(
+ 'github.com/storybookjs/storybook.git'
+ );
});
- it("trims off ssh://git@", () => {
- expect(
- normalizeGitUrl("ssh://git@github.com:storybookjs/storybook.git"),
- ).toEqual("github.com/storybookjs/storybook.git");
+ it('trims off ssh://git@', () => {
+ expect(normalizeGitUrl('ssh://git@github.com:storybookjs/storybook.git')).toEqual(
+ 'github.com/storybookjs/storybook.git'
+ );
});
- it("adds .git if missing", () => {
- expect(normalizeGitUrl("https://github.com/storybookjs/storybook")).toEqual(
- "github.com/storybookjs/storybook.git",
+ it('adds .git if missing', () => {
+ expect(normalizeGitUrl('https://github.com/storybookjs/storybook')).toEqual(
+ 'github.com/storybookjs/storybook.git'
);
});
- it("trims off #hash", () => {
- expect(
- normalizeGitUrl("https://github.com/storybookjs/storybook.git#next"),
- ).toEqual("github.com/storybookjs/storybook.git");
+ it('trims off #hash', () => {
+ expect(normalizeGitUrl('https://github.com/storybookjs/storybook.git#next')).toEqual(
+ 'github.com/storybookjs/storybook.git'
+ );
});
- it("trims off extra whitespace", () => {
- expect(
- normalizeGitUrl("https://github.com/storybookjs/storybook.git#next\n"),
- ).toEqual("github.com/storybookjs/storybook.git");
+ it('trims off extra whitespace', () => {
+ expect(normalizeGitUrl('https://github.com/storybookjs/storybook.git#next\n')).toEqual(
+ 'github.com/storybookjs/storybook.git'
+ );
- expect(
- normalizeGitUrl("https://github.com/storybookjs/storybook.git\n"),
- ).toEqual("github.com/storybookjs/storybook.git");
+ expect(normalizeGitUrl('https://github.com/storybookjs/storybook.git\n')).toEqual(
+ 'github.com/storybookjs/storybook.git'
+ );
});
});
-describe("unhashedProjectId", () => {
- it("does not touch unix paths", () => {
+describe('unhashedProjectId', () => {
+ it('does not touch unix paths', () => {
expect(
- unhashedProjectId(
- "https://github.com/storybookjs/storybook.git\n",
- "path/to/storybook",
- ),
- ).toBe("github.com/storybookjs/storybook.gitpath/to/storybook");
+ unhashedProjectId('https://github.com/storybookjs/storybook.git\n', 'path/to/storybook')
+ ).toBe('github.com/storybookjs/storybook.gitpath/to/storybook');
});
- it("normalizes windows paths", () => {
+ it('normalizes windows paths', () => {
expect(
- unhashedProjectId(
- "https://github.com/storybookjs/storybook.git\n",
- "path\\to\\storybook",
- ),
- ).toBe("github.com/storybookjs/storybook.gitpath/to/storybook");
+ unhashedProjectId('https://github.com/storybookjs/storybook.git\n', 'path\\to\\storybook')
+ ).toBe('github.com/storybookjs/storybook.gitpath/to/storybook');
});
});
-describe("getProjectSince", () => {
+describe('getProjectSince', () => {
beforeEach(() => {
vi.clearAllMocks();
vi.resetModules();
});
- it("returns the Storybook creation date from git log output", () => {
+ it('returns the Storybook creation date from git log output', () => {
vi.mocked(executeCommandSync).mockReturnValue(
- "2025-12-11 16:24:01 +0530\n" + "2014-12-11 19:09:10 +0530",
+ '2025-12-11 16:24:01 +0530\n' + '2014-12-11 19:09:10 +0530'
);
- expect(getProjectSince()).toEqual(new Date("2025-12-11T10:54:01.000Z"));
+ expect(getProjectSince()).toEqual(new Date('2025-12-11T10:54:01.000Z'));
});
- it("returns undefined if git log output is empty", async () => {
- vi.mocked(executeCommandSync).mockReturnValue("");
+ it('returns undefined if git log output is empty', async () => {
+ vi.mocked(executeCommandSync).mockReturnValue('');
- const { getProjectSince: getProjSince } = await import("./anonymous-id");
+ const { getProjectSince: getProjSince } = await import('./anonymous-id');
expect(getProjSince()).toBeUndefined();
});
- it("returns undefined if git log fails", async () => {
+ it('returns undefined if git log fails', async () => {
vi.mocked(executeCommandSync).mockImplementation(() => {
- throw new Error("git not available");
+ throw new Error('git not available');
});
- const { getProjectSince: getProjSince } = await import("./anonymous-id");
+ const { getProjectSince: getProjSince } = await import('./anonymous-id');
expect(getProjSince()).toBeUndefined();
});
});
-describe("getAnonymousProjectId", () => {
+describe('getAnonymousProjectId', () => {
beforeEach(() => {
vi.clearAllMocks();
vi.resetModules();
- vi.spyOn(process, "cwd").mockReturnValue("/path/to/project/root");
+ vi.spyOn(process, 'cwd').mockReturnValue('/path/to/project/root');
});
- it("returns hashed project id for Storybook repo when git command succeeds", async () => {
- vi.mocked(executeCommandSync).mockReturnValue(
- "git@github.com:storybookjs/storybook.git",
- );
+ it('returns hashed project id for Storybook repo when git command succeeds', async () => {
+ vi.mocked(executeCommandSync).mockReturnValue('git@github.com:storybookjs/storybook.git');
const result = getAnonymousProjectId();
- expect(result).toMatch(
- "061e4ee22a1f7c079849d97234b3be94d016fb1f24ba11878c41f8b48c0213bf",
- );
+ expect(result).toMatch('061e4ee22a1f7c079849d97234b3be94d016fb1f24ba11878c41f8b48c0213bf');
});
- it("returns undefined when git command fails", async () => {
- const { getAnonymousProjectId: getAnonId } = await import("./anonymous-id");
+ it('returns undefined when git command fails', async () => {
+ const { getAnonymousProjectId: getAnonId } = await import('./anonymous-id');
vi.mocked(executeCommandSync).mockImplementation(() => {
- throw new Error("git not available");
+ throw new Error('git not available');
});
const result = getAnonId();
From f6aeb66ecbb5067c13b03175c6d102b0e7404bbe Mon Sep 17 00:00:00 2001
From: Steve Dodier-Lazaro
Date: Mon, 23 Mar 2026 20:41:10 +0100
Subject: [PATCH 09/14] Clean up after agent
---
code/core/src/manager-api/store.ts | 7 ++++---
code/core/src/manager-api/typings.d.ts | 1 -
code/core/src/manager/typings.d.ts | 6 ------
3 files changed, 4 insertions(+), 10 deletions(-)
diff --git a/code/core/src/manager-api/store.ts b/code/core/src/manager-api/store.ts
index a92748e67705..3c12a98ef389 100644
--- a/code/core/src/manager-api/store.ts
+++ b/code/core/src/manager-api/store.ts
@@ -12,8 +12,7 @@ storeSetup(store._);
const STORAGE_KEY_BASE = '@storybook/manager/store';
/**
- * Storage key scoped per Storybook instance via `STORYBOOK_INSTANCE_ID` (a Git-derived project
- * hash).
+ * Storage key scoped per Storybook instance (a Git-derived project hash).
*/
export const STORAGE_KEY = `${STORAGE_KEY_BASE}/${getAnonymousProjectId() || 'anonymous'}`;
@@ -88,7 +87,9 @@ export default class Store {
const initialState = { ...base, ...get(store.local), ...get(store.session) };
// Record the current version in localStorage so future migrations can be version-aware
- persistVersion(store.local);
+ if (this.upstreamPersistence) {
+ persistVersion(store.local);
+ }
return initialState;
}
diff --git a/code/core/src/manager-api/typings.d.ts b/code/core/src/manager-api/typings.d.ts
index a4043e9265f9..7bf67c97fc33 100644
--- a/code/core/src/manager-api/typings.d.ts
+++ b/code/core/src/manager-api/typings.d.ts
@@ -10,4 +10,3 @@ declare var STORYBOOK_ADDON_STATE: Record;
declare var STORYBOOK_FRAMEWORK: import('storybook/internal/types').SupportedFramework | undefined;
declare var STORYBOOK_RENDERER: import('storybook/internal/types').SupportedRenderer | undefined;
declare var STORYBOOK_BUILDER: import('storybook/internal/types').SupportedBuilder | undefined;
-declare var STORYBOOK_INSTANCE_ID: string | undefined;
diff --git a/code/core/src/manager/typings.d.ts b/code/core/src/manager/typings.d.ts
index 4b67ef96c38e..fb3fa90c7557 100644
--- a/code/core/src/manager/typings.d.ts
+++ b/code/core/src/manager/typings.d.ts
@@ -34,10 +34,4 @@ declare var __STORYBOOK_CLIENT_LOGGER__: any;
declare var __STORYBOOK_ADDONS_CHANNEL__: any;
declare var __STORYBOOK_TYPES__: any;
declare var STORYBOOK_RENDERER: string | undefined;
-/**
- * A stable, anonymous project identifier derived from Git info (remote URL + working directory
- * path). Used to scope localStorage keys per Storybook instance so that persistence is not shared
- * across different projects. Falls back to 'anonymous' when Git info is unavailable.
- */
-declare var STORYBOOK_INSTANCE_ID: string | undefined;
declare var sendTelemetryError: (error: any) => void;
From 690f85667dbafda80d94097dbae4f04f257d43fd Mon Sep 17 00:00:00 2001
From: Steve Dodier-Lazaro
Date: Mon, 23 Mar 2026 21:14:05 +0100
Subject: [PATCH 10/14] Pass instance ID via globals to not load Node in
manager
---
.../src/builder-manager/utils/framework.ts | 2 +
code/core/src/manager-api/store.ts | 13 ++++--
code/core/src/manager-api/tests/store.test.js | 44 ++++++++++++-------
code/core/src/manager-api/typings.d.ts | 1 +
code/core/src/manager/typings.d.ts | 1 +
code/core/src/typings.d.ts | 1 +
6 files changed, 42 insertions(+), 20 deletions(-)
diff --git a/code/core/src/builder-manager/utils/framework.ts b/code/core/src/builder-manager/utils/framework.ts
index df89e68a6336..68924bfbda85 100644
--- a/code/core/src/builder-manager/utils/framework.ts
+++ b/code/core/src/builder-manager/utils/framework.ts
@@ -4,6 +4,7 @@ import {
frameworkToRenderer,
getFrameworkName,
} from 'storybook/internal/common';
+import { getAnonymousProjectId } from 'storybook/internal/telemetry';
import { type Options, SupportedBuilder } from 'storybook/internal/types';
export const buildFrameworkGlobalsFromOptions = async (options: Options) => {
@@ -26,6 +27,7 @@ export const buildFrameworkGlobalsFromOptions = async (options: Options) => {
globals.STORYBOOK_FRAMEWORK = framework;
globals.STORYBOOK_RENDERER = renderer;
globals.STORYBOOK_NETWORK_ADDRESS = options.networkAddress;
+ globals.STORYBOOK_ANONYMOUS_PROJECT_ID = getAnonymousProjectId();
return globals;
};
diff --git a/code/core/src/manager-api/store.ts b/code/core/src/manager-api/store.ts
index 3c12a98ef389..d7057fa50bbb 100644
--- a/code/core/src/manager-api/store.ts
+++ b/code/core/src/manager-api/store.ts
@@ -4,7 +4,6 @@ import store from 'store2';
import storeSetup from './lib/store-setup';
import type { State } from './root';
import { version as currentVersion } from './version';
-import { getAnonymousProjectId } from 'storybook/internal/telemetry';
// setting up the store, overriding set and get to use telejson
storeSetup(store._);
@@ -12,9 +11,11 @@ storeSetup(store._);
const STORAGE_KEY_BASE = '@storybook/manager/store';
/**
- * Storage key scoped per Storybook instance (a Git-derived project hash).
+ * Storage key scoped per Storybook instance (a Git-derived project hash). The project ID is
+ * injected as a window global by the manager builder at build time so the manager (a browser app)
+ * does not need to depend on Node-only telemetry code.
*/
-export const STORAGE_KEY = `${STORAGE_KEY_BASE}/${getAnonymousProjectId() || 'anonymous'}`;
+export const STORAGE_KEY = `${STORAGE_KEY_BASE}/${globalThis.STORYBOOK_ANONYMOUS_PROJECT_ID || 'anonymous'}`;
/**
* Key used to store the Storybook version alongside persisted data. This allows future migration
@@ -84,7 +85,11 @@ export default class Store {
// We don't only merge at the very top level (the same way as React setState)
// when you set keys, so it makes sense to do the same in combining the two storage modes
// Really, you shouldn't store the same key in both places
- const initialState = { ...base, ...get(store.local), ...get(store.session) };
+ const initialState = {
+ ...base,
+ ...get(store.local),
+ ...get(store.session),
+ };
// Record the current version in localStorage so future migrations can be version-aware
if (this.upstreamPersistence) {
diff --git a/code/core/src/manager-api/tests/store.test.js b/code/core/src/manager-api/tests/store.test.js
index 0e23b613c524..27c0c4d5861f 100644
--- a/code/core/src/manager-api/tests/store.test.js
+++ b/code/core/src/manager-api/tests/store.test.js
@@ -1,4 +1,4 @@
-import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import flushPromises from 'flush-promises';
import store2 from 'store2';
@@ -21,28 +21,27 @@ vi.mock('store2', () => ({
vi.mock('../version', () => ({ version: '0.0.0-test' }));
-vi.mock('storybook/internal/telemetry', () => ({
- getAnonymousProjectId: vi.fn(),
-}));
-
describe('store', () => {
describe('STORAGE_KEY', () => {
+ let originalProjectId;
+
beforeEach(() => {
vi.resetModules();
+ originalProjectId = globalThis.STORYBOOK_ANONYMOUS_PROJECT_ID;
+ });
+
+ afterEach(() => {
+ globalThis.STORYBOOK_ANONYMOUS_PROJECT_ID = originalProjectId;
});
it('scopes storage key to the project ID when available', async () => {
- vi.doMock('storybook/internal/telemetry', () => ({
- getAnonymousProjectId: () => 'test-project-id',
- }));
+ globalThis.STORYBOOK_ANONYMOUS_PROJECT_ID = 'test-project-id';
const { STORAGE_KEY: key } = await import('../store');
expect(key).toBe('@storybook/manager/store/test-project-id');
});
it('falls back to anonymous when project ID is unavailable', async () => {
- vi.doMock('storybook/internal/telemetry', () => ({
- getAnonymousProjectId: () => undefined,
- }));
+ globalThis.STORYBOOK_ANONYMOUS_PROJECT_ID = undefined;
const { STORAGE_KEY: key } = await import('../store');
expect(key).toBe('@storybook/manager/store/anonymous');
});
@@ -59,8 +58,15 @@ describe('store', () => {
});
it('sensibly combines local+session storage for initial state', () => {
- store2.session.get.mockReturnValueOnce({ foo: 'bar', combined: { a: 'b' } });
- store2.local.get.mockReturnValueOnce({ foo: 'baz', another: 'value', combined: { c: 'd' } });
+ store2.session.get.mockReturnValueOnce({
+ foo: 'bar',
+ combined: { a: 'b' },
+ });
+ store2.local.get.mockReturnValueOnce({
+ foo: 'baz',
+ another: 'value',
+ combined: { c: 'd' },
+ });
const store = new Store({});
expect(store.getInitialState()).toEqual({
@@ -101,7 +107,9 @@ describe('store', () => {
await store.setState({ foo: 'bar' }, { persistence: 'session' });
expect(setState).toHaveBeenCalledWith({ foo: 'bar' }, expect.any(Function));
- expect(store2.session.set).toHaveBeenCalledWith(STORAGE_KEY, { foo: 'bar' });
+ expect(store2.session.set).toHaveBeenCalledWith(STORAGE_KEY, {
+ foo: 'bar',
+ });
expect(store2.local.set).not.toHaveBeenCalled();
});
@@ -114,7 +122,9 @@ describe('store', () => {
expect(setState).toHaveBeenCalledWith({ foo: 'bar' }, expect.any(Function));
expect(store2.session.set).not.toHaveBeenCalled();
- expect(store2.local.set).toHaveBeenCalledWith(STORAGE_KEY, { foo: 'bar' });
+ expect(store2.local.set).toHaveBeenCalledWith(STORAGE_KEY, {
+ foo: 'bar',
+ });
});
it('properly patches existing values', async () => {
@@ -197,7 +207,9 @@ describe('store', () => {
await store.setState(patch, { persistence: 'session' });
expect(patch).toHaveBeenCalledWith('OLD_STATE');
- expect(store2.session.set).toHaveBeenCalledWith(STORAGE_KEY, { foo: 'bar' });
+ expect(store2.session.set).toHaveBeenCalledWith(STORAGE_KEY, {
+ foo: 'bar',
+ });
});
});
});
diff --git a/code/core/src/manager-api/typings.d.ts b/code/core/src/manager-api/typings.d.ts
index 7bf67c97fc33..c72db4ff54b9 100644
--- a/code/core/src/manager-api/typings.d.ts
+++ b/code/core/src/manager-api/typings.d.ts
@@ -10,3 +10,4 @@ declare var STORYBOOK_ADDON_STATE: Record;
declare var STORYBOOK_FRAMEWORK: import('storybook/internal/types').SupportedFramework | undefined;
declare var STORYBOOK_RENDERER: import('storybook/internal/types').SupportedRenderer | undefined;
declare var STORYBOOK_BUILDER: import('storybook/internal/types').SupportedBuilder | undefined;
+declare var STORYBOOK_ANONYMOUS_PROJECT_ID: string | undefined;
diff --git a/code/core/src/manager/typings.d.ts b/code/core/src/manager/typings.d.ts
index fb3fa90c7557..01bd88981c53 100644
--- a/code/core/src/manager/typings.d.ts
+++ b/code/core/src/manager/typings.d.ts
@@ -33,5 +33,6 @@ declare var __STORYBOOK_ICONS__: any;
declare var __STORYBOOK_CLIENT_LOGGER__: any;
declare var __STORYBOOK_ADDONS_CHANNEL__: any;
declare var __STORYBOOK_TYPES__: any;
+declare var STORYBOOK_ANONYMOUS_PROJECT_ID: string | undefined;
declare var STORYBOOK_RENDERER: string | undefined;
declare var sendTelemetryError: (error: any) => void;
diff --git a/code/core/src/typings.d.ts b/code/core/src/typings.d.ts
index 512933868d83..aefbc2249ec8 100644
--- a/code/core/src/typings.d.ts
+++ b/code/core/src/typings.d.ts
@@ -17,6 +17,7 @@ declare var STORYBOOK_RENDERER: import('./types/modules/renderers').SupportedRen
declare var STORYBOOK_HOOKS_CONTEXT: any;
declare var STORYBOOK_CURRENT_TASK_LOG: undefined | null | Array;
+declare var STORYBOOK_ANONYMOUS_PROJECT_ID: string | undefined;
declare var STORYBOOK_NETWORK_ADDRESS: string | undefined;
declare var PREVIEW_URL: string | undefined;
From fc4038387e30a407162d1b54592ae034fe836ad2 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 26 Mar 2026 08:58:04 +0000
Subject: [PATCH 11/14] Plan for addressing review feedback
Co-authored-by: Sidnioulz <5108577+Sidnioulz@users.noreply.github.com>
Agent-Logs-Url: https://github.com/storybookjs/storybook/sessions/1172a9ba-085a-4149-916f-63bf6a11d4d3
---
.../docs/rules/await-interactions.md | 8 ++---
.../eslint-plugin/docs/rules/csf-component.md | 4 +--
.../docs/rules/default-exports.md | 2 +-
.../docs/rules/hierarchy-separator.md | 4 +--
.../docs/rules/meta-inline-properties.md | 4 +--
.../docs/rules/meta-satisfies-type.md | 8 ++---
.../docs/rules/no-redundant-story-name.md | 4 +--
.../docs/rules/no-renderer-packages.md | 14 ++++----
.../eslint-plugin/docs/rules/no-stories-of.md | 6 ++--
.../docs/rules/no-title-property-in-meta.md | 2 +-
.../docs/rules/no-uninstalled-addons.md | 13 +++++---
.../eslint-plugin/docs/rules/story-exports.md | 4 +--
.../docs/rules/use-storybook-expect.md | 4 +--
.../rules/use-storybook-testing-library.md | 6 ++--
docs/configure/integration/eslint-plugin.mdx | 32 +++++++++----------
15 files changed, 59 insertions(+), 56 deletions(-)
diff --git a/code/lib/eslint-plugin/docs/rules/await-interactions.md b/code/lib/eslint-plugin/docs/rules/await-interactions.md
index 16cde9402495..1945dc55f802 100644
--- a/code/lib/eslint-plugin/docs/rules/await-interactions.md
+++ b/code/lib/eslint-plugin/docs/rules/await-interactions.md
@@ -13,28 +13,28 @@ Storybook provides an instrumented version of the testing library in the [storyb
Examples of **incorrect** code for this rule:
```js
-import { userEvent, within } from 'storybook/test';
+import { userEvent, within } from "storybook/test";
// or from the legacy package "@storybook/testing-library";
MyStory.play = (context) => {
const canvas = within(context.canvasElement);
// not awaited!
- userEvent.click(canvas.getByRole('button'));
+ userEvent.click(canvas.getByRole("button"));
};
```
Examples of **correct** code for this rule:
```js
-import { userEvent, within } from 'storybook/test';
+import { userEvent, within } from "storybook/test";
// or from the legacy package "@storybook/testing-library";
MyStory.play = async (context) => {
const canvas = within(context.canvasElement);
// awaited 👍
- await userEvent.click(canvas.getByRole('button'));
+ await userEvent.click(canvas.getByRole("button"));
};
```
diff --git a/code/lib/eslint-plugin/docs/rules/csf-component.md b/code/lib/eslint-plugin/docs/rules/csf-component.md
index c37678324a05..ed50122f004b 100644
--- a/code/lib/eslint-plugin/docs/rules/csf-component.md
+++ b/code/lib/eslint-plugin/docs/rules/csf-component.md
@@ -14,7 +14,7 @@ Examples of **incorrect** code for this rule:
```js
export default {
- title: 'Button',
+ title: "Button",
};
```
@@ -22,7 +22,7 @@ Examples of **correct** code for this rule:
```js
export default {
- title: 'Button',
+ title: "Button",
component: Button,
};
```
diff --git a/code/lib/eslint-plugin/docs/rules/default-exports.md b/code/lib/eslint-plugin/docs/rules/default-exports.md
index 57393270964b..ff3bd7456d8e 100644
--- a/code/lib/eslint-plugin/docs/rules/default-exports.md
+++ b/code/lib/eslint-plugin/docs/rules/default-exports.md
@@ -21,7 +21,7 @@ Examples of **correct** code for this rule:
```js
export default {
- title: 'Button',
+ title: "Button",
args: { primary: true },
component: Button,
};
diff --git a/code/lib/eslint-plugin/docs/rules/hierarchy-separator.md b/code/lib/eslint-plugin/docs/rules/hierarchy-separator.md
index ce5dfb0ed340..a67d4392f34b 100644
--- a/code/lib/eslint-plugin/docs/rules/hierarchy-separator.md
+++ b/code/lib/eslint-plugin/docs/rules/hierarchy-separator.md
@@ -14,7 +14,7 @@ Examples of **incorrect** code for this rule:
```js
export default {
- title: 'Components|Forms/Input',
+ title: "Components|Forms/Input",
component: Input,
};
```
@@ -23,7 +23,7 @@ Examples of **correct** code for this rule:
```js
export default {
- title: 'Components/Forms/Input',
+ title: "Components/Forms/Input",
component: Input,
};
```
diff --git a/code/lib/eslint-plugin/docs/rules/meta-inline-properties.md b/code/lib/eslint-plugin/docs/rules/meta-inline-properties.md
index 05347855dc23..ba0bc088ffd7 100644
--- a/code/lib/eslint-plugin/docs/rules/meta-inline-properties.md
+++ b/code/lib/eslint-plugin/docs/rules/meta-inline-properties.md
@@ -13,7 +13,7 @@ This rule encourages you to use inline property definitions for the default expo
Examples of **incorrect** code for this rule:
```js
-const title = 'Button';
+const title = "Button";
const args = { primary: true };
export default {
@@ -27,7 +27,7 @@ Examples of **correct** code for this rule:
```js
export default {
- title: 'Button',
+ title: "Button",
args: { primary: true },
component: Button,
};
diff --git a/code/lib/eslint-plugin/docs/rules/meta-satisfies-type.md b/code/lib/eslint-plugin/docs/rules/meta-satisfies-type.md
index 52afe887e3a8..52378e3791e0 100644
--- a/code/lib/eslint-plugin/docs/rules/meta-satisfies-type.md
+++ b/code/lib/eslint-plugin/docs/rules/meta-satisfies-type.md
@@ -16,7 +16,7 @@ Examples of **incorrect** code for this rule:
```ts
export default {
- title: 'Button',
+ title: "Button",
args: { primary: true },
component: Button,
};
@@ -24,7 +24,7 @@ export default {
```ts
const meta: Meta = {
- title: 'Button',
+ title: "Button",
args: { primary: true },
component: Button,
};
@@ -35,7 +35,7 @@ Examples of **correct** code for this rule:
```ts
export default {
- title: 'Button',
+ title: "Button",
args: { primary: true },
component: Button,
} satisfies Meta;
@@ -43,7 +43,7 @@ export default {
```ts
const meta = {
- title: 'Button',
+ title: "Button",
args: { primary: true },
component: Button,
} satisfies Meta;
diff --git a/code/lib/eslint-plugin/docs/rules/no-redundant-story-name.md b/code/lib/eslint-plugin/docs/rules/no-redundant-story-name.md
index 44079b842c52..4528de5c3aa1 100644
--- a/code/lib/eslint-plugin/docs/rules/no-redundant-story-name.md
+++ b/code/lib/eslint-plugin/docs/rules/no-redundant-story-name.md
@@ -15,7 +15,7 @@ Examples of **incorrect** code for this rule:
```js
export const PrimaryButton = {
// no need for this, as Storybook will resolve to this name already
- name: 'Primary Button',
+ name: "Primary Button",
};
```
@@ -23,7 +23,7 @@ Examples of **correct** code for this rule:
```js
export const PrimaryButton = {
- name: 'I am the primary',
+ name: "I am the primary",
};
```
diff --git a/code/lib/eslint-plugin/docs/rules/no-renderer-packages.md b/code/lib/eslint-plugin/docs/rules/no-renderer-packages.md
index 8fa50646898a..72b9ec657039 100644
--- a/code/lib/eslint-plugin/docs/rules/no-renderer-packages.md
+++ b/code/lib/eslint-plugin/docs/rules/no-renderer-packages.md
@@ -24,19 +24,19 @@ Examples of **incorrect** code for this rule:
```js
// Don't import renderer packages directly
-import { something } from '@storybook/react';
-import { something } from '@storybook/vue3';
-import { something } from '@storybook/web-components';
+import { something } from "@storybook/react";
+import { something } from "@storybook/vue3";
+import { something } from "@storybook/web-components";
```
Examples of **correct** code for this rule:
```js
// Do use the appropriate framework package for your build tool
-import { something } from '@storybook/react-vite'; // For Vite
-import { something } from '@storybook/vue3-vite'; // For Vite
-import { something } from '@storybook/web-components-vite'; // For Vite
-import { something } from '@storybook/nextjs'; // For Next.js
+import { something } from "@storybook/react-vite"; // For Vite
+import { something } from "@storybook/vue3-vite"; // For Vite
+import { something } from "@storybook/web-components-vite"; // For Vite
+import { something } from "@storybook/nextjs"; // For Next.js
```
## When Not To Use It
diff --git a/code/lib/eslint-plugin/docs/rules/no-stories-of.md b/code/lib/eslint-plugin/docs/rules/no-stories-of.md
index fba61b3bfc6c..d4e42f9b1183 100644
--- a/code/lib/eslint-plugin/docs/rules/no-stories-of.md
+++ b/code/lib/eslint-plugin/docs/rules/no-stories-of.md
@@ -13,11 +13,11 @@ Starting with Storybook 5.2, the Component Story Format ([CSF](https://storybook
Examples of **incorrect** code for this rule:
```js
-import { storiesOf } from '@storybook/react';
+import { storiesOf } from "@storybook/react";
-import Button from '../components/Button';
+import Button from "../components/Button";
-storiesOf('Button', module).add('primary', () => );
+storiesOf("Button", module).add("primary", () => );
```
Examples of **correct** code for this rule:
diff --git a/code/lib/eslint-plugin/docs/rules/no-title-property-in-meta.md b/code/lib/eslint-plugin/docs/rules/no-title-property-in-meta.md
index f5c32df25567..8c4d548c4ad5 100644
--- a/code/lib/eslint-plugin/docs/rules/no-title-property-in-meta.md
+++ b/code/lib/eslint-plugin/docs/rules/no-title-property-in-meta.md
@@ -15,7 +15,7 @@ Examples of **incorrect** code for this rule:
```js
export default {
- title: 'Components/Forms/Input',
+ title: "Components/Forms/Input",
component: Input,
};
```
diff --git a/code/lib/eslint-plugin/docs/rules/no-uninstalled-addons.md b/code/lib/eslint-plugin/docs/rules/no-uninstalled-addons.md
index f407987a7cfb..742ea6180c63 100644
--- a/code/lib/eslint-plugin/docs/rules/no-uninstalled-addons.md
+++ b/code/lib/eslint-plugin/docs/rules/no-uninstalled-addons.md
@@ -72,7 +72,10 @@ This rule assumes that the `package.json` is located in the root of your project
```js
module.exports = {
rules: {
- 'storybook/no-uninstalled-addons': ['error', { packageJsonLocation: './folder/package.json' }],
+ "storybook/no-uninstalled-addons": [
+ "error",
+ { packageJsonLocation: "./folder/package.json" },
+ ],
},
};
```
@@ -86,11 +89,11 @@ You can also ignore specific addons by providing an ignore array in the options:
```js
module.exports = {
rules: {
- 'storybook/no-uninstalled-addons': [
- 'error',
+ "storybook/no-uninstalled-addons": [
+ "error",
{
- packageJsonLocation: './folder/package.json',
- ignore: ['custom-addon'],
+ packageJsonLocation: "./folder/package.json",
+ ignore: ["custom-addon"],
},
],
},
diff --git a/code/lib/eslint-plugin/docs/rules/story-exports.md b/code/lib/eslint-plugin/docs/rules/story-exports.md
index dbc9c44904cb..7a1be8e6f900 100644
--- a/code/lib/eslint-plugin/docs/rules/story-exports.md
+++ b/code/lib/eslint-plugin/docs/rules/story-exports.md
@@ -14,7 +14,7 @@ Examples of **incorrect** code for this rule:
```js
export default {
- title: 'Button',
+ title: "Button",
args: { primary: true },
component: Button,
};
@@ -25,7 +25,7 @@ Examples of **correct** code for this rule:
```js
export default {
- title: 'Button',
+ title: "Button",
args: { primary: true },
component: Button,
};
diff --git a/code/lib/eslint-plugin/docs/rules/use-storybook-expect.md b/code/lib/eslint-plugin/docs/rules/use-storybook-expect.md
index ae197c50ad37..d7c54c24a2ba 100644
--- a/code/lib/eslint-plugin/docs/rules/use-storybook-expect.md
+++ b/code/lib/eslint-plugin/docs/rules/use-storybook-expect.md
@@ -24,9 +24,9 @@ Examples of **correct** code for this rule:
```js
// Correct import.
-import { expect } from 'storybook/test';
+import { expect } from "storybook/test";
// or this, which is now considered legacy
-import { expect } from '@storybook/jest';
+import { expect } from "@storybook/jest";
Default.play = async () => {
// Using imported expect from storybook package
diff --git a/code/lib/eslint-plugin/docs/rules/use-storybook-testing-library.md b/code/lib/eslint-plugin/docs/rules/use-storybook-testing-library.md
index aefddb556d49..a08b40b8c687 100644
--- a/code/lib/eslint-plugin/docs/rules/use-storybook-testing-library.md
+++ b/code/lib/eslint-plugin/docs/rules/use-storybook-testing-library.md
@@ -15,7 +15,7 @@ Examples of **incorrect** code for this rule:
```js
// wrong import!
-import { within } from '@testing-library/react';
+import { within } from "@testing-library/react";
Default.play = async (context) => {
const canvas = within(context.canvasElement);
@@ -26,9 +26,9 @@ Examples of **correct** code for this rule:
```js
// correct import.
-import { within } from 'storybook/test';
+import { within } from "storybook/test";
// or this, which is now considered legacy
-import { within } from '@storybook/testing-library';
+import { within } from "@storybook/testing-library";
Default.play = async (context) => {
const canvas = within(context.canvasElement);
diff --git a/docs/configure/integration/eslint-plugin.mdx b/docs/configure/integration/eslint-plugin.mdx
index fa5263e16401..6928d7050833 100644
--- a/docs/configure/integration/eslint-plugin.mdx
+++ b/docs/configure/integration/eslint-plugin.mdx
@@ -1,5 +1,5 @@
---
-title: 'ESLint plugin'
+title: "ESLint plugin"
hideRendererSelector: true
sidebar:
order: 5
@@ -40,10 +40,10 @@ For more details on why this line is required in the `.eslintignore` file, refer
If you are using [flat config style](https://eslint.org/docs/latest/use/configure/configuration-files-new), add this to your configuration file:
```js title="eslint.config.js"
-import { defineConfig, globalIgnores } from 'eslint/config';
+import { defineConfig, globalIgnores } from "eslint/config";
export default defineConfig([
- globalIgnores(['!.storybook'], 'Include Storybook Directory'),
+ globalIgnores(["!.storybook"], "Include Storybook Directory"),
// ...
]);
```
@@ -92,12 +92,12 @@ Optionally, you can override, add to, or disable individual rules. You likely do
Use the `eslint.config.js` file to configure rules using the [flat config style](https://eslint.org/docs/latest/use/configure/configuration-files-new). This is the default in ESLint v9, but can be used starting from ESLint v8.57.0. See also: https://eslint.org/docs/latest/use/configure/configuration-files-new.
```js title="eslint.config.js"
-import storybook from 'eslint-plugin-storybook';
+import storybook from "eslint-plugin-storybook";
// Replace the eslint/config package with @eslint/config-helpers if you're using an older version of ESLint.
-import { defineConfig } from 'eslint/config';
+import { defineConfig } from "eslint/config";
export default defineConfig([
- ...storybook.configs['flat/recommended'],
+ ...storybook.configs["flat/recommended"],
// Add more configuration options and generic rulesets here, such as js.configs.recommended
]);
```
@@ -105,13 +105,13 @@ export default defineConfig([
In case you are using utility functions from tools like `tseslint`, you might need to register the plugin a little differently:
```ts title="eslint.config.ts"
-import storybook from 'eslint-plugin-storybook';
-import somePlugin from 'some-plugin';
-import tseslint from 'typescript-eslint';
+import storybook from "eslint-plugin-storybook";
+import somePlugin from "some-plugin";
+import tseslint from "typescript-eslint";
export default tseslint.config(
somePlugin,
- storybook.configs['flat/recommended'] // notice that it is not destructured
+ storybook.configs["flat/recommended"], // notice that it is not destructured
);
```
@@ -120,19 +120,19 @@ export default tseslint.config(
Optionally, you can override, add, or disable individual rules. You likely don't want these settings to be applied to every file, so ensure that you add a flat config section in your `eslint.config.js` file that applies the overrides only to your story files.
```js title="eslint.config.js"
-import storybook from 'eslint-plugin-storybook';
-import { defineConfig } from 'eslint/config';
+import storybook from "eslint-plugin-storybook";
+import { defineConfig } from "eslint/config";
export default defineConfig([
- ...storybook.configs['flat/recommended'],
+ ...storybook.configs["flat/recommended"],
{
// 👇 This should match the `stories` property in .storybook/main.js|ts
- files: ['**/*.stories.@(ts|tsx|js|jsx|mjs|cjs)'],
+ files: ["**/*.stories.@(ts|tsx|js|jsx|mjs|cjs)"],
rules: {
// 👇 Enable this rule
- 'storybook/csf-component': 'error',
+ "storybook/csf-component": "error",
// 👇 Disable this rule
- 'storybook/default-exports': 'off',
+ "storybook/default-exports": "off",
},
},
]);
From d5ae983c40caad8922e5d2acead63cc8b3ff448f Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 26 Mar 2026 09:01:14 +0000
Subject: [PATCH 12/14] Restore migration logic, scope lastViewedStoryIds,
dev-only project ID
Co-authored-by: Sidnioulz <5108577+Sidnioulz@users.noreply.github.com>
Agent-Logs-Url: https://github.com/storybookjs/storybook/sessions/1172a9ba-085a-4149-916f-63bf6a11d4d3
---
.../src/builder-manager/utils/framework.ts | 5 +++-
code/core/src/manager-api/store.ts | 12 +++++++++-
code/core/src/manager-api/tests/store.test.js | 23 +++++++++++++++++++
.../components/sidebar/useLastViewed.ts | 6 +++--
4 files changed, 42 insertions(+), 4 deletions(-)
diff --git a/code/core/src/builder-manager/utils/framework.ts b/code/core/src/builder-manager/utils/framework.ts
index 68924bfbda85..f230fdde6216 100644
--- a/code/core/src/builder-manager/utils/framework.ts
+++ b/code/core/src/builder-manager/utils/framework.ts
@@ -27,7 +27,10 @@ export const buildFrameworkGlobalsFromOptions = async (options: Options) => {
globals.STORYBOOK_FRAMEWORK = framework;
globals.STORYBOOK_RENDERER = renderer;
globals.STORYBOOK_NETWORK_ADDRESS = options.networkAddress;
- globals.STORYBOOK_ANONYMOUS_PROJECT_ID = getAnonymousProjectId();
+ if (options.configType === 'DEVELOPMENT') {
+ /** Anonymous project ID derived from Git info, falls back to `anonymous` when it can't be computed. */
+ globals.STORYBOOK_ANONYMOUS_PROJECT_ID = getAnonymousProjectId();
+ }
return globals;
};
diff --git a/code/core/src/manager-api/store.ts b/code/core/src/manager-api/store.ts
index d7057fa50bbb..17df4207b767 100644
--- a/code/core/src/manager-api/store.ts
+++ b/code/core/src/manager-api/store.ts
@@ -28,7 +28,17 @@ function persistVersion(storage: StoreAPI) {
}
function get(storage: StoreAPI) {
- const data = storage.get(STORAGE_KEY);
+ let data = storage.get(STORAGE_KEY);
+
+ // Migration: if no data exists under the instance-specific key, try the old shared key.
+ // This is a one-time migration that preserves existing settings when upgrading.
+ if (!data && STORAGE_KEY !== STORAGE_KEY_BASE) {
+ data = storage.get(STORAGE_KEY_BASE);
+ if (data) {
+ storage.set(STORAGE_KEY, data);
+ }
+ }
+
return data || {};
}
diff --git a/code/core/src/manager-api/tests/store.test.js b/code/core/src/manager-api/tests/store.test.js
index 27c0c4d5861f..0b134e9901f2 100644
--- a/code/core/src/manager-api/tests/store.test.js
+++ b/code/core/src/manager-api/tests/store.test.js
@@ -22,6 +22,10 @@ vi.mock('store2', () => ({
vi.mock('../version', () => ({ version: '0.0.0-test' }));
describe('store', () => {
+ beforeEach(() => {
+ vi.resetAllMocks();
+ });
+
describe('STORAGE_KEY', () => {
let originalProjectId;
@@ -57,6 +61,25 @@ describe('store', () => {
expect(store2.local.set).toHaveBeenCalledWith(`${STORAGE_KEY}/__version__`, '0.0.0-test');
});
+ it('migrates data from the old shared key to the instance-scoped key', () => {
+ const legacyData = { shortcuts: { fullScreen: ['alt', 'F'] } };
+
+ // First call to get() returns nothing under the scoped key
+ store2.local.get.mockImplementation((key) => {
+ if (key === '@storybook/manager/store') {
+ return legacyData;
+ }
+ return undefined;
+ });
+ store2.session.get.mockReturnValueOnce({});
+
+ const s = new Store({});
+ const state = s.getInitialState();
+
+ expect(state).toEqual(expect.objectContaining(legacyData));
+ expect(store2.local.set).toHaveBeenCalledWith(STORAGE_KEY, legacyData);
+ });
+
it('sensibly combines local+session storage for initial state', () => {
store2.session.get.mockReturnValueOnce({
foo: 'bar',
diff --git a/code/core/src/manager/components/sidebar/useLastViewed.ts b/code/core/src/manager/components/sidebar/useLastViewed.ts
index d64840ee5add..7c290fe35f4a 100644
--- a/code/core/src/manager/components/sidebar/useLastViewed.ts
+++ b/code/core/src/manager/components/sidebar/useLastViewed.ts
@@ -5,11 +5,13 @@ import store from 'store2';
import type { Selection, StoryRef } from './types';
-const save = debounce((value) => store.set('lastViewedStoryIds', value), 1000);
+const LAST_VIEWED_KEY = `@storybook/manager/lastViewedStoryIds/${globalThis.STORYBOOK_ANONYMOUS_PROJECT_ID || 'anonymous'}`;
+
+const save = debounce((value) => store.set(LAST_VIEWED_KEY, value), 1000);
export const useLastViewed = (selection: Selection) => {
const initialLastViewedStoryIds = useMemo((): StoryRef[] => {
- const items = store.get('lastViewedStoryIds');
+ const items = store.get(LAST_VIEWED_KEY);
if (!items || !Array.isArray(items)) {
return [];
From 40ec7767db9d930962ee6527576ff77aa4835682 Mon Sep 17 00:00:00 2001
From: Steve Dodier-Lazaro
Date: Thu, 2 Apr 2026 14:03:30 +0200
Subject: [PATCH 13/14] format
---
package.json | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/package.json b/package.json
index 079b0867da1d..7a34927ddf99 100644
--- a/package.json
+++ b/package.json
@@ -20,24 +20,24 @@
"get-report-message": "cd scripts; yarn get-report-message",
"get-sandbox-dir": "cd scripts; yarn get-sandbox-dir",
"i": "yarn",
+ "postinstall": "husky",
"knip": "cd code; yarn knip",
"lint": "cd code; yarn lint",
"nx": "nx",
"pretty-docs": "cd scripts; yarn install >/dev/null; yarn docs:fmt:write",
"start": "yarn task --task dev --template react-vite/default-ts --start-from=install",
+ "storybook:vitest": "NODE_OPTIONS=--max_old_space_size=4096 vitest watch --project storybook-ui",
+ "storybook:vitest:inspect": "INSPECT=true NODE_OPTIONS=--max_old_space_size=4096 vitest run --project storybook-ui",
"svelte-ecosystem-ci:before-test": "./scripts/ecosystem-ci/before-test.sh svelte-kit/skeleton-ts",
"svelte-ecosystem-ci:build": "./scripts/ecosystem-ci/build.sh svelte-kit/skeleton-ts svelte",
"svelte-ecosystem-ci:test": "./scripts/ecosystem-ci/test.sh svelte-kit/skeleton-ts",
"task": "yarn --cwd=./scripts task",
- "storybook:vitest": "NODE_OPTIONS=--max_old_space_size=4096 vitest watch --project storybook-ui",
- "storybook:vitest:inspect": "INSPECT=true NODE_OPTIONS=--max_old_space_size=4096 vitest run --project storybook-ui",
"test": "NODE_OPTIONS=--max_old_space_size=4096 vitest run",
"test:watch": "NODE_OPTIONS=--max_old_space_size=4096 vitest watch",
"upload-bench": "cd scripts; yarn upload-bench",
"vite-ecosystem-ci:before-test": "./scripts/ecosystem-ci/before-test.sh react-vite/default-ts",
"vite-ecosystem-ci:build": "./scripts/ecosystem-ci/build.sh react-vite/default-ts",
- "vite-ecosystem-ci:test": "./scripts/ecosystem-ci/test.sh react-vite/default-ts",
- "postinstall": "husky"
+ "vite-ecosystem-ci:test": "./scripts/ecosystem-ci/test.sh react-vite/default-ts"
},
"resolutions": {
"@babel/runtime": "latest",
From 2da328e494638a05b1bc5ab54e0ebf57d3a9296d Mon Sep 17 00:00:00 2001
From: Steve Dodier-Lazaro
Date: Thu, 2 Apr 2026 14:15:01 +0200
Subject: [PATCH 14/14] Merge next
---
.github/workflows/nx.yml | 2 +-
.husky/pre-commit | 4 -
.lintstagedrc.mjs | 11 +
.nx/workflows/distribution-config.yaml | 1 -
.oxfmtrc.json | 65 +-
.vscode/extensions.json | 5 +-
.vscode/settings.json | 44 +-
AGENTS.md | 4 +
CHANGELOG.md | 11 +
CHANGELOG.prerelease.md | 44 ++
code/.eslintrc.js | 26 +
code/.oxfmtrc.json | 34 -
code/.storybook/bench/bench.stories.tsx | 2 +-
code/.storybook/preview.tsx | 8 +-
code/.storybook/storybook.setup.ts | 2 +-
code/addons/a11y/build-config.ts | 2 +-
code/addons/a11y/package.json | 2 +-
code/addons/a11y/src/a11yRunner.test.ts | 4 +-
code/addons/a11y/src/a11yRunner.ts | 6 +-
code/addons/a11y/src/a11yRunnerUtils.test.ts | 2 +-
code/addons/a11y/src/a11yRunnerUtils.ts | 4 +-
code/addons/a11y/src/axeRuleMappingHelper.ts | 4 +-
.../a11y/src/components/A11YPanel.stories.tsx | 12 +-
.../a11y/src/components/A11YPanel.test.tsx | 6 +-
code/addons/a11y/src/components/A11YPanel.tsx | 10 +-
.../a11y/src/components/A11yContext.test.tsx | 6 +-
.../a11y/src/components/A11yContext.tsx | 48 +-
.../a11y/src/components/Report/Details.tsx | 6 +-
.../src/components/Report/Report.stories.tsx | 8 +-
.../a11y/src/components/Report/Report.tsx | 6 +-
code/addons/a11y/src/components/Tabs.tsx | 4 +-
.../TestDiscrepancyMessage.stories.tsx | 4 +-
.../src/components/TestDiscrepancyMessage.tsx | 2 +-
.../components/VisionSimulator.stories.tsx | 4 +-
.../a11y/src/components/VisionSimulator.tsx | 4 +-
code/addons/a11y/src/index.ts | 10 +-
code/addons/a11y/src/manager.test.tsx | 4 +-
code/addons/a11y/src/manager.tsx | 12 +-
code/addons/a11y/src/postinstall.ts | 2 +-
code/addons/a11y/src/preview.test.tsx | 10 +-
code/addons/a11y/src/preview.tsx | 8 +-
code/addons/a11y/src/types.ts | 2 +-
code/addons/a11y/src/withVisionSimulator.ts | 2 +-
code/addons/a11y/vitest.config.ts | 2 +-
code/addons/docs/package.json | 2 +-
code/addons/docs/src/blocks/blocks/Canvas.tsx | 8 +-
.../docs/src/blocks/components/Preview.tsx | 3 +
.../docs/src/blocks/components/Toolbar.tsx | 28 +-
.../src/blocks/controls/Color.stories.tsx | 50 +-
.../addons/docs/src/blocks/controls/Color.tsx | 15 +-
code/addons/docs/src/blocks/controls/types.ts | 8 +
code/addons/docs/src/mdx-plugin.ts | 53 +-
code/addons/links/build-config.ts | 2 +-
code/addons/links/package.json | 2 +-
code/addons/links/src/index.ts | 4 +-
code/addons/links/src/manager.ts | 2 +-
code/addons/links/src/preview.ts | 2 +-
.../links/src/react/components/link.test.tsx | 2 +-
.../links/src/react/components/link.tsx | 2 +-
code/addons/links/src/react/index.ts | 2 +-
code/addons/links/src/utils.test.ts | 2 +-
code/addons/links/src/utils.ts | 2 +-
code/addons/links/vitest.config.ts | 2 +-
code/addons/onboarding/build-config.ts | 2 +-
.../example-stories/Button.stories.tsx | 2 +-
code/addons/onboarding/package.json | 2 +-
code/addons/onboarding/src/Onboarding.tsx | 14 +-
code/addons/onboarding/src/Survey.tsx | 4 +-
.../components/Confetti/Confetti.stories.tsx | 2 +-
.../src/components/List/List.stories.tsx | 4 +-
.../onboarding/src/components/List/List.tsx | 2 +-
.../src/components/List/ListItem/ListItem.tsx | 6 +-
.../IntentSurvey/IntentSurvey.stories.tsx | 2 +-
.../features/IntentSurvey/IntentSurvey.tsx | 2 +-
.../SplashScreen/SplashScreen.stories.tsx | 2 +-
code/addons/onboarding/src/manager.tsx | 6 +-
code/addons/onboarding/src/preset.ts | 2 +-
code/addons/onboarding/vitest.config.ts | 2 +-
code/addons/pseudo-states/build-config.ts | 2 +-
code/addons/pseudo-states/package.json | 2 +-
code/addons/pseudo-states/src/index.ts | 8 +-
code/addons/pseudo-states/src/manager.ts | 4 +-
.../src/manager/PseudoStateTool.tsx | 2 +-
code/addons/pseudo-states/src/preview.ts | 4 +-
.../src/preview/rewriteStyleSheet.test.ts | 4 +-
.../src/preview/rewriteStyleSheet.ts | 4 +-
.../src/preview/splitSelectors.test.ts | 2 +-
.../src/preview/withPseudoState.ts | 6 +-
.../src/stories/Button.stories.tsx | 30 +-
.../src/stories/CSSAtRules.stories.tsx | 30 +-
.../src/stories/CustomElement.stories.tsx | 43 +-
.../stories/CustomElementNested.stories.tsx | 43 +-
.../src/stories/CustomElementNested.tsx | 2 +-
.../src/stories/Input.stories.tsx | 2 +-
.../src/stories/NestedRules.stories.tsx | 2 +-
.../src/stories/Portal.stories.tsx | 2 +-
.../src/stories/PseudoStateGrid.tsx | 20 +
.../src/stories/ShadowRoot.stories.tsx | 32 +-
.../stories/ShadowRootWithPart.stories.tsx | 32 +-
code/addons/pseudo-states/src/types.test-d.ts | 4 +-
code/addons/pseudo-states/src/types.ts | 2 +-
code/addons/pseudo-states/vitest.config.ts | 2 +-
code/addons/themes/build-config.ts | 2 +-
code/addons/themes/package.json | 2 +-
code/addons/themes/src/constants.ts | 2 +-
.../src/decorators/class-name.decorator.tsx | 4 +-
.../decorators/data-attribute.decorator.tsx | 4 +-
code/addons/themes/src/decorators/helpers.ts | 4 +-
code/addons/themes/src/decorators/index.ts | 8 +-
.../src/decorators/provider.decorator.tsx | 4 +-
code/addons/themes/src/index.ts | 8 +-
code/addons/themes/src/manager.tsx | 4 +-
code/addons/themes/src/preview.ts | 2 +-
code/addons/themes/src/theme-switcher.tsx | 4 +-
code/addons/themes/vitest.config.ts | 2 +-
code/addons/vitest/build-config.ts | 2 +-
code/addons/vitest/package.json | 2 +-
.../vitest/src/components/Description.tsx | 6 +-
.../components/GlobalErrorModal.stories.tsx | 4 +-
.../src/components/GlobalErrorModal.tsx | 4 +-
.../src/components/RelativeTime.stories.tsx | 2 +-
.../src/components/SidebarContextMenu.tsx | 4 +-
.../components/TestProviderRender.stories.tsx | 8 +-
.../src/components/TestProviderRender.tsx | 10 +-
.../src/components/TestStatusIcon.stories.tsx | 2 +-
code/addons/vitest/src/constants.ts | 6 +-
code/addons/vitest/src/logger.ts | 2 +-
code/addons/vitest/src/manager-store.mock.ts | 2 +-
code/addons/vitest/src/manager-store.ts | 4 +-
code/addons/vitest/src/manager.tsx | 10 +-
.../vitest/src/node/boot-test-runner.test.ts | 10 +-
.../vitest/src/node/boot-test-runner.ts | 16 +-
.../vitest/src/node/coverage-reporter.ts | 4 +-
code/addons/vitest/src/node/reporter.ts | 4 +-
.../vitest/src/node/test-manager.test.ts | 8 +-
code/addons/vitest/src/node/test-manager.ts | 8 +-
code/addons/vitest/src/node/vitest-manager.ts | 12 +-
code/addons/vitest/src/node/vitest.ts | 6 +-
code/addons/vitest/src/postinstall.test.ts | 2 +-
code/addons/vitest/src/postinstall.ts | 6 +-
code/addons/vitest/src/preset.ts | 10 +-
.../src/updateVitestFile.config.3.2.test.ts | 4 +-
.../src/updateVitestFile.config.4.test.ts | 4 +-
.../src/updateVitestFile.config.test.ts | 4 +-
.../updateVitestFile.config.workspace.test.ts | 4 +-
.../vitest/src/updateVitestFile.test.ts | 2 +-
code/addons/vitest/src/updateVitestFile.ts | 8 +-
.../vitest/src/use-test-provider-state.ts | 13 +-
code/addons/vitest/src/utils.ts | 2 +-
code/addons/vitest/src/vitest-plugin/index.ts | 14 +-
.../src/vitest-plugin/setup-file.test.ts | 2 +-
.../vitest/src/vitest-plugin/setup-file.ts | 2 +-
.../vitest/src/vitest-plugin/test-utils.ts | 2 +-
code/addons/vitest/src/vitest-plugin/utils.ts | 7 +-
.../src/vitest-plugin/viewports.test.ts | 2 +-
code/addons/vitest/vitest.config.ts | 2 +-
code/builders/builder-vite/build-config.ts | 2 +-
code/builders/builder-vite/input/iframe.html | 8 +-
code/builders/builder-vite/package.json | 2 +-
code/builders/builder-vite/src/build.ts | 12 +-
.../src/codegen-importfn-script.test.ts | 2 +-
.../src/codegen-importfn-script.ts | 2 +-
.../src/codegen-modern-iframe-script.test.ts | 6 +-
.../src/codegen-modern-iframe-script.ts | 4 +-
.../src/codegen-project-annotations.ts | 9 +-
code/builders/builder-vite/src/index.test.ts | 402 ++++++++++++
code/builders/builder-vite/src/index.ts | 156 ++++-
.../src/plugins/code-generator-plugin.ts | 14 +-
.../builder-vite/src/plugins/index.ts | 18 +-
.../src/plugins/inject-export-order-plugin.ts | 43 +-
.../src/plugins/storybook-entry-plugin.ts | 6 +-
.../storybook-external-globals-plugin.test.ts | 2 +-
.../storybook-external-globals-plugin.ts | 44 +-
.../storybook-optimize-deps-plugin.test.ts | 2 +-
.../plugins/storybook-optimize-deps-plugin.ts | 4 +-
.../storybook-project-annotations-plugin.ts | 4 +-
.../src/plugins/storybook-runtime-plugin.ts | 2 +-
.../src/plugins/strip-story-hmr-boundaries.ts | 26 +-
.../src/plugins/vite-inject-mocker/plugin.ts | 13 +-
.../src/plugins/vite-mock/plugin.ts | 23 +-
.../src/plugins/webpack-stats-plugin.ts | 2 +-
code/builders/builder-vite/src/preset.ts | 12 +-
.../builder-vite/src/transform-iframe-html.ts | 2 +-
.../src/utils/build-module-graph.test.ts | 153 +++++
.../src/utils/build-module-graph.ts | 82 +++
.../src/utils/has-vite-plugins.test.ts | 2 +-
.../utils/process-preview-annotation.test.ts | 4 +-
.../src/utils/without-vite-plugins.test.ts | 2 +-
.../builder-vite/src/vite-config.test.ts | 4 +-
code/builders/builder-vite/src/vite-config.ts | 6 +-
code/builders/builder-vite/src/vite-server.ts | 4 +-
code/builders/builder-vite/vitest.config.ts | 2 +-
.../builders/builder-webpack5/build-config.ts | 2 +-
code/builders/builder-webpack5/package.json | 2 +-
code/builders/builder-webpack5/src/index.ts | 4 +-
.../src/presets/custom-webpack-preset.ts | 6 +-
.../src/presets/preview-preset.ts | 2 +-
.../src/preview/iframe-webpack.config.ts | 4 +-
.../src/preview/virtual-module-mapping.ts | 2 +-
.../builder-webpack5/vitest.config.ts | 2 +-
code/core/build-config.ts | 2 +-
code/core/package.json | 2 +-
code/core/scripts/generate-source-files.ts | 7 +-
code/core/src/__tests/preview-errors.test.ts | 2 +-
code/core/src/__tests/server-errors.test.ts | 2 +-
code/core/src/__tests/storybook-error.test.ts | 2 +-
code/core/src/actions/addArgs.ts | 2 +-
code/core/src/actions/addArgsHelpers.test.ts | 2 +-
code/core/src/actions/addArgsHelpers.ts | 2 +-
.../actions/components/ActionLogger/index.tsx | 4 +-
code/core/src/actions/components/Title.tsx | 2 +-
.../actions/containers/ActionLogger/index.tsx | 8 +-
code/core/src/actions/decorator.ts | 4 +-
code/core/src/actions/index.ts | 6 +-
code/core/src/actions/loaders.ts | 2 +-
code/core/src/actions/manager.tsx | 6 +-
code/core/src/actions/models/ActionDisplay.ts | 2 +-
.../src/actions/models/ActionsFunction.ts | 4 +-
code/core/src/actions/models/ActionsMap.ts | 2 +-
.../actions/models/HandlerFunction.test-d.ts | 2 +-
code/core/src/actions/models/index.ts | 12 +-
code/core/src/actions/preview.ts | 6 +-
code/core/src/actions/runtime/action.ts | 6 +-
code/core/src/actions/runtime/actions.ts | 6 +-
.../src/actions/runtime/configureActions.ts | 2 +-
code/core/src/actions/runtime/index.ts | 6 +-
.../src/babel/expression-resolver.test.ts | 2 +-
code/core/src/babel/index.ts | 6 +-
.../src/babel/vitest-config-helpers.test.ts | 5 +-
code/core/src/babel/vitest-config-helpers.ts | 4 +-
code/core/src/backgrounds/components/Tool.tsx | 11 +-
code/core/src/backgrounds/decorator.ts | 8 +-
code/core/src/backgrounds/defaults.ts | 2 +-
code/core/src/backgrounds/manager.tsx | 4 +-
code/core/src/backgrounds/preview.ts | 6 +-
code/core/src/backgrounds/types.ts | 2 +-
code/core/src/bin/core.ts | 8 +-
code/core/src/bin/dispatcher.ts | 4 +-
code/core/src/bin/loader.test.ts | 2 +-
code/core/src/bin/loader.ts | 2 +-
code/core/src/builder-manager/index.ts | 22 +-
code/core/src/builder-manager/utils/data.ts | 4 +-
.../src/builder-manager/utils/files.test.ts | 2 +-
code/core/src/builder-manager/utils/files.ts | 2 +-
.../src/builder-manager/utils/template.ts | 2 +-
code/core/src/channels/index.test.ts | 4 +-
code/core/src/channels/index.ts | 22 +-
code/core/src/channels/main.ts | 2 +-
code/core/src/channels/postmessage/index.ts | 4 +-
code/core/src/channels/websocket/index.ts | 2 +-
.../src/cli/AddonVitestService.constants.ts | 2 +-
code/core/src/cli/AddonVitestService.test.ts | 4 +-
code/core/src/cli/AddonVitestService.ts | 4 +-
code/core/src/cli/NpmOptions.ts | 2 +-
code/core/src/cli/dirs.ts | 2 +-
code/core/src/cli/eslintPlugin.test.ts | 6 +-
code/core/src/cli/eslintPlugin.ts | 2 +-
code/core/src/cli/globalSettings.test.ts | 2 +-
code/core/src/cli/globalSettings.ts | 2 +-
code/core/src/cli/helpers.test.ts | 4 +-
code/core/src/cli/helpers.ts | 2 +-
code/core/src/cli/index.ts | 18 +-
code/core/src/client-logger/index.test.ts | 2 +-
code/core/src/common/config.test.ts | 2 +-
code/core/src/common/index.ts | 95 ++-
.../src/common/js-package-manager/BUNProxy.ts | 12 +-
.../JsPackageManager.test.ts | 2 +-
.../js-package-manager/JsPackageManager.ts | 12 +-
.../JsPackageManagerFactory.test.ts | 16 +-
.../JsPackageManagerFactory.ts | 20 +-
.../js-package-manager/NPMProxy.test.ts | 12 +-
.../src/common/js-package-manager/NPMProxy.ts | 12 +-
.../js-package-manager/PNPMProxy.test.ts | 12 +-
.../common/js-package-manager/PNPMProxy.ts | 12 +-
.../js-package-manager/Yarn1Proxy.test.ts | 8 +-
.../common/js-package-manager/Yarn1Proxy.ts | 14 +-
.../js-package-manager/Yarn2Proxy.test.ts | 6 +-
.../common/js-package-manager/Yarn2Proxy.ts | 16 +-
.../src/common/js-package-manager/index.ts | 8 +-
code/core/src/common/presets.test.ts | 4 +-
code/core/src/common/presets.ts | 10 +-
.../utils/__tests__/interpret-files.test.ts | 2 +-
.../utils/__tests__/normalize-stories.test.ts | 2 +-
.../src/common/utils/__tests__/paths.test.ts | 2 +-
.../common/utils/__tests__/template.test.ts | 2 +-
code/core/src/common/utils/cache.ts | 4 +-
code/core/src/common/utils/cli.test.ts | 2 +-
code/core/src/common/utils/cli.ts | 6 +-
code/core/src/common/utils/envs.ts | 2 +-
code/core/src/common/utils/file-cache.ts | 2 +-
code/core/src/common/utils/formatter.test.ts | 2 +-
.../utils/get-addon-annotations.test.ts | 2 +-
.../src/common/utils/get-addon-annotations.ts | 2 +-
.../src/common/utils/get-addon-names.test.ts | 2 +-
code/core/src/common/utils/get-addon-names.ts | 2 +-
.../common/utils/get-framework-name.test.ts | 2 +-
.../src/common/utils/get-framework-name.ts | 4 +-
.../common/utils/get-renderer-name.test.ts | 2 +-
.../src/common/utils/get-renderer-name.ts | 6 +-
.../src/common/utils/get-story-id.test.ts | 2 +-
code/core/src/common/utils/get-story-id.ts | 4 +-
.../utils/get-storybook-configuration.test.ts | 2 +-
.../src/common/utils/get-storybook-info.ts | 14 +-
.../common/utils/get-storybook-refs.test.ts | 2 +-
.../src/common/utils/get-storybook-refs.ts | 2 +-
.../src/common/utils/interpret-require.ts | 4 +-
.../core/src/common/utils/load-main-config.ts | 6 +-
.../utils/load-manager-or-addons-file.ts | 2 +-
.../utils/load-preview-or-config-file.ts | 2 +-
.../src/common/utils/normalize-path.test.ts | 2 +-
.../src/common/utils/normalize-stories.ts | 4 +-
code/core/src/common/utils/paths.ts | 2 +-
code/core/src/common/utils/posix.test.ts | 2 +-
code/core/src/common/utils/remove.ts | 4 +-
.../utils/resolve-path-in-sb-cache.test.ts | 6 +-
.../common/utils/resolve-path-in-sb-cache.ts | 2 +-
.../utils/scan-and-transform-files.test.ts | 4 +-
.../common/utils/scan-and-transform-files.ts | 4 +-
.../utils/setup-addon-in-config.test.ts | 8 +-
.../src/common/utils/setup-addon-in-config.ts | 6 +-
.../utils/sync-main-preview-addons.test.ts | 4 +-
.../common/utils/sync-main-preview-addons.ts | 4 +-
code/core/src/common/utils/template.ts | 2 +-
.../common/utils/transform-imports.test.ts | 2 +-
code/core/src/common/utils/utils.test.ts | 225 +++++++
.../src/common/utils/validate-config.test.ts | 2 +-
code/core/src/common/utils/validate-config.ts | 4 +-
.../utils/validate-configuration-files.ts | 2 +-
.../utils/write-file-with-retry.test.ts | 2 +-
code/core/src/common/versions.ts | 84 +--
.../components/DetachedDebuggerMessage.tsx | 2 +-
.../components/EmptyState.tsx | 2 +-
.../components/Interaction.stories.tsx | 8 +-
.../components/Interaction.tsx | 14 +-
.../components/InteractionsPanel.stories.tsx | 10 +-
.../components/InteractionsPanel.tsx | 18 +-
.../components/MatcherResult.stories.tsx | 2 +-
.../components/MatcherResult.tsx | 4 +-
.../components/MethodCall.stories.tsx | 4 +-
.../components/MethodCall.tsx | 2 +-
.../components/Panel.test.ts | 101 ++-
.../component-testing/components/Panel.tsx | 12 +-
.../components/PanelTitle.tsx | 8 +-
.../components/StatusBadge.stories.tsx | 2 +-
.../components/StatusIcon.stories.tsx | 4 +-
.../components/StatusIcon.tsx | 2 +-
.../TestDiscrepancyMessage.stories.tsx | 4 +-
.../components/TestDiscrepancyMessage.tsx | 4 +-
.../components/Toolbar.stories.tsx | 2 +-
.../component-testing/components/Toolbar.tsx | 6 +-
.../components/test-fn.stories.tsx | 2 +-
.../src/component-testing/manager.test.tsx | 6 +-
code/core/src/component-testing/manager.tsx | 8 +-
.../core/src/component-testing/mocks/index.ts | 2 +-
code/core/src/component-testing/utils.ts | 2 +-
.../brand/StorybookIcon.stories.tsx | 2 +-
.../brand/StorybookLogo.stories.tsx | 2 +-
.../ActionBar/ActionBar.stories.tsx | 2 +-
.../ActionList/ActionList.stories.tsx | 8 +-
.../components/ActionList/ActionList.tsx | 6 +-
.../components/Badge/Badge.stories.tsx | 2 +-
.../components/components/Bar/Bar.stories.tsx | 4 +-
.../components/Bar/FlexBar.stories.tsx | 4 +-
.../components/Button/Button.stories.tsx | 4 +-
.../components/components/Button/Button.tsx | 4 +-
.../InteractiveTooltipWrapper.stories.tsx | 4 +-
.../helpers/InteractiveTooltipWrapper.tsx | 4 +-
.../components/Card/Card.stories.tsx | 4 +-
.../Collapsible/Collapsible.stories.tsx | 6 +-
.../ErrorFormatter/ErrorFormatter.stories.tsx | 2 +-
.../components/Form/Checkbox.stories.tsx | 2 +-
.../components/Form/Field.stories.tsx | 8 +-
.../src/components/components/Form/Form.tsx | 14 +-
.../components/Form/Input.stories.tsx | 2 +-
.../src/components/components/Form/Input.tsx | 2 +-
.../components/Form/Radio.stories.tsx | 2 +-
.../components/Form/Select.stories.tsx | 2 +-
.../src/components/components/Form/Select.tsx | 4 +-
.../components/Form/Textarea.stories.tsx | 2 +-
.../components/components/Form/Textarea.tsx | 2 +-
.../components/Loader/Loader.stories.tsx | 2 +-
.../components/components/Loader/Loader.tsx | 2 +-
.../components/Modal/Modal.stories.tsx | 6 +-
.../components/Modal/Modal.styled.tsx | 4 +-
.../src/components/components/Modal/Modal.tsx | 4 +-
.../components/Popover/Popover.stories.tsx | 4 +-
.../components/components/Popover/Popover.tsx | 2 +-
.../Popover/PopoverProvider.stories.tsx | 6 +-
.../components/Popover/PopoverProvider.tsx | 4 +-
.../ProgressSpinner.stories.tsx | 2 +-
.../ScrollArea/ScrollArea.stories.tsx | 2 +-
.../components/Select/Select.stories.tsx | 4 +-
.../components/components/Select/Select.tsx | 12 +-
.../Select/SelectOption.stories.tsx | 6 +-
.../components/Select/SelectOption.tsx | 2 +-
.../Tabs/EmptyTabContent.stories.tsx | 2 +-
.../components/Tabs/StatelessTabList.tsx | 2 +-
.../components/Tabs/StatelessTabPanel.tsx | 2 +-
.../Tabs/StatelessTabsView.stories.tsx | 10 +-
.../components/Tabs/StatelessTabsView.tsx | 6 +-
.../components/Tabs/TabList.stories.tsx | 10 +-
.../components/components/Tabs/TabList.tsx | 4 +-
.../components/Tabs/TabPanel.stories.tsx | 10 +-
.../components/components/Tabs/TabPanel.tsx | 4 +-
.../components/Tabs/Tabs.helpers.tsx | 2 +-
.../components/components/Tabs/Tabs.hooks.tsx | 10 +-
.../components/Tabs/Tabs.stories.tsx | 4 +-
.../src/components/components/Tabs/Tabs.tsx | 10 +-
.../components/Tabs/TabsView.stories.tsx | 4 +-
.../components/components/Tabs/TabsView.tsx | 8 +-
.../ToggleButton/ToggleButton.stories.tsx | 2 +-
.../components/ToggleButton/ToggleButton.tsx | 2 +-
.../Toolbar/AbstractToolbar.stories.tsx | 4 +-
.../components/Toolbar/Toolbar.stories.tsx | 4 +-
.../components/components/Toolbar/Toolbar.tsx | 2 +-
.../components/Zoom/Zoom.stories.tsx | 2 +-
.../src/components/components/Zoom/Zoom.tsx | 4 +-
.../components/addon-panel/addon-panel.tsx | 2 +-
.../components/icon/icon.stories.tsx | 4 +-
.../placeholder/placeholder.stories.tsx | 4 +-
.../components/spaced/Spaced.stories.tsx | 2 +-
.../syntaxhighlighter/formatter.test.ts | 2 +-
.../components/syntaxhighlighter/formatter.ts | 2 +-
.../lazy-syntaxhighlighter.tsx | 8 +-
.../syntaxhighlighter-types.ts | 2 +-
.../syntaxhighlighter.stories.tsx | 2 +-
.../syntaxhighlighter/syntaxhighlighter.tsx | 10 +-
.../components/tooltip/ListItem.stories.tsx | 2 +-
.../components/tooltip/Tooltip.stories.tsx | 4 +-
.../components/components/tooltip/Tooltip.tsx | 2 +-
.../tooltip/TooltipLinkList.stories.tsx | 4 +-
.../components/tooltip/TooltipLinkList.tsx | 4 +-
.../tooltip/TooltipMessage.stories.tsx | 8 +-
.../components/tooltip/TooltipMessage.tsx | 2 +-
.../tooltip/TooltipNote.stories.tsx | 6 +-
.../tooltip/TooltipProvider.stories.tsx | 6 +-
.../components/tooltip/TooltipProvider.tsx | 2 +-
.../tooltip/WithTooltip.stories.tsx | 4 +-
.../components/tooltip/lazy-WithTooltip.tsx | 4 +-
.../typography/DocumentWrapper.stories.tsx | 2 +-
.../typography/DocumentWrapper.test.tsx | 2 +-
.../components/typography/ResetWrapper.tsx | 2 +-
.../components/typography/components.tsx | 46 +-
.../components/typography/elements/A.test.tsx | 2 +-
.../components/typography/elements/A.tsx | 4 +-
.../typography/elements/Blockquote.tsx | 2 +-
.../components/typography/elements/Code.tsx | 8 +-
.../components/typography/elements/DL.tsx | 2 +-
.../components/typography/elements/Div.tsx | 2 +-
.../components/typography/elements/H1.tsx | 2 +-
.../components/typography/elements/H2.tsx | 2 +-
.../components/typography/elements/H3.tsx | 2 +-
.../components/typography/elements/H4.tsx | 2 +-
.../components/typography/elements/H5.tsx | 2 +-
.../components/typography/elements/H6.tsx | 2 +-
.../components/typography/elements/LI.tsx | 2 +-
.../components/typography/elements/OL.tsx | 2 +-
.../components/typography/elements/P.tsx | 2 +-
.../components/typography/elements/Pre.tsx | 2 +-
.../components/typography/elements/Span.tsx | 2 +-
.../components/typography/elements/TT.tsx | 2 +-
.../components/typography/elements/Table.tsx | 2 +-
.../components/typography/elements/UL.tsx | 2 +-
.../typography/link/link.stories.tsx | 2 +-
.../components/typography/link/link.test.tsx | 4 +-
code/core/src/components/index.ts | 184 +++---
.../src/controls/components/ControlsPanel.tsx | 8 +-
.../controls/components/SaveStory.stories.tsx | 2 +-
code/core/src/controls/components/Title.tsx | 2 +-
code/core/src/controls/index.ts | 2 +-
code/core/src/controls/manager.tsx | 10 +-
code/core/src/controls/preview.ts | 2 +-
.../src/core-events/data/argtypes-info.ts | 2 +-
code/core/src/core-events/index.test.ts | 4 +-
code/core/src/core-events/index.ts | 16 +-
code/core/src/core-server/build-dev.ts | 42 +-
code/core/src/core-server/build-index.test.ts | 2 +-
code/core/src/core-server/build-index.ts | 4 +-
code/core/src/core-server/build-static.ts | 20 +-
.../ChangeDetectionService.test.ts | 593 ++++++++++++++++++
.../ChangeDetectionService.ts | 352 +++++++++++
.../change-detection/GitDiffProvider.test.ts | 100 +++
.../change-detection/GitDiffProvider.ts | 92 +++
.../core-server/change-detection/errors.ts | 13 +
.../src/core-server/change-detection/index.ts | 8 +
.../core-server/change-detection/readiness.ts | 41 ++
.../change-detection/trace-changed.test.ts | 151 +++++
.../change-detection/trace-changed.ts | 51 ++
code/core/src/core-server/dev-server.ts | 55 +-
code/core/src/core-server/index.ts | 39 +-
code/core/src/core-server/load.ts | 2 +-
.../src/core-server/presets/common-manager.ts | 14 +-
.../presets/common-override-preset.ts | 2 +-
.../src/core-server/presets/common-preset.ts | 39 +-
.../src/core-server/presets/favicon.test.ts | 2 +-
.../create-new-story-channel.test.ts | 2 +-
.../create-new-story-channel.ts | 2 +-
.../file-search-channel.test.ts | 8 +-
.../server-channel/file-search-channel.ts | 6 +-
.../ghost-stories-channel.test.ts | 4 +-
.../server-channel/ghost-stories-channel.ts | 4 +-
.../server-channel/telemetry-channel.test.ts | 2 +-
code/core/src/core-server/standalone.ts | 6 +-
code/core/src/core-server/stores/status.ts | 8 +-
.../src/core-server/stores/test-provider.ts | 8 +-
.../utils/StoryIndexGenerator.test.ts | 8 +-
.../core-server/utils/StoryIndexGenerator.ts | 14 +-
.../utils/__tests__/IndexingError.test.ts | 2 +-
.../utils/__tests__/autoName.test.ts | 2 +-
.../getHostValidationMiddleware.test.ts | 2 +-
.../utils/__tests__/index-extraction.test.ts | 6 +-
.../__tests__/remove-mdx-stories.test.ts | 2 +-
.../utils/__tests__/server-address.test.ts | 2 +-
.../utils/__tests__/server-channel.test.ts | 2 +-
.../utils/__tests__/server-statics.test.ts | 4 +-
.../utils/__tests__/validate-token.test.ts | 2 +-
code/core/src/core-server/utils/checklist.ts | 4 +-
code/core/src/core-server/utils/constants.ts | 2 +-
.../utils/copy-all-static-files.ts | 2 +-
.../core/src/core-server/utils/doTelemetry.ts | 8 +-
.../src/core-server/utils/generate-story.ts | 2 +-
.../src/core-server/utils/get-builders.ts | 4 +-
.../utils/get-caching-middleware.ts | 2 +-
.../utils/get-component-variable-name.test.ts | 2 +-
.../get-dummy-args-from-argtypes.test.ts | 4 +-
.../utils/get-new-story-file.test.ts | 4 +-
.../core-server/utils/get-new-story-file.ts | 14 +-
.../core-server/utils/get-server-channel.ts | 8 +-
.../utils/getAccessControlMiddleware.ts | 2 +-
.../utils/getHostValidationMiddleware.ts | 2 +-
.../ghost-stories/component-analyzer.test.ts | 2 +-
.../ghost-stories/get-candidates.test.ts | 4 +-
.../utils/ghost-stories/get-candidates.ts | 2 +-
.../ghost-stories/parse-vitest-report.test.ts | 2 +-
.../ghost-stories/parse-vitest-report.ts | 10 +-
.../utils/ghost-stories/run-story-tests.ts | 4 +-
.../src/core-server/utils/index-json.test.ts | 10 +-
code/core/src/core-server/utils/index-json.ts | 6 +-
.../utils/manifests/manifests.test.ts | 4 +-
.../core-server/utils/manifests/manifests.ts | 4 +-
.../manifests/render-components-manifest.ts | 4 +-
.../csf-factory-template.test.ts | 2 +-
.../csf-factory-template.ts | 2 +-
.../new-story-templates/javascript.test.ts | 2 +-
.../utils/new-story-templates/javascript.ts | 2 +-
.../new-story-templates/typescript.test.ts | 2 +-
.../utils/new-story-templates/typescript.ts | 2 +-
.../utils/open-browser/open-in-browser.ts | 2 +-
.../utils/open-browser/opener.test.ts | 2 +-
.../core-server/utils/open-browser/opener.ts | 4 +-
.../utils/output-startup-information.ts | 2 +-
.../utils/parser/generic-parser.test.ts | 2 +-
.../utils/parser/generic-parser.ts | 2 +-
.../src/core-server/utils/parser/index.ts | 4 +-
.../src/core-server/utils/safeString.test.ts | 2 +-
.../duplicate-story-with-new-name.test.ts | 4 +-
.../duplicate-story-with-new-name.ts | 2 +-
.../utils/save-story/save-story.ts | 6 +-
.../update-args-in-csf-file.test.ts | 4 +-
.../save-story/update-args-in-csf-file.ts | 4 +-
.../core-server/utils/search-files.test.ts | 2 +-
.../core-server/utils/server-address.test.ts | 2 +-
.../src/core-server/utils/server-statics.ts | 2 +-
.../utils/strip-comments-and-strings.test.ts | 2 +-
.../core-server/utils/summarizeIndex.test.ts | 4 +-
.../src/core-server/utils/summarizeIndex.ts | 4 +-
.../core-server/utils/summarizeStats.test.ts | 2 +-
.../utils/warnOnIncompatibleAddons.ts | 2 +-
.../utils/watch-story-specifiers.test.ts | 2 +-
code/core/src/core-server/utils/whats-new.ts | 2 +-
.../src/core-server/withTelemetry.test.ts | 172 ++++-
code/core/src/core-server/withTelemetry.ts | 34 +-
code/core/src/csf-tools/ConfigFile.test.ts | 2 +-
code/core/src/csf-tools/ConfigFile.ts | 2 +-
code/core/src/csf-tools/CsfFile.test.ts | 8 +-
code/core/src/csf-tools/CsfFile.ts | 6 +-
code/core/src/csf-tools/enrichCsf.test.ts | 6 +-
code/core/src/csf-tools/enrichCsf.ts | 2 +-
.../csf-tools/getStorySortParameter.test.ts | 2 +-
.../src/csf-tools/getStorySortParameter.ts | 2 +-
code/core/src/csf-tools/index.ts | 12 +-
code/core/src/csf-tools/storyIndexer.test.ts | 2 +-
.../component-transformer.test.ts | 2 +-
.../vitest-plugin/component-transformer.ts | 4 +-
.../vitest-plugin/transformer.test.ts | 4 +-
.../csf-tools/vitest-plugin/transformer.ts | 2 +-
code/core/src/csf/core-annotations.ts | 36 +-
code/core/src/csf/csf-factories.test.ts | 2 +-
code/core/src/csf/csf-factories.ts | 8 +-
.../src/csf/includeConditionalArg.test.ts | 4 +-
code/core/src/csf/includeConditionalArg.ts | 2 +-
code/core/src/csf/index.test.ts | 2 +-
code/core/src/csf/index.ts | 10 +-
code/core/src/csf/story.test.ts | 2 +-
code/core/src/csf/story.ts | 8 +-
code/core/src/csf/toStartCaseStr.test.ts | 2 +-
.../argTypes/convert/convert.test.ts | 4 +-
.../argTypes/convert/flow/convert.ts | 2 +-
.../docs-tools/argTypes/convert/flow/index.ts | 4 +-
.../src/docs-tools/argTypes/convert/index.ts | 12 +-
.../argTypes/convert/proptypes/convert.ts | 4 +-
.../argTypes/convert/proptypes/index.ts | 4 +-
.../argTypes/convert/typescript/convert.ts | 4 +-
.../argTypes/convert/typescript/index.ts | 4 +-
.../argTypes/docgen/createPropDef.ts | 18 +-
.../docgen/extractDocgenProps.test.ts | 4 +-
.../argTypes/docgen/extractDocgenProps.ts | 16 +-
.../docgen/flow/createDefaultValue.ts | 8 +-
.../docgen/flow/createPropDef.test.ts | 4 +-
.../argTypes/docgen/flow/createPropDef.ts | 6 +-
.../argTypes/docgen/flow/createType.ts | 6 +-
.../src/docs-tools/argTypes/docgen/index.ts | 8 +-
.../docgen/typeScript/createDefaultValue.ts | 8 +-
.../docgen/typeScript/createPropDef.test.ts | 4 +-
.../docgen/typeScript/createPropDef.ts | 6 +-
.../argTypes/docgen/typeScript/createType.ts | 6 +-
.../src/docs-tools/argTypes/docgen/types.ts | 4 +-
.../argTypes/docgen/utils/docgenInfo.ts | 4 +-
.../docs-tools/argTypes/docgen/utils/index.ts | 6 +-
.../argTypes/enhanceArgTypes.test.ts | 2 +-
.../docs-tools/argTypes/enhanceArgTypes.ts | 2 +-
code/core/src/docs-tools/argTypes/index.ts | 12 +-
.../docs-tools/argTypes/jsdocParser.test.ts | 2 +-
.../src/docs-tools/argTypes/jsdocParser.ts | 2 +-
.../src/docs-tools/argTypes/utils.test.ts | 2 +-
code/core/src/docs-tools/argTypes/utils.ts | 2 +-
code/core/src/docs-tools/index.ts | 4 +-
code/core/src/highlight/index.ts | 4 +-
code/core/src/highlight/preview.ts | 4 +-
code/core/src/highlight/types.ts | 2 +-
.../src/highlight/useHighlights.stories.tsx | 6 +-
code/core/src/highlight/useHighlights.ts | 6 +-
code/core/src/highlight/utils.test.ts | 2 +-
code/core/src/highlight/utils.ts | 6 +-
code/core/src/instrumenter/index.ts | 6 +-
.../src/instrumenter/instrumenter.test.ts | 64 +-
code/core/src/instrumenter/instrumenter.ts | 8 +-
code/core/src/instrumenter/preview-api.ts | 4 +-
code/core/src/instrumenter/types.ts | 2 +-
code/core/src/manager-api/context.ts | 2 +-
code/core/src/manager-api/index.mock.ts | 14 +-
code/core/src/manager-api/index.ts | 16 +-
code/core/src/manager-api/initial-state.ts | 4 +-
code/core/src/manager-api/lib/addons.ts | 4 +-
code/core/src/manager-api/lib/events.ts | 4 +-
code/core/src/manager-api/lib/filter-param.ts | 39 ++
.../core/src/manager-api/lib/shortcut.test.ts | 6 +-
code/core/src/manager-api/lib/shortcut.ts | 6 +-
code/core/src/manager-api/lib/stories.test.ts | 6 +-
code/core/src/manager-api/lib/stories.ts | 29 +-
code/core/src/manager-api/lib/types.tsx | 4 +-
code/core/src/manager-api/lib/url.ts | 10 +
code/core/src/manager-api/modules/addons.ts | 4 +-
code/core/src/manager-api/modules/channel.ts | 4 +-
code/core/src/manager-api/modules/globals.ts | 4 +-
code/core/src/manager-api/modules/layout.ts | 6 +-
.../src/manager-api/modules/notifications.ts | 2 +-
.../manager-api/modules/open-in-editor.tsx | 4 +-
code/core/src/manager-api/modules/provider.ts | 2 +-
code/core/src/manager-api/modules/refs.ts | 4 +-
code/core/src/manager-api/modules/settings.ts | 2 +-
.../core/src/manager-api/modules/shortcuts.ts | 23 +-
code/core/src/manager-api/modules/statuses.ts | 53 ++
code/core/src/manager-api/modules/stories.ts | 354 +++++++----
code/core/src/manager-api/modules/tags.ts | 111 ++++
code/core/src/manager-api/modules/url.ts | 68 +-
code/core/src/manager-api/modules/versions.ts | 4 +-
.../core/src/manager-api/modules/whatsnew.tsx | 2 +-
code/core/src/manager-api/root.tsx | 56 +-
code/core/src/manager-api/store.ts | 41 +-
.../manager-api/stores/__mocks__/checklist.ts | 6 +-
.../manager-api/stores/__mocks__/status.ts | 8 +-
.../stores/__mocks__/test-provider.ts | 8 +-
code/core/src/manager-api/stores/checklist.ts | 6 +-
code/core/src/manager-api/stores/status.ts | 8 +-
.../src/manager-api/stores/test-provider.ts | 8 +-
code/core/src/manager-api/test-utils/store.ts | 4 +-
.../core/src/manager-api/tests/events.test.ts | 4 +-
.../src/manager-api/tests/globals.test.ts | 10 +-
.../src/manager-api/tests/intersect.test.ts | 2 +-
.../core/src/manager-api/tests/layout.test.ts | 14 +-
code/core/src/manager-api/tests/refs.test.ts | 8 +-
.../src/manager-api/tests/statuses.test.ts | 280 +++++++++
code/core/src/manager-api/tests/store.test.js | 16 +-
.../src/manager-api/tests/stories.test.ts | 481 +++++++++++++-
code/core/src/manager-api/tests/tags.test.js | 41 ++
code/core/src/manager-api/tests/url.test.js | 32 +
code/core/src/manager-api/version.ts | 2 +-
code/core/src/manager-errors.ts | 4 +-
code/core/src/manager/App.tsx | 12 +-
code/core/src/manager/__tests__/index.test.ts | 2 +-
.../src/manager/components/Focus/Focus.tsx | 2 +-
.../components/Optional/Optional.stories.tsx | 2 +-
.../Particles/Particles.stories.tsx | 4 +-
.../manager/components/TextFlip.stories.tsx | 6 +-
.../TourGuide/HighlightElement.stories.tsx | 4 +-
.../TourGuide/TourGuide.stories.tsx | 2 +-
.../components/TourGuide/TourGuide.tsx | 4 +-
.../ManagerErrorBoundary.stories.tsx | 4 +-
.../components/error-boundary/index.ts | 2 +-
.../components/layout/Drag.stories.tsx | 2 +-
.../src/manager/components/layout/Drag.tsx | 6 +-
.../components/layout/Layout.stories.tsx | 8 +-
.../src/manager/components/layout/Layout.tsx | 18 +-
.../components/layout/LayoutProvider.tsx | 4 +-
.../components/layout/MainAreaContainer.tsx | 4 +-
.../components/layout/PanelContainer.tsx | 4 +-
.../components/layout/SidebarContainer.tsx | 2 +-
.../manager/components/layout/useDragging.ts | 4 +-
.../components/layout/useLandmarkIndicator.ts | 2 +-
.../mobile/about/MobileAbout.stories.tsx | 4 +-
.../components/mobile/about/MobileAbout.tsx | 6 +-
.../mobile/navigation/MobileAddonsDrawer.tsx | 2 +-
.../mobile/navigation/MobileMenuDrawer.tsx | 4 +-
.../navigation/MobileNavigation.stories.tsx | 50 +-
.../mobile/navigation/MobileNavigation.tsx | 14 +-
.../NotificationItem.stories.tsx | 2 +-
.../notifications/NotificationItem.tsx | 2 +-
.../NotificationList.stories.tsx | 4 +-
.../notifications/NotificationList.tsx | 4 +-
.../components/panel/Panel.stories.tsx | 4 +-
.../src/manager/components/panel/Panel.tsx | 8 +-
.../components/preview/FramesRenderer.tsx | 4 +-
.../components/preview/Iframe.stories.tsx | 2 +-
.../preview/NumericInput.stories.tsx | 4 +-
.../manager/components/preview/Preview.tsx | 14 +-
.../manager/components/preview/Toolbar.tsx | 6 +-
.../components/preview/Viewport.stories.tsx | 4 +-
.../manager/components/preview/Viewport.tsx | 6 +-
.../manager/components/preview/Wrappers.tsx | 4 +-
.../components/preview/tools/addons.tsx | 18 +-
.../manager/components/preview/tools/menu.tsx | 18 +-
.../preview/tools/share.stories.tsx | 2 +-
.../components/preview/tools/share.tsx | 2 +-
.../components/preview/tools/zoom.stories.tsx | 4 +-
.../manager/components/preview/tools/zoom.tsx | 4 +-
.../sidebar/ChecklistWidget.stories.tsx | 8 +-
.../components/sidebar/ChecklistWidget.tsx | 10 +-
.../components/sidebar/ContextMenu.tsx | 14 +-
.../sidebar/CreateNewStoryFileModal.tsx | 8 +-
.../components/sidebar/Explorer.stories.tsx | 10 +-
.../manager/components/sidebar/Explorer.tsx | 10 +-
.../sidebar/FileSearchList.stories.tsx | 2 +-
.../components/sidebar/FileSearchList.tsx | 6 +-
.../FileSearchListSkeleton.stories.tsx | 2 +-
.../sidebar/FileSearchListSkeleton.tsx | 2 +-
.../sidebar/FileSearchModal.stories.tsx | 4 +-
.../components/sidebar/FileSearchModal.tsx | 6 +-
.../sidebar/FileSearchModal.utils.test.tsx | 2 +-
.../sidebar/FileSearchModal.utils.tsx | 2 +-
...sFilter.stories.tsx => Filter.stories.tsx} | 102 ++-
...y-helpers.tsx => Filter.story-helpers.tsx} | 15 +-
.../sidebar/{TagsFilter.tsx => Filter.tsx} | 42 +-
...el.stories.tsx => FilterPanel.stories.tsx} | 142 ++++-
.../components/sidebar/FilterPanel.tsx | 253 ++++++++
.../components/sidebar/FilterPanel.utils.ts | 53 ++
.../components/sidebar/FilterPanelLink.tsx | 72 +++
.../components/sidebar/Heading.stories.tsx | 2 +-
.../manager/components/sidebar/Heading.tsx | 6 +-
.../components/sidebar/HighlightStyles.tsx | 2 +-
.../sidebar/IconSymbols.stories.tsx | 2 +-
.../components/sidebar/IconSymbols.tsx | 68 +-
.../components/sidebar/Menu.stories.tsx | 10 +-
.../src/manager/components/sidebar/Menu.tsx | 4 +-
.../manager/components/sidebar/RefBlocks.tsx | 6 +-
.../components/sidebar/RefIndicator.tsx | 8 +-
.../components/sidebar/Refs.stories.tsx | 10 +-
.../src/manager/components/sidebar/Refs.tsx | 14 +-
.../components/sidebar/Search.stories.tsx | 16 +-
.../src/manager/components/sidebar/Search.tsx | 14 +-
.../sidebar/SearchResults.stories.tsx | 10 +-
.../components/sidebar/SearchResults.tsx | 18 +-
.../components/sidebar/Sidebar.stories.tsx | 143 ++++-
.../manager/components/sidebar/Sidebar.tsx | 34 +-
.../sidebar/SidebarBottom.stories.tsx | 4 +-
.../components/sidebar/SidebarBottom.tsx | 6 +-
.../components/sidebar/StatusButton.tsx | 21 +-
.../components/sidebar/StatusContext.tsx | 21 +-
.../components/sidebar/TagsFilterPanel.tsx | 322 ----------
.../sidebar/TestingWidget.stories.tsx | 4 +-
.../components/sidebar/TestingWidget.tsx | 6 +-
.../components/sidebar/Tree.stories.tsx | 8 +-
.../src/manager/components/sidebar/Tree.tsx | 63 +-
.../components/sidebar/TreeNode.stories.tsx | 8 +-
.../manager/components/sidebar/TreeNode.tsx | 4 +-
.../sidebar/__tests__/Sidebar.test.tsx | 4 +-
.../components/sidebar/mockdata.large.ts | 2 +-
.../components/sidebar/useChecklist.ts | 6 +-
.../sidebar/useDynamicFavicon.stories.tsx | 2 +-
.../manager/components/sidebar/useExpanded.ts | 6 +-
.../components/sidebar/useFilterData.tsx | 100 +++
.../components/sidebar/useHighlighted.ts | 6 +-
.../components/sidebar/useLastViewed.ts | 2 +-
.../upgrade/UpgradeBlock.stories.tsx | 2 +-
.../components/upgrade/UpgradeBlock.tsx | 2 +-
.../src/manager/container/Menu.stories.tsx | 6 +-
code/core/src/manager/container/Menu.tsx | 6 +-
.../src/manager/container/Notifications.tsx | 2 +-
code/core/src/manager/container/Panel.tsx | 6 +-
code/core/src/manager/container/Preview.tsx | 20 +-
code/core/src/manager/container/Sidebar.tsx | 6 +-
code/core/src/manager/globals-runtime.ts | 6 +-
code/core/src/manager/globals.ts | 2 +-
code/core/src/manager/globals/exports.ts | 1 +
.../manager/globals/globals-module-info.ts | 4 +-
code/core/src/manager/globals/runtime.ts | 2 +-
code/core/src/manager/index.stories.tsx | 6 +-
code/core/src/manager/index.tsx | 10 +-
code/core/src/manager/manager-stores.mock.ts | 14 +-
code/core/src/manager/runtime.tsx | 8 +-
code/core/src/manager/settings/About.tsx | 2 +-
code/core/src/manager/settings/AboutPage.tsx | 2 +-
.../settings/Checklist/Checklist.stories.tsx | 8 +-
.../manager/settings/Checklist/Checklist.tsx | 6 +-
.../manager/settings/GuidePage.stories.tsx | 8 +-
code/core/src/manager/settings/GuidePage.tsx | 4 +-
.../settings/SettingsFooter.stories.tsx | 2 +-
.../src/manager/settings/ShortcutsPage.tsx | 2 +-
.../src/manager/settings/about.stories.tsx | 4 +-
code/core/src/manager/settings/index.tsx | 12 +-
.../manager/settings/shortcuts.stories.tsx | 4 +-
code/core/src/manager/settings/shortcuts.tsx | 2 +-
.../settings/whats_new_footer.stories.tsx | 2 +-
.../src/manager/settings/whats_new_page.tsx | 2 +-
code/core/src/manager/utils/status.test.ts | 92 ++-
code/core/src/manager/utils/status.tsx | 107 +++-
code/core/src/manager/utils/tree.ts | 4 +-
code/core/src/measure/Tool.tsx | 2 +-
code/core/src/measure/box-model/labels.ts | 2 +-
code/core/src/measure/box-model/visualizer.ts | 8 +-
code/core/src/measure/manager.tsx | 4 +-
code/core/src/measure/preview.ts | 6 +-
code/core/src/measure/withMeasure.ts | 6 +-
code/core/src/mocking-utils/automock.ts | 2 +-
code/core/src/mocking-utils/extract.test.ts | 4 +-
code/core/src/mocking-utils/extract.ts | 2 +-
code/core/src/mocking-utils/index.ts | 12 +-
code/core/src/mocking-utils/resolve.ts | 2 +-
code/core/src/node-logger/index.test.ts | 4 +-
code/core/src/node-logger/index.ts | 16 +-
code/core/src/node-logger/logger/console.ts | 2 +-
code/core/src/node-logger/logger/index.ts | 8 +-
.../src/node-logger/logger/log-tracker.ts | 2 +-
code/core/src/node-logger/logger/logger.ts | 10 +-
code/core/src/node-logger/prompts/index.ts | 6 +-
.../src/node-logger/prompts/prompt-config.ts | 4 +-
.../node-logger/prompts/prompt-functions.ts | 8 +-
.../prompts/prompt-provider-clack.ts | 8 +-
code/core/src/node-logger/tasks.test.ts | 2 +-
code/core/src/node-logger/tasks.ts | 6 +-
code/core/src/node-logger/wrap-utils.test.ts | 2 +-
code/core/src/outline/OutlineSelector.tsx | 2 +-
code/core/src/outline/manager.tsx | 4 +-
code/core/src/outline/preview.ts | 6 +-
code/core/src/outline/withOutline.ts | 6 +-
code/core/src/preview-api/Errors.stories.tsx | 2 +-
code/core/src/preview-api/addons.ts | 2 +-
code/core/src/preview-api/index.ts | 24 +-
.../src/preview-api/modules/addons/hooks.ts | 2 +-
.../src/preview-api/modules/addons/index.ts | 8 +-
.../src/preview-api/modules/addons/main.ts | 2 +-
.../modules/addons/make-decorator.test.ts | 2 +-
.../modules/preview-web/Preview.tsx | 10 +-
.../PreviewWeb.integration.test.ts | 18 +-
.../preview-web/PreviewWeb.mockdata.ts | 6 +-
.../modules/preview-web/PreviewWeb.test.ts | 10 +-
.../modules/preview-web/PreviewWeb.tsx | 8 +-
.../preview-web/PreviewWithSelection.tsx | 20 +-
.../modules/preview-web/SelectionStore.ts | 2 +-
.../modules/preview-web/UrlStore.test.ts | 2 +-
.../modules/preview-web/UrlStore.ts | 4 +-
.../modules/preview-web/WebView.ts | 2 +-
.../docs-context/DocsContext.test.ts | 6 +-
.../preview-web/docs-context/DocsContext.ts | 4 +-
.../modules/preview-web/emitTransformCode.ts | 4 +-
.../preview-api/modules/preview-web/index.ts | 28 +-
.../preview-web/parseArgsParam.test.ts | 2 +-
.../preview-web/render/CsfDocsRender.test.ts | 10 +-
.../preview-web/render/CsfDocsRender.ts | 12 +-
.../preview-web/render/MdxDocsRender.test.ts | 8 +-
.../preview-web/render/MdxDocsRender.ts | 14 +-
.../preview-web/render/StoryRender.test.ts | 6 +-
.../modules/preview-web/render/StoryRender.ts | 8 +-
.../preview-web/render/mount-utils.test.ts | 2 +-
.../preview-web/simulate-pageload.test.ts | 2 +-
.../modules/store/ArgsStore.test.ts | 2 +-
.../preview-api/modules/store/ArgsStore.ts | 2 +-
.../modules/store/GlobalsStore.test.ts | 2 +-
.../preview-api/modules/store/GlobalsStore.ts | 4 +-
.../modules/store/StoryIndexStore.test.ts | 2 +-
.../modules/store/StoryStore.test.ts | 10 +-
.../preview-api/modules/store/StoryStore.ts | 12 +-
.../preview-api/modules/store/args.test.ts | 2 +-
.../modules/store/autoTitle.test.ts | 2 +-
.../modules/store/csf/beforeAll.test.ts | 2 +-
.../modules/store/csf/composeConfigs.test.ts | 2 +-
.../modules/store/csf/composeConfigs.ts | 8 +-
.../preview-api/modules/store/csf/index.ts | 24 +-
.../csf/normalizeComponentAnnotations.ts | 2 +-
.../store/csf/normalizeInputTypes.test.ts | 2 +-
.../csf/normalizeProjectAnnotations.test.ts | 2 +-
.../store/csf/normalizeProjectAnnotations.ts | 8 +-
.../modules/store/csf/normalizeStory.test.ts | 2 +-
.../modules/store/csf/normalizeStory.ts | 4 +-
.../store/csf/portable-stories.test.ts | 8 +-
.../modules/store/csf/portable-stories.ts | 20 +-
.../modules/store/csf/prepareStory.test.ts | 12 +-
.../modules/store/csf/prepareStory.ts | 14 +-
.../modules/store/csf/processCSFFile.test.ts | 2 +-
.../modules/store/csf/processCSFFile.ts | 4 +-
.../modules/store/csf/stepRunners.test.ts | 2 +-
.../modules/store/decorators.test.ts | 2 +-
.../preview-api/modules/store/hooks.test.ts | 4 +-
.../src/preview-api/modules/store/hooks.ts | 2 +-
.../src/preview-api/modules/store/index.ts | 24 +-
.../modules/store/inferArgTypes.test.ts | 2 +-
.../modules/store/inferArgTypes.ts | 2 +-
.../modules/store/inferControls.test.ts | 2 +-
.../modules/store/inferControls.ts | 4 +-
.../modules/store/parameters.test.ts | 2 +-
.../preview-api/modules/store/sortStories.ts | 2 +-
.../modules/store/storySort.test.ts | 2 +-
code/core/src/preview-api/preview-web.ts | 2 +-
code/core/src/preview-api/store.ts | 2 +-
code/core/src/preview-errors.ts | 6 +-
code/core/src/preview/globals.ts | 2 +-
code/core/src/preview/globals/runtime.ts | 2 +-
.../src/preview/preview-navigator.stories.tsx | 2 +-
code/core/src/preview/runtime.ts | 8 +-
code/core/src/router/index.ts | 6 +-
code/core/src/router/router.tsx | 4 +-
code/core/src/router/types.ts | 2 +-
code/core/src/router/utils.test.ts | 2 +-
code/core/src/router/utils.ts | 4 +-
code/core/src/server-errors.ts | 19 +-
.../checklist-store/checklistData.state.ts | 2 +-
.../shared/checklist-store/checklistData.tsx | 24 +-
code/core/src/shared/checklist-store/index.ts | 8 +-
code/core/src/shared/constants/tags.ts | 2 +-
.../src/shared/status-store/index.test-d.ts | 8 +-
.../src/shared/status-store/index.test.ts | 8 +-
code/core/src/shared/status-store/index.ts | 45 +-
.../test-provider-store/index.test-d.ts | 6 +-
.../shared/test-provider-store/index.test.ts | 8 +-
.../src/shared/test-provider-store/index.ts | 6 +-
.../universal-store/__mocks__/instances.ts | 2 +-
.../shared/universal-store/index.test-d.ts | 8 +-
.../src/shared/universal-store/index.test.ts | 8 +-
code/core/src/shared/universal-store/index.ts | 4 +-
.../src/shared/universal-store/instances.ts | 2 +-
code/core/src/shared/universal-store/mock.ts | 4 +-
code/core/src/shared/universal-store/types.ts | 2 +-
.../use-universal-store-manager.test.ts | 8 +-
.../use-universal-store-manager.ts | 2 +-
.../use-universal-store-preview.ts | 2 +-
.../utils/categorize-render-errors.test.ts | 2 +-
.../shared/utils/categorize-render-errors.ts | 2 +-
.../shared/utils/ecosystem-identifier.test.ts | 2 +-
code/core/src/telemetry/anonymous-id.test.ts | 8 +-
code/core/src/telemetry/anonymous-id.ts | 2 +-
code/core/src/telemetry/detect-agent.test.ts | 2 +-
.../src/telemetry/error-collector.test.ts | 2 +-
code/core/src/telemetry/event-cache.test.ts | 6 +-
code/core/src/telemetry/event-cache.ts | 2 +-
.../exec-command-count-lines.test.ts | 2 +-
.../get-application-file-count.test.ts | 2 +-
.../telemetry/get-application-file-count.ts | 4 +-
.../telemetry/get-chromatic-version.test.ts | 2 +-
.../src/telemetry/get-framework-info.test.ts | 2 +-
code/core/src/telemetry/get-framework-info.ts | 2 +-
.../telemetry/get-has-router-package.test.ts | 2 +-
.../src/telemetry/get-has-router-package.ts | 2 +-
.../src/telemetry/get-known-packages.test.ts | 6 +-
code/core/src/telemetry/get-known-packages.ts | 4 +-
.../src/telemetry/get-monorepo-type.test.ts | 4 +-
.../get-package-manager-info.test.ts | 2 +-
.../src/telemetry/get-package-manager-info.ts | 2 +-
.../get-portable-stories-usage.test.ts | 2 +-
.../telemetry/get-portable-stories-usage.ts | 4 +-
code/core/src/telemetry/index.ts | 28 +-
code/core/src/telemetry/package-json.ts | 2 +-
.../src/telemetry/run-telemetry-operation.ts | 2 +-
code/core/src/telemetry/sanitize.test.ts | 2 +-
code/core/src/telemetry/session-id.test.ts | 2 +-
.../src/telemetry/storybook-metadata.test.ts | 38 +-
code/core/src/telemetry/storybook-metadata.ts | 24 +-
code/core/src/telemetry/telemetry.test.ts | 4 +-
code/core/src/telemetry/telemetry.ts | 14 +-
code/core/src/telemetry/types.ts | 6 +-
code/core/src/test/expect.ts | 2 +-
code/core/src/test/index.test.ts | 6 +-
code/core/src/test/index.ts | 8 +-
code/core/src/test/spy.test.ts | 2 +-
code/core/src/test/testing-library.ts | 2 +-
code/core/src/theming/convert.ts | 12 +-
code/core/src/theming/create.ts | 8 +-
code/core/src/theming/ensure.ts | 6 +-
code/core/src/theming/global.ts | 2 +-
code/core/src/theming/index.ts | 20 +-
code/core/src/theming/modules/syntax.ts | 2 +-
code/core/src/theming/themes/dark.ts | 4 +-
code/core/src/theming/themes/light.ts | 4 +-
code/core/src/theming/types.ts | 4 +-
.../src/toolbar/components/ToolbarManager.tsx | 4 +-
.../toolbar/components/ToolbarMenuSelect.tsx | 8 +-
code/core/src/toolbar/index.ts | 6 +-
code/core/src/toolbar/manager.tsx | 4 +-
code/core/src/toolbar/types.ts | 2 +-
code/core/src/toolbar/utils/get-selected.ts | 2 +-
.../utils/normalize-toolbar-arg-type.ts | 2 +-
.../src/toolbar/utils/register-shortcuts.ts | 4 +-
code/core/src/types/index.ts | 40 +-
code/core/src/types/modules/addons.ts | 14 +-
code/core/src/types/modules/api-stories.ts | 8 +-
code/core/src/types/modules/api.ts | 18 +-
code/core/src/types/modules/channelApi.ts | 4 +-
code/core/src/types/modules/composedStory.ts | 4 +-
code/core/src/types/modules/core-common.ts | 39 +-
code/core/src/types/modules/csf.ts | 2 +-
code/core/src/types/modules/docs.ts | 4 +-
code/core/src/types/modules/indexer.ts | 2 +-
code/core/src/types/modules/status.ts | 3 +-
code/core/src/types/modules/story.ts | 2 +-
code/core/src/types/modules/test-provider.ts | 2 +-
.../core/src/types/modules/universal-store.ts | 6 +-
code/core/src/viewport/components/Tool.tsx | 4 +-
code/core/src/viewport/defaults.ts | 2 +-
code/core/src/viewport/index.ts | 8 +-
code/core/src/viewport/manager.tsx | 4 +-
code/core/src/viewport/preview.ts | 4 +-
code/core/src/viewport/responsiveViewport.tsx | 2 +-
code/core/src/viewport/useViewport.ts | 6 +-
code/core/src/viewport/viewportIcons.tsx | 2 +-
code/core/vitest.config.ts | 2 +-
code/e2e-tests/addon-a11y.spec.ts | 2 +-
code/e2e-tests/addon-actions.spec.ts | 2 +-
code/e2e-tests/addon-backgrounds.spec.ts | 2 +-
code/e2e-tests/addon-controls.spec.ts | 2 +-
code/e2e-tests/addon-docs.spec.ts | 2 +-
code/e2e-tests/addon-onboarding.spec.ts | 2 +-
code/e2e-tests/addon-toolbars.spec.ts | 2 +-
code/e2e-tests/addon-viewport.spec.ts | 2 +-
code/e2e-tests/component-tests.spec.ts | 2 +-
code/e2e-tests/framework-nextjs.spec.ts | 2 +-
code/e2e-tests/framework-svelte.spec.ts | 2 +-
code/e2e-tests/framework-vue3.spec.ts | 2 +-
code/e2e-tests/manager.spec.ts | 2 +-
code/e2e-tests/module-mocking.spec.ts | 2 +-
code/e2e-tests/navigation.spec.ts | 2 +-
code/e2e-tests/preview-api.spec.ts | 2 +-
code/e2e-tests/sb-module-mocking.spec.ts | 2 +-
code/e2e-tests/storybook-hooks.spec.ts | 4 +-
code/e2e-tests/storybook.setup.ts | 2 +-
code/e2e-tests/tags.spec.ts | 2 +-
code/e2e-tests/util.ts | 2 +-
code/frameworks/angular/build-config.ts | 2 +-
code/frameworks/angular/package.json | 2 +-
.../src/builders/build-storybook/index.ts | 6 +-
.../src/builders/start-storybook/index.ts | 6 +-
.../src/builders/utils/run-compodoc.spec.ts | 2 +-
.../client/angular-beta/AbstractRenderer.ts | 12 +-
.../src/client/angular-beta/CanvasRenderer.ts | 4 +-
.../ComputesTemplateFromComponent.test.ts | 6 +-
.../ComputesTemplateFromComponent.ts | 6 +-
.../src/client/angular-beta/DocsRenderer.ts | 6 +-
.../angular-beta/RendererFactory.test.ts | 6 +-
.../client/angular-beta/RendererFactory.ts | 6 +-
.../angular-beta/StorybookModule.test.ts | 8 +-
.../client/angular-beta/StorybookModule.ts | 8 +-
.../client/angular-beta/StorybookProvider.ts | 2 +-
.../angular-beta/StorybookWrapperComponent.ts | 10 +-
.../angular-beta/utils/BootstrapQueue.test.ts | 2 +-
.../utils/NgComponentAnalyzer.test.ts | 2 +-
.../utils/NgModulesAnalyzer.test.ts | 2 +-
.../utils/PropertyExtractor.test.ts | 6 +-
.../angular-beta/utils/PropertyExtractor.ts | 4 +-
.../angular/src/client/argsToTemplate.test.ts | 4 +-
.../angular/src/client/argsToTemplate.ts | 2 +-
.../angular/src/client/compodoc.test.ts | 4 +-
.../frameworks/angular/src/client/compodoc.ts | 2 +-
code/frameworks/angular/src/client/config.ts | 8 +-
.../angular/src/client/csf-factories.test.ts | 4 +-
.../angular/src/client/decorateStory.test.ts | 6 +-
.../angular/src/client/decorateStory.ts | 4 +-
.../angular/src/client/decorators.test.ts | 4 +-
.../angular/src/client/decorators.ts | 6 +-
code/frameworks/angular/src/client/index.ts | 14 +-
.../angular/src/client/portable-stories.ts | 4 +-
code/frameworks/angular/src/client/preview.ts | 8 +-
.../angular/src/client/public-types.ts | 4 +-
code/frameworks/angular/src/client/render.ts | 4 +-
code/frameworks/angular/src/index.ts | 6 +-
code/frameworks/angular/src/node/index.ts | 2 +-
code/frameworks/angular/src/preset.ts | 14 +-
code/frameworks/angular/src/renderer.ts | 12 +-
.../framework-preset-angular-cli.test.ts | 4 +-
.../server/framework-preset-angular-cli.ts | 4 +-
.../server/framework-preset-angular-ivy.ts | 4 +-
.../angular/src/server/preset-options.ts | 2 +-
code/frameworks/angular/vitest.config.ts | 2 +-
code/frameworks/ember/build-config.ts | 2 +-
code/frameworks/ember/package.json | 2 +-
.../ember/src/client/preview/config.ts | 6 +-
.../ember/src/client/preview/render.ts | 2 +-
code/frameworks/ember/src/index.ts | 2 +-
code/frameworks/ember/src/node/index.ts | 2 +-
code/frameworks/ember/src/preset.ts | 14 +-
.../server/framework-preset-babel-ember.ts | 2 +-
code/frameworks/ember/vitest.config.ts | 2 +-
code/frameworks/html-vite/build-config.ts | 2 +-
code/frameworks/html-vite/package.json | 2 +-
code/frameworks/html-vite/src/index.ts | 2 +-
code/frameworks/html-vite/src/node/index.ts | 2 +-
code/frameworks/nextjs-vite/build-config.ts | 2 +-
code/frameworks/nextjs-vite/package.json | 4 +-
.../src/export-mocks/headers/index.ts | 4 +-
.../src/head-manager/decorator.tsx | 2 +-
code/frameworks/nextjs-vite/src/index.ts | 8 +-
code/frameworks/nextjs-vite/src/node/index.ts | 2 +-
.../nextjs-vite/src/portable-stories.ts | 4 +-
code/frameworks/nextjs-vite/src/preset.ts | 22 +-
code/frameworks/nextjs-vite/src/preview.tsx | 8 +-
.../src/routing/app-router-provider.tsx | 2 +-
.../nextjs-vite/src/routing/decorator.tsx | 6 +-
code/frameworks/nextjs-vite/src/utils.ts | 2 +-
code/frameworks/nextjs-vite/vitest.config.ts | 2 +-
code/frameworks/nextjs/build-config.ts | 2 +-
code/frameworks/nextjs/package.json | 2 +-
code/frameworks/nextjs/src/aliases/webpack.ts | 6 +-
code/frameworks/nextjs/src/babel/loader.ts | 2 +-
code/frameworks/nextjs/src/babel/preset.ts | 8 +-
.../src/compatibility/compatibility-map.ts | 2 +-
code/frameworks/nextjs/src/config/webpack.ts | 2 +-
code/frameworks/nextjs/src/css/webpack.ts | 2 +-
.../nextjs/src/export-mocks/headers/index.ts | 4 +-
.../nextjs/src/export-mocks/index.ts | 2 +-
.../nextjs/src/export-mocks/webpack.ts | 2 +-
.../nextjs/src/font/babel/index.test.ts | 2 +-
.../frameworks/nextjs/src/font/babel/index.ts | 2 +-
.../google/get-font-face-declarations.ts | 2 +-
.../local/get-font-face-declarations.ts | 2 +-
.../loader/storybook-nextjs-font-loader.ts | 10 +-
.../nextjs/src/head-manager/decorator.tsx | 2 +-
.../nextjs/src/images/decorator.tsx | 2 +-
.../nextjs/src/images/next-image.tsx | 4 +-
.../nextjs/src/images/next-legacy-image.tsx | 4 +-
code/frameworks/nextjs/src/images/webpack.ts | 2 +-
code/frameworks/nextjs/src/index.ts | 8 +-
code/frameworks/nextjs/src/node/index.ts | 2 +-
.../frameworks/nextjs/src/portable-stories.ts | 4 +-
code/frameworks/nextjs/src/preset.ts | 34 +-
code/frameworks/nextjs/src/preview.tsx | 8 +-
.../src/routing/app-router-provider.tsx | 2 +-
.../nextjs/src/routing/decorator.tsx | 6 +-
.../nextjs/src/styledJsx/webpack.ts | 2 +-
code/frameworks/nextjs/src/swc/loader.ts | 2 +-
code/frameworks/nextjs/src/utils.ts | 2 +-
code/frameworks/nextjs/vitest.config.ts | 2 +-
code/frameworks/preact-vite/build-config.ts | 2 +-
code/frameworks/preact-vite/package.json | 2 +-
code/frameworks/preact-vite/src/index.ts | 2 +-
code/frameworks/preact-vite/src/node/index.ts | 2 +-
code/frameworks/preact-vite/src/preset.ts | 2 +-
code/frameworks/preact-vite/vitest.config.ts | 2 +-
.../react-native-web-vite/build-config.ts | 2 +-
.../react-native-web-vite/package.json | 2 +-
.../react-native-web-vite/src/index.ts | 2 +-
.../react-native-web-vite/src/node/index.ts | 2 +-
.../react-native-web-vite/src/preset.ts | 2 +-
.../react-native-web-vite/vitest.config.ts | 2 +-
code/frameworks/react-vite/build-config.ts | 2 +-
code/frameworks/react-vite/package.json | 4 +-
code/frameworks/react-vite/src/index.ts | 2 +-
code/frameworks/react-vite/src/node/index.ts | 2 +-
.../react-vite/src/plugins/docgen-resolver.ts | 11 +-
.../src/plugins/react-docgen.test.ts | 2 +-
.../react-vite/src/plugins/react-docgen.ts | 10 +-
code/frameworks/react-vite/src/preset.ts | 4 +-
code/frameworks/react-vite/vitest.config.ts | 2 +-
.../frameworks/react-webpack5/build-config.ts | 2 +-
code/frameworks/react-webpack5/package.json | 2 +-
code/frameworks/react-webpack5/src/index.ts | 2 +-
.../react-webpack5/src/node/index.ts | 2 +-
code/frameworks/react-webpack5/src/preset.ts | 16 +-
.../react-webpack5/vitest.config.ts | 2 +-
.../server-webpack5/build-config.ts | 2 +-
code/frameworks/server-webpack5/package.json | 2 +-
code/frameworks/server-webpack5/src/index.ts | 2 +-
.../server-webpack5/src/node/index.ts | 2 +-
code/frameworks/server-webpack5/src/preset.ts | 14 +-
.../server-webpack5/vitest.config.ts | 2 +-
code/frameworks/svelte-vite/build-config.ts | 2 +-
code/frameworks/svelte-vite/package.json | 2 +-
code/frameworks/svelte-vite/src/index.ts | 2 +-
code/frameworks/svelte-vite/src/node/index.ts | 2 +-
.../svelte-vite/src/plugins/svelte-docgen.ts | 55 +-
code/frameworks/svelte-vite/src/preset.ts | 6 +-
code/frameworks/svelte-vite/vitest.config.ts | 2 +-
code/frameworks/sveltekit/build-config.ts | 2 +-
code/frameworks/sveltekit/package.json | 2 +-
code/frameworks/sveltekit/src/index.ts | 4 +-
code/frameworks/sveltekit/src/node/index.ts | 2 +-
.../sveltekit/src/portable-stories.ts | 2 +-
code/frameworks/sveltekit/src/preset.ts | 6 +-
code/frameworks/sveltekit/src/preview.ts | 2 +-
code/frameworks/sveltekit/src/types.ts | 4 +-
code/frameworks/sveltekit/src/vite-plugin.ts | 2 +-
code/frameworks/sveltekit/vitest.config.ts | 2 +-
code/frameworks/vue3-vite/build-config.ts | 2 +-
code/frameworks/vue3-vite/package.json | 2 +-
code/frameworks/vue3-vite/src/index.ts | 2 +-
code/frameworks/vue3-vite/src/node/index.ts | 2 +-
.../src/plugins/vue-component-meta.ts | 210 ++++---
.../vue3-vite/src/plugins/vue-docgen.ts | 25 +-
code/frameworks/vue3-vite/src/preset.ts | 8 +-
code/frameworks/vue3-vite/src/vite-plugin.ts | 2 +-
code/frameworks/vue3-vite/vitest.config.ts | 2 +-
.../web-components-vite/build-config.ts | 2 +-
.../web-components-vite/package.json | 2 +-
.../web-components-vite/src/index.ts | 2 +-
.../web-components-vite/src/node/index.ts | 2 +-
.../web-components-vite/vitest.config.ts | 2 +-
code/lib/cli-sb/package.json | 2 +-
code/lib/cli-sb/vitest.config.ts | 2 +-
code/lib/cli-storybook/build-config.ts | 2 +-
code/lib/cli-storybook/package.json | 2 +-
code/lib/cli-storybook/src/add.test.ts | 4 +-
code/lib/cli-storybook/src/add.ts | 4 +-
.../autoblock/block-dependencies-versions.ts | 4 +-
.../block-experimental-addon-test.test.ts | 2 +-
.../block-experimental-addon-test.ts | 2 +-
.../src/autoblock/block-major-version.test.ts | 2 +-
.../src/autoblock/block-major-version.ts | 2 +-
.../src/autoblock/block-node-version.ts | 2 +-
.../autoblock/block-webpack5-frameworks.ts | 4 +-
.../cli-storybook/src/autoblock/index.test.ts | 4 +-
code/lib/cli-storybook/src/autoblock/index.ts | 12 +-
code/lib/cli-storybook/src/autoblock/utils.ts | 4 +-
.../fixes/addon-a11y-addon-test.test.ts | 2 +-
.../fixes/addon-a11y-addon-test.ts | 4 +-
.../fixes/addon-a11y-parameters.test.ts | 2 +-
.../fixes/addon-a11y-parameters.ts | 4 +-
.../fixes/addon-experimental-test.test.ts | 4 +-
.../fixes/addon-experimental-test.ts | 4 +-
.../fixes/addon-globals-api.test.ts | 4 +-
.../automigrate/fixes/addon-globals-api.ts | 4 +-
.../fixes/addon-mdx-gfm-remove.test.ts | 4 +-
.../automigrate/fixes/addon-mdx-gfm-remove.ts | 2 +-
.../addon-storysource-code-panel.test.ts | 9 +-
.../fixes/addon-storysource-code-panel.ts | 6 +-
.../fixes/consolidated-imports.test.ts | 2 +-
.../automigrate/fixes/consolidated-imports.ts | 4 +-
.../automigrate/fixes/eslint-plugin.test.ts | 4 +-
.../src/automigrate/fixes/eslint-plugin.ts | 2 +-
.../fixes/fix-faux-esm-require.test.ts | 4 +-
.../automigrate/fixes/fix-faux-esm-require.ts | 4 +-
.../src/automigrate/fixes/index.ts | 46 +-
.../automigrate/fixes/initial-globals.test.ts | 4 +-
.../src/automigrate/fixes/initial-globals.ts | 2 +-
.../fixes/migrate-addon-console.test.ts | 4 +-
.../fixes/migrate-addon-console.ts | 2 +-
.../fixes/nextjs-to-nextjs-vite.test.ts | 35 +-
.../fixes/nextjs-to-nextjs-vite.ts | 10 +-
.../fixes/remove-addon-interactions.test.ts | 2 +-
.../fixes/remove-addon-interactions.ts | 2 +-
.../fixes/remove-docs-autodocs.test.ts | 4 +-
.../automigrate/fixes/remove-docs-autodocs.ts | 4 +-
.../fixes/remove-essentials.test.ts | 8 +-
.../automigrate/fixes/remove-essentials.ts | 8 +-
.../fixes/renderer-to-framework.test.ts | 4 +-
.../fixes/renderer-to-framework.ts | 2 +-
.../fixes/rnstorybook-config.test.ts | 4 +-
.../automigrate/fixes/rnstorybook-config.ts | 2 +-
.../storybook-package-name-conflict.test.ts | 54 ++
.../fixes/storybook-package-name-conflict.ts | 44 ++
...ade-storybook-related-dependencies.test.ts | 4 +-
.../upgrade-storybook-related-dependencies.ts | 4 +-
.../fixes/wrap-getAbsolutePath.test.ts | 4 +-
.../automigrate/fixes/wrap-getAbsolutePath.ts | 4 +-
.../helpers/addon-a11y-parameters.test.ts | 2 +-
.../helpers/addon-a11y-parameters.ts | 2 +-
.../helpers/logMigrationSummary.ts | 4 +-
.../helpers/mainConfigFile.test.ts | 6 +-
.../src/automigrate/helpers/mainConfigFile.ts | 2 +-
.../src/automigrate/index.test.ts | 6 +-
.../cli-storybook/src/automigrate/index.ts | 12 +-
.../src/automigrate/multi-project.test.ts | 4 +-
.../src/automigrate/multi-project.ts | 14 +-
code/lib/cli-storybook/src/bin/index.ts | 2 +-
code/lib/cli-storybook/src/bin/run.ts | 14 +-
.../src/codemod/csf-factories.ts | 10 +-
.../helpers/config-to-csf-factory.test.ts | 2 +-
.../codemod/helpers/config-to-csf-factory.ts | 4 +-
.../helpers/csf-factories-utils.test.ts | 2 +-
.../helpers/remove-unused-types.test.ts | 4 +-
.../codemod/helpers/remove-unused-types.ts | 2 +-
.../helpers/story-to-csf-factory.test.ts | 2 +-
.../codemod/helpers/story-to-csf-factory.ts | 6 +-
.../src/doctor/getDuplicatedDepsWarnings.ts | 2 +-
.../getIncompatibleStorybookPackages.test.ts | 4 +-
.../getIncompatibleStorybookPackages.ts | 2 +-
code/lib/cli-storybook/src/doctor/index.ts | 14 +-
.../lib/cli-storybook/src/postinstallAddon.ts | 4 +-
.../cli-storybook/src/sandbox-templates.ts | 4 +-
code/lib/cli-storybook/src/sandbox.ts | 4 +-
code/lib/cli-storybook/src/upgrade.test.ts | 4 +-
code/lib/cli-storybook/src/upgrade.ts | 12 +-
code/lib/cli-storybook/src/util.ts | 8 +-
code/lib/cli-storybook/src/warn.test.ts | 2 +-
code/lib/cli-storybook/vitest.config.ts | 2 +-
code/lib/codemod/build-config.ts | 2 +-
code/lib/codemod/package.json | 2 +-
code/lib/codemod/src/index.test.ts | 2 +-
code/lib/codemod/src/index.ts | 2 +-
.../transforms/__tests__/csf-2-to-3.test.ts | 2 +-
.../__tests__/find-implicit-spies.test.ts | 2 +-
.../upgrade-deprecated-types.test.ts | 2 +-
code/lib/codemod/src/transforms/csf-2-to-3.ts | 2 +-
code/lib/codemod/vitest.config.ts | 2 +-
code/lib/core-webpack/build-config.ts | 2 +-
code/lib/core-webpack/package.json | 2 +-
.../core-webpack/src/importPipeline.test.ts | 2 +-
code/lib/core-webpack/src/index.ts | 12 +-
.../src/merge-webpack-config.test.ts | 2 +-
.../core-webpack/src/merge-webpack-config.ts | 2 +-
code/lib/core-webpack/src/to-importFn.test.ts | 2 +-
code/lib/core-webpack/src/to-importFn.ts | 2 +-
.../src/to-require-context.test.ts | 2 +-
code/lib/core-webpack/vitest.config.ts | 2 +-
code/lib/create-storybook/build-config.ts | 2 +-
code/lib/create-storybook/package.json | 2 +-
code/lib/create-storybook/src/bin/index.ts | 2 +-
code/lib/create-storybook/src/bin/run.ts | 4 +-
.../AddonConfigurationCommand.test.ts | 13 +-
.../src/commands/AddonConfigurationCommand.ts | 10 +-
.../DependencyInstallationCommand.test.ts | 4 +-
.../commands/DependencyInstallationCommand.ts | 2 +-
.../src/commands/FinalizationCommand.test.ts | 2 +-
.../src/commands/FrameworkDetectionCommand.ts | 6 +-
.../GeneratorExecutionCommand.test.ts | 12 +-
.../src/commands/GeneratorExecutionCommand.ts | 12 +-
.../commands/PreflightCheckCommand.test.ts | 29 +-
.../src/commands/PreflightCheckCommand.ts | 31 +-
.../commands/ProjectDetectionCommand.test.ts | 6 +-
.../src/commands/ProjectDetectionCommand.ts | 4 +-
.../commands/UserPreferencesCommand.test.ts | 8 +-
.../src/commands/UserPreferencesCommand.ts | 6 +-
.../create-storybook/src/commands/index.ts | 22 +-
.../src/dependency-collector.test.ts | 2 +-
.../src/generators/ANGULAR/index.ts | 2 +-
.../src/generators/EMBER/index.ts | 2 +-
.../src/generators/GeneratorRegistry.test.ts | 4 +-
.../src/generators/GeneratorRegistry.ts | 2 +-
.../src/generators/HTML/index.ts | 2 +-
.../src/generators/NEXTJS/index.ts | 2 +-
.../src/generators/NUXT/index.ts | 2 +-
.../src/generators/PREACT/index.ts | 2 +-
.../src/generators/QWIK/index.ts | 2 +-
.../src/generators/REACT/index.ts | 2 +-
.../src/generators/REACT_NATIVE/index.ts | 2 +-
.../generators/REACT_NATIVE_AND_RNW/index.ts | 6 +-
.../src/generators/REACT_NATIVE_WEB/index.ts | 2 +-
.../src/generators/REACT_SCRIPTS/index.ts | 2 +-
.../src/generators/SERVER/index.ts | 2 +-
.../src/generators/SOLID/index.ts | 2 +-
.../src/generators/SVELTE/index.ts | 2 +-
.../src/generators/SVELTEKIT/index.ts | 2 +-
.../src/generators/VUE3/index.ts | 2 +-
.../src/generators/WEB-COMPONENTS/index.ts | 2 +-
.../src/generators/baseGenerator.ts | 6 +-
.../src/generators/configure.test.ts | 2 +-
.../create-storybook/src/generators/index.ts | 4 +-
.../src/generators/modules/GeneratorModule.ts | 2 +-
.../src/generators/registerGenerators.ts | 40 +-
.../create-storybook/src/generators/types.ts | 4 +-
code/lib/create-storybook/src/index.ts | 2 +-
.../lib/create-storybook/src/initiate.test.ts | 2 +-
code/lib/create-storybook/src/initiate.ts | 12 +-
.../src/scaffold-new-project.ts | 2 +-
.../src/services/AddonService.test.ts | 2 +-
.../FeatureCompatibilityService.test.ts | 2 +-
.../FrameworkDetectionService.test.ts | 2 +-
.../src/services/ProjectTypeService.test.ts | 4 +-
.../src/services/ProjectTypeService.ts | 2 +-
.../src/services/TelemetryService.test.ts | 2 +-
.../src/services/TelemetryService.ts | 2 +-
.../src/services/VersionService.test.ts | 2 +-
.../create-storybook/src/services/index.ts | 10 +-
code/lib/create-storybook/vitest.config.ts | 2 +-
code/lib/csf-plugin/build-config.ts | 2 +-
code/lib/csf-plugin/package.json | 2 +-
code/lib/csf-plugin/src/index.ts | 4 +-
.../lib/csf-plugin/src/rollup-based-plugin.ts | 2 +-
code/lib/csf-plugin/vitest.config.ts | 2 +-
code/lib/eslint-plugin/build-config.ts | 2 +-
.../eslint-plugin/docs/rules/no-stories-of.md | 4 +-
code/lib/eslint-plugin/package.json | 2 +-
code/lib/eslint-plugin/scripts/update-all.ts | 4 +-
.../eslint-plugin/scripts/update-configs.ts | 6 +-
.../scripts/update-lib-configs.ts | 6 +-
.../scripts/update-lib-flat-configs.ts | 10 +-
.../eslint-plugin/scripts/update-lib-index.ts | 11 +-
.../scripts/update-rules-list.ts | 6 +-
.../eslint-plugin/scripts/utils/categories.ts | 6 +-
code/lib/eslint-plugin/scripts/utils/docs.ts | 4 +-
code/lib/eslint-plugin/scripts/utils/rules.ts | 4 +-
.../eslint-plugin/scripts/utils/updates.ts | 4 +-
.../src/configs/flat/addon-interactions.ts | 2 +-
.../src/configs/flat/csf-strict.ts | 2 +-
.../lib/eslint-plugin/src/configs/flat/csf.ts | 2 +-
.../src/configs/flat/recommended.ts | 2 +-
code/lib/eslint-plugin/src/index.ts | 48 +-
.../src/rules/await-interactions.test.ts | 4 +-
.../src/rules/await-interactions.ts | 6 +-
.../rules/context-in-play-function.test.ts | 4 +-
.../src/rules/context-in-play-function.ts | 6 +-
.../src/rules/csf-component.test.ts | 4 +-
.../eslint-plugin/src/rules/csf-component.ts | 8 +-
.../src/rules/default-exports.test.ts | 4 +-
.../src/rules/default-exports.ts | 6 +-
.../src/rules/hierarchy-separator.test.ts | 4 +-
.../src/rules/hierarchy-separator.ts | 8 +-
.../src/rules/meta-inline-properties.test.ts | 4 +-
.../src/rules/meta-inline-properties.ts | 6 +-
.../src/rules/meta-satisfies-type.test.ts | 4 +-
.../src/rules/meta-satisfies-type.ts | 6 +-
.../src/rules/no-redundant-story-name.test.ts | 4 +-
.../src/rules/no-redundant-story-name.ts | 6 +-
.../src/rules/no-renderer-packages.test.ts | 4 +-
.../src/rules/no-renderer-packages.ts | 4 +-
.../src/rules/no-stories-of.test.ts | 4 +-
.../eslint-plugin/src/rules/no-stories-of.ts | 4 +-
.../rules/no-title-property-in-meta.test.ts | 4 +-
.../src/rules/no-title-property-in-meta.ts | 8 +-
.../src/rules/no-uninstalled-addons.test.ts | 4 +-
.../src/rules/no-uninstalled-addons.ts | 8 +-
.../src/rules/prefer-pascal-case.test.ts | 4 +-
.../src/rules/prefer-pascal-case.ts | 8 +-
.../src/rules/story-exports.test.ts | 4 +-
.../eslint-plugin/src/rules/story-exports.ts | 8 +-
.../src/rules/use-storybook-expect.test.ts | 4 +-
.../src/rules/use-storybook-expect.ts | 6 +-
.../use-storybook-testing-library.test.ts | 4 +-
.../rules/use-storybook-testing-library.ts | 6 +-
code/lib/eslint-plugin/src/types/index.ts | 2 +-
.../src/utils/create-storybook-rule.ts | 4 +-
code/lib/eslint-plugin/src/utils/index.ts | 2 +-
code/lib/eslint-plugin/vitest.config.ts | 2 +-
code/lib/react-dom-shim/build-config.ts | 2 +-
code/lib/react-dom-shim/package.json | 2 +-
code/lib/react-dom-shim/src/preset.ts | 2 +-
code/package.json | 23 +-
code/presets/create-react-app/build-config.ts | 2 +-
code/presets/create-react-app/package.json | 2 +-
.../src/helpers/checkPresets.ts | 2 +-
.../src/helpers/getReactScriptsPath.ts | 2 +-
.../src/helpers/processCraConfig.ts | 2 +-
code/presets/create-react-app/src/index.ts | 12 +-
code/presets/react-webpack/build-config.ts | 2 +-
code/presets/react-webpack/package.json | 2 +-
.../react-webpack/src/cra-config.test.ts | 2 +-
.../react-webpack/src/framework-preset-cra.ts | 2 +-
.../src/framework-preset-react-docs.test.ts | 2 +-
.../src/framework-preset-react-docs.ts | 2 +-
code/presets/react-webpack/src/index.ts | 2 +-
.../src/loaders/docgen-resolver.ts | 11 +-
.../src/loaders/react-docgen-loader.test.ts | 2 +-
.../src/loaders/react-docgen-loader.ts | 9 +-
code/presets/server-webpack/build-config.ts | 2 +-
code/presets/server-webpack/package.json | 2 +-
code/presets/server-webpack/src/index.ts | 4 +-
.../server-webpack/src/lib/compiler/index.ts | 8 +-
.../lib/compiler/json-to-csf-compiler.test.ts | 2 +-
.../src/lib/compiler/stringifier.ts | 2 +-
code/presets/server-webpack/src/loader.ts | 2 +-
code/presets/server-webpack/vitest.config.ts | 2 +-
code/project.json | 7 +
code/renderers/html/build-config.ts | 2 +-
code/renderers/html/package.json | 2 +-
code/renderers/html/src/entry-preview-docs.ts | 4 +-
code/renderers/html/src/entry-preview.ts | 4 +-
code/renderers/html/src/index.ts | 6 +-
code/renderers/html/src/portable-stories.ts | 4 +-
code/renderers/html/src/public-types.ts | 2 +-
code/renderers/html/src/render.ts | 2 +-
code/renderers/html/vitest.config.ts | 2 +-
code/renderers/preact/build-config.ts | 2 +-
code/renderers/preact/package.json | 2 +-
code/renderers/preact/src/entry-preview.ts | 2 +-
code/renderers/preact/src/index.ts | 6 +-
code/renderers/preact/src/portable-stories.ts | 4 +-
.../preact/src/public-types.test.tsx | 4 +-
code/renderers/preact/src/public-types.ts | 2 +-
code/renderers/preact/src/render.tsx | 2 +-
code/renderers/preact/vitest.config.ts | 2 +-
code/renderers/react/build-config.ts | 2 +-
code/renderers/react/package.json | 2 +-
.../src/__test__/Button.csf4.stories.tsx | 4 +-
.../react/src/__test__/Button.stories.tsx | 6 +-
.../__test__/ComponentWithError.stories.tsx | 4 +-
.../portable-stories-factory.test.tsx | 4 +-
.../__test__/portable-stories-legacy.test.tsx | 6 +-
.../src/__test__/portable-stories.test.tsx | 8 +-
code/renderers/react/src/applyDecorators.ts | 2 +-
.../ComponentMetaManager.test.ts | 6 +-
.../componentMeta/ComponentMetaManager.ts | 6 +-
...mponentMetaProject.storyExtraction.test.ts | 2 +-
.../ComponentMetaProject.test.ts | 4 +-
.../componentMeta/ComponentMetaProject.ts | 6 +-
...omponentMetaExtractor.defaultValue.test.ts | 2 +-
.../componentMetaExtractor.detection.test.ts | 2 +-
...componentMetaExtractor.displayName.test.ts | 2 +-
...ponentMetaExtractor.parentTracking.test.ts | 2 +-
.../componentMetaExtractor.props.test.ts | 2 +-
.../componentMetaExtractor.qa.test.ts | 2 +-
.../componentMetaExtractor.test-helpers.ts | 8 +-
.../componentMeta/componentMetaExtractor.ts | 4 +-
.../generateCodeSnippet.test.tsx | 2 +-
.../componentManifest/generateCodeSnippet.ts | 2 +-
.../src/componentManifest/generator.test.ts | 10 +-
.../react/src/componentManifest/generator.ts | 16 +-
.../getComponentImports.test.ts | 6 +-
.../componentManifest/getComponentImports.ts | 10 +-
.../src/componentManifest/jsdocTags.test.ts | 2 +-
.../react/src/componentManifest/jsdocTags.ts | 2 +-
.../src/componentManifest/memfs-test-setup.ts | 4 +-
.../src/componentManifest/reactDocgen.test.ts | 4 +-
.../src/componentManifest/reactDocgen.ts | 12 +-
.../reactDocgen/docgenResolver.ts | 11 +-
.../extractReactDocgenInfo.test.ts | 4 +-
.../reactDocgen/extractReactDocgenInfo.ts | 8 +-
.../extractReactTypescriptDocgenInfo.test.ts | 2 +-
.../extractReactTypescriptDocgenInfo.ts | 4 +-
.../reactDocgenTypescript.ts | 10 +-
.../react/src/componentManifest/utils.test.ts | 93 ++-
.../react/src/componentManifest/utils.ts | 18 +-
.../react/src/csf-factories.test.tsx | 4 +-
code/renderers/react/src/enrichCsf.test.ts | 2 +-
code/renderers/react/src/enrichCsf.ts | 2 +-
.../react/src/entry-preview-argtypes.ts | 4 +-
.../renderers/react/src/entry-preview-docs.ts | 6 +-
code/renderers/react/src/entry-preview.tsx | 12 +-
.../react/src/extractArgTypes.test.ts | 6 +-
code/renderers/react/src/extractArgTypes.ts | 2 +-
code/renderers/react/src/extractProps.ts | 6 +-
code/renderers/react/src/index.ts | 10 +-
code/renderers/react/src/mount.ts | 2 +-
code/renderers/react/src/portable-stories.tsx | 8 +-
code/renderers/react/src/preset.test.ts | 2 +-
code/renderers/react/src/preset.ts | 12 +-
code/renderers/react/src/preview.tsx | 10 +-
.../renderers/react/src/public-types.test.tsx | 4 +-
code/renderers/react/src/public-types.ts | 2 +-
code/renderers/react/src/render.tsx | 2 +-
code/renderers/react/src/renderToCanvas.tsx | 4 +-
code/renderers/react/vitest.config.ts | 2 +-
code/renderers/server/build-config.ts | 2 +-
code/renderers/server/package.json | 2 +-
code/renderers/server/src/entry-preview.ts | 2 +-
code/renderers/server/src/index.ts | 4 +-
code/renderers/server/src/public-types.ts | 2 +-
code/renderers/server/src/render.ts | 4 +-
code/renderers/server/vitest.config.ts | 2 +-
code/renderers/svelte/build-config.ts | 2 +-
code/renderers/svelte/package.json | 2 +-
.../__test__/composeStories/Button.stories.ts | 2 +-
.../composeStories/portable-stories.test.ts | 6 +-
code/renderers/svelte/src/decorators.ts | 2 +-
.../svelte/src/entry-preview-docs.ts | 4 +-
code/renderers/svelte/src/entry-preview.ts | 12 +-
.../svelte/src/extractArgTypes.test.ts | 2 +-
.../src/extractComponentDescription.test.ts | 2 +-
code/renderers/svelte/src/index.ts | 6 +-
code/renderers/svelte/src/mount.ts | 2 +-
code/renderers/svelte/src/portable-stories.ts | 6 +-
.../renderers/svelte/src/public-types.test.ts | 4 +-
code/renderers/svelte/src/public-types.ts | 2 +-
code/renderers/svelte/src/render.ts | 2 +-
code/renderers/svelte/vitest.config.ts | 2 +-
code/renderers/vue3/build-config.ts | 2 +-
code/renderers/vue3/package.json | 2 +-
.../composeStories/Button.stories.ts | 2 +-
.../composeStories/portable-stories.test.ts | 4 +-
code/renderers/vue3/src/csf-factories.test.ts | 4 +-
code/renderers/vue3/src/decorateStory.ts | 2 +-
code/renderers/vue3/src/entry-preview-docs.ts | 2 +-
code/renderers/vue3/src/entry-preview.ts | 10 +-
.../vue3/src/extractArgTypes.test.ts | 4 +-
code/renderers/vue3/src/globals.ts | 2 +-
code/renderers/vue3/src/index.ts | 12 +-
code/renderers/vue3/src/mount.ts | 2 +-
code/renderers/vue3/src/portable-stories.ts | 6 +-
code/renderers/vue3/src/preview.ts | 8 +-
code/renderers/vue3/src/public-types.test.ts | 4 +-
code/renderers/vue3/src/public-types.ts | 2 +-
code/renderers/vue3/src/render.test.ts | 2 +-
code/renderers/vue3/src/render.ts | 2 +-
code/renderers/vue3/vitest.config.ts | 2 +-
code/renderers/web-components/build-config.ts | 2 +-
code/renderers/web-components/package.json | 2 +-
.../web-components/src/csf-factories.test.ts | 4 +-
.../src/entry-preview-argtypes.ts | 4 +-
.../web-components/src/entry-preview-docs.ts | 4 +-
.../web-components/src/entry-preview.ts | 2 +-
code/renderers/web-components/src/index.ts | 12 +-
.../web-components/src/portable-stories.ts | 4 +-
code/renderers/web-components/src/preview.ts | 6 +-
.../web-components/src/public-types.ts | 2 +-
code/renderers/web-components/src/render.ts | 2 +-
.../renderers/web-components/vitest.config.ts | 2 +-
code/tsconfig.json | 1 +
docs/.oxfmtrc.json | 4 -
docs/_snippets/chromatic-github-action.md | 6 +-
docs/_snippets/ghp-github-action.md | 6 +-
docs/_snippets/render-custom-in-meta.md | 2 +
.../storybook-main-rdt-monorepo-include.md | 35 ++
docs/api/main-config/main-config-features.mdx | 11 +
docs/configure/integration/eslint-plugin.mdx | 6 +-
docs/configure/integration/typescript.mdx | 16 +
docs/versions/latest.json | 2 +-
docs/versions/next.json | 2 +-
package.json | 7 +-
scripts/.eslintrc.cjs | 26 +
scripts/bench/bench-packages.ts | 8 +-
scripts/bench/browse.ts | 2 +-
scripts/bench/utils.ts | 2 +-
scripts/build-package.ts | 6 +-
scripts/check-package.ts | 6 +-
scripts/check/check-package.ts | 4 +-
scripts/ci/common-jobs.ts | 24 +-
scripts/ci/init-empty.ts | 8 +-
scripts/ci/main.ts | 29 +-
scripts/ci/sandboxes.ts | 14 +-
scripts/ci/test-storybooks.ts | 6 +-
scripts/ci/utils/executors.ts | 2 +-
scripts/ci/utils/helpers.ts | 4 +-
scripts/ci/utils/types.ts | 6 +-
scripts/combine-compodoc.ts | 4 +-
scripts/create-nx-sandbox-projects.ts | 4 +-
.../ecosystem-ci/existing-resolutions.test.ts | 2 +-
scripts/event-log-checker.ts | 4 +-
scripts/get-report-message.ts | 4 +-
scripts/get-sandbox-dir.ts | 4 +-
scripts/package.json | 11 -
scripts/prepare-sandbox.ts | 6 +-
scripts/project.json | 5 -
.../__tests__/cancel-preparation-runs.test.ts | 4 +-
.../__tests__/generate-pr-description.test.ts | 4 +-
.../release/__tests__/is-pr-frozen.test.ts | 14 +-
.../release/__tests__/label-patches.test.ts | 8 +-
scripts/release/__tests__/version.test.ts | 6 +-
.../release/__tests__/write-changelog.test.ts | 8 +-
scripts/release/cancel-preparation-runs.ts | 4 +-
scripts/release/generate-pr-description.ts | 10 +-
scripts/release/get-changelog-from-file.ts | 4 +-
scripts/release/get-current-version.ts | 2 +-
scripts/release/get-version-changelog.ts | 6 +-
scripts/release/is-pr-frozen.ts | 6 +-
scripts/release/is-prerelease.ts | 4 +-
scripts/release/is-version-published.ts | 4 +-
scripts/release/label-patches.ts | 8 +-
scripts/release/pick-patches.ts | 6 +-
scripts/release/publish.ts | 2 +-
scripts/release/unreleased-changes-exists.ts | 8 +-
scripts/release/utils/get-changes.ts | 8 +-
scripts/release/version.ts | 6 +-
scripts/release/write-changelog.ts | 4 +-
scripts/run-registry.ts | 10 +-
scripts/sandbox/generate.ts | 20 +-
scripts/sandbox/publish.ts | 8 +-
scripts/sandbox/utils/getPort.ts | 4 +-
scripts/sandbox/utils/git.ts | 2 +-
scripts/sandbox/utils/template.ts | 4 +-
scripts/sandbox/utils/yarn.ts | 2 +-
scripts/snippets/codemod.ts | 6 +-
scripts/task.ts | 50 +-
scripts/tasks/bench.ts | 10 +-
scripts/tasks/build.ts | 12 +-
scripts/tasks/check-sandbox.ts | 6 +-
scripts/tasks/check.ts | 6 +-
scripts/tasks/chromatic.ts | 6 +-
scripts/tasks/compile.ts | 8 +-
scripts/tasks/dev.ts | 14 +-
scripts/tasks/e2e-tests-build.ts | 17 +-
scripts/tasks/e2e-tests-dev.ts | 4 +-
scripts/tasks/generate.ts | 6 +-
scripts/tasks/install.ts | 6 +-
scripts/tasks/publish.ts | 4 +-
scripts/tasks/run-registry.ts | 8 +-
scripts/tasks/sandbox-parts.ts | 36 +-
scripts/tasks/sandbox.ts | 12 +-
scripts/tasks/serve.ts | 14 +-
scripts/tasks/smoke-test.ts | 4 +-
scripts/tasks/sync-docs.ts | 4 +-
scripts/tasks/test-runner-build.ts | 17 +-
scripts/tasks/test-runner-dev.ts | 4 +-
scripts/tasks/vitest-test.ts | 6 +-
scripts/tsconfig.json | 1 +
scripts/upload-bench.ts | 6 +-
scripts/utils/cli-step.ts | 6 +-
scripts/utils/cli-utils.ts | 2 +-
scripts/utils/filterExistsInCodeDir.ts | 2 +-
scripts/utils/main-js.ts | 6 +-
scripts/utils/options.test.ts | 2 +-
scripts/utils/tools.ts | 2 +-
scripts/utils/workspace.ts | 2 +-
scripts/utils/yarn.ts | 8 +-
.../e2e-tests/component-testing.spec.ts | 24 +-
.../react/e2e-tests/component-testing.spec.ts | 28 +-
yarn.lock | 83 +--
1710 files changed, 10762 insertions(+), 5504 deletions(-)
create mode 100644 .lintstagedrc.mjs
delete mode 100644 code/.oxfmtrc.json
create mode 100644 code/addons/pseudo-states/src/stories/PseudoStateGrid.tsx
create mode 100644 code/builders/builder-vite/src/index.test.ts
create mode 100644 code/builders/builder-vite/src/utils/build-module-graph.test.ts
create mode 100644 code/builders/builder-vite/src/utils/build-module-graph.ts
create mode 100644 code/core/src/common/utils/utils.test.ts
create mode 100644 code/core/src/core-server/change-detection/ChangeDetectionService.test.ts
create mode 100644 code/core/src/core-server/change-detection/ChangeDetectionService.ts
create mode 100644 code/core/src/core-server/change-detection/GitDiffProvider.test.ts
create mode 100644 code/core/src/core-server/change-detection/GitDiffProvider.ts
create mode 100644 code/core/src/core-server/change-detection/errors.ts
create mode 100644 code/core/src/core-server/change-detection/index.ts
create mode 100644 code/core/src/core-server/change-detection/readiness.ts
create mode 100644 code/core/src/core-server/change-detection/trace-changed.test.ts
create mode 100644 code/core/src/core-server/change-detection/trace-changed.ts
create mode 100644 code/core/src/manager-api/lib/filter-param.ts
create mode 100644 code/core/src/manager-api/lib/url.ts
create mode 100644 code/core/src/manager-api/modules/statuses.ts
create mode 100644 code/core/src/manager-api/modules/tags.ts
create mode 100644 code/core/src/manager-api/tests/statuses.test.ts
create mode 100644 code/core/src/manager-api/tests/tags.test.js
rename code/core/src/manager/components/sidebar/{TagsFilter.stories.tsx => Filter.stories.tsx} (76%)
rename code/core/src/manager/components/sidebar/{TagsFilter.story-helpers.tsx => Filter.story-helpers.tsx} (94%)
rename code/core/src/manager/components/sidebar/{TagsFilter.tsx => Filter.tsx} (70%)
rename code/core/src/manager/components/sidebar/{TagsFilterPanel.stories.tsx => FilterPanel.stories.tsx} (68%)
create mode 100644 code/core/src/manager/components/sidebar/FilterPanel.tsx
create mode 100644 code/core/src/manager/components/sidebar/FilterPanel.utils.ts
create mode 100644 code/core/src/manager/components/sidebar/FilterPanelLink.tsx
delete mode 100644 code/core/src/manager/components/sidebar/TagsFilterPanel.tsx
create mode 100644 code/core/src/manager/components/sidebar/useFilterData.tsx
create mode 100644 code/lib/cli-storybook/src/automigrate/fixes/storybook-package-name-conflict.test.ts
create mode 100644 code/lib/cli-storybook/src/automigrate/fixes/storybook-package-name-conflict.ts
delete mode 100644 docs/.oxfmtrc.json
create mode 100644 docs/_snippets/storybook-main-rdt-monorepo-include.md
diff --git a/.github/workflows/nx.yml b/.github/workflows/nx.yml
index 97480fe4bc8a..f3b048a2122a 100644
--- a/.github/workflows/nx.yml
+++ b/.github/workflows/nx.yml
@@ -32,7 +32,7 @@ jobs:
runs-on: ubuntu-latest
env:
- ALL_TASKS: compile,check,knip,test,pretty-docs,lint,sandbox,build,e2e-tests,e2e-tests-dev,test-runner,vitest-integration,check-sandbox,e2e-ui,jest,vitest,playwright-ct
+ ALL_TASKS: compile,check,knip,test,lint,fmt,sandbox,build,e2e-tests,e2e-tests-dev,test-runner,vitest-integration,check-sandbox,e2e-ui,jest,vitest,playwright-ct
steps:
- uses: actions/checkout@v4
with:
diff --git a/.husky/pre-commit b/.husky/pre-commit
index 36c4e990898b..fe596ea4aea0 100644
--- a/.husky/pre-commit
+++ b/.husky/pre-commit
@@ -1,7 +1,3 @@
if [ -z "$SKIP_STORYBOOK_GIT_HOOKS" ]; then
- cd code
- yarn lint-staged
-
- cd ../scripts
yarn lint-staged
fi
diff --git a/.lintstagedrc.mjs b/.lintstagedrc.mjs
new file mode 100644
index 000000000000..9f5654bdc2e8
--- /dev/null
+++ b/.lintstagedrc.mjs
@@ -0,0 +1,11 @@
+import { detectAgent } from 'std-env';
+
+const fmtCmd = detectAgent().name ? 'oxfmt' : 'oxfmt --check';
+
+export default {
+ 'code/**/*.{js,jsx,mjs,ts,tsx,html,json}': [fmtCmd, 'yarn --cwd code lint:js:cmd'],
+ 'scripts/**/*.{html,js,json,jsx,mjs,ts,tsx}': ['yarn --cwd scripts lint:js:cmd'],
+ 'docs/_snippets/**/*.{js,jsx,mjs,ts,tsx,html,json}': [fmtCmd],
+ '**/*.ejs': ['yarn --cwd scripts exec ejslint'],
+ '**/package.json': ['yarn --cwd scripts lint:package'],
+};
diff --git a/.nx/workflows/distribution-config.yaml b/.nx/workflows/distribution-config.yaml
index 594e6c32bc68..61d36251c5cc 100644
--- a/.nx/workflows/distribution-config.yaml
+++ b/.nx/workflows/distribution-config.yaml
@@ -22,7 +22,6 @@ assignment-rules:
- targets:
- check
- lint
- - pretty-docs
- knip
run-on:
- agent: linux-js
diff --git a/.oxfmtrc.json b/.oxfmtrc.json
index 87673614cded..69973902038b 100644
--- a/.oxfmtrc.json
+++ b/.oxfmtrc.json
@@ -1,4 +1,5 @@
{
+ "$schema": "./node_modules/oxfmt/configuration_schema.json",
"printWidth": 100,
"tabWidth": 2,
"bracketSpacing": true,
@@ -7,20 +8,62 @@
"arrowParens": "always",
"sortPackageJson": false,
"ignorePatterns": [
- "code",
- "test-storybooks",
- "node_modules",
+ "*.bundle.js",
+ "*.js.map",
".yarn",
- ".nx",
".vscode",
- ".github",
- "*.md",
- "*.mdx",
+ ".nx/cache",
+ ".nx/workspace-data",
+ "dist",
+ "build",
+ "bench",
+ "coverage",
+ "node_modules",
+ "storybook-static",
+ "built-storybooks",
+ "ember-output",
+ "code/core/assets",
+ "code/core/report",
+ "code/core/src/core-server/presets/common-manager.ts",
+ "code/core/src/core-server/utils/__search-files-tests__",
+ "code/core/src/core-server/utils/__mockdata__/src/Empty.stories.ts",
+ "code/lib/codemod/src/transforms/__testfixtures__",
+ "code/frameworks/angular/template/**",
+ "code/lib/eslint-plugin",
+ "docs/versions/*.json",
+ ".prettierrc",
+ "test-storybooks",
"*.yml",
"*.yaml",
- "docs/versions",
- "CHANGELOG*",
- "MIGRATION*",
- "CONTRIBUTING*"
+ "*.md",
+ "*.mdx",
+ "!docs/_snippets/**"
+ ],
+ "overrides": [
+ {
+ "files": ["docs/_snippets/**"],
+ "options": {
+ "trailingComma": "all"
+ }
+ },
+ {
+ "files": ["*.md", "*.mdx"],
+ "options": {
+ "importOrderSeparation": false,
+ "importOrderSortSpecifiers": false
+ }
+ },
+ {
+ "files": ["*.component.html"],
+ "options": {
+ "parser": "angular"
+ }
+ },
+ {
+ "files": ["**/frameworks/angular/src/**/*.ts", "**/frameworks/angular/template/**/*.ts"],
+ "options": {
+ "parser": "babel-ts"
+ }
+ }
]
}
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
index 020ac953c668..ca2530047fdd 100644
--- a/.vscode/extensions.json
+++ b/.vscode/extensions.json
@@ -3,6 +3,7 @@
"dbaeumer.vscode-eslint",
"EditorConfig.EditorConfig",
"unifiedjs.vscode-mdx",
- "yzhang.markdown-all-in-one"
+ "yzhang.markdown-all-in-one",
+ "oxc.oxc-vscode"
]
-}
+}
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 04ea0343d429..0b27179c12cc 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,30 +1,28 @@
{
- "[javascript]": {
- "editor.defaultFormatter": "dbaeumer.vscode-eslint",
- "editor.formatOnSave": true
- },
- "[javascriptreact]": {
- "editor.defaultFormatter": "dbaeumer.vscode-eslint",
- "editor.formatOnSave": true
- },
- "[typescript]": {
+ "[javascript][javascriptreact][typescript][typescriptreact][json][jsonc]": {
"editor.defaultFormatter": "oxc.oxc-vscode",
"editor.formatOnSave": true
},
- "[typescriptreact]": {
- "editor.defaultFormatter": "dbaeumer.vscode-eslint",
- "editor.formatOnSave": true
- },
"editor.codeActionsOnSave": {
- "source.fixAll.eslint": "explicit"
+ "source.fixAll.eslint": "explicit",
+ "source.fixAll.oxc": "explicit"
},
"editor.formatOnSave": true,
+ "[mdx]": {
+ "editor.formatOnSave": false
+ },
"editor.tabSize": 2,
- "eslint.format.enable": true,
"eslint.options": {
"cache": true,
"cacheLocation": ".cache/eslint",
- "extensions": [".js", ".jsx", ".mjs", ".json", ".ts", ".tsx"]
+ "extensions": [
+ ".js",
+ ".jsx",
+ ".mjs",
+ ".json",
+ ".ts",
+ ".tsx"
+ ]
},
"eslint.useESLintClass": true,
"eslint.validate": [
@@ -35,9 +33,13 @@
"typescript",
"typescriptreact"
],
- "eslint.workingDirectories": ["./code", "./scripts"],
+ "eslint.workingDirectories": [
+ "./code",
+ "./scripts"
+ ],
"files.associations": {
- "*.js": "javascriptreact"
+ "*.js": "javascriptreact",
+ ".oxfmtrc.json": "json"
},
"javascript.preferences.importModuleSpecifier": "relative",
"javascript.preferences.quoteStyle": "single",
@@ -48,7 +50,9 @@
"typescript.preferences.preferTypeOnlyAutoImports": true,
"typescript.preferences.quoteStyle": "single",
"typescript.preferGoToSourceDefinition": true,
- "typescript.tsdk": "./typescript/lib",
+ "typescript.tsdk": "./node_modules/typescript/lib",
"vitest.workspaceConfig": "./code/vitest.workspace.ts",
"vitest.rootConfig": "./code/vitest.workspace.ts",
-}
+ "oxc.fmt.configPath": ".oxfmtrc.json",
+ "oxc.enable.oxlint": false,
+}
\ No newline at end of file
diff --git a/AGENTS.md b/AGENTS.md
index 6840a21614a8..5bb2810461b9 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -243,6 +243,10 @@ Use Storybook loggers instead of raw `console.*` in normal code paths:
- Server-side: `storybook/internal/node-logger`
- Client-side: `storybook/internal/client-logger`
+For TypeScript source in the repo, prefer explicit file extensions for relative code imports and exports such as `./foo.ts` or `./bar.tsx` when the target is another TS/JS module in this repository. Keep framework-specific component imports like `.vue` and `.svelte` in the form already expected by their package tooling.
+
+The pre-commit hook automatically detects AI agents (via `std-env`) and switches from check-only to write mode, so formatting is auto-fixed when agents commit.
+
Avoid `console.log`, `console.warn`, and `console.error` unless the file is isolated enough that importing the logger is not reasonable.
## Troubleshooting
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4c7c7d8530de..bebcb8ad665f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,14 @@
+## 10.3.4
+
+- Addon-a11y: Clear status transition timer on unmount to prevent test flake - [#34203](https://github.com/storybookjs/storybook/pull/34203), thanks @mixelburg!
+- Bug: Skip re-processing already transformed config files for CSF factories - [#34273](https://github.com/storybookjs/storybook/pull/34273), thanks @huang-julien!
+- Builder-Vite: Use djb2 hash to prevent variable name collisions in builder-vite - [#34274](https://github.com/storybookjs/storybook/pull/34274), thanks @chida09!
+- CLI: Prompt for init crash reports - [#34316](https://github.com/storybookjs/storybook/pull/34316), thanks @JReinhold!
+- CSF4: Fix duplicate preview loading issue in Vitest - [#34361](https://github.com/storybookjs/storybook/pull/34361), thanks @valentinpalkovic!
+- Core: Fix WebSocket connection for StackBlitz/WebContainers - [#34281](https://github.com/storybookjs/storybook/pull/34281), thanks @ghengeveld!
+- React-Docgen: Try .tsx fallback when resolving .js ESM imports in docgen resolvers - [#34393](https://github.com/storybookjs/storybook/pull/34393), thanks @mixelburg!
+- React-Vite: Upgrade @joshwooding/vite-plugin-react-docgen-typescript to 0.7.0 - [#34335](https://github.com/storybookjs/storybook/pull/34335), thanks @beeswhacks!
+
## 10.3.3
- Addon-Vitest: Streamline vite(st) config detection across init and postinstall - [#34193](https://github.com/storybookjs/storybook/pull/34193), thanks @valentinpalkovic!
diff --git a/CHANGELOG.prerelease.md b/CHANGELOG.prerelease.md
index ff97514a4cd9..aa166cb5f228 100644
--- a/CHANGELOG.prerelease.md
+++ b/CHANGELOG.prerelease.md
@@ -1,3 +1,47 @@
+## 10.4.0-alpha.7
+
+- CLI: Explicitly tell whether smoke tests passed or failed - [#34419](https://github.com/storybookjs/storybook/pull/34419), thanks @Sidnioulz!
+- Core: Add `ChangeDetectionService` and wire up builder-vite - [#34369](https://github.com/storybookjs/storybook/pull/34369), thanks @ghengeveld!
+- Maintenance: Extract parseFilterParam shared helper from tags and statuses modules - [#34436](https://github.com/storybookjs/storybook/pull/34436), thanks @mixelburg!
+- Sidebar: Add status-based filtering with refactored status architecture - [#34339](https://github.com/storybookjs/storybook/pull/34339), thanks @valentinpalkovic!
+- UI: Fix global shortcuts not showing region focus indicator - [#34201](https://github.com/storybookjs/storybook/pull/34201), thanks @Sidnioulz!
+
+## 10.4.0-alpha.6
+
+- Builder-Vite: Add onModuleGraphChange method - [#34323](https://github.com/storybookjs/storybook/pull/34323), thanks @ghengeveld!
+- CLI: Add automigrate check for 'storybook' package name conflict - [#34290](https://github.com/storybookjs/storybook/pull/34290), thanks @whdjh!
+- CLI: Prompt for init crash reports - [#34316](https://github.com/storybookjs/storybook/pull/34316), thanks @JReinhold!
+- CSF4: Fix duplicate preview loading issue in Vitest - [#34361](https://github.com/storybookjs/storybook/pull/34361), thanks @valentinpalkovic!
+- Maintenance: Add assertions outside step incorrectly nested in interactions panel - [#34296](https://github.com/storybookjs/storybook/pull/34296), thanks @majiayu000!
+- Maintenance: Extract getBuilderOptions helper across framewo… - [#34260](https://github.com/storybookjs/storybook/pull/34260), thanks @alex-js-ltd!
+- Maintenance: Use errorToErrorLike in boot-test-runner for consistent stack deduplication - [#34385](https://github.com/storybookjs/storybook/pull/34385), thanks @mixelburg!
+- Onboarding: Fix checklist MDX instructions - [#33193](https://github.com/storybookjs/storybook/pull/33193), thanks @kylegach!
+- React-Docgen: Add tsconfig fallback chain and warning for monorepos - [#34353](https://github.com/storybookjs/storybook/pull/34353), thanks @viditkbhatnagar!
+- React-Docgen: Try .tsx fallback when resolving .js ESM imports in docgen resolvers - [#34393](https://github.com/storybookjs/storybook/pull/34393), thanks @mixelburg!
+- UI: Fix mobile navigation when renderLabel returns a React node - [#34262](https://github.com/storybookjs/storybook/pull/34262), thanks @Nathan54Villaume!
+- Vite: Use vite hook filter for performance improvements - [#34022](https://github.com/storybookjs/storybook/pull/34022), thanks @huang-julien!
+
+## 10.4.0-alpha.5
+
+- Addon-a11y: Clear status transition timer on unmount to prevent test flake - [#34203](https://github.com/storybookjs/storybook/pull/34203), thanks @mixelburg!
+- Builder-Vite: Use djb2 hash to prevent variable name collisions in builder-vite - [#34274](https://github.com/storybookjs/storybook/pull/34274), thanks @chida09!
+- CLI: Fix Next.js Vite automigration corrupting configs already using `@storybook/nextjs-vite` - [#34249](https://github.com/storybookjs/storybook/pull/34249), thanks @nathanjessen!
+- Core: Add changeDetection feature flag - [#34314](https://github.com/storybookjs/storybook/pull/34314), thanks @valentinpalkovic!
+- Manager: URL-based tag filter state + filter-aware initial story selection - [#34283](https://github.com/storybookjs/storybook/pull/34283), thanks @valentinpalkovic!
+- React-Vite: Upgrade @joshwooding/vite-plugin-react-docgen-typescript to 0.7.0 - [#34335](https://github.com/storybookjs/storybook/pull/34335), thanks @beeswhacks!
+- Refactor: Extract shared `PseudoStateGrid` component in pseudo-states stories - [#34334](https://github.com/storybookjs/storybook/pull/34334), thanks @copilot-swe-agent!
+
+## 10.4.0-alpha.4
+
+- Addon-Docs: Add Reset story button to re-render stories in docs - [#34086](https://github.com/storybookjs/storybook/pull/34086), thanks @6810779s!
+- Code: Fix inline code blocks inside links removing link affordance - [#33903](https://github.com/storybookjs/storybook/pull/33903), thanks @yatishgoel!
+- Controls: Add maxPresetColors option to ColorControl - [#33998](https://github.com/storybookjs/storybook/pull/33998), thanks @mixelburg!
+- Core: Fix WebSocket connection for StackBlitz/WebContainers - [#34281](https://github.com/storybookjs/storybook/pull/34281), thanks @ghengeveld!
+- Dependencies: Update `vite-plugin-storybook-nextjs` to ^3.2.4 - [#34280](https://github.com/storybookjs/storybook/pull/34280), thanks @k35o!
+- React: Add component metadata extraction via Volar-style LanguageService - [#33914](https://github.com/storybookjs/storybook/pull/33914), thanks @kasperpeulen!
+- StatusValue: Add 'status-value:' - [#34305](https://github.com/storybookjs/storybook/pull/34305), thanks @valentinpalkovic!
+- UI: Ensure Controls panel can scroll horizontally for now - [#34248](https://github.com/storybookjs/storybook/pull/34248), thanks @Sidnioulz!
+
## 10.4.0-alpha.3
- Addon-Vitest: Streamline vite(st) config detection across init and postinstall - [#34193](https://github.com/storybookjs/storybook/pull/34193), thanks @valentinpalkovic!
diff --git a/code/.eslintrc.js b/code/.eslintrc.js
index b1e2ad68d238..c856ed13b790 100644
--- a/code/.eslintrc.js
+++ b/code/.eslintrc.js
@@ -229,5 +229,31 @@ module.exports = {
'storybook/no-renderer-packages': 'off',
},
},
+ {
+ files: ['**/*.ts', '**/*.tsx'],
+ excludedFiles: [
+ '**/*.d.ts',
+ '**/docs/**/*',
+ '**/template/**/*',
+ '**/templates/**/*',
+ '**/__testfixtures__/**/*',
+ '**/__mocks-ng-workspace__/**/*',
+ ],
+ rules: {
+ 'import-x/extensions': [
+ 'error',
+ 'always',
+ {
+ ignorePackages: true,
+ checkTypeImports: true,
+ fix: true,
+ pathGroupOverrides: [
+ { pattern: 'storybook/**', action: 'ignore' },
+ { pattern: '@storybook/**', action: 'ignore' },
+ ],
+ },
+ ],
+ },
+ },
],
};
diff --git a/code/.oxfmtrc.json b/code/.oxfmtrc.json
deleted file mode 100644
index 5de8a18c6d3d..000000000000
--- a/code/.oxfmtrc.json
+++ /dev/null
@@ -1,34 +0,0 @@
-{
- "printWidth": 100,
- "tabWidth": 2,
- "bracketSpacing": true,
- "trailingComma": "es5",
- "singleQuote": true,
- "arrowParens": "always",
- "sortPackageJson": false,
- "ignorePatterns": [
- "*.mdx",
- "*.md",
- "*.bundle.js",
- "*.js.map",
- ".yarn",
- ".vscode",
- ".nx/cache",
- ".nx/workspace-data",
- "dist",
- "build",
- "bench",
- "coverage",
- "node_modules",
- "storybook-static",
- "built-storybooks",
- "ember-output",
- "core/assets",
- "core/report",
- "core/src/core-server/presets/common-manager.ts",
- "core/src/core-server/utils/__search-files-tests__",
- "core/src/core-server/utils/__mockdata__/src/Empty.stories.ts",
- "lib/codemod/src/transforms/__testfixtures__",
- "**/frameworks/angular/template/**"
- ]
-}
diff --git a/code/.storybook/bench/bench.stories.tsx b/code/.storybook/bench/bench.stories.tsx
index a2bddbb169c1..c7a70727b365 100644
--- a/code/.storybook/bench/bench.stories.tsx
+++ b/code/.storybook/bench/bench.stories.tsx
@@ -2,7 +2,7 @@ import React from 'react';
import type { Meta } from '@storybook/react-vite';
-import { safeMetafileArg } from '../../../scripts/bench/safe-args';
+import { safeMetafileArg } from '../../../scripts/bench/safe-args.ts';
// @ts-expect-error - TS doesn't know about import.meta.glob from Vite
const allMetafiles = import.meta.glob('../../bench/esbuild-metafiles/**/*.json', {
diff --git a/code/.storybook/preview.tsx b/code/.storybook/preview.tsx
index d6ca299ed18f..06d346f0efa4 100644
--- a/code/.storybook/preview.tsx
+++ b/code/.storybook/preview.tsx
@@ -29,10 +29,10 @@ import {
useTheme,
} from 'storybook/theming';
-import { DocsPageWrapper } from '../addons/docs/src/blocks/components';
-import * as templatePreview from '../core/template/stories/preview';
-import '../renderers/react/template/components/index';
-import { isChromatic } from './isChromatic';
+import { DocsPageWrapper } from '../addons/docs/src/blocks/components/index.ts';
+import * as templatePreview from '../core/template/stories/preview.ts';
+import '../renderers/react/template/components/index.js';
+import { isChromatic } from './isChromatic.ts';
sb.mock(import('@storybook/global'), { spy: true });
diff --git a/code/.storybook/storybook.setup.ts b/code/.storybook/storybook.setup.ts
index 91cc5bcdd8fb..b9e5370656d2 100644
--- a/code/.storybook/storybook.setup.ts
+++ b/code/.storybook/storybook.setup.ts
@@ -4,7 +4,7 @@ import { setProjectAnnotations } from '@storybook/react';
import { userEvent as storybookEvent, expect as storybookExpect } from 'storybook/test';
-import preview from './preview';
+import preview from './preview.tsx';
vi.spyOn(console, 'warn').mockImplementation((...args) => console.log(...args));
diff --git a/code/addons/a11y/build-config.ts b/code/addons/a11y/build-config.ts
index 6313a6e261f6..226d421c14ec 100644
--- a/code/addons/a11y/build-config.ts
+++ b/code/addons/a11y/build-config.ts
@@ -1,4 +1,4 @@
-import type { BuildEntries } from '../../../scripts/build/utils/entry-utils';
+import type { BuildEntries } from '../../../scripts/build/utils/entry-utils.ts';
const config: BuildEntries = {
entries: {
diff --git a/code/addons/a11y/package.json b/code/addons/a11y/package.json
index aa58bf767051..1ef8dd41c2fb 100644
--- a/code/addons/a11y/package.json
+++ b/code/addons/a11y/package.json
@@ -1,6 +1,6 @@
{
"name": "@storybook/addon-a11y",
- "version": "10.4.0-alpha.3",
+ "version": "10.4.0-alpha.7",
"description": "Storybook Addon A11y: Test UI component compliance with WCAG web accessibility standards",
"keywords": [
"a11y",
diff --git a/code/addons/a11y/src/a11yRunner.test.ts b/code/addons/a11y/src/a11yRunner.test.ts
index 3f2f6ef86eff..9018e7417c6e 100644
--- a/code/addons/a11y/src/a11yRunner.test.ts
+++ b/code/addons/a11y/src/a11yRunner.test.ts
@@ -3,7 +3,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
import { addons } from 'storybook/preview-api';
-import { EVENTS } from './constants';
+import { EVENTS } from './constants.ts';
vi.mock('storybook/preview-api');
const mockedAddons = vi.mocked(addons);
@@ -19,7 +19,7 @@ describe('a11yRunner', () => {
});
it('should listen to events', async () => {
- await import('./a11yRunner');
+ await import('./a11yRunner.ts');
expect(mockedAddons.getChannel).toHaveBeenCalled();
expect(mockChannel.on).toHaveBeenCalledWith(EVENTS.MANUAL, expect.any(Function));
diff --git a/code/addons/a11y/src/a11yRunner.ts b/code/addons/a11y/src/a11yRunner.ts
index 250bd013830c..00fb4540f8a7 100644
--- a/code/addons/a11y/src/a11yRunner.ts
+++ b/code/addons/a11y/src/a11yRunner.ts
@@ -5,9 +5,9 @@ import { global } from '@storybook/global';
import type { AxeResults, ContextProp, ContextSpec } from 'axe-core';
import { addons, waitForAnimations } from 'storybook/preview-api';
-import { withLinkPaths } from './a11yRunnerUtils';
-import { EVENTS } from './constants';
-import type { A11yParameters } from './params';
+import { withLinkPaths } from './a11yRunnerUtils.ts';
+import { EVENTS } from './constants.ts';
+import type { A11yParameters } from './params.ts';
const { document } = global;
diff --git a/code/addons/a11y/src/a11yRunnerUtils.test.ts b/code/addons/a11y/src/a11yRunnerUtils.test.ts
index e07bb15926c8..b951862090b5 100644
--- a/code/addons/a11y/src/a11yRunnerUtils.test.ts
+++ b/code/addons/a11y/src/a11yRunnerUtils.test.ts
@@ -3,7 +3,7 @@ import { describe, expect, it } from 'vitest';
import type { AxeResults } from 'axe-core';
-import { withLinkPaths } from './a11yRunnerUtils';
+import { withLinkPaths } from './a11yRunnerUtils.ts';
describe('a11yRunnerUtils', () => {
describe('withLinkPaths', () => {
diff --git a/code/addons/a11y/src/a11yRunnerUtils.ts b/code/addons/a11y/src/a11yRunnerUtils.ts
index a51dcbe68b7c..302360b31093 100644
--- a/code/addons/a11y/src/a11yRunnerUtils.ts
+++ b/code/addons/a11y/src/a11yRunnerUtils.ts
@@ -2,8 +2,8 @@ import { global } from '@storybook/global';
import type { AxeResults, Result } from 'axe-core';
-import { PANEL_ID } from './constants';
-import type { EnhancedResults } from './types';
+import { PANEL_ID } from './constants.ts';
+import type { EnhancedResults } from './types.ts';
const { document } = global;
diff --git a/code/addons/a11y/src/axeRuleMappingHelper.ts b/code/addons/a11y/src/axeRuleMappingHelper.ts
index c3dbdd51d54e..4ade9e2718e5 100644
--- a/code/addons/a11y/src/axeRuleMappingHelper.ts
+++ b/code/addons/a11y/src/axeRuleMappingHelper.ts
@@ -1,5 +1,5 @@
-import { combinedRulesMap } from './AccessibilityRuleMaps';
-import type { EnhancedResult } from './types';
+import { combinedRulesMap } from './AccessibilityRuleMaps.ts';
+import type { EnhancedResult } from './types.ts';
export const getTitleForAxeResult = (axeResult: EnhancedResult): string =>
combinedRulesMap[axeResult.id]?.title || axeResult.id;
diff --git a/code/addons/a11y/src/components/A11YPanel.stories.tsx b/code/addons/a11y/src/components/A11YPanel.stories.tsx
index 8638ba2ad6b7..6cfb68a2f51c 100644
--- a/code/addons/a11y/src/components/A11YPanel.stories.tsx
+++ b/code/addons/a11y/src/components/A11YPanel.stories.tsx
@@ -4,12 +4,12 @@ import { ManagerContext } from 'storybook/manager-api';
import { expect, fn, userEvent, waitFor, within } from 'storybook/test';
import { styled } from 'storybook/theming';
-import preview from '../../../../.storybook/preview';
-import { results } from '../results.mock';
-import { type EnhancedResults, RuleType } from '../types';
-import { A11YPanel } from './A11YPanel';
-import { A11yContext } from './A11yContext';
-import type { A11yContextStore } from './A11yContext';
+import preview from '../../../../.storybook/preview.tsx';
+import { results } from '../results.mock.ts';
+import { type EnhancedResults, RuleType } from '../types.ts';
+import { A11YPanel } from './A11YPanel.tsx';
+import { A11yContext } from './A11yContext.tsx';
+import type { A11yContextStore } from './A11yContext.tsx';
const emptyResults: EnhancedResults = {
passes: [],
diff --git a/code/addons/a11y/src/components/A11YPanel.test.tsx b/code/addons/a11y/src/components/A11YPanel.test.tsx
index 9352981fc613..12040e9eb902 100644
--- a/code/addons/a11y/src/components/A11YPanel.test.tsx
+++ b/code/addons/a11y/src/components/A11YPanel.test.tsx
@@ -8,9 +8,9 @@ import React from 'react';
import * as managerApi from 'storybook/manager-api';
import { ThemeProvider, convert, themes } from 'storybook/theming';
-import { type EnhancedResults } from '../types';
-import { A11YPanel } from './A11YPanel';
-import { type A11yContextStore, useA11yContext } from './A11yContext';
+import { type EnhancedResults } from '../types.ts';
+import { A11YPanel } from './A11YPanel.tsx';
+import { type A11yContextStore, useA11yContext } from './A11yContext.tsx';
vi.mock('storybook/manager-api');
const mockedManagerApi = vi.mocked(managerApi);
diff --git a/code/addons/a11y/src/components/A11YPanel.tsx b/code/addons/a11y/src/components/A11YPanel.tsx
index 85210ab278c6..a08c3cded1b2 100644
--- a/code/addons/a11y/src/components/A11YPanel.tsx
+++ b/code/addons/a11y/src/components/A11YPanel.tsx
@@ -6,11 +6,11 @@ import { SyncIcon } from '@storybook/icons';
import { styled } from 'storybook/theming';
-import { RuleType } from '../types';
-import { useA11yContext } from './A11yContext';
-import { Report } from './Report/Report';
-import { Tabs } from './Tabs';
-import { TestDiscrepancyMessage } from './TestDiscrepancyMessage';
+import { RuleType } from '../types.ts';
+import { useA11yContext } from './A11yContext.tsx';
+import { Report } from './Report/Report.tsx';
+import { Tabs } from './Tabs.tsx';
+import { TestDiscrepancyMessage } from './TestDiscrepancyMessage.tsx';
const RotatingIcon = styled(SyncIcon)(({ theme }) => ({
animation: `${theme.animation.rotate360} 1s linear infinite;`,
diff --git a/code/addons/a11y/src/components/A11yContext.test.tsx b/code/addons/a11y/src/components/A11yContext.test.tsx
index 4d72f31e2955..f6ee6c5a969e 100644
--- a/code/addons/a11y/src/components/A11yContext.test.tsx
+++ b/code/addons/a11y/src/components/A11yContext.test.tsx
@@ -13,9 +13,9 @@ import {
import type { AxeResults } from 'axe-core';
import * as api from 'storybook/manager-api';
-import { EVENTS, UI_STATE_ID } from '../constants';
-import { RuleType } from '../types';
-import { A11yContextProvider, useA11yContext } from './A11yContext';
+import { EVENTS, UI_STATE_ID } from '../constants.ts';
+import { RuleType } from '../types.ts';
+import { A11yContextProvider, useA11yContext } from './A11yContext.tsx';
vi.mock('storybook/manager-api');
const mockedApi = vi.mocked(api);
diff --git a/code/addons/a11y/src/components/A11yContext.tsx b/code/addons/a11y/src/components/A11yContext.tsx
index 90c6f2f3f328..a381d42320f4 100644
--- a/code/addons/a11y/src/components/A11yContext.tsx
+++ b/code/addons/a11y/src/components/A11yContext.tsx
@@ -1,5 +1,13 @@
import type { FC, PropsWithChildren } from 'react';
-import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
+import React, {
+ createContext,
+ useCallback,
+ useContext,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from 'react';
import {
STORY_CHANGED,
@@ -24,12 +32,17 @@ import {
import type { Report } from 'storybook/preview-api';
import { convert, themes } from 'storybook/theming';
-import { getFriendlySummaryForAxeResult, getTitleForAxeResult } from '../axeRuleMappingHelper';
-import { ADDON_ID, EVENTS, STATUS_TYPE_ID_A11Y, STATUS_TYPE_ID_COMPONENT_TEST } from '../constants';
-import type { A11yParameters } from '../params';
-import type { A11yReport, EnhancedResult, EnhancedResults, Status } from '../types';
-import { RuleType } from '../types';
-import type { TestDiscrepancy } from './TestDiscrepancyMessage';
+import { getFriendlySummaryForAxeResult, getTitleForAxeResult } from '../axeRuleMappingHelper.ts';
+import {
+ ADDON_ID,
+ EVENTS,
+ STATUS_TYPE_ID_A11Y,
+ STATUS_TYPE_ID_COMPONENT_TEST,
+} from '../constants.ts';
+import type { A11yParameters } from '../params.ts';
+import type { A11yReport, EnhancedResult, EnhancedResults, Status } from '../types.ts';
+import { RuleType } from '../types.ts';
+import type { TestDiscrepancy } from './TestDiscrepancyMessage.tsx';
// These elements should not be highlighted because they usually cover the whole page.
// They may still appear in the results and be selectable though.
@@ -140,9 +153,22 @@ export const A11yContextProvider: FC = (props) => {
}, [setState, storyId]);
const handleToggleHighlight = useCallback(() => {
- setState((prev) => ({ ...prev, ui: { ...prev.ui, highlighted: !prev.ui.highlighted } }));
+ setState((prev) => ({
+ ...prev,
+ ui: { ...prev.ui, highlighted: !prev.ui.highlighted },
+ }));
}, [setState]);
+ const statusTimerRef = useRef | null>(null);
+
+ useEffect(() => {
+ return () => {
+ if (statusTimerRef.current !== null) {
+ clearTimeout(statusTimerRef.current);
+ }
+ };
+ }, []);
+
const [selectedItems, setSelectedItems] = useState
`;
+ )}${docs.map((doc) => `- ${doc}
`).join('')}
`;
return;
}
}
diff --git a/code/builders/builder-vite/package.json b/code/builders/builder-vite/package.json
index d62471eab30d..f3e89f0d3942 100644
--- a/code/builders/builder-vite/package.json
+++ b/code/builders/builder-vite/package.json
@@ -1,6 +1,6 @@
{
"name": "@storybook/builder-vite",
- "version": "10.4.0-alpha.3",
+ "version": "10.4.0-alpha.7",
"description": "A Storybook builder to dev and build with Vite",
"keywords": [
"storybook",
diff --git a/code/builders/builder-vite/src/build.ts b/code/builders/builder-vite/src/build.ts
index 3a0891fae7aa..3f88b0e48e30 100644
--- a/code/builders/builder-vite/src/build.ts
+++ b/code/builders/builder-vite/src/build.ts
@@ -4,12 +4,12 @@ import type { Options } from 'storybook/internal/types';
import { dedent } from 'ts-dedent';
import type { InlineConfig } from 'vite';
-import { createViteLogger } from './logger';
-import type { WebpackStatsPlugin } from './plugins';
-import { hasVitePlugins } from './utils/has-vite-plugins';
-import { bundlerOptionsKey } from './utils/vite-features';
-import { withoutVitePlugins } from './utils/without-vite-plugins';
-import { commonConfig } from './vite-config';
+import { createViteLogger } from './logger.ts';
+import type { WebpackStatsPlugin } from './plugins/index.ts';
+import { hasVitePlugins } from './utils/has-vite-plugins.ts';
+import { bundlerOptionsKey } from './utils/vite-features.ts';
+import { withoutVitePlugins } from './utils/without-vite-plugins.ts';
+import { commonConfig } from './vite-config.ts';
function findPlugin(config: InlineConfig, name: string) {
return config.plugins?.find((p) => p && 'name' in p && p.name === name);
diff --git a/code/builders/builder-vite/src/codegen-importfn-script.test.ts b/code/builders/builder-vite/src/codegen-importfn-script.test.ts
index 35edde4662e6..d88880bba10e 100644
--- a/code/builders/builder-vite/src/codegen-importfn-script.test.ts
+++ b/code/builders/builder-vite/src/codegen-importfn-script.test.ts
@@ -2,7 +2,7 @@ import { describe, expect, it, vi } from 'vitest';
import type { StoryIndex } from 'storybook/internal/types';
-import { generateImportFnScriptCode } from './codegen-importfn-script';
+import { generateImportFnScriptCode } from './codegen-importfn-script.ts';
describe('generateImportFnScriptCode', () => {
it('should correctly map story paths to import functions for POSIX paths', async () => {
diff --git a/code/builders/builder-vite/src/codegen-importfn-script.ts b/code/builders/builder-vite/src/codegen-importfn-script.ts
index b1f67fa72221..dbd4d3c040f6 100644
--- a/code/builders/builder-vite/src/codegen-importfn-script.ts
+++ b/code/builders/builder-vite/src/codegen-importfn-script.ts
@@ -4,7 +4,7 @@ import { genDynamicImport, genObjectFromRawEntries } from 'knitwork';
import { join, normalize, relative } from 'pathe';
import { dedent } from 'ts-dedent';
-import { getUniqueImportPaths } from './utils/unique-import-paths';
+import { getUniqueImportPaths } from './utils/unique-import-paths.ts';
/**
* This function takes the story index and creates a mapping between the stories' relative paths to
diff --git a/code/builders/builder-vite/src/codegen-modern-iframe-script.test.ts b/code/builders/builder-vite/src/codegen-modern-iframe-script.test.ts
index 9b3e1c6c7625..e5161f98a6b3 100644
--- a/code/builders/builder-vite/src/codegen-modern-iframe-script.test.ts
+++ b/code/builders/builder-vite/src/codegen-modern-iframe-script.test.ts
@@ -1,8 +1,8 @@
import { describe, expect, it } from 'vitest';
-import { generateModernIframeScriptCodeFromPreviews } from './codegen-modern-iframe-script';
-import { generateAddonSetupCode } from './codegen-set-addon-channel';
-import { optimizeViteDeps } from './preset';
+import { generateModernIframeScriptCodeFromPreviews } from './codegen-modern-iframe-script.ts';
+import { generateAddonSetupCode } from './codegen-set-addon-channel.ts';
+import { optimizeViteDeps } from './preset.ts';
describe('generateModernIframeScriptCodeFromPreviews', () => {
it('handle one annotation', async () => {
diff --git a/code/builders/builder-vite/src/codegen-modern-iframe-script.ts b/code/builders/builder-vite/src/codegen-modern-iframe-script.ts
index e8a615db5b59..f756e13445e8 100644
--- a/code/builders/builder-vite/src/codegen-modern-iframe-script.ts
+++ b/code/builders/builder-vite/src/codegen-modern-iframe-script.ts
@@ -4,8 +4,8 @@ import type { Options } from 'storybook/internal/types';
import { dedent } from 'ts-dedent';
-import { VIRTUAL_ID as PROJECT_ANNOTATIONS_VIRTUAL_ID } from './plugins/storybook-project-annotations-plugin';
-import { SB_VIRTUAL_FILES } from './virtual-file-names';
+import { VIRTUAL_ID as PROJECT_ANNOTATIONS_VIRTUAL_ID } from './plugins/storybook-project-annotations-plugin.ts';
+import { SB_VIRTUAL_FILES } from './virtual-file-names.ts';
export async function generateModernIframeScriptCode(options: Options) {
const frameworkName = await getFrameworkName(options);
diff --git a/code/builders/builder-vite/src/codegen-project-annotations.ts b/code/builders/builder-vite/src/codegen-project-annotations.ts
index 9d13c9390ec9..78dc6499f2fe 100644
--- a/code/builders/builder-vite/src/codegen-project-annotations.ts
+++ b/code/builders/builder-vite/src/codegen-project-annotations.ts
@@ -6,7 +6,7 @@ import { genArrayFromRaw, genImport, genSafeVariableName } from 'knitwork';
import { filename } from 'pathe/utils';
import { dedent } from 'ts-dedent';
-import { processPreviewAnnotation } from './utils/process-preview-annotation';
+import { processPreviewAnnotation } from './utils/process-preview-annotation.ts';
/** Generates the code for the `PROJECT_ANNOTATIONS_FILE` virtual module. */
export async function generateProjectAnnotationsCode(options: Options, projectRoot: string) {
@@ -105,6 +105,11 @@ export function generateProjectAnnotationsCodeFromPreviews(options: {
`.trim();
}
+/** djb2 hash — http://www.cse.yorku.ca/~oz/hash.html */
function hash(value: string) {
- return value.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0);
+ let acc = 5381;
+ for (let i = 0; i < value.length; i++) {
+ acc = ((acc << 5) + acc + value.charCodeAt(i)) >>> 0;
+ }
+ return acc;
}
diff --git a/code/builders/builder-vite/src/index.test.ts b/code/builders/builder-vite/src/index.test.ts
new file mode 100644
index 000000000000..cdcccfd3823b
--- /dev/null
+++ b/code/builders/builder-vite/src/index.test.ts
@@ -0,0 +1,402 @@
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+
+import { logger } from 'storybook/internal/node-logger';
+import type { ModuleNode as StorybookModuleNode, Options } from 'storybook/internal/types';
+import type { ViteDevServer } from 'vite';
+
+import { bail, onModuleGraphChange, start } from './index.ts';
+import { createViteServer } from './vite-server.ts';
+
+vi.mock('storybook/internal/node-logger', { spy: true });
+vi.mock('./vite-server', { spy: true });
+
+type ViteModuleNodeLike = {
+ file: string | null;
+ type: StorybookModuleNode['type'];
+ importers: Set;
+ importedModules: Set;
+};
+
+function createViteModuleNode(
+ file: string | null,
+ type: StorybookModuleNode['type'] = 'js'
+): ViteModuleNodeLike {
+ return {
+ file,
+ type,
+ importers: new Set(),
+ importedModules: new Set(),
+ };
+}
+
+function createFileToModulesMap(...entries: Array<[string, Set]>) {
+ return new Map(entries) as ViteDevServer['moduleGraph']['fileToModulesMap'];
+}
+
+type WatcherHandler = (...args: unknown[]) => void;
+type FakeWatcher = {
+ on: ReturnType;
+ off: ReturnType;
+ emit: (event: string, ...args: unknown[]) => void;
+ listenerCount: (event: string) => number;
+};
+
+function createFakeViteServer() {
+ const watcherListeners = new Map>();
+ const watcher = {} as FakeWatcher;
+
+ watcher.on = vi.fn((event: string, handler: WatcherHandler) => {
+ const handlers = watcherListeners.get(event) ?? new Set();
+ handlers.add(handler);
+ watcherListeners.set(event, handlers);
+ return watcher;
+ });
+ watcher.off = vi.fn((event: string, handler: WatcherHandler) => {
+ watcherListeners.get(event)?.delete(handler);
+ return watcher;
+ });
+ watcher.emit = (event: string, ...args: unknown[]) => {
+ watcherListeners.get(event)?.forEach((handler) => {
+ handler(...args);
+ });
+ watcherListeners.get('all')?.forEach((handler) => {
+ handler(event, ...args);
+ });
+ };
+ watcher.listenerCount = (event: string) => watcherListeners.get(event)?.size ?? 0;
+
+ return {
+ watcher,
+ moduleGraph: {
+ fileToModulesMap: createFileToModulesMap(),
+ },
+ middlewares: {
+ handle: vi.fn(),
+ },
+ warmupRequest: vi.fn().mockResolvedValue(undefined),
+ transformIndexHtml: vi.fn().mockResolvedValue(''),
+ waitForRequestsIdle: vi.fn().mockResolvedValue(undefined),
+ close: vi.fn().mockResolvedValue(undefined),
+ } as unknown as ViteDevServer & {
+ watcher: typeof watcher;
+ };
+}
+
+function createStartArgs(storyImportPaths: string[] = []): Parameters[0] {
+ const indexGenerator = {
+ getIndex: vi.fn().mockResolvedValue({
+ entries: Object.fromEntries(
+ storyImportPaths.map((importPath, index) => [`story-${index}`, { importPath }])
+ ),
+ }),
+ };
+
+ return {
+ startTime: process.hrtime(),
+ options: {
+ presets: {
+ apply: vi.fn().mockResolvedValue(indexGenerator),
+ },
+ } as unknown as Options,
+ router: {
+ get: vi.fn(),
+ use: vi.fn(),
+ } as unknown as Parameters[0]['router'],
+ server: {} as Parameters[0]['server'],
+ channel: {} as Parameters[0]['channel'],
+ };
+}
+
+describe('onModuleGraphChange', () => {
+ let fakeViteServer: ReturnType;
+
+ async function startChangeDetection(
+ fileToModulesMap = createFileToModulesMap([
+ '/src/Button.tsx',
+ new Set([createViteModuleNode('/src/Button.tsx')]),
+ ])
+ ) {
+ fakeViteServer.moduleGraph.fileToModulesMap = fileToModulesMap;
+ await start(createStartArgs([...fileToModulesMap.keys()]));
+ await vi.advanceTimersByTimeAsync(1000);
+ }
+
+ beforeEach(() => {
+ vi.useFakeTimers();
+ fakeViteServer = createFakeViteServer();
+ vi.mocked(createViteServer).mockResolvedValue(fakeViteServer);
+ vi.mocked(logger.error).mockImplementation(() => undefined);
+ });
+
+ afterEach(async () => {
+ await bail();
+ vi.useRealTimers();
+ vi.clearAllMocks();
+ });
+
+ it('registers callbacks and unsubscribes them', async () => {
+ const cb = vi.fn();
+ const unsubscribe = onModuleGraphChange(cb);
+
+ expect(unsubscribe).toEqual(expect.any(Function));
+
+ await startChangeDetection();
+ cb.mockClear();
+
+ fakeViteServer.watcher.emit('change', '/src/Button.tsx');
+
+ await vi.advanceTimersByTimeAsync(100);
+
+ expect(cb).toHaveBeenCalledTimes(1);
+
+ unsubscribe();
+
+ fakeViteServer.watcher.emit('change', '/src/Button.tsx');
+ await vi.advanceTimersByTimeAsync(100);
+
+ expect(cb).toHaveBeenCalledTimes(1);
+ });
+
+ it('passes the module graph payload to listeners', async () => {
+ const entry = createViteModuleNode('/src/Button.tsx');
+ const fileToModulesMap = createFileToModulesMap(['/src/Button.tsx', new Set([entry])]);
+
+ const cb = vi.fn();
+ onModuleGraphChange(cb);
+
+ await startChangeDetection(fileToModulesMap);
+ cb.mockClear();
+
+ fakeViteServer.watcher.emit('change', '/src/Button.tsx');
+
+ await vi.advanceTimersByTimeAsync(100);
+
+ const event = cb.mock.calls[0]?.[0];
+ const moduleGraph = event?.moduleGraph;
+
+ expect(cb).toHaveBeenCalledWith({
+ type: 'moduleGraph',
+ moduleGraph: expect.any(Map),
+ });
+ expect(moduleGraph?.has('/src/Button.tsx')).toBe(true);
+ });
+
+ it('triggers change events after the debounce delay', async () => {
+ const cb = vi.fn();
+ onModuleGraphChange(cb);
+
+ await startChangeDetection();
+ cb.mockClear();
+
+ fakeViteServer.watcher.emit('change', '/src/Button.tsx');
+
+ await vi.advanceTimersByTimeAsync(50);
+ expect(cb).not.toHaveBeenCalled();
+
+ await vi.advanceTimersByTimeAsync(50);
+ expect(cb).toHaveBeenCalledTimes(1);
+ });
+
+ it('triggers add events after the debounce delay', async () => {
+ const cb = vi.fn();
+ onModuleGraphChange(cb);
+
+ await startChangeDetection();
+ cb.mockClear();
+
+ fakeViteServer.watcher.emit('add', '/src/Button.tsx');
+
+ await vi.advanceTimersByTimeAsync(100);
+
+ expect(cb).toHaveBeenCalledTimes(1);
+ });
+
+ it('triggers unlink events after the debounce delay', async () => {
+ const cb = vi.fn();
+ onModuleGraphChange(cb);
+
+ await startChangeDetection();
+ cb.mockClear();
+
+ fakeViteServer.watcher.emit('unlink', '/src/Button.tsx');
+
+ await vi.advanceTimersByTimeAsync(100);
+
+ expect(cb).toHaveBeenCalledTimes(1);
+ });
+
+ it('debounces multiple rapid events into a single callback', async () => {
+ const cb = vi.fn();
+ onModuleGraphChange(cb);
+
+ await startChangeDetection();
+ cb.mockClear();
+
+ fakeViteServer.watcher.emit('change', '/src/Button.tsx');
+ fakeViteServer.watcher.emit('add', '/src/Button.tsx');
+ fakeViteServer.watcher.emit('change', '/src/Button.tsx');
+
+ await vi.advanceTimersByTimeAsync(100);
+
+ expect(cb).toHaveBeenCalledTimes(1);
+ });
+
+ it('notifies multiple listeners', async () => {
+ const cb1 = vi.fn();
+ const cb2 = vi.fn();
+
+ onModuleGraphChange(cb1);
+ onModuleGraphChange(cb2);
+
+ await startChangeDetection();
+ cb1.mockClear();
+ cb2.mockClear();
+
+ fakeViteServer.watcher.emit('change', '/src/Button.tsx');
+
+ await vi.advanceTimersByTimeAsync(100);
+
+ expect(cb1).toHaveBeenCalledTimes(1);
+ expect(cb2).toHaveBeenCalledTimes(1);
+ });
+
+ it('removes the all-event watcher during bail to avoid leaks across restarts', async () => {
+ const cb = vi.fn();
+ onModuleGraphChange(cb);
+
+ await startChangeDetection();
+ expect(fakeViteServer.watcher.listenerCount('all')).toBe(1);
+
+ await bail();
+ expect(fakeViteServer.watcher.listenerCount('all')).toBe(0);
+
+ await start(createStartArgs(['/src/Button.tsx']));
+ await vi.advanceTimersByTimeAsync(1000);
+ expect(fakeViteServer.watcher.listenerCount('all')).toBe(0);
+
+ fakeViteServer.watcher.emit('change', '/src/Button.tsx');
+ await vi.advanceTimersByTimeAsync(100);
+
+ expect(cb).not.toHaveBeenCalled();
+ expect(fakeViteServer.watcher.off).toHaveBeenCalledWith('all', expect.any(Function));
+ });
+
+ it('clears the module-graph polling interval during bail', async () => {
+ onModuleGraphChange(vi.fn());
+
+ await start(createStartArgs());
+ await vi.advanceTimersByTimeAsync(0);
+
+ expect(vi.getTimerCount()).toBe(1);
+
+ await bail();
+
+ fakeViteServer.moduleGraph.fileToModulesMap = createFileToModulesMap([
+ '/src/Button.tsx',
+ new Set([createViteModuleNode('/src/Button.tsx')]),
+ ]);
+
+ await vi.advanceTimersByTimeAsync(1000);
+
+ expect(vi.getTimerCount()).toBe(0);
+ expect(fakeViteServer.waitForRequestsIdle).not.toHaveBeenCalled();
+ });
+
+ it('rejects listeners registered after start', async () => {
+ await start(createStartArgs());
+
+ expect(() => onModuleGraphChange(vi.fn())).toThrow(
+ 'Vite module graph listeners must be registered before the builder starts.'
+ );
+ });
+
+ it('does not reattach the watcher if bail runs while waiting for idle requests', async () => {
+ const cb = vi.fn();
+ onModuleGraphChange(cb);
+
+ let resolveIdle: (() => void) | undefined;
+ fakeViteServer.moduleGraph.fileToModulesMap = createFileToModulesMap([
+ '/src/Button.tsx',
+ new Set([createViteModuleNode('/src/Button.tsx')]),
+ ]);
+ fakeViteServer.waitForRequestsIdle = vi.fn().mockImplementation(
+ () =>
+ new Promise((resolve) => {
+ resolveIdle = resolve;
+ })
+ );
+
+ await start(createStartArgs(['/src/Button.tsx']));
+ await vi.advanceTimersByTimeAsync(1000);
+
+ await bail();
+ resolveIdle?.();
+ await Promise.resolve();
+ await Promise.resolve();
+
+ expect(fakeViteServer.watcher.listenerCount('all')).toBe(0);
+ expect(fakeViteServer.watcher.on).not.toHaveBeenCalledWith('all', expect.any(Function));
+ expect(cb).not.toHaveBeenCalled();
+ });
+
+ it('logs and swallows rejected startup work', async () => {
+ const cb = vi.fn();
+ onModuleGraphChange(cb);
+
+ const args = createStartArgs();
+ vi.mocked(args.options.presets.apply).mockResolvedValue({
+ getIndex: vi.fn().mockRejectedValue(new Error('index failed')),
+ } as never);
+
+ await expect(start(args)).resolves.toBeDefined();
+ await Promise.resolve();
+
+ expect(logger.error).toHaveBeenCalledWith('Failed to initialize Vite change detection');
+ expect(logger.error).toHaveBeenCalledWith(expect.objectContaining({ message: 'index failed' }));
+ expect(cb).toHaveBeenCalledWith({
+ type: 'error',
+ error: expect.objectContaining({ message: 'index failed' }),
+ });
+ expect(vi.getTimerCount()).toBe(0);
+ });
+
+ it('logs polling failures and clears the interval', async () => {
+ const cb = vi.fn();
+ onModuleGraphChange(cb);
+ fakeViteServer.moduleGraph.fileToModulesMap = createFileToModulesMap([
+ '/src/Button.tsx',
+ new Set([createViteModuleNode('/src/Button.tsx')]),
+ ]);
+ fakeViteServer.waitForRequestsIdle = vi.fn().mockRejectedValue(new Error('idle failed'));
+
+ await start(createStartArgs(['/src/Button.tsx']));
+ await vi.advanceTimersByTimeAsync(1000);
+
+ expect(logger.error).toHaveBeenCalledWith('Failed to complete Vite change detection startup');
+ expect(logger.error).toHaveBeenCalledWith(expect.objectContaining({ message: 'idle failed' }));
+ expect(cb).toHaveBeenCalledWith({
+ type: 'error',
+ error: expect.objectContaining({ message: 'idle failed' }),
+ });
+ expect(vi.getTimerCount()).toBe(0);
+ expect(fakeViteServer.watcher.listenerCount('all')).toBe(0);
+ });
+
+ it('notifies listeners when module graph startup times out', async () => {
+ const cb = vi.fn();
+ onModuleGraphChange(cb);
+
+ await start(createStartArgs(['/src/Button.tsx']));
+ await vi.advanceTimersByTimeAsync(31_000);
+
+ expect(logger.error).toHaveBeenCalledWith('Failed to complete Vite change detection startup');
+ expect(cb).toHaveBeenCalledWith({
+ type: 'unavailable',
+ reason: 'Timed out while waiting for the Vite module graph to initialize',
+ error: expect.objectContaining({
+ message: 'Timed out while waiting for the Vite module graph to initialize',
+ }),
+ });
+ expect(vi.getTimerCount()).toBe(0);
+ });
+});
diff --git a/code/builders/builder-vite/src/index.ts b/code/builders/builder-vite/src/index.ts
index f447e76b0419..30f218613ba5 100644
--- a/code/builders/builder-vite/src/index.ts
+++ b/code/builders/builder-vite/src/index.ts
@@ -2,19 +2,31 @@
import { readFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
-import { NoStatsForViteDevError } from 'storybook/internal/server-errors';
-import type { Middleware, Options } from 'storybook/internal/types';
+import { logger } from 'storybook/internal/node-logger';
+import {
+ NoStatsForViteDevError,
+ ViteModuleGraphSubscriptionError,
+} from 'storybook/internal/server-errors';
+import type { StoryIndexGenerator } from 'storybook/internal/core-server';
+import type {
+ Builder,
+ Middleware,
+ ModuleGraph,
+ ModuleGraphChangeEvent,
+ Options,
+} from 'storybook/internal/types';
import type { ViteDevServer } from 'vite';
-import { build as viteBuild } from './build';
-import type { ViteBuilder } from './types';
-import { createViteServer } from './vite-server';
+import { build as viteBuild } from './build.ts';
+import type { ViteBuilder } from './types.ts';
+import { createViteServer } from './vite-server.ts';
+import { buildModuleGraph } from './utils/build-module-graph.ts';
-export { withoutVitePlugins } from './utils/without-vite-plugins';
-export { hasVitePlugins } from './utils/has-vite-plugins';
+export { withoutVitePlugins } from './utils/without-vite-plugins.ts';
+export { hasVitePlugins } from './utils/has-vite-plugins.ts';
-export * from './types';
+export * from './types.ts';
function iframeHandler(options: Options, server: ViteDevServer): Middleware {
return async (req, res) => {
@@ -33,22 +45,150 @@ function iframeHandler(options: Options, server: ViteDevServer): Middleware {
}
let server: ViteDevServer;
+const listeners = new Set<(event: ModuleGraphChangeEvent) => void>();
+let debounce: ReturnType | undefined;
+let watcherChangeHandler: (() => void) | undefined;
+let waitForModuleGraph: ReturnType | undefined;
+let moduleGraphRegistrationClosed = false;
+
+function clearModuleGraphPolling(): void {
+ if (waitForModuleGraph) {
+ clearInterval(waitForModuleGraph);
+ waitForModuleGraph = undefined;
+ }
+}
+
+function notifyListeners(moduleGraph: ModuleGraph): void {
+ listeners.forEach((listener) => {
+ listener({ type: 'moduleGraph', moduleGraph });
+ });
+}
+
+function notifyListenersOfStartupFailure(
+ event: Extract
+): void {
+ listeners.forEach((listener) => {
+ listener(event);
+ });
+}
export async function bail(): Promise {
+ if (watcherChangeHandler) {
+ server?.watcher.off('all', watcherChangeHandler);
+ watcherChangeHandler = undefined;
+ }
+
+ clearModuleGraphPolling();
+
+ if (debounce) {
+ clearTimeout(debounce);
+ debounce = undefined;
+ }
+
+ moduleGraphRegistrationClosed = false;
+ listeners.clear();
return server?.close();
}
+export const onModuleGraphChange: NonNullable['onModuleGraphChange']> = (cb) => {
+ if (moduleGraphRegistrationClosed) {
+ throw new ViteModuleGraphSubscriptionError();
+ }
+ listeners.add(cb);
+
+ return () => {
+ listeners.delete(cb);
+ };
+};
+
+const startChangeDetection = async (options: Options) => {
+ const startTime = process.hrtime();
+ const indexGenerator = await options.presets.apply('storyIndexGenerator');
+ const storyIndex = await indexGenerator.getIndex();
+ const importPaths = new Set(Object.values(storyIndex.entries).map((entry) => entry.importPath));
+
+ // Warm up the module graph for all story files
+ await Promise.all(Array.from(importPaths, (importPath) => server.warmupRequest(importPath)));
+
+ // Wait for the module graph to be ready by polling for it to be non-empty
+ waitForModuleGraph = setInterval(() => {
+ void (async () => {
+ try {
+ if (!watcherChangeHandler) {
+ clearModuleGraphPolling();
+ return;
+ }
+
+ if (process.hrtime(startTime)[0] > 30) {
+ clearModuleGraphPolling();
+ const error = new Error(
+ 'Timed out while waiting for the Vite module graph to initialize'
+ );
+ logger.error('Failed to complete Vite change detection startup');
+ logger.error(error);
+ notifyListenersOfStartupFailure({
+ type: 'unavailable',
+ reason: error.message,
+ error,
+ });
+ return;
+ }
+
+ if (server.moduleGraph.fileToModulesMap.size > 0) {
+ clearModuleGraphPolling();
+ await server.waitForRequestsIdle();
+ if (!watcherChangeHandler) {
+ return;
+ }
+
+ server.watcher.on('all', watcherChangeHandler);
+ watcherChangeHandler();
+ }
+ } catch (error) {
+ clearModuleGraphPolling();
+ logger.error('Failed to complete Vite change detection startup');
+ logger.error(error instanceof Error ? error : String(error));
+ notifyListenersOfStartupFailure({
+ type: 'error',
+ error: error instanceof Error ? error : new Error(String(error)),
+ });
+ }
+ })();
+ }, 1000);
+};
+
export const start: ViteBuilder['start'] = async ({
startTime,
options,
router,
server: devServer,
}) => {
+ moduleGraphRegistrationClosed = true;
server = await createViteServer(options as Options, devServer);
router.get('/iframe.html', iframeHandler(options as Options, server));
router.use(server.middlewares);
+ if (listeners.size > 0) {
+ // Debounce handler to prevent multiple callback invocations when multiple files are edited
+ watcherChangeHandler = () => {
+ clearTimeout(debounce);
+ debounce = setTimeout(() => {
+ notifyListeners(buildModuleGraph(server.moduleGraph.fileToModulesMap));
+ }, 100);
+ };
+ // We intentionally don't await this. Cleanup happens in bail().
+ void startChangeDetection(options).catch((error) => {
+ clearModuleGraphPolling();
+ logger.error('Failed to initialize Vite change detection');
+ logger.error(error instanceof Error ? error : String(error));
+ notifyListenersOfStartupFailure({
+ type: 'error',
+ error: error instanceof Error ? error : new Error(String(error)),
+ });
+ });
+ }
+
return {
bail,
stats: {
diff --git a/code/builders/builder-vite/src/plugins/code-generator-plugin.ts b/code/builders/builder-vite/src/plugins/code-generator-plugin.ts
index 91afe7da7f0c..78d31bcf3dab 100644
--- a/code/builders/builder-vite/src/plugins/code-generator-plugin.ts
+++ b/code/builders/builder-vite/src/plugins/code-generator-plugin.ts
@@ -6,17 +6,17 @@ import type { Options } from 'storybook/internal/types';
import type { Plugin } from 'vite';
-import { importMetaResolve } from '../../../../core/src/shared/utils/module';
-import { generateImportFnScriptCode } from '../codegen-importfn-script';
-import { generateModernIframeScriptCode } from '../codegen-modern-iframe-script';
-import { generateAddonSetupCode } from '../codegen-set-addon-channel';
-import { transformIframeHtml } from '../transform-iframe-html';
-import { bundlerOptionsKey, ensureRolldownOptions } from '../utils/vite-features';
+import { importMetaResolve } from '../../../../core/src/shared/utils/module.ts';
+import { generateImportFnScriptCode } from '../codegen-importfn-script.ts';
+import { generateModernIframeScriptCode } from '../codegen-modern-iframe-script.ts';
+import { generateAddonSetupCode } from '../codegen-set-addon-channel.ts';
+import { transformIframeHtml } from '../transform-iframe-html.ts';
+import { bundlerOptionsKey, ensureRolldownOptions } from '../utils/vite-features.ts';
import {
SB_VIRTUAL_FILES,
SB_VIRTUAL_FILE_IDS,
getResolvedVirtualModuleId,
-} from '../virtual-file-names';
+} from '../virtual-file-names.ts';
export function codeGeneratorPlugin(options: Options) {
const iframePath = fileURLToPath(importMetaResolve('@storybook/builder-vite/input/iframe.html'));
diff --git a/code/builders/builder-vite/src/plugins/index.ts b/code/builders/builder-vite/src/plugins/index.ts
index 078886a86b4a..1815cfab952e 100644
--- a/code/builders/builder-vite/src/plugins/index.ts
+++ b/code/builders/builder-vite/src/plugins/index.ts
@@ -1,12 +1,12 @@
// Builder-internal plugins (used by vite-config.ts to assemble the builder's plugin stack)
-export { storybookOptimizeDepsPlugin } from './storybook-optimize-deps-plugin';
-export { storybookEntryPlugin } from './storybook-entry-plugin';
-export { pluginWebpackStats } from './webpack-stats-plugin';
-export type { WebpackStatsPlugin } from './webpack-stats-plugin';
+export { storybookOptimizeDepsPlugin } from './storybook-optimize-deps-plugin.ts';
+export { storybookEntryPlugin } from './storybook-entry-plugin.ts';
+export { pluginWebpackStats } from './webpack-stats-plugin.ts';
+export type { WebpackStatsPlugin } from './webpack-stats-plugin.ts';
// Lower-level plugins re-exported for internal use and tests
-export { injectExportOrderPlugin } from './inject-export-order-plugin';
-export { stripStoryHMRBoundary } from './strip-story-hmr-boundaries';
-export { codeGeneratorPlugin } from './code-generator-plugin';
-export { csfPlugin } from './csf-plugin';
-export { storybookExternalGlobalsPlugin } from './storybook-external-globals-plugin';
+export { injectExportOrderPlugin } from './inject-export-order-plugin.ts';
+export { stripStoryHMRBoundary } from './strip-story-hmr-boundaries.ts';
+export { codeGeneratorPlugin } from './code-generator-plugin.ts';
+export { csfPlugin } from './csf-plugin.ts';
+export { storybookExternalGlobalsPlugin } from './storybook-external-globals-plugin.ts';
diff --git a/code/builders/builder-vite/src/plugins/inject-export-order-plugin.ts b/code/builders/builder-vite/src/plugins/inject-export-order-plugin.ts
index 91091a86811b..9cb2d658c85e 100644
--- a/code/builders/builder-vite/src/plugins/inject-export-order-plugin.ts
+++ b/code/builders/builder-vite/src/plugins/inject-export-order-plugin.ts
@@ -12,29 +12,32 @@ export async function injectExportOrderPlugin() {
name: 'storybook:inject-export-order-plugin',
// This should only run after the typescript has been transpiled
enforce: 'post',
- async transform(code: string, id: string) {
- if (!filter(id)) {
- return undefined;
- }
+ transform: {
+ filter: { id: include },
+ async handler(code: string, id: string) {
+ if (!filter(id)) {
+ return undefined;
+ }
- // TODO: Maybe convert `injectExportOrderPlugin` to function that returns object,
- // and run `await init;` once and then call `parse()` without `await`,
- // instead of calling `await parse()` every time.
- const [, exports] = await parse(code);
+ // TODO: Maybe convert `injectExportOrderPlugin` to function that returns object,
+ // and run `await init;` once and then call `parse()` without `await`,
+ // instead of calling `await parse()` every time.
+ const [, exports] = await parse(code);
- const exportNames = exports.map((e) => code.substring(e.s, e.e));
+ const exportNames = exports.map((e) => code.substring(e.s, e.e));
- if (exportNames.includes('__namedExportsOrder')) {
- // user has defined named exports already
- return undefined;
- }
- const s = new MagicString(code);
- const orderedExports = exportNames.filter((e) => e !== 'default');
- s.append(`;export const __namedExportsOrder = ${JSON.stringify(orderedExports)};`);
- return {
- code: s.toString(),
- map: s.generateMap({ hires: true, source: id }),
- };
+ if (exportNames.includes('__namedExportsOrder')) {
+ // user has defined named exports already
+ return undefined;
+ }
+ const s = new MagicString(code);
+ const orderedExports = exportNames.filter((e) => e !== 'default');
+ s.append(`;export const __namedExportsOrder = ${JSON.stringify(orderedExports)};`);
+ return {
+ code: s.toString(),
+ map: s.generateMap({ hires: true, source: id }),
+ };
+ },
},
} satisfies Plugin;
}
diff --git a/code/builders/builder-vite/src/plugins/storybook-entry-plugin.ts b/code/builders/builder-vite/src/plugins/storybook-entry-plugin.ts
index f4c0ba3d38d3..437d3456f888 100644
--- a/code/builders/builder-vite/src/plugins/storybook-entry-plugin.ts
+++ b/code/builders/builder-vite/src/plugins/storybook-entry-plugin.ts
@@ -2,9 +2,9 @@ import type { Options } from 'storybook/internal/types';
import type { Plugin } from 'vite';
-import { codeGeneratorPlugin } from './code-generator-plugin';
-import { injectExportOrderPlugin } from './inject-export-order-plugin';
-import { stripStoryHMRBoundary } from './strip-story-hmr-boundaries';
+import { codeGeneratorPlugin } from './code-generator-plugin.ts';
+import { injectExportOrderPlugin } from './inject-export-order-plugin.ts';
+import { stripStoryHMRBoundary } from './strip-story-hmr-boundaries.ts';
/**
* A composite Vite plugin that manages the generation and injection of virtual entry points for
diff --git a/code/builders/builder-vite/src/plugins/storybook-external-globals-plugin.test.ts b/code/builders/builder-vite/src/plugins/storybook-external-globals-plugin.test.ts
index 4b5c001f0e3d..ceaae2475df7 100644
--- a/code/builders/builder-vite/src/plugins/storybook-external-globals-plugin.test.ts
+++ b/code/builders/builder-vite/src/plugins/storybook-external-globals-plugin.test.ts
@@ -1,6 +1,6 @@
import { expect, it } from 'vitest';
-import { rewriteImport } from './storybook-external-globals-plugin';
+import { rewriteImport } from './storybook-external-globals-plugin.ts';
const packageName = '@storybook/package';
const globals = { [packageName]: '_STORYBOOK_PACKAGE_' };
diff --git a/code/builders/builder-vite/src/plugins/storybook-external-globals-plugin.ts b/code/builders/builder-vite/src/plugins/storybook-external-globals-plugin.ts
index 54bb15a28c96..ccc15137aa13 100644
--- a/code/builders/builder-vite/src/plugins/storybook-external-globals-plugin.ts
+++ b/code/builders/builder-vite/src/plugins/storybook-external-globals-plugin.ts
@@ -55,6 +55,9 @@ export async function storybookExternalGlobalsPlugin(options: Options): Promise<
await init;
const { mergeAlias } = await import('vite');
+ const globalsList = Object.keys(externals);
+ const globalsCodeFilter = new RegExp(globalsList.map(escapeKeys).join('|'));
+
return {
name: 'storybook:external-globals-plugin',
enforce: 'post',
@@ -88,28 +91,29 @@ export async function storybookExternalGlobalsPlugin(options: Options): Promise<
};
},
// Replace imports with variables destructured from global scope
- async transform(code: string, id: string) {
- const globalsList = Object.keys(externals);
-
- if (globalsList.every((glob) => !code.includes(glob))) {
- return undefined;
- }
-
- const [imports] = parse(code);
- const src = new MagicString(code);
- imports.forEach(({ n: path, ss: startPosition, se: endPosition }) => {
- const packageName = path;
- if (packageName && globalsList.includes(packageName)) {
- const importStatement = src.slice(startPosition, endPosition);
- const transformedImport = rewriteImport(importStatement, externals, packageName);
- src.update(startPosition, endPosition, transformedImport);
+ transform: {
+ filter: { code: globalsCodeFilter },
+ async handler(code: string, id: string) {
+ if (globalsList.every((glob) => !code.includes(glob))) {
+ return undefined;
}
- });
- return {
- code: src.toString(),
- map: null,
- };
+ const [imports] = parse(code);
+ const src = new MagicString(code);
+ imports.forEach(({ n: path, ss: startPosition, se: endPosition }) => {
+ const packageName = path;
+ if (packageName && globalsList.includes(packageName)) {
+ const importStatement = src.slice(startPosition, endPosition);
+ const transformedImport = rewriteImport(importStatement, externals, packageName);
+ src.update(startPosition, endPosition, transformedImport);
+ }
+ });
+
+ return {
+ code: src.toString(),
+ map: null,
+ };
+ },
},
} satisfies Plugin;
}
diff --git a/code/builders/builder-vite/src/plugins/storybook-optimize-deps-plugin.test.ts b/code/builders/builder-vite/src/plugins/storybook-optimize-deps-plugin.test.ts
index b9f1062943ba..48ff1e107f8b 100644
--- a/code/builders/builder-vite/src/plugins/storybook-optimize-deps-plugin.test.ts
+++ b/code/builders/builder-vite/src/plugins/storybook-optimize-deps-plugin.test.ts
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';
-import { escapeGlobPath, getMockRedirectIncludeEntries } from './storybook-optimize-deps-plugin';
+import { escapeGlobPath, getMockRedirectIncludeEntries } from './storybook-optimize-deps-plugin.ts';
describe('escapeGlobPath', () => {
it('should not modify a plain path without special characters', () => {
diff --git a/code/builders/builder-vite/src/plugins/storybook-optimize-deps-plugin.ts b/code/builders/builder-vite/src/plugins/storybook-optimize-deps-plugin.ts
index cf3e2e8cc3f0..26517acb20e1 100644
--- a/code/builders/builder-vite/src/plugins/storybook-optimize-deps-plugin.ts
+++ b/code/builders/builder-vite/src/plugins/storybook-optimize-deps-plugin.ts
@@ -6,8 +6,8 @@ import type { Options, PreviewAnnotation, StoryIndex } from 'storybook/internal/
import { resolve } from 'pathe';
import { type Plugin } from 'vite';
-import { processPreviewAnnotation } from '../utils/process-preview-annotation';
-import { getUniqueImportPaths } from '../utils/unique-import-paths';
+import { processPreviewAnnotation } from '../utils/process-preview-annotation.ts';
+import { getUniqueImportPaths } from '../utils/unique-import-paths.ts';
/**
* Escapes special glob characters in a file path so Vite's dep optimizer treats it as a literal
diff --git a/code/builders/builder-vite/src/plugins/storybook-project-annotations-plugin.ts b/code/builders/builder-vite/src/plugins/storybook-project-annotations-plugin.ts
index beb879493f64..31c58d2588b0 100644
--- a/code/builders/builder-vite/src/plugins/storybook-project-annotations-plugin.ts
+++ b/code/builders/builder-vite/src/plugins/storybook-project-annotations-plugin.ts
@@ -2,8 +2,8 @@ import type { Options } from 'storybook/internal/types';
import type { Plugin } from 'vite';
-import { generateProjectAnnotationsCode } from '../codegen-project-annotations';
-import { getResolvedVirtualModuleId } from '../virtual-file-names';
+import { generateProjectAnnotationsCode } from '../codegen-project-annotations.ts';
+import { getResolvedVirtualModuleId } from '../virtual-file-names.ts';
export const VIRTUAL_ID = 'virtual:/@storybook/builder-vite/project-annotations.js';
const RESOLVED_VIRTUAL_ID = getResolvedVirtualModuleId(VIRTUAL_ID);
diff --git a/code/builders/builder-vite/src/plugins/storybook-runtime-plugin.ts b/code/builders/builder-vite/src/plugins/storybook-runtime-plugin.ts
index a60d66eace7e..f810f539514f 100644
--- a/code/builders/builder-vite/src/plugins/storybook-runtime-plugin.ts
+++ b/code/builders/builder-vite/src/plugins/storybook-runtime-plugin.ts
@@ -3,7 +3,7 @@ import type { Options } from 'storybook/internal/types';
import type { Plugin } from 'vite';
-import { stringifyProcessEnvs } from '../envs';
+import { stringifyProcessEnvs } from '../envs.ts';
export interface StorybookRuntimePluginOptions {
externals: Record;
diff --git a/code/builders/builder-vite/src/plugins/strip-story-hmr-boundaries.ts b/code/builders/builder-vite/src/plugins/strip-story-hmr-boundaries.ts
index baa433d1afed..50ab71411071 100644
--- a/code/builders/builder-vite/src/plugins/strip-story-hmr-boundaries.ts
+++ b/code/builders/builder-vite/src/plugins/strip-story-hmr-boundaries.ts
@@ -9,22 +9,26 @@ import type { Plugin } from 'vite';
export async function stripStoryHMRBoundary() {
const { createFilter } = await import('vite');
- const filter = createFilter(/\.stories\.(tsx?|jsx?|svelte|vue)$/);
+ const include = /\.stories\.(tsx?|jsx?|svelte|vue)$/;
+ const filter = createFilter(include);
return {
name: 'storybook:strip-hmr-boundary-plugin',
enforce: 'post',
- async transform(src, id) {
- if (!filter(id)) {
- return undefined;
- }
+ transform: {
+ filter: { id: include },
+ async handler(src, id) {
+ if (!filter(id)) {
+ return undefined;
+ }
- const s = new MagicString(src);
- s.replace(/import\.meta\.hot\.accept\w*/, '(function hmrBoundaryNoop(){})');
+ const s = new MagicString(src);
+ s.replace(/import\.meta\.hot\.accept\w*/, '(function hmrBoundaryNoop(){})');
- return {
- code: s.toString(),
- map: s.generateMap({ hires: true, source: id }),
- };
+ return {
+ code: s.toString(),
+ map: s.generateMap({ hires: true, source: id }),
+ };
+ },
},
} satisfies Plugin;
}
diff --git a/code/builders/builder-vite/src/plugins/vite-inject-mocker/plugin.ts b/code/builders/builder-vite/src/plugins/vite-inject-mocker/plugin.ts
index 79dfaa64410f..61ff78434487 100644
--- a/code/builders/builder-vite/src/plugins/vite-inject-mocker/plugin.ts
+++ b/code/builders/builder-vite/src/plugins/vite-inject-mocker/plugin.ts
@@ -41,11 +41,14 @@ export const viteInjectMockerRuntime = (options: {
});
}
},
- resolveId(source) {
- if (source === ENTRY_PATH) {
- return mockerRuntimePath;
- }
- return undefined;
+ resolveId: {
+ filter: { id: /\/vite-inject-mocker-entry\.js/ },
+ handler(source) {
+ if (source === ENTRY_PATH) {
+ return mockerRuntimePath;
+ }
+ return undefined;
+ },
},
transformIndexHtml(html: string) {
const headTag = html.match(/]*>/);
diff --git a/code/builders/builder-vite/src/plugins/vite-mock/plugin.ts b/code/builders/builder-vite/src/plugins/vite-mock/plugin.ts
index 5f60995c05b0..2bfd141e4709 100644
--- a/code/builders/builder-vite/src/plugins/vite-mock/plugin.ts
+++ b/code/builders/builder-vite/src/plugins/vite-mock/plugin.ts
@@ -14,7 +14,7 @@ import type { CoreConfig } from 'storybook/internal/types';
import { normalize } from 'pathe';
import type { Plugin, ResolvedConfig } from 'vite';
-import { type MockCall, getCleanId, invalidateAllRelatedModules } from './utils';
+import { type MockCall, getCleanId, invalidateAllRelatedModules } from './utils.ts';
export interface MockPluginOptions {
/** The absolute path to the preview.tsx file where mocks are defined. */
@@ -162,16 +162,19 @@ export function viteMockPlugin(options: MockPluginOptions): Plugin[] {
},
{
name: 'storybook:mock-loader-preview',
- transform(code, id) {
- if (id === normalizedPreviewConfigPath) {
- try {
- return rewriteSbMockImportCalls(code);
- } catch (e) {
- logger.debug(`Could not transform sb.mock(import(...)) calls in ${id}: ${e}`);
- return null;
+ transform: {
+ filter: { id: normalizedPreviewConfigPath },
+ handler(code, id) {
+ if (id === normalizedPreviewConfigPath) {
+ try {
+ return rewriteSbMockImportCalls(code);
+ } catch (e) {
+ logger.debug(`Could not transform sb.mock(import(...)) calls in ${id}: ${e}`);
+ return null;
+ }
}
- }
- return null;
+ return null;
+ },
},
},
];
diff --git a/code/builders/builder-vite/src/plugins/webpack-stats-plugin.ts b/code/builders/builder-vite/src/plugins/webpack-stats-plugin.ts
index 0f6c1c441be9..6c0c430feee7 100644
--- a/code/builders/builder-vite/src/plugins/webpack-stats-plugin.ts
+++ b/code/builders/builder-vite/src/plugins/webpack-stats-plugin.ts
@@ -11,7 +11,7 @@ import {
SB_VIRTUAL_FILES,
getOriginalVirtualModuleId,
getResolvedVirtualModuleId,
-} from '../virtual-file-names';
+} from '../virtual-file-names.ts';
/*
* Reason, Module are copied from chromatic types
diff --git a/code/builders/builder-vite/src/preset.ts b/code/builders/builder-vite/src/preset.ts
index cb4a772559aa..caca1a993d99 100644
--- a/code/builders/builder-vite/src/preset.ts
+++ b/code/builders/builder-vite/src/preset.ts
@@ -3,12 +3,12 @@ import type { Options } from 'storybook/internal/types';
import type { PluginOption } from 'vite';
-import { storybookConfigPlugin } from './plugins/storybook-config-plugin';
-import { storybookOptimizeDepsPlugin } from './plugins/storybook-optimize-deps-plugin';
-import { storybookProjectAnnotationsPlugin } from './plugins/storybook-project-annotations-plugin';
-import { storybookSanitizeEnvs } from './plugins/storybook-runtime-plugin';
-import { viteInjectMockerRuntime } from './plugins/vite-inject-mocker/plugin';
-import { viteMockPlugin } from './plugins/vite-mock/plugin';
+import { storybookConfigPlugin } from './plugins/storybook-config-plugin.ts';
+import { storybookOptimizeDepsPlugin } from './plugins/storybook-optimize-deps-plugin.ts';
+import { storybookProjectAnnotationsPlugin } from './plugins/storybook-project-annotations-plugin.ts';
+import { storybookSanitizeEnvs } from './plugins/storybook-runtime-plugin.ts';
+import { viteInjectMockerRuntime } from './plugins/vite-inject-mocker/plugin.ts';
+import { viteMockPlugin } from './plugins/vite-mock/plugin.ts';
export const optimizeViteDeps: string[] = ['storybook/internal/preview/runtime'];
diff --git a/code/builders/builder-vite/src/transform-iframe-html.ts b/code/builders/builder-vite/src/transform-iframe-html.ts
index 2e7a6750dca1..697d69da10b8 100644
--- a/code/builders/builder-vite/src/transform-iframe-html.ts
+++ b/code/builders/builder-vite/src/transform-iframe-html.ts
@@ -1,7 +1,7 @@
import { normalizeStories } from 'storybook/internal/common';
import type { DocsOptions, Options, TagsOptions } from 'storybook/internal/types';
-import { SB_VIRTUAL_FILES } from './virtual-file-names';
+import { SB_VIRTUAL_FILES } from './virtual-file-names.ts';
export type PreviewHtml = string | undefined;
diff --git a/code/builders/builder-vite/src/utils/build-module-graph.test.ts b/code/builders/builder-vite/src/utils/build-module-graph.test.ts
new file mode 100644
index 000000000000..9545b7b434e8
--- /dev/null
+++ b/code/builders/builder-vite/src/utils/build-module-graph.test.ts
@@ -0,0 +1,153 @@
+import { describe, expect, it, vi } from 'vitest';
+
+import type { ModuleNode as StorybookModuleNode } from 'storybook/internal/types';
+import type { ViteDevServer } from 'vite';
+
+import { buildModuleGraph } from './build-module-graph.ts';
+
+vi.mock('./vite-server', () => ({
+ createViteServer: vi.fn(),
+}));
+
+type ViteModuleNodeLike = {
+ file: string | null;
+ type: StorybookModuleNode['type'];
+ importers: Set;
+ importedModules: Set;
+};
+
+function createViteModuleNode(
+ file: string | null,
+ type: StorybookModuleNode['type'] = 'js'
+): ViteModuleNodeLike {
+ return {
+ file,
+ type,
+ importers: new Set(),
+ importedModules: new Set(),
+ };
+}
+
+function createFileToModulesMap(...entries: Array<[string, Set]>) {
+ return new Map(entries) as ViteDevServer['moduleGraph']['fileToModulesMap'];
+}
+
+function getFirstNode(
+ file: string,
+ moduleGraph: ReturnType
+): StorybookModuleNode {
+ const moduleNode = moduleGraph.get(file)?.values().next().value;
+ if (!moduleNode) {
+ throw new Error(`Expected module node for ${file}`);
+ }
+ return moduleNode;
+}
+
+describe('buildModuleGraph', () => {
+ it('converts vite module nodes into the shared module graph shape', () => {
+ const entry = createViteModuleNode('/src/entry.ts');
+ const component = createViteModuleNode('/src/component.ts');
+ const styles = createViteModuleNode('/src/component.css', 'css');
+
+ entry.importedModules.add(component);
+ component.importers.add(entry);
+ component.importedModules.add(styles);
+ styles.importers.add(component);
+
+ const moduleGraph = buildModuleGraph(
+ createFileToModulesMap(
+ ['/src/entry.ts', new Set([entry])],
+ ['/src/component.ts', new Set([component])],
+ ['/src/component.css', new Set([styles])]
+ )
+ );
+
+ const entryNode = getFirstNode('/src/entry.ts', moduleGraph);
+ const componentNode = getFirstNode('/src/component.ts', moduleGraph);
+ const styleNode = getFirstNode('/src/component.css', moduleGraph);
+
+ expect(entryNode.file).toBe('/src/entry.ts');
+ expect(componentNode.type).toBe('js');
+ expect(styleNode.type).toBe('css');
+
+ expect(entryNode.importedModules).toEqual(new Set([componentNode]));
+ expect(componentNode.importers).toEqual(new Set([entryNode]));
+ expect(componentNode.importedModules).toEqual(new Set([styleNode]));
+ expect(styleNode.importers).toEqual(new Set([componentNode]));
+ });
+
+ it('reuses the same converted node identity across relationships', () => {
+ const shared = createViteModuleNode('/src/shared.ts');
+ const importerA = createViteModuleNode('/src/a.ts');
+ const importerB = createViteModuleNode('/src/b.ts');
+
+ importerA.importedModules.add(shared);
+ importerB.importedModules.add(shared);
+ shared.importers.add(importerA);
+ shared.importers.add(importerB);
+
+ const moduleGraph = buildModuleGraph(
+ createFileToModulesMap(
+ ['/src/shared.ts', new Set([shared])],
+ ['/src/a.ts', new Set([importerA])],
+ ['/src/b.ts', new Set([importerB])]
+ )
+ );
+
+ const sharedNode = getFirstNode('/src/shared.ts', moduleGraph);
+ const importerANode = getFirstNode('/src/a.ts', moduleGraph);
+ const importerBNode = getFirstNode('/src/b.ts', moduleGraph);
+
+ expect(importerANode.importedModules.has(sharedNode)).toBe(true);
+ expect(importerBNode.importedModules.has(sharedNode)).toBe(true);
+ expect(sharedNode.importers).toEqual(new Set([importerANode, importerBNode]));
+ });
+
+ it('skips related vite module nodes without a file', () => {
+ const entry = createViteModuleNode('/src/entry.ts');
+ const virtualModule = createViteModuleNode(null);
+
+ entry.importedModules.add(virtualModule);
+ virtualModule.importers.add(entry);
+
+ const moduleGraph = buildModuleGraph(
+ createFileToModulesMap(['/src/entry.ts', new Set([entry])])
+ );
+ const entryNode = getFirstNode('/src/entry.ts', moduleGraph);
+
+ expect(moduleGraph.size).toBe(1);
+ expect(entryNode.importedModules.size).toBe(0);
+ });
+
+ it('preserves edges for related vite module nodes discovered before their file path is known', () => {
+ const entry = createViteModuleNode('/src/entry.ts');
+ const component = createViteModuleNode(null);
+
+ entry.importedModules.add(component);
+ component.importers.add(entry);
+
+ const moduleGraph = buildModuleGraph(
+ createFileToModulesMap(
+ ['/src/entry.ts', new Set([entry])],
+ ['/src/component.ts', new Set([component])]
+ )
+ );
+
+ const entryNode = getFirstNode('/src/entry.ts', moduleGraph);
+ const componentNode = getFirstNode('/src/component.ts', moduleGraph);
+
+ expect(entryNode.importedModules).toEqual(new Set([componentNode]));
+ expect(componentNode.importers).toEqual(new Set([entryNode]));
+ });
+
+ it('keeps multiple module identities for the same file', () => {
+ const clientModule = createViteModuleNode('/src/shared.ts');
+ const ssrModule = createViteModuleNode('/src/shared.ts');
+
+ const moduleGraph = buildModuleGraph(
+ createFileToModulesMap(['/src/shared.ts', new Set([clientModule, ssrModule])])
+ );
+
+ expect(moduleGraph.get('/src/shared.ts')?.size).toBe(2);
+ });
+});
diff --git a/code/builders/builder-vite/src/utils/build-module-graph.ts b/code/builders/builder-vite/src/utils/build-module-graph.ts
new file mode 100644
index 000000000000..4d14235bd88e
--- /dev/null
+++ b/code/builders/builder-vite/src/utils/build-module-graph.ts
@@ -0,0 +1,82 @@
+import type { ModuleGraph, ModuleNode } from 'storybook/internal/types';
+
+import type { ViteDevServer, ModuleNode as ViteModuleNode } from 'vite';
+
+export function buildModuleGraph(
+ fileToModulesMap: ViteDevServer['moduleGraph']['fileToModulesMap']
+): ModuleGraph {
+ const moduleGraph: ModuleGraph = new Map();
+ const moduleNodeMap = new WeakMap