Merge branch 'valentin/6-to-8-automigrations' into valentin/enhance-mdx-to-csf-codemod

This commit is contained in:
Valentin Palkovic 2024-02-27 14:35:57 +01:00 committed by GitHub
commit 78fa304252
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 239 additions and 87 deletions

View File

@ -0,0 +1,23 @@
import { global as globalThis } from '@storybook/global';
export default {
title: 'Multiple CSF Files Same Title',
component: globalThis.Components.Html,
tags: ['autodocs'],
args: {
content: '<p>paragraph</p>',
},
parameters: {
chromatic: { disable: true },
},
};
export const DefaultA = {};
export const SpanContent = {
args: { content: '<span>span</span>' },
};
export const CodeContent = {
args: { content: '<code>code</code>' },
};

View File

@ -0,0 +1,23 @@
import { global as globalThis } from '@storybook/global';
export default {
title: 'Multiple CSF Files Same Title',
component: globalThis.Components.Html,
tags: ['autodocs'],
args: {
content: '<p>paragraph</p>',
},
parameters: {
chromatic: { disable: true },
},
};
export const DefaultB = {};
export const H1Content = {
args: { content: '<h1>heading 1</h1>' },
};
export const H2Content = {
args: { content: '<h2>heading 2</h2>' },
};

View File

@ -210,4 +210,19 @@ test.describe('addon-docs', () => {
await expect(componentReactVersion).toHaveText(expectedReactVersion);
await expect(componentReactDomVersion).toHaveText(expectedReactVersion);
});
test('should have stories from multiple CSF files in autodocs', async ({ page }) => {
const sbPage = new SbPage(page);
await sbPage.navigateToStory('/addons/docs/multiple-csf-files-same-title', 'docs');
const root = sbPage.previewRoot();
const storyHeadings = root.locator('.sb-anchor > h3');
await expect(await storyHeadings.count()).toBe(6);
await expect(storyHeadings.nth(0)).toHaveText('Default A');
await expect(storyHeadings.nth(1)).toHaveText('Span Content');
await expect(storyHeadings.nth(2)).toHaveText('Code Content');
await expect(storyHeadings.nth(3)).toHaveText('Default B');
await expect(storyHeadings.nth(4)).toHaveText('H 1 Content');
await expect(storyHeadings.nth(5)).toHaveText('H 2 Content');
});
});

View File

