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

Filter by extension

Filter by extension


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

- Core: Storybook failed to load iframe.html when publishing - [#33896](https://github.com/storybookjs/storybook/pull/33896), thanks @danielalanbates!
- Manager-API: Update refs sequentially in experimental_setFilter - [#33958](https://github.com/storybookjs/storybook/pull/33958), thanks @ia319!
- React: Handle render identifier in manifest snippet generation - [#33940](https://github.com/storybookjs/storybook/pull/33940), thanks @kasperpeulen!

## 10.2.14

- CLI: Set STORYBOOK environment variable - [#33938](https://github.com/storybookjs/storybook/pull/33938), thanks @yannbf!
Expand Down
8 changes: 6 additions & 2 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@

## Supported Versions

We release patches for fixing security vulnerabilities, primarily focusing on the latest release only.
We release patches for security vulnerabilities, primarily focusing on the latest major version.

In the event of a high-risk vulnerability, we may backport the security fixes to the minor versions of the software, starting from the latest minor version up to the latest major release. The decision to backport security fixes to older versions will be made based on a risk assessment and the feasibility of implementing the patch in those versions.
Security fixes are backported to the previous two major versions only for vulnerabilities with High or Critical CVSS scores (7.0+). The decision to backport is made based on severity assessment and the feasibility of implementing the patch in those versions.

- Latest major version: All security vulnerabilities
- Previous two major versions: High or Critical CVSS scores only
- Older versions: Not supported (Users should upgrade to a supported version)

## Reporting a Vulnerability

Expand Down
6 changes: 3 additions & 3 deletions code/core/src/manager-api/modules/stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -705,9 +705,9 @@ export const init: ModuleFn<SubAPI, SubState> = ({
await api.setIndex(index);

const refs = await fullAPI.getRefs();
Object.entries(refs).forEach(([refId, { internal_index, ...ref }]) => {
fullAPI.setRef(refId, { ...ref, storyIndex: internal_index }, true);
});
for (const [refId, { internal_index, ...ref }] of Object.entries(refs)) {
await fullAPI.setRef(refId, { ...ref, storyIndex: internal_index }, true);
}

provider.channel?.emit(SET_FILTER, { id });
},
Expand Down
3 changes: 2 additions & 1 deletion code/core/src/manager-api/modules/url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,8 @@ export const init: ModuleFn<SubAPI, SubState> = (moduleArgs) => {
base === 'origin' ? originAddress : base === 'network' ? networkAddress : pathname;
const previewBase = refId
? refs[refId].url + '/iframe.html'
: global.PREVIEW_URL || `${managerBase.replace(/\/[^/]*$/, '/')}iframe.html`;
: global.PREVIEW_URL ||
`${managerBase.replace(/\/[^/]*\.html$/, '').replace(/\/?$/, '/')}iframe.html`;

const refParam = refId ? `&refId=${encodeURIComponent(refId)}` : '';
const { args = '', globals = '', ...otherParams } = queryParams;
Expand Down
45 changes: 45 additions & 0 deletions code/core/src/manager-api/tests/url.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -486,4 +486,49 @@ describe('getStoryHrefs', () => {
expect(managerHref).toEqual('/index.html?path=/story/test--story');
expect(previewHref).toEqual('/iframe.html?id=test--story&viewMode=story');
});

it('correctly links when hosted at a subpath without trailing slash', () => {
const { api, state } = initURL({
store,
provider: { channel: new EventEmitter() },
state: { location: { pathname: '/design-system', search: '' } },
navigate: vi.fn(),
fullAPI: { getCurrentStoryData: () => ({ id: 'test--story' }) },
});
store.setState(state);

const { managerHref, previewHref } = api.getStoryHrefs('test--story');
expect(managerHref).toEqual('/design-system?path=/story/test--story');
expect(previewHref).toEqual('/design-system/iframe.html?id=test--story&viewMode=story');
});

it('correctly links when hosted at a subpath with trailing slash', () => {
const { api, state } = initURL({
store,
provider: { channel: new EventEmitter() },
state: { location: { pathname: '/design-system/', search: '' } },
navigate: vi.fn(),
fullAPI: { getCurrentStoryData: () => ({ id: 'test--story' }) },
});
store.setState(state);

const { managerHref, previewHref } = api.getStoryHrefs('test--story');
expect(managerHref).toEqual('/design-system/?path=/story/test--story');
expect(previewHref).toEqual('/design-system/iframe.html?id=test--story&viewMode=story');
});

it('correctly links when hosted at a subpath with index.html', () => {
const { api, state } = initURL({
store,
provider: { channel: new EventEmitter() },
state: { location: { pathname: '/design-system/index.html', search: '' } },
navigate: vi.fn(),
fullAPI: { getCurrentStoryData: () => ({ id: 'test--story' }) },
});
store.setState(state);

const { managerHref, previewHref } = api.getStoryHrefs('test--story');
expect(managerHref).toEqual('/design-system/index.html?path=/story/test--story');
expect(previewHref).toEqual('/design-system/iframe.html?id=test--story&viewMode=story');
});
});
3 changes: 2 additions & 1 deletion code/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -220,5 +220,6 @@
"Dependency Upgrades"
]
]
}
},
"deferredNextVersion": "10.2.15"
}
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,38 @@ test('CSF2 - with args', () => {
);
});

test('render: Template (identifier referencing local function)', () => {
const input = withCSF3(dedent`
const Template = (args) => <Button {...args} label="String"></Button>
export const Interactive: Story = { render: Template }
`);
expect(generateExample(input)).toMatchInlineSnapshot(
`"const Interactive = () => <Button label="String">Click me</Button>;"`
);
});

test('render: Template (identifier referencing local function declaration)', () => {
const input = withCSF3(dedent`
function Template(args) { return <Button {...args} label="String"></Button> }
export const Interactive: Story = { render: Template }
`);
expect(generateExample(input)).toMatchInlineSnapshot(`
"function Interactive() {
return <Button label="String">Click me</Button>;
}"
`);
});

test('render: Template (identifier referencing unresolvable function)', () => {
// When Template can't be resolved (e.g. imported), fall back to no-function JSX synthesis
const input = withCSF3(dedent`
export const Interactive: Story = { render: Template }
`);
expect(generateExample(input)).toMatchInlineSnapshot(
`"const Interactive = () => <Button>Click me</Button>;"`
);
});

test('Custom Render', () => {
const input = withCSF3(dedent`
export const CustomRender: Story = { render: () => <Button label="String"></Button> }
Expand All @@ -242,6 +274,82 @@ test('CustomRenderWithOverideArgs only', async () => {
);
});

test('Meta level render: Template (identifier referencing local function)', async () => {
const input = dedent`
import type { Meta } from '@storybook/react';
import { Button } from '@design-system/button';

const Template = (args) => <Button {...args} override="overide" />;

const meta: Meta<typeof Button> = {
render: Template,
args: {
children: 'Click me'
}
};
export default meta;

export const CustomRenderWithOverideArgs = {
args: { foo: 'bar', override: 'value' }
};
`;
expect(generateExample(input)).toMatchInlineSnapshot(
`"const CustomRenderWithOverideArgs = () => <Button foo="bar" override="overide">Click me</Button>;"`
);
});

test('Meta level render: Template (identifier referencing unresolvable function)', async () => {
const input = dedent`
import type { Meta } from '@storybook/react';
import { Button } from '@design-system/button';

const meta: Meta<typeof Button> = {
component: Button,
render: Template,
args: {
children: 'Click me'
}
};
export default meta;

export const Fallback = {
args: { foo: 'bar' }
};
`;
// Falls back to no-function JSX synthesis using component name
expect(generateExample(input)).toMatchInlineSnapshot(
`"const Fallback = () => <Button foo="bar">Click me</Button>;"`
);
});

test('Story unresolvable render does not fall back to meta render', async () => {
// When a story has `render: ImportedTemplate` (unresolvable) and meta has an inline render,
// the story's render should take precedence — fall through to no-function JSX synthesis,
// NOT use meta's render function.
const input = dedent`
import type { Meta } from '@storybook/react';
import { Button } from '@design-system/button';

const meta: Meta<typeof Button> = {
component: Button,
render: (args) => <Button {...args} extra="from-meta" />,
args: {
children: 'Click me'
}
};
export default meta;

export const StoryWithImportedRender = {
render: ImportedTemplate,
args: { label: 'hello' }
};
`;
// Should NOT contain extra="from-meta" — that would mean meta's render leaked through
expect(generateExample(input)).toMatchInlineSnapshot(
`"const StoryWithImportedRender = () => <Button label="hello">Click me</Button>;"`
);
});

test('Meta level render', async () => {
const input = dedent`
import type { Meta } from '@storybook/react';
Expand Down
90 changes: 70 additions & 20 deletions code/renderers/react/src/componentManifest/generateCodeSnippet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export function getCodeSnippet(
(t.isStringLiteral(prop.node) && prop.node.value === 'bind');

if (obj.isIdentifier() && isBind) {
const resolved = resolveBindIdentifierInit(storyDeclaration, obj);
const resolved = resolveIdentifierInit(storyDeclaration, obj);

if (resolved) {
normalizedPath = resolved;
Expand Down Expand Up @@ -118,28 +118,61 @@ export function getCodeSnippet(
? metaPath.get('properties').filter((p) => p.isObjectProperty())
: [];

const getRenderPath = (object: NodePath<t.ObjectProperty>[]) => {
// Tri-state render resolution: distinguishes "no render property" from
// "render exists but couldn't be resolved" so that an unresolvable story-level
// render (e.g. `render: ImportedTemplate`) doesn't incorrectly fall back to meta's render.
type RenderResolution =
| { kind: 'missing' }
| {
kind: 'resolved';
path: NodePath<t.ArrowFunctionExpression | t.FunctionExpression | t.FunctionDeclaration>;
}
| { kind: 'unresolved' };

const getRenderPath = (object: NodePath<t.ObjectProperty>[]): RenderResolution => {
const renderPath = object.find((p) => keyOf(p.node) === 'render')?.get('value');

if (renderPath?.isIdentifier()) {
componentName = renderPath.node.name;
if (!renderPath) {
return { kind: 'missing' };
}
if (
renderPath &&
!(renderPath.isArrowFunctionExpression() || renderPath.isFunctionExpression())
) {

// If render is an identifier (e.g. `render: Template`), try to resolve it
if (renderPath.isIdentifier()) {
const resolved = resolveIdentifierInit(storyDeclaration, renderPath);
if (
resolved &&
(resolved.isArrowFunctionExpression() ||
resolved.isFunctionExpression() ||
resolved.isFunctionDeclaration())
) {
return { kind: 'resolved', path: resolved };
}
// Render property exists but couldn't be resolved — don't fall back to meta's render
return { kind: 'unresolved' };
}

if (!(renderPath.isArrowFunctionExpression() || renderPath.isFunctionExpression())) {
throw renderPath.buildCodeFrameError(
'Expected render to be an arrow function or function expression'
);
}

return renderPath;
return { kind: 'resolved', path: renderPath };
};

const metaRenderPath = getRenderPath(metaProps);
const renderPath = getRenderPath(storyProps);

storyFn ??= renderPath ?? metaRenderPath;
const metaRender = getRenderPath(metaProps);
const storyRender = getRenderPath(storyProps);

// Story render takes precedence. Only fall back to meta render when the story
// has no render property at all — NOT when it has one that couldn't be resolved.
if (!storyFn) {
storyFn =
storyRender.kind === 'resolved'
? storyRender.path
: storyRender.kind === 'missing' && metaRender.kind === 'resolved'
? metaRender.path
: undefined;
}

// Collect args
const metaArgs = metaArgsRecord(metaObj ?? null);
Expand Down Expand Up @@ -201,7 +234,13 @@ export function getCodeSnippet(

if (changed) {
return t.isFunctionDeclaration(fn)
? t.functionDeclaration(fn.id, [], t.blockStatement(newBody), fn.generator, fn.async)
? t.functionDeclaration(
t.identifier(storyName),
[],
t.blockStatement(newBody),
fn.generator,
fn.async
)
: t.variableDeclaration('const', [
t.variableDeclarator(
t.identifier(storyName),
Expand All @@ -212,7 +251,7 @@ export function getCodeSnippet(
}

return t.isFunctionDeclaration(fn)
? fn
? t.functionDeclaration(t.identifier(storyName), fn.params, fn.body, fn.generator, fn.async)
: t.variableDeclaration('const', [t.variableDeclarator(t.identifier(storyName), fn)]);
}

Expand Down Expand Up @@ -541,17 +580,28 @@ function transformArgsSpreadsInJsx(
return { node: t.jsxFragment(node.openingFragment, node.closingFragment, fragChildren), changed };
}

/** Resolve the initializer for an identifier used as `Template.bind(...)`. */
function resolveBindIdentifierInit(
storyPath: NodePath<t.Node>,
identifier: NodePath<t.Identifier>
) {
/** Resolve the initializer for an identifier (e.g. `Template.bind({})` or `render: Template`). */
function resolveIdentifierInit(storyPath: NodePath<t.Node>, identifier: NodePath<t.Identifier>) {
const programPath = storyPath.findParent((p) => p.isProgram()) as NodePath<t.Program> | null;

if (!programPath) {
return null;
}

// Check for function declarations: `function Template(args) { ... }` or `export function Template(args) { ... }`
for (const stmt of programPath.get('body')) {
if (stmt.isFunctionDeclaration() && stmt.node.id?.name === identifier.node.name) {
return stmt;
}
if (stmt.isExportNamedDeclaration()) {
const decl = stmt.get('declaration');
if (decl.isFunctionDeclaration() && decl.node.id?.name === identifier.node.name) {
return decl;
}
}
}

// Check for variable declarations: `const Template = (args) => ...`
const declarators = programPath.get('body').flatMap((stmt) => {
if (stmt.isVariableDeclaration()) {
return stmt.get('declarations');
Expand Down
21 changes: 12 additions & 9 deletions docs/releases/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,18 @@ npm create storybook@next

## Supported Versions

We actively maintain the latest major version of Storybook. Within the current major, we patch only the latest minor version. Most fixes and new work go into the next minor (or sometimes major) and are not backported. Critical security fixes may be backported more broadly across the current major version, and in rare cases (such as for a short period immediately following a new major), to the previous major.

For example, if the latest version is `9.2.1`:

- We support `9.x.x` versions and release `9.2.x` patch versions
- Most fixes and new work will be released as `9.3.0-alpha.x` versions
- If the next release is a major version, it would be `10.0.0-alpha.x`
- We will backport critical security fixes to `9.1.x` or `9.0.x`
- Rarely, we may backport critical fixes to `8.6.x` as necessary
We actively maintain the latest major version of Storybook. Within the current major, we patch only the latest minor version. Most fixes and new work go into the next minor (or sometimes major) and are not backported. Critical security fixes may be backported more broadly based on severity:
- Latest major: Receives all security fixes
- Previous two majors: Receive security patches for **High or Critical [CVSS vulnerabilities](https://en.wikipedia.org/wiki/Common_Vulnerability_Scoring_System) only**
- Older versions: No longer recieves any patches

For example, if the latest version is `10.2.1`:

- We support `10.x.x` versions and release `10.2.x` patch versions
- Most fixes and new work will be released as `10.3.0-alpha.x` versions
- If the next release is a major version, it would be `11.0.0-alpha.x`
- We will backport **High or Critical** security fixes to the latest minor of `9.x.x` and `8.x.x`
- Versions `7.x.x` and older will not receive security patches

For compatibility with other libraries and tools in the JavaScript ecosystem, please refer to the [compatibility tracker](https://github.com/storybookjs/storybook/issues/23279).

Expand Down
Loading