diff --git a/CHANGELOG.md b/CHANGELOG.md
index 14dc6a61ecc7..256196a7e79a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,9 @@
+## 10.2.12
+
+- Core: Sanitize inputs for save from controls - [#33868](https://github.com/storybookjs/storybook/pull/33868), thanks @valentinpalkovic!
+- Telemetry: Add project age - [#33910](https://github.com/storybookjs/storybook/pull/33910), thanks @shilman!
+- Webpack: Improve performance of module-mocking plugins - [#33169](https://github.com/storybookjs/storybook/pull/33169), thanks @valentinpalkovic!
+
## 10.2.11
- Addon-Vitest: Fix postinstall a11y installation - [#33888](https://github.com/storybookjs/storybook/pull/33888), thanks @valentinpalkovic!
diff --git a/CHANGELOG.prerelease.md b/CHANGELOG.prerelease.md
index 80d0e45f1981..f6df38746483 100644
--- a/CHANGELOG.prerelease.md
+++ b/CHANGELOG.prerelease.md
@@ -1,3 +1,11 @@
+## 10.3.0-alpha.11
+
+- Addon Pseudo-states: Process all nested css rules - [#33605](https://github.com/storybookjs/storybook/pull/33605), thanks @hpohlmeyer!
+- Core: Avoid hanging when inferring args for recursive calls on DOM elemens - [#33922](https://github.com/storybookjs/storybook/pull/33922), thanks @valentinpalkovic!
+- Core: Sanitize inputs for save from controls - [#33868](https://github.com/storybookjs/storybook/pull/33868), thanks @valentinpalkovic!
+- Telemetry: Add project age - [#33910](https://github.com/storybookjs/storybook/pull/33910), thanks @shilman!
+- Viewport: Prioritize story viewport globals and avoid user-global pollution - [#33849](https://github.com/storybookjs/storybook/pull/33849), thanks @ia319!
+
## 10.3.0-alpha.10
- Addon-Vitest: Fix postinstall a11y installation - [#33888](https://github.com/storybookjs/storybook/pull/33888), thanks @valentinpalkovic!
diff --git a/code/addons/pseudo-states/src/preview/rewriteStyleSheet.ts b/code/addons/pseudo-states/src/preview/rewriteStyleSheet.ts
index e41a7a62ea58..d9eedaef0208 100644
--- a/code/addons/pseudo-states/src/preview/rewriteStyleSheet.ts
+++ b/code/addons/pseudo-states/src/preview/rewriteStyleSheet.ts
@@ -198,24 +198,28 @@ const rewriteRuleContainer = (
// @ts-expect-error We're adding this nonstandard property below
numRewritten = cssRule.__pseudoStatesRewrittenCount;
} else {
- if ('cssRules' in cssRule && (cssRule.cssRules as CSSRuleList).length) {
- numRewritten = rewriteRuleContainer(
- cssRule as CSSGroupingRule,
- rewriteLimit - count,
- forShadowDOM
- );
- } else {
- if (!('selectorText' in cssRule)) {
- continue;
- }
- const styleRule = cssRule as CSSStyleRule;
+ let styleRule = cssRule as CSSStyleRule;
+
+ // Modify the rule, if it contains a pseudo state
+ if ('selectorText' in styleRule) {
if (matchOne.test(styleRule.selectorText)) {
const newRule = rewriteRule(styleRule, forShadowDOM);
ruleContainer.deleteRule(index);
ruleContainer.insertRule(newRule, index);
+ styleRule = ruleContainer.cssRules[index] as CSSStyleRule;
numRewritten = 1;
}
}
+
+ // If it has nested rules, check them as well
+ if ('cssRules' in styleRule && (styleRule.cssRules as CSSRuleList).length) {
+ numRewritten = rewriteRuleContainer(
+ styleRule as CSSGroupingRule,
+ rewriteLimit - count,
+ forShadowDOM
+ );
+ }
+
// @ts-expect-error We're adding this nonstandard property
cssRule.__processed = true;
// @ts-expect-error We're adding this nonstandard property
diff --git a/code/addons/pseudo-states/src/stories/NestedRules.stories.tsx b/code/addons/pseudo-states/src/stories/NestedRules.stories.tsx
new file mode 100644
index 000000000000..3b52327a73f0
--- /dev/null
+++ b/code/addons/pseudo-states/src/stories/NestedRules.stories.tsx
@@ -0,0 +1,25 @@
+import type { Meta, StoryObj } from '@storybook/react-vite';
+
+import { Button } from './NestedRules';
+
+const meta = {
+ title: 'NestedRules',
+ component: Button,
+ render: (args, context) => ,
+} satisfies Meta;
+
+export default meta;
+
+type Story = StoryObj;
+
+export const NestedHover: Story = {
+ parameters: {
+ pseudo: { focusVisible: true },
+ },
+ // TODO: Use this test once the pseudostates addon uses the beforeEach API
+ // play: async ({ canvas }) => {
+ // const button = canvas.getByRole('button')!;
+ // await expect(getComputedStyle(button).textDecorationLine).toBe('underline');
+ // await expect(getComputedStyle(button).textDecorationColor).toBe('rgb(255, 0, 0)');
+ // },
+};
diff --git a/code/addons/pseudo-states/src/stories/NestedRules.tsx b/code/addons/pseudo-states/src/stories/NestedRules.tsx
new file mode 100644
index 000000000000..41a047caca9c
--- /dev/null
+++ b/code/addons/pseudo-states/src/stories/NestedRules.tsx
@@ -0,0 +1,7 @@
+import React from 'react';
+
+import './nested.css';
+
+export const Button = (props: React.ButtonHTMLAttributes) => (
+
+);
diff --git a/code/addons/pseudo-states/src/stories/nested.css b/code/addons/pseudo-states/src/stories/nested.css
new file mode 100644
index 000000000000..d6c3e9685330
--- /dev/null
+++ b/code/addons/pseudo-states/src/stories/nested.css
@@ -0,0 +1,21 @@
+button {
+ display: inline-block;
+ cursor: pointer;
+ border: 0;
+ border-radius: 3em;
+ background-color: #1ea7fd;
+ padding: 11px 20px;
+ color: white;
+ font-weight: 700;
+ font-size: 14px;
+ line-height: 1;
+ font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
+}
+
+.nested-focus-visible {
+ &:focus-visible {
+ @supports (color: color-mix(in lab, red, red)) {
+ text-decoration: underline red;
+ }
+ }
+}
diff --git a/code/core/src/core-server/utils/get-new-story-file.test.ts b/code/core/src/core-server/utils/get-new-story-file.test.ts
index 86f77e93ad9f..806b44a94cb1 100644
--- a/code/core/src/core-server/utils/get-new-story-file.test.ts
+++ b/code/core/src/core-server/utils/get-new-story-file.test.ts
@@ -171,6 +171,42 @@ describe('get-new-story-file', () => {
expect(storyFileContent).not.toContain(STORYBOOK_FN_PLACEHOLDER);
});
+ it('should prevent XSS by escaping special characters in the component file name', async () => {
+ const { storyFileContent } = await getNewStoryFile(
+ {
+ componentFilePath: "src/stories/Button';alert(document.domain);var a='.tsx",
+ componentExportName: 'Button',
+ componentIsDefaultExport: true,
+ componentExportCount: 1,
+ },
+ {
+ presets: {
+ apply: (val: string) => {
+ if (val === 'framework') {
+ return Promise.resolve('@storybook/nextjs');
+ }
+ },
+ },
+ } as unknown as Options
+ );
+
+ expect(storyFileContent).toMatchInlineSnapshot(`
+ "import type { Meta, StoryObj } from '@storybook/nextjs';
+
+ import Buttonalert(documentDomain);varA=\\' from './Button\\';alert(document.domain);var a=\\'';
+
+ const meta = {
+ component: Buttonalert(documentDomain);varA=\\',
+ } satisfies Meta;
+
+ export default meta;
+
+ type Story = StoryObj;
+
+ export const Default: Story = {};"
+`);
+ });
+
it('should create a new story file (CSF factory)', async () => {
const configDir = join(__dirname, '.storybook');
const previewConfigPath = join(configDir, 'preview.ts');
diff --git a/code/core/src/core-server/utils/get-new-story-file.ts b/code/core/src/core-server/utils/get-new-story-file.ts
index 5abbeb10082c..ea48a9d68cd6 100644
--- a/code/core/src/core-server/utils/get-new-story-file.ts
+++ b/code/core/src/core-server/utils/get-new-story-file.ts
@@ -26,6 +26,7 @@ import {
import { getCsfFactoryTemplateForNewStoryFile } from './new-story-templates/csf-factory-template';
import { getJavaScriptTemplateForNewStoryFile } from './new-story-templates/javascript';
import { getTypeScriptTemplateForNewStoryFile } from './new-story-templates/typescript';
+import { escapeForTemplate } from './safeString';
export async function getNewStoryFile(
{
@@ -41,7 +42,7 @@ export async function getNewStoryFile(
const base = basename(componentFilePath);
const extension = extname(componentFilePath);
- const basenameWithoutExtension = base.replace(extension, '');
+ const basenameWithoutExtension = escapeForTemplate(base.replace(extension, ''));
const dir = dirname(componentFilePath);
const { storyFileName, isTypescript, storyFileExtension } = getStoryMetadata(componentFilePath);
@@ -98,7 +99,9 @@ export async function getNewStoryFile(
const storyFilePath = join(getProjectRoot(), dir);
const relPath = relative(storyFilePath, previewConfigPath);
const pathWithoutExt = relPath.replace(/\.(ts|js|mts|cts|tsx|jsx)$/, '');
- previewImportPath = pathWithoutExt.startsWith('.') ? pathWithoutExt : `./${pathWithoutExt}`;
+ previewImportPath = escapeForTemplate(
+ pathWithoutExt.startsWith('.') ? pathWithoutExt : `./${pathWithoutExt}`
+ );
}
}
diff --git a/code/core/src/core-server/utils/safeString.test.ts b/code/core/src/core-server/utils/safeString.test.ts
new file mode 100644
index 000000000000..0eb4559bb027
--- /dev/null
+++ b/code/core/src/core-server/utils/safeString.test.ts
@@ -0,0 +1,36 @@
+import { describe, expect, it } from 'vitest';
+
+import { escapeForTemplate } from './safeString';
+
+describe('safeString', () => {
+ describe('escapeForTemplate', () => {
+ it('should escape backticks in template strings', () => {
+ expect(escapeForTemplate('button`s.tsx')).toMatchInlineSnapshot('"button\\`s.tsx"');
+ });
+
+ it('should escape dollar signs for template expressions', () => {
+ expect(escapeForTemplate('button$file.tsx')).toMatchInlineSnapshot('"button\\$file.tsx"');
+ });
+
+ it('should escape backslashes', () => {
+ expect(escapeForTemplate('button\\file.tsx')).toMatchInlineSnapshot('"button\\\\file.tsx"');
+ });
+
+ it('should escape quotes', () => {
+ expect(escapeForTemplate("button's.tsx")).toMatchInlineSnapshot(`"button\\'s.tsx"`);
+ expect(escapeForTemplate('button"s.tsx')).toMatchInlineSnapshot(`"button\\"s.tsx"`);
+ });
+
+ it('should handle multiple special characters', () => {
+ expect(escapeForTemplate('button`${file}\\path.tsx')).toMatchInlineSnapshot(
+ `"button\\\`\\\${file}\\\\path.tsx"`
+ );
+ });
+
+ it('should preserve normal file paths', () => {
+ expect(escapeForTemplate('./src/components/Button.tsx')).toMatchInlineSnapshot(
+ '"./src/components/Button.tsx"'
+ );
+ });
+ });
+});
diff --git a/code/core/src/core-server/utils/safeString.ts b/code/core/src/core-server/utils/safeString.ts
new file mode 100644
index 000000000000..c4bf8025e4dc
--- /dev/null
+++ b/code/core/src/core-server/utils/safeString.ts
@@ -0,0 +1,18 @@
+/**
+ * Escape special characters in a string for safe use within template literals in generated code.
+ * This escapes backticks and template expression delimiters.
+ *
+ * @example
+ *
+ * ```ts
+ * const fileName = "button's.tsx";
+ * const template = `import Button from './${escapeForTemplate(fileName)}'`;
+ * // Results in: import Button from './button\\'s.tsx'
+ * ```
+ */
+export function escapeForTemplate(str: string): string {
+ return str
+ .replace(/\\/g, '\\\\') // Escape backslashes first
+ .replace(/(['"$`])/g, '\\$&') // Then escape quotes, dollar signs, and backticks
+ .replace(/[\n\r]/g, '\\$&'); // Then newlines
+}
diff --git a/code/core/src/preview-api/modules/store/inferArgTypes.ts b/code/core/src/preview-api/modules/store/inferArgTypes.ts
index 04637f18ac59..f4e69dd6b3c8 100644
--- a/code/core/src/preview-api/modules/store/inferArgTypes.ts
+++ b/code/core/src/preview-api/modules/store/inferArgTypes.ts
@@ -6,7 +6,12 @@ import { dedent } from 'ts-dedent';
import { combineParameters } from './parameters';
-const inferType = (value: any, name: string, visited: Set): SBType => {
+const inferType = (
+ value: any,
+ name: string,
+ visited: Set,
+ cache: Map
+): SBType => {
const type = typeof value;
switch (type) {
case 'boolean':
@@ -19,6 +24,12 @@ const inferType = (value: any, name: string, visited: Set): SBType => {
break;
}
if (value) {
+ // Check cache first for previously computed results
+ if (cache.has(value)) {
+ return cache.get(value)!;
+ }
+
+ // Check for cycles (currently being processed in this path)
if (visited.has(value)) {
logger.warn(dedent`
We've detected a cycle in arg '${name}'. Args should be JSON-serializable.
@@ -29,25 +40,36 @@ const inferType = (value: any, name: string, visited: Set): SBType => {
`);
return { name: 'other', value: 'cyclic object' };
}
+
visited.add(value);
+
+ let result: SBType;
+
if (Array.isArray(value)) {
const childType: SBType =
value.length > 0
- ? inferType(value[0], name, new Set(visited))
+ ? inferType(value[0], name, visited, cache)
: { name: 'other', value: 'unknown' };
- return { name: 'array', value: childType };
+ result = { name: 'array', value: childType };
+ } else {
+ const fieldTypes = mapValues(value, (field) => inferType(field, name, visited, cache));
+ result = { name: 'object', value: fieldTypes };
}
- const fieldTypes = mapValues(value, (field) => inferType(field, name, new Set(visited)));
- return { name: 'object', value: fieldTypes };
+
+ visited.delete(value); // Remove from current path after processing
+ cache.set(value, result); // Cache the result for future lookups
+
+ return result;
}
return { name: 'object', value: {} };
};
export const inferArgTypes: ArgTypesEnhancer = (context) => {
const { id, argTypes: userArgTypes = {}, initialArgs = {} } = context;
+ const cache = new Map();
const argTypes = mapValues(initialArgs, (arg, key) => ({
name: key,
- type: inferType(arg, `${id}.${key}`, new Set()),
+ type: inferType(arg, `${id}.${key}`, new Set(), cache),
}));
const userArgTypesNames = mapValues(userArgTypes, (argType, key) => ({
name: key,
diff --git a/code/core/src/telemetry/anonymous-id.test.ts b/code/core/src/telemetry/anonymous-id.test.ts
index 8277ca547e85..cdee088797ab 100644
--- a/code/core/src/telemetry/anonymous-id.test.ts
+++ b/code/core/src/telemetry/anonymous-id.test.ts
@@ -1,6 +1,27 @@
-import { describe, expect, it } from 'vitest';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
-import { normalizeGitUrl, unhashedProjectId } from './anonymous-id';
+import { executeCommandSync } from 'storybook/internal/common';
+
+import {
+ getAnonymousProjectId,
+ getProjectSince,
+ normalizeGitUrl,
+ unhashedProjectId,
+} from './anonymous-id';
+
+vi.mock(import('storybook/internal/common'), async (actualModule) => {
+ const actual = await actualModule();
+
+ return {
+ ...actual,
+ executeCommandSync: vi.fn(actual.executeCommandSync),
+ getProjectRoot: () => '/path/to/project/root',
+ };
+});
+
+beforeEach(() => {
+ vi.mocked(executeCommandSync).mockReset();
+});
describe('normalizeGitUrl', () => {
it('trims off https://', () => {
@@ -105,3 +126,64 @@ describe('unhashedProjectId', () => {
).toBe('github.com/storybookjs/storybook.gitpath/to/storybook');
});
});
+
+describe('getProjectSince', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ vi.resetModules();
+ });
+
+ 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'
+ );
+
+ 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('');
+
+ const { getProjectSince: getProjSince } = await import('./anonymous-id');
+
+ expect(getProjSince()).toBeUndefined();
+ });
+
+ it('returns undefined if git log fails', async () => {
+ vi.mocked(executeCommandSync).mockImplementation(() => {
+ throw new Error('git not available');
+ });
+
+ const { getProjectSince: getProjSince } = await import('./anonymous-id');
+
+ expect(getProjSince()).toBeUndefined();
+ });
+});
+
+describe('getAnonymousProjectId', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ vi.resetModules();
+
+ 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');
+ const result = getAnonymousProjectId();
+
+ expect(result).toMatch('061e4ee22a1f7c079849d97234b3be94d016fb1f24ba11878c41f8b48c0213bf');
+ });
+
+ 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');
+ });
+
+ const result = getAnonId();
+
+ expect(result).toBeUndefined();
+ });
+});
diff --git a/code/core/src/telemetry/anonymous-id.ts b/code/core/src/telemetry/anonymous-id.ts
index 951a268a6263..a66fb88f0538 100644
--- a/code/core/src/telemetry/anonymous-id.ts
+++ b/code/core/src/telemetry/anonymous-id.ts
@@ -1,8 +1,7 @@
import { relative } from 'node:path';
-import { getProjectRoot } from 'storybook/internal/common';
+import { executeCommandSync, getProjectRoot } from 'storybook/internal/common';
-import { execSync } from 'child_process';
// eslint-disable-next-line depend/ban-dependencies
import slash from 'slash';
@@ -33,6 +32,8 @@ export function unhashedProjectId(remoteUrl: string, projectRootPath: string) {
}
let anonymousProjectId: string;
+let getProjectSinceResult: Date | undefined;
+
export const getAnonymousProjectId = () => {
if (anonymousProjectId) {
return anonymousProjectId;
@@ -41,15 +42,45 @@ export const getAnonymousProjectId = () => {
try {
const projectRootPath = relative(getProjectRoot(), process.cwd());
- const originBuffer = execSync(`git config --local --get remote.origin.url`, {
+ const result = executeCommandSync({
+ command: 'git',
+ args: ['config', '--get', 'remote.origin.url'],
timeout: 1000,
- stdio: `pipe`,
});
- anonymousProjectId = oneWayHash(unhashedProjectId(String(originBuffer), projectRootPath));
+ anonymousProjectId = oneWayHash(unhashedProjectId(result, projectRootPath));
} catch (_) {
//
}
return anonymousProjectId;
};
+
+export const getProjectSince = () => {
+ try {
+ if (getProjectSinceResult) {
+ return getProjectSinceResult;
+ }
+
+ const dateBuffer = executeCommandSync({
+ command: 'git',
+ args: ['log', '--reverse', '--format=%cd', '--date=iso'],
+ timeout: 1000,
+ });
+
+ const firstLine = String(dateBuffer).trim().split('\n')[0];
+
+ const date = new Date(firstLine);
+
+ if (Number.isNaN(date.getTime())) {
+ return undefined;
+ }
+
+ getProjectSinceResult = date;
+ return date;
+ } catch (_) {
+ //
+ }
+
+ return undefined;
+};
diff --git a/code/core/src/telemetry/telemetry.ts b/code/core/src/telemetry/telemetry.ts
index b84ae26b47ac..cb5b903cd085 100644
--- a/code/core/src/telemetry/telemetry.ts
+++ b/code/core/src/telemetry/telemetry.ts
@@ -10,7 +10,7 @@ import { nanoid } from 'nanoid';
import { version } from '../../package.json';
import { resolvePackageDir } from '../shared/utils/module';
-import { getAnonymousProjectId } from './anonymous-id';
+import { getAnonymousProjectId, getProjectSince } from './anonymous-id';
import { detectAgent } from './detect-agent';
import { set as saveToCache } from './event-cache';
import { fetch } from './fetch';
@@ -107,6 +107,7 @@ export async function sendTelemetry(
: {
...globalContext,
anonymousId: getAnonymousProjectId(),
+ projectSince: getProjectSince()?.getTime(),
};
let request: any;
diff --git a/code/core/src/viewport/useViewport.ts b/code/core/src/viewport/useViewport.ts
index 04be3db8445a..7e1e3eea9b20 100644
--- a/code/core/src/viewport/useViewport.ts
+++ b/code/core/src/viewport/useViewport.ts
@@ -78,13 +78,17 @@ const parseGlobals = (
};
}
- // Ensure URL-defined viewports (user globals) override story globals.
- // Spreading is not sufficient here, because undefined would still override defined values.
const global = normalizeGlobal(globals?.[PARAM_KEY]);
const userGlobal = normalizeGlobal(userGlobals?.[PARAM_KEY]);
const storyGlobal = normalizeGlobal(storyGlobals?.[PARAM_KEY]);
- const value = userGlobal?.value ?? storyGlobal?.value ?? global?.value;
- const isRotated = userGlobal?.isRotated ?? storyGlobal?.isRotated ?? global?.isRotated ?? false;
+ const storyHasViewport = PARAM_KEY in storyGlobals;
+
+ // Story-level viewport globals override user globals for the current story.
+ const primaryGlobal = storyHasViewport ? storyGlobal : userGlobal;
+ const secondaryGlobal = storyHasViewport ? userGlobal : storyGlobal;
+ const value = primaryGlobal?.value ?? secondaryGlobal?.value ?? global?.value;
+ const isRotated =
+ primaryGlobal?.isRotated ?? secondaryGlobal?.isRotated ?? global?.isRotated ?? false;
const keys = Object.keys(options);
const isLocked = disable || PARAM_KEY in storyGlobals || !keys.length;
@@ -185,27 +189,21 @@ export const useViewport = () => {
[update, isRotated]
);
- useEffect(() => {
- // Reset the viewport to the story global value if the story defines one, regardless of URL state
- if (PARAM_KEY in storyGlobals) {
- update(normalizeGlobal(storyGlobals?.[PARAM_KEY], false));
- lastSelectedOption.current = undefined;
- }
- }, [storyGlobals, update]);
-
useEffect(() => {
// Skip if parameter not loaded to avoid race condition with default MINIMAL_VIEWPORTS
if (!parameter) {
return;
}
- // Reset the viewport to the story global value if the URL state defines an invalid option
+ // Track valid options; if invalid and no story-level viewport is set, reset to default
if (option) {
if (Object.hasOwn(options, option)) {
lastSelectedOption.current = option;
} else {
lastSelectedOption.current = undefined;
- update(normalizeGlobal(storyGlobals?.[PARAM_KEY], false));
+ if (!(PARAM_KEY in storyGlobals)) {
+ update({ value: undefined, isRotated: false });
+ }
}
}
}, [parameter, storyGlobals, options, option, update]);
diff --git a/code/package.json b/code/package.json
index 7b1206950b78..007a7f68afde 100644
--- a/code/package.json
+++ b/code/package.json
@@ -220,5 +220,6 @@
"Dependency Upgrades"
]
]
- }
+ },
+ "deferredNextVersion": "10.3.0-alpha.11"
}
diff --git a/docs/configure/telemetry.mdx b/docs/configure/telemetry.mdx
index 2d25e6d7ddeb..4aacc45029e7 100644
--- a/docs/configure/telemetry.mdx
+++ b/docs/configure/telemetry.mdx
@@ -62,6 +62,14 @@ Will generate the following output:
{
"anonymousId": "8bcfdfd5f9616a1923dd92adf89714331b2d18693c722e05152a47f8093392bb",
"eventType": "dev",
+ "context": {
+ "isTTY": true,
+ "platform": "macOS",
+ "nodeVersion": "24.11.0",
+ "storybookVersion": "10.3.0-alpha.9",
+ "cliVersion": "10.3.0-alpha.9",
+ "projectSince": 1717334400000
+ },
"payload": {
"versionStatus": "cached",
"storyIndex": {
diff --git a/docs/versions/next.json b/docs/versions/next.json
index bb7b7651757c..2d686769827a 100644
--- a/docs/versions/next.json
+++ b/docs/versions/next.json
@@ -1 +1 @@
-{"version":"10.3.0-alpha.10","info":{"plain":"- Addon-Vitest: Fix postinstall a11y installation - [#33888](https://github.com/storybookjs/storybook/pull/33888), thanks @valentinpalkovic!\n- Builder-Vite: Use preview annotations as entry points for optimizeDeps - [#33875](https://github.com/storybookjs/storybook/pull/33875), thanks @copilot-swe-agent!\n- React Native Web: Fix inconsistent example stories - [#33891](https://github.com/storybookjs/storybook/pull/33891), thanks @danielalanbates!\n- Webpack: Improve performance of module-mocking plugins - [#33169](https://github.com/storybookjs/storybook/pull/33169), thanks @valentinpalkovic!"}}
\ No newline at end of file
+{"version":"10.3.0-alpha.11","info":{"plain":"- Addon Pseudo-states: Process all nested css rules - [#33605](https://github.com/storybookjs/storybook/pull/33605), thanks @hpohlmeyer!\n- Core: Avoid hanging when inferring args for recursive calls on DOM elemens - [#33922](https://github.com/storybookjs/storybook/pull/33922), thanks @valentinpalkovic!\n- Core: Sanitize inputs for save from controls - [#33868](https://github.com/storybookjs/storybook/pull/33868), thanks @valentinpalkovic!\n- Telemetry: Add project age - [#33910](https://github.com/storybookjs/storybook/pull/33910), thanks @shilman!\n- Viewport: Prioritize story viewport globals and avoid user-global pollution - [#33849](https://github.com/storybookjs/storybook/pull/33849), thanks @ia319!"}}
\ No newline at end of file
diff --git a/scripts/tasks/compile.ts b/scripts/tasks/compile.ts
index eaa4ef2a699f..ec48e01a6692 100644
--- a/scripts/tasks/compile.ts
+++ b/scripts/tasks/compile.ts
@@ -11,7 +11,6 @@ const amountOfVCPUs = 2;
const parallel = `--parallel=${process.env.CI ? amountOfVCPUs - 1 : maxConcurrentTasks}`;
-const linkedContents = `export * from '../../src/manager-api/index.ts';`;
const linkCommand = `yarn nx run-many -t compile ${parallel}`;
const noLinkCommand = `yarn nx run-many -t compile -c production ${parallel}`;
@@ -20,19 +19,12 @@ export const compile: Task = {
dependsOn: ['install'],
async ready({ codeDir }, { link }) {
try {
- // To check if the code has been compiled as we need, we check the compiled output of
- // `@storybook/preview`. To check if it has been built for publishing (i.e. `--no-link`),
- // we check if it built types or references source files directly.
- const contents = await readFile(
- resolve(codeDir, './core/dist/manager-api/index.d.ts'),
- 'utf8'
- );
- const isLinkedContents = contents.indexOf(linkedContents) !== -1;
-
if (link) {
- return isLinkedContents;
+ await readFile(resolve(codeDir, './core/dist/manager-api/index.js'), 'utf8');
+ } else {
+ await readFile(resolve(codeDir, './core/dist/manager-api/index.d.ts'), 'utf8');
}
- return !isLinkedContents;
+ return true;
} catch (err) {
return false;
}
diff --git a/scripts/utils/yarn.ts b/scripts/utils/yarn.ts
index 16015b518f81..4684393653f5 100644
--- a/scripts/utils/yarn.ts
+++ b/scripts/utils/yarn.ts
@@ -98,6 +98,9 @@ export const addWorkaroundResolutions = async ({
const additionalReact19Resolutions = [
'nextjs/default-ts',
'nextjs/prerelease',
+ 'nextjs-vite/15-ts',
+ 'nextjs-vite/default-ts',
+ 'nextjs-vite/14-ts',
'react-native-web-vite/expo-ts',
].includes(key)
? {