@ -81,7 +81,7 @@ export const blocker = createBlocker({
default:
return dedent`
Support for ${data.packageName} version < ${data.minimumVersion} has been removed.
Since version 8, Storybook needs minimum version of ${data.minimumVersion}, but you had version ${data.installedVersion}.
Since version 8, Storybook needs a minimum version of ${data.minimumVersion}, but you have version ${data.installedVersion}.
Please update this dependency.
`;

View File

@ -84,7 +84,7 @@ export const mdxgfm: Fix<Options> = {
Because of this you need to explicitly add the GFM plugin in the addon-docs options:
https://storybook.js.org/docs/react/writing-docs/mdx#lack-of-github-flavored-markdown-gfm
We recommend you follow the guide on the link above, however we can add a temporary Storybook addon that helps make this migration easier.
We recommend that you follow the guide in the link above; however, we can add a temporary Storybook addon to help make this migration easier.
We'll install the addon and add it to your storybook config.
`;
},

View File

@ -76,7 +76,8 @@ export const removeArgtypesRegex: Fix<{ argTypesRegex: NodePath; previewConfigPa
Make sure to assign an explicit ${chalk.cyan('fn')} to your args for those usages.
For more information please visit our migration guide: https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#implicit-actions-can-not-be-used-during-rendering-for-example-in-the-play-function
For more information please visit our docs:
https://storybook.js.org/docs/8.0/essentials/actions#via-storybooktest-fn-spy-function
`;
},
};

View File

@ -23,7 +23,7 @@ export interface RunOptions<ResultType> {
* promptType defines how the user will be prompted to apply an automigration fix
* - auto: the fix will be applied automatically
* - manual: the user will be prompted to apply the fix
* - notification: the user will be notified about some changes. A fix isn't required, thought
* - notification: the user will be notified about some changes. A fix isn't required, though
*/
export type Prompt = 'auto' | 'manual' | 'notification';

View File

@ -104,7 +104,6 @@ export class Yarn2Proxy extends JsPackageManager {
}
public async findInstallations(pattern: string[]) {
console.log(['info', '--name-only', '--recursive', ...pattern].join(' '));
const commandResult = await this.executeCommand({
command: 'yarn',
args: ['info', '--name-only', '--recursive', ...pattern],

View File

@ -17,7 +17,7 @@ import { join, relative, resolve } from 'path';
import { deprecate } from '@storybook/node-logger';
import { dedent } from 'ts-dedent';
import { readFile } from 'fs-extra';
import { MissingBuilderError } from '@storybook/core-events/server-errors';
import { MissingBuilderError, NoStatsForViteDevError } from '@storybook/core-events/server-errors';
import { storybookDevServer } from './dev-server';
import { outputStats } from './utils/output-stats';
import { outputStartupInformation } from './utils/output-startup-information';
@ -192,7 +192,16 @@ export async function buildDevStandalone(
if (options.smokeTest) {
const warnings: Error[] = [];
warnings.push(...(managerStats?.toJson()?.warnings || []));
warnings.push(...(previewStats?.toJson()?.warnings || []));
try {
warnings.push(...(previewStats?.toJson()?.warnings || []));
} catch (err) {
if (err instanceof NoStatsForViteDevError) {
// pass, the Vite builder has no warnings in the stats object anyway,
// but no stats at all in dev mode
} else {
throw err;
}
}
const problems = warnings
.filter((warning) => !warning.message.includes(`export 'useInsertionEffect'`))

View File

@ -517,11 +517,8 @@ export function useArgTypes(): ArgTypes {
export { addons } from './lib/addons';
/**
* We need to rename this so it's not compiled to a straight re-export
* Our globalization plugin can't handle an import and export of the same name in different lines
* @deprecated
*/
// We need to rename this so it's not compiled to a straight re-export
// Our globalization plugin can't handle an import and export of the same name in different lines
const typesX = types;
export { typesX as types };

View File

@ -33,6 +33,52 @@ describe('referenceCSFFile', () => {
});
});
describe('attachCSFFile', () => {
const firstCsfParts = csfFileParts('first-meta--first-story', 'first-meta');
const secondCsfParts = csfFileParts('second-meta--second-story', 'second-meta');
const store = {
componentStoriesFromCSFFile: ({ csfFile }: { csfFile: CSFFile }) =>
csfFile === firstCsfParts.csfFile ? [firstCsfParts.story] : [secondCsfParts.story],
} as unknown as StoryStore<Renderer>;
it('attaches multiple CSF files', () => {
// Arrange - create a context with both CSF files
const context = new DocsContext(channel, store, renderStoryToElement, [
firstCsfParts.csfFile,
secondCsfParts.csfFile,
]);
// Act - attach the first CSF file
context.attachCSFFile(firstCsfParts.csfFile);
// Assert - the first story is now the primary story and the only component story
expect(context.storyById()).toEqual(firstCsfParts.story);
expect(context.componentStories()).toEqual([firstCsfParts.story]);
// Assert - stories from both CSF files are available
expect(context.componentStoriesFromCSFFile(firstCsfParts.csfFile)).toEqual([
firstCsfParts.story,
]);
expect(context.componentStoriesFromCSFFile(secondCsfParts.csfFile)).toEqual([
secondCsfParts.story,
]);
// Act - attach the second CSF file
context.attachCSFFile(secondCsfParts.csfFile);
// Assert - the first story is still the primary story but both stories are available
expect(context.storyById()).toEqual(firstCsfParts.story);
expect(context.componentStories()).toEqual([firstCsfParts.story, secondCsfParts.story]);
// Act - attach the second CSF file again
context.attachCSFFile(secondCsfParts.csfFile);
// Assert - still only two stories are available
expect(context.storyById()).toEqual(firstCsfParts.story);
expect(context.componentStories()).toEqual([firstCsfParts.story, secondCsfParts.story]);
});
});
describe('resolveOf', () => {
const { story, csfFile, storyExport, metaExport, moduleExports, component } = csfFileParts();

View File

@ -27,7 +27,7 @@ export class DocsContext<TRenderer extends Renderer> implements DocsContextProps
private nameToStoryId: Map<StoryName, StoryId>;
private attachedCSFFile?: CSFFile<TRenderer>;
private attachedCSFFiles: Set<CSFFile<TRenderer>>;
private primaryStory?: PreparedStory<TRenderer>;
@ -38,11 +38,12 @@ export class DocsContext<TRenderer extends Renderer> implements DocsContextProps
/** The CSF files known (via the index) to be refererenced by this docs file */
csfFiles: CSFFile<TRenderer>[]
) {
this.componentStoriesValue = [];
this.storyIdToCSFFile = new Map();
this.exportToStory = new Map();
this.exportsToCSFFile = new Map();
this.nameToStoryId = new Map();
this.componentStoriesValue = [];
this.attachedCSFFiles = new Set();
csfFiles.forEach((csfFile, index) => {
this.referenceCSFFile(csfFile);
@ -71,10 +72,15 @@ export class DocsContext<TRenderer extends Renderer> implements DocsContextProps
if (!this.exportsToCSFFile.has(csfFile.moduleExports)) {
throw new Error('Cannot attach a CSF file that has not been referenced');
}
if (this.attachedCSFFiles.has(csfFile)) {
// this CSF file is already attached, don't do anything
return;
}
this.attachedCSFFile = csfFile;
this.attachedCSFFiles.add(csfFile);
const stories = this.store.componentStoriesFromCSFFile({ csfFile });
stories.forEach((story) => {
this.nameToStoryId.set(story.name, story.id);
this.componentStoriesValue.push(story);
@ -115,15 +121,18 @@ export class DocsContext<TRenderer extends Renderer> implements DocsContextProps
return { type: 'story', story: this.primaryStory } as TResolvedExport;
}
if (!this.attachedCSFFile)
if (this.attachedCSFFiles.size === 0)
throw new Error(
`No CSF file attached to this docs file, did you forget to use <Meta of={} />?`
);
if (moduleExportType === 'meta')
return { type: 'meta', csfFile: this.attachedCSFFile } as TResolvedExport;
const firstAttachedCSFFile = Array.from(this.attachedCSFFiles)[0];
const { component } = this.attachedCSFFile.meta;
if (moduleExportType === 'meta') {
return { type: 'meta', csfFile: firstAttachedCSFFile } as TResolvedExport;
}
const { component } = firstAttachedCSFFile.meta;
if (!component)
throw new Error(
`Attached CSF file does not defined a component, did you forget to export one?`

View File

@ -1,6 +1,6 @@
import type { CSFFile, PreparedStory } from '@storybook/types';
export function csfFileParts() {
export function csfFileParts(storyId = 'meta--story', metaId = 'meta') {
// These compose the raw exports of the CSF file
const component = {};
const metaExport = { component };
@ -9,13 +9,13 @@ export function csfFileParts() {
// This is the prepared story + CSF file after SB has processed them
const storyAnnotations = {
id: 'meta--story',
id: storyId,
moduleExport: storyExport,
} as CSFFile['stories'][string];
const story = { id: 'meta--story', moduleExport: storyExport } as PreparedStory;
const meta = { id: 'meta', title: 'Meta', component, moduleExports } as CSFFile['meta'];
const story = { id: storyId, moduleExport: storyExport } as PreparedStory;
const meta = { id: metaId, title: 'Meta', component, moduleExports } as CSFFile['meta'];
const csfFile = {
stories: { 'meta--story': storyAnnotations },
stories: { [storyId]: storyAnnotations },
meta,
moduleExports,
} as CSFFile;

View File

@ -358,7 +358,12 @@ export interface Addon_BaseType {
* This is called as a function, so if you want to use hooks,
* your function needs to return a JSX.Element within which components are rendered
*/
render: (renderOptions: Partial<Addon_RenderOptions>) => ReactElement<any, any> | null;
render: (props: Partial<Addon_RenderOptions>) => ReturnType<FC<Partial<Addon_RenderOptions>>>;
// TODO: for Storybook 9 I'd like to change this to be:
// render: FC<Partial<Addon_RenderOptions>>;
// This would bring it in line with how every other addon is set up.
// We'd need to change how the render function is called in the manager:
// https://github.com/storybookjs/storybook/blob/4e6fc0dde0842841d99cb3cf5148ca293a950301/code/ui/manager/src/components/preview/Preview.tsx#L105
/**
* @unstable
*/

View File

@ -130,7 +130,7 @@ export const renderJsx = (code: React.ReactElement, options: JSXOptions) => {
return string;
}).join('\n');
return result.replace(/function\s+noRefCheck\(\)\s+\{\}/g, '() => {}');
return result.replace(/function\s+noRefCheck\(\)\s*\{\}/g, '() => {}');
};
const defaultOpts = {

View File

@ -5,4 +5,4 @@
export let content = '';
</script>
<div>{@html content}></div>
<div>{@html content}</div>

View File

@ -1,5 +1,5 @@
import type { FC, PropsWithChildren } from 'react';
import React, { createContext, useContext, useState } from 'react';
import React, { createContext, useContext, useMemo, useState } from 'react';
import { useMediaQuery } from '../hooks/useMedia';
import { BREAKPOINT } from '../../constants';
@ -32,22 +32,29 @@ export const LayoutProvider: FC<PropsWithChildren> = ({ children }) => {
const isDesktop = useMediaQuery(`(min-width: ${BREAKPOINT}px)`);
const isMobile = !isDesktop;
return (
<LayoutContext.Provider
value={{
isMobileMenuOpen,
setMobileMenuOpen,
isMobileAboutOpen,
setMobileAboutOpen,
isMobilePanelOpen,
setMobilePanelOpen,
isDesktop,
isMobile,
}}
>
{children}
</LayoutContext.Provider>
const contextValue = useMemo(
() => ({
isMobileMenuOpen,
setMobileMenuOpen,
isMobileAboutOpen,
setMobileAboutOpen,
isMobilePanelOpen,
setMobilePanelOpen,
isDesktop,
isMobile,
}),
[
isMobileMenuOpen,
setMobileMenuOpen,
isMobileAboutOpen,
setMobileAboutOpen,
isMobilePanelOpen,
setMobilePanelOpen,
isDesktop,
isMobile,
]
);
return <LayoutContext.Provider value={contextValue}>{children}</LayoutContext.Provider>;
};
export const useLayout = () => useContext(LayoutContext);

View File

@ -5,7 +5,7 @@ import Downshift from 'downshift';
import type { FuseOptions } from 'fuse.js';
import Fuse from 'fuse.js';
import { global } from '@storybook/global';
import React, { useMemo, useRef, useState, useCallback } from 'react';
import React, { useRef, useState, useCallback } from 'react';
import { CloseIcon, SearchIcon } from '@storybook/icons';
import { DEFAULT_REF_ID } from './Sidebar';
import type {
@ -176,8 +176,8 @@ export const Search = React.memo<{
[api, inputRef, showAllComponents, DEFAULT_REF_ID]
);
const list: SearchItem[] = useMemo(() => {
return dataset.entries.reduce<SearchItem[]>((acc, [refId, { index, status }]) => {
const makeFuse = useCallback(() => {
const list = dataset.entries.reduce<SearchItem[]>((acc, [refId, { index, status }]) => {
const groupStatus = getGroupStatus(index || {}, status);
if (index) {
@ -196,12 +196,12 @@ export const Search = React.memo<{
}
return acc;
}, []);
return new Fuse(list, options);
}, [dataset]);
const fuse = useMemo(() => new Fuse(list, options), [list]);
const getResults = useCallback(
(input: string) => {
const fuse = makeFuse();
if (!input) return [];
let results: DownshiftItem[] = [];
@ -229,7 +229,7 @@ export const Search = React.memo<{
return results;
},
[allComponents, fuse]
[allComponents, makeFuse]
);
const stateReducer = useCallback(

View File

@ -481,55 +481,73 @@ export const Tree = React.memo<{
const groupStatus = useMemo(() => getGroupStatus(collapsedData, status), [collapsedData, status]);
return (
<Container ref={containerRef} hasOrphans={isMain && orphanIds.length > 0}>
<IconSymbols />
{collapsedItems.map((itemId) => {
const item = collapsedData[itemId];
const id = createId(itemId, refId);
if (item.type === 'root') {
const descendants = expandableDescendants[item.id];
const isFullyExpanded = descendants.every((d: string) => expanded[d]);
return (
// @ts-expect-error (TODO)
<Root
key={id}
item={item}
refId={refId}
isOrphan={false}
isDisplayed
isSelected={selectedStoryId === itemId}
isExpanded={!!expanded[itemId]}
setExpanded={setExpanded}
isFullyExpanded={isFullyExpanded}
expandableDescendants={descendants}
onSelectStoryId={onSelectStoryId}
/>
);
}
const isDisplayed = !item.parent || ancestry[itemId].every((a: string) => expanded[a]);
const color = groupStatus[itemId] ? statusMapping[groupStatus[itemId]][1] : null;
const treeItems = useMemo(() => {
return collapsedItems.map((itemId) => {
const item = collapsedData[itemId];
const id = createId(itemId, refId);
if (item.type === 'root') {
const descendants = expandableDescendants[item.id];
const isFullyExpanded = descendants.every((d: string) => expanded[d]);
return (
<Node
api={api}
// @ts-expect-error (TODO)
<Root
key={id}
item={item}
status={status?.[itemId]}
refId={refId}
color={color}
docsMode={docsMode}
isOrphan={orphanIds.some((oid) => itemId === oid || itemId.startsWith(`${oid}-`))}
isDisplayed={isDisplayed}
isOrphan={false}
isDisplayed
isSelected={selectedStoryId === itemId}
isExpanded={!!expanded[itemId]}
setExpanded={setExpanded}
isFullyExpanded={isFullyExpanded}
expandableDescendants={descendants}
onSelectStoryId={onSelectStoryId}
/>
);
})}
}
const isDisplayed = !item.parent || ancestry[itemId].every((a: string) => expanded[a]);
const color = groupStatus[itemId] ? statusMapping[groupStatus[itemId]][1] : null;
return (
<Node
api={api}
key={id}
item={item}
status={status?.[itemId]}
refId={refId}
color={color}
docsMode={docsMode}
isOrphan={orphanIds.some((oid) => itemId === oid || itemId.startsWith(`${oid}-`))}
isDisplayed={isDisplayed}
isSelected={selectedStoryId === itemId}
isExpanded={!!expanded[itemId]}
setExpanded={setExpanded}
onSelectStoryId={onSelectStoryId}
/>
);
});
}, [
ancestry,
api,
collapsedData,
collapsedItems,
docsMode,
expandableDescendants,
expanded,
groupStatus,
onSelectStoryId,
orphanIds,
refId,
selectedStoryId,
setExpanded,
status,
]);
return (
<Container ref={containerRef} hasOrphans={isMain && orphanIds.length > 0}>
<IconSymbols />
{treeItems}
</Container>
);
});