mirror of
https://github.com/storybookjs/storybook.git
synced 2025-04-09 00:19:13 +08:00
Merge pull request #28886 from storybookjs/norbert/linting-part3
Linting: Jsdoc & Bracket style consistency
This commit is contained in:
commit
010e68f96a
@ -16,7 +16,7 @@ import {
|
||||
|
||||
import { DocsContext } from '@storybook/blocks';
|
||||
import { global } from '@storybook/global';
|
||||
import type { Decorator, ReactRenderer } from '@storybook/react';
|
||||
import type { Decorator, Loader, ReactRenderer } from '@storybook/react';
|
||||
|
||||
import { MINIMAL_VIEWPORTS } from '@storybook/addon-viewport';
|
||||
|
||||
@ -95,7 +95,6 @@ const StackContainer = ({ children, layout }) => (
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
// margin: layout === 'fullscreen' ? 0 : '-1rem',
|
||||
}}
|
||||
>
|
||||
<style dangerouslySetInnerHTML={{ __html: 'html, body, #storybook-root { height: 100%; }' }} />
|
||||
@ -124,15 +123,21 @@ const preview = (window as any).__STORYBOOK_PREVIEW__ as PreviewWeb<ReactRendere
|
||||
const channel = (window as any).__STORYBOOK_ADDONS_CHANNEL__ as Channel;
|
||||
export const loaders = [
|
||||
/**
|
||||
* This loader adds a DocsContext to the story, which is required for the most Blocks to work.
|
||||
* A story will specify which stories they need in the index with:
|
||||
* This loader adds a DocsContext to the story, which is required for the most Blocks to work. A
|
||||
* story will specify which stories they need in the index with:
|
||||
*
|
||||
* ```ts
|
||||
* parameters: {
|
||||
* relativeCsfPaths: ['../stories/MyStory.stories.tsx'], // relative to the story
|
||||
* relativeCsfPaths: ['../stories/MyStory.stories.tsx'], // relative to the story
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* The DocsContext will then be added via the decorator below.
|
||||
*/
|
||||
async ({ parameters: { relativeCsfPaths, attached = true } }) => {
|
||||
if (!relativeCsfPaths) return {};
|
||||
if (!relativeCsfPaths) {
|
||||
return {};
|
||||
}
|
||||
const csfFiles = await Promise.all(
|
||||
(relativeCsfPaths as string[]).map(async (blocksRelativePath) => {
|
||||
const projectRelativePath = `./lib/blocks/src/${blocksRelativePath.replace(
|
||||
@ -185,7 +190,8 @@ export const decorators = [
|
||||
<Story />
|
||||
),
|
||||
/**
|
||||
* This decorator renders the stories side-by-side, stacked or default based on the theme switcher in the toolbar
|
||||
* This decorator renders the stories side-by-side, stacked or default based on the theme switcher
|
||||
* in the toolbar
|
||||
*/
|
||||
(StoryFn, { globals, playFunction, args, storyGlobals, parameters }) => {
|
||||
let theme = globals.sb_theme;
|
||||
@ -267,9 +273,9 @@ export const decorators = [
|
||||
}
|
||||
},
|
||||
/**
|
||||
* This decorator shows the current state of the arg named in the
|
||||
* parameters.withRawArg property, by updating the arg in the onChange function
|
||||
* this also means that the arg will sync with the control panel
|
||||
* This decorator shows the current state of the arg named in the parameters.withRawArg property,
|
||||
* by updating the arg in the onChange function this also means that the arg will sync with the
|
||||
* control panel
|
||||
*
|
||||
* If parameters.withRawArg is not set, this decorator will do nothing
|
||||
*/
|
||||
|
@ -15,10 +15,7 @@ let activeStoryId: string | undefined;
|
||||
|
||||
const defaultParameters = { config: {}, options: {} };
|
||||
|
||||
/**
|
||||
* Handle A11yContext events.
|
||||
* Because the event are sent without manual check, we split calls
|
||||
*/
|
||||
/** Handle A11yContext events. Because the event are sent without manual check, we split calls */
|
||||
const handleRequest = async (storyId: string, input: A11yParameters | null) => {
|
||||
if (!input?.manual) {
|
||||
await run(storyId, input ?? defaultParameters);
|
||||
|
@ -105,7 +105,9 @@ export const A11yContextProvider: React.FC<React.PropsWithChildren<A11yContextPr
|
||||
}
|
||||
}, [active, handleClearHighlights, emit, storyEntry]);
|
||||
|
||||
if (!active) return null;
|
||||
if (!active) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<A11yContext.Provider
|
||||
|
@ -11,8 +11,8 @@ const isInInitialArgs = (name: string, initialArgs: Args) =>
|
||||
typeof initialArgs[name] === 'undefined' && !(name in initialArgs);
|
||||
|
||||
/**
|
||||
* Automatically add action args for argTypes whose name
|
||||
* matches a regex, such as `^on.*` for react-style `onClick` etc.
|
||||
* Automatically add action args for argTypes whose name matches a regex, such as `^on.*` for
|
||||
* react-style `onClick` etc.
|
||||
*/
|
||||
|
||||
export const inferActionsFromArgTypesRegex: ArgsEnhancer<Renderer> = (context) => {
|
||||
@ -39,9 +39,7 @@ export const inferActionsFromArgTypesRegex: ArgsEnhancer<Renderer> = (context) =
|
||||
}, {} as Args);
|
||||
};
|
||||
|
||||
/**
|
||||
* Add action args for list of strings.
|
||||
*/
|
||||
/** Add action args for list of strings. */
|
||||
export const addActionsFromArgTypes: ArgsEnhancer<Renderer> = (context) => {
|
||||
const {
|
||||
initialArgs,
|
||||
|
@ -48,7 +48,11 @@ export const ActionLogger = ({ actions, onClear }: ActionLoggerProps) => {
|
||||
|
||||
useEffect(() => {
|
||||
// Scroll to bottom, when the action panel was already scrolled down
|
||||
if (wasAtBottom) wrapperRef.current.scrollTop = wrapperRef.current.scrollHeight;
|
||||
|
||||
// Scroll to bottom, when the action panel was already scrolled down
|
||||
if (wasAtBottom) {
|
||||
wrapperRef.current.scrollTop = wrapperRef.current.scrollHeight;
|
||||
}
|
||||
}, [wasAtBottom, actions.length]);
|
||||
|
||||
return (
|
||||
|
@ -12,7 +12,10 @@ const logActionsWhenMockCalled: LoaderFunction = (context) => {
|
||||
const {
|
||||
parameters: { actions },
|
||||
} = context;
|
||||
if (actions?.disable) return;
|
||||
|
||||
if (actions?.disable) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!subscribed &&
|
||||
@ -22,7 +25,10 @@ const logActionsWhenMockCalled: LoaderFunction = (context) => {
|
||||
const onMockCall = global.__STORYBOOK_TEST_ON_MOCK_CALL__ as typeof onMockCallType;
|
||||
onMockCall((mock, args) => {
|
||||
const name = mock.getMockName();
|
||||
if (name === 'spy') return;
|
||||
|
||||
if (name === 'spy') {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Make this a configurable API in 8.2
|
||||
if (
|
||||
|
@ -14,7 +14,10 @@ import { config } from './configureActions';
|
||||
type SyntheticEvent = any; // import('react').SyntheticEvent;
|
||||
const findProto = (obj: unknown, callback: (proto: any) => boolean): Function | null => {
|
||||
const proto = Object.getPrototypeOf(obj);
|
||||
if (!proto || callback(proto)) return proto;
|
||||
|
||||
if (!proto || callback(proto)) {
|
||||
return proto;
|
||||
}
|
||||
return findProto(proto, callback);
|
||||
};
|
||||
const isReactSyntheticEvent = (e: unknown): e is SyntheticEvent =>
|
||||
|
@ -61,16 +61,21 @@ export const ControlsPanel = ({ saveStory, createStory }: ControlsPanelProps) =>
|
||||
// If the story is prepared, then show the args table
|
||||
// and reset the loading states
|
||||
useEffect(() => {
|
||||
if (previewInitialized) setIsLoading(false);
|
||||
if (previewInitialized) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [previewInitialized]);
|
||||
|
||||
const hasControls = Object.values(rows).some((arg) => arg?.control);
|
||||
|
||||
const withPresetColors = Object.entries(rows).reduce((acc, [key, arg]) => {
|
||||
const control = arg?.control;
|
||||
if (typeof control !== 'object' || control?.type !== 'color' || control?.presetColors)
|
||||
|
||||
if (typeof control !== 'object' || control?.type !== 'color' || control?.presetColors) {
|
||||
acc[key] = arg;
|
||||
else acc[key] = { ...arg, control: { ...control, presetColors } };
|
||||
} else {
|
||||
acc[key] = { ...arg, control: { ...control, presetColors } };
|
||||
}
|
||||
return acc;
|
||||
}, {} as ArgTypes);
|
||||
|
||||
|
@ -105,7 +105,9 @@ export const SaveStory = ({ saveStory, createStory, resetArgs }: SaveStoryProps)
|
||||
const [errorMessage, setErrorMessage] = React.useState(null);
|
||||
|
||||
const onSaveStory = async () => {
|
||||
if (saving) return;
|
||||
if (saving) {
|
||||
return;
|
||||
}
|
||||
setSaving(true);
|
||||
await saveStory().catch(() => {});
|
||||
setSaving(false);
|
||||
@ -125,7 +127,10 @@ export const SaveStory = ({ saveStory, createStory, resetArgs }: SaveStoryProps)
|
||||
};
|
||||
const onSubmitForm = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
if (saving) return;
|
||||
|
||||
if (saving) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setErrorMessage(null);
|
||||
setSaving(true);
|
||||
|
@ -41,7 +41,9 @@ function Title() {
|
||||
|
||||
const stringifyArgs = (args: Record<string, any>) =>
|
||||
JSON.stringify(args, (_, value) => {
|
||||
if (typeof value === 'function') return '__sb_empty_function_arg__';
|
||||
if (typeof value === 'function') {
|
||||
return '__sb_empty_function_arg__';
|
||||
}
|
||||
return value;
|
||||
});
|
||||
|
||||
@ -50,7 +52,10 @@ addons.register(ADDON_ID, (api) => {
|
||||
|
||||
const saveStory = async () => {
|
||||
const data = api.getCurrentStoryData();
|
||||
if (data.type !== 'story') throw new Error('Not a story');
|
||||
|
||||
if (data.type !== 'story') {
|
||||
throw new Error('Not a story');
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await experimental_requestResponse<
|
||||
@ -60,7 +65,9 @@ addons.register(ADDON_ID, (api) => {
|
||||
// Only send updated args
|
||||
args: stringifyArgs(
|
||||
Object.entries(data.args || {}).reduce<Args>((acc, [key, value]) => {
|
||||
if (!deepEqual(value, data.initialArgs?.[key])) acc[key] = value;
|
||||
if (!deepEqual(value, data.initialArgs?.[key])) {
|
||||
acc[key] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {})
|
||||
),
|
||||
@ -98,7 +105,10 @@ addons.register(ADDON_ID, (api) => {
|
||||
|
||||
const createStory = async (name: string) => {
|
||||
const data = api.getCurrentStoryData();
|
||||
if (data.type !== 'story') throw new Error('Not a story');
|
||||
|
||||
if (data.type !== 'story') {
|
||||
throw new Error('Not a story');
|
||||
}
|
||||
|
||||
const response = await experimental_requestResponse<
|
||||
SaveStoryRequestPayload,
|
||||
@ -146,9 +156,14 @@ addons.register(ADDON_ID, (api) => {
|
||||
});
|
||||
|
||||
channel.on(SAVE_STORY_RESPONSE, (data: ResponseData<SaveStoryResponsePayload>) => {
|
||||
if (!data.success) return;
|
||||
if (!data.success) {
|
||||
return;
|
||||
}
|
||||
const story = api.getCurrentStoryData();
|
||||
if (story.type !== 'story') return;
|
||||
|
||||
if (story.type !== 'story') {
|
||||
return;
|
||||
}
|
||||
|
||||
api.resetStoryArgs(story);
|
||||
if (data.payload.newStoryId) {
|
||||
|
@ -28,7 +28,9 @@ export async function mdxPlugin(options: Options): Promise<Plugin> {
|
||||
name: 'storybook:mdx-plugin',
|
||||
enforce: 'pre',
|
||||
async transform(src, id) {
|
||||
if (!filter(id)) return undefined;
|
||||
if (!filter(id)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const mdxLoaderOptions: CompileOptions = await presets.apply('mdxLoaderOptions', {
|
||||
...mdxPluginOptions,
|
||||
|
@ -11,9 +11,8 @@ import rehypeSlug from 'rehype-slug';
|
||||
import type { CompileOptions } from './compiler';
|
||||
|
||||
/**
|
||||
* Get the resolvedReact preset, which points either to
|
||||
* the user's react dependencies or the react dependencies shipped with addon-docs
|
||||
* if the user has not installed react explicitly.
|
||||
* Get the resolvedReact preset, which points either to the user's react dependencies or the react
|
||||
* dependencies shipped with addon-docs if the user has not installed react explicitly.
|
||||
*/
|
||||
const getResolvedReact = async (options: Options) => {
|
||||
const resolvedReact = (await options.presets.apply('resolvedReact', {})) as any;
|
||||
@ -66,10 +65,13 @@ async function webpack(
|
||||
|
||||
let alias;
|
||||
|
||||
/** Add aliases for `@storybook/addon-docs` & `@storybook/blocks`
|
||||
* These must be singletons to avoid multiple instances of react & emotion being loaded, both would cause the components to fail to render.
|
||||
/**
|
||||
* Add aliases for `@storybook/addon-docs` & `@storybook/blocks` These must be singletons to avoid
|
||||
* multiple instances of react & emotion being loaded, both would cause the components to fail to
|
||||
* render.
|
||||
*
|
||||
* In the future the `@storybook/theming` and `@storybook/components` can be removed, as they should be singletons in the future due to the peerDependency on `storybook` package.
|
||||
* In the future the `@storybook/theming` and `@storybook/components` can be removed, as they
|
||||
* should be singletons in the future due to the peerDependency on `storybook` package.
|
||||
*/
|
||||
const cliPath = dirname(require.resolve('storybook/package.json'));
|
||||
const themingPath = join(cliPath, 'core', 'theming', 'index.js');
|
||||
@ -193,10 +195,14 @@ export const viteFinal = async (config: any, options: Options) => {
|
||||
...(isAbsolute(reactDom) && { 'react-dom/server': `${reactDom}/server.browser.js` }),
|
||||
'react-dom': reactDom,
|
||||
'@mdx-js/react': mdx,
|
||||
/** Add aliases for `@storybook/addon-docs` & `@storybook/blocks`
|
||||
* These must be singletons to avoid multiple instances of react & emotion being loaded, both would cause the components to fail to render.
|
||||
/**
|
||||
* Add aliases for `@storybook/addon-docs` & `@storybook/blocks` These must be singletons
|
||||
* to avoid multiple instances of react & emotion being loaded, both would cause the
|
||||
* components to fail to render.
|
||||
*
|
||||
* In the future the `@storybook/theming` and `@storybook/components` can be removed, as they should be singletons in the future due to the peerDependency on `storybook` package.
|
||||
* In the future the `@storybook/theming` and `@storybook/components` can be removed, as
|
||||
* they should be singletons in the future due to the peerDependency on `storybook`
|
||||
* package.
|
||||
*/
|
||||
'@storybook/theming/create': themingCreatePath,
|
||||
'@storybook/theming': themingPath,
|
||||
@ -224,11 +230,10 @@ const webpackX = webpack as any;
|
||||
const docsX = docs as any;
|
||||
|
||||
/**
|
||||
* If the user has not installed react explicitly in their project,
|
||||
* the resolvedReact preset will not be set.
|
||||
* We then set it here in addon-docs to use addon-docs's react version that always exists.
|
||||
* This is just a fallback that never overrides the existing preset,
|
||||
* but ensures that there is always a resolved react.
|
||||
* If the user has not installed react explicitly in their project, the resolvedReact preset will
|
||||
* not be set. We then set it here in addon-docs to use addon-docs's react version that always
|
||||
* exists. This is just a fallback that never overrides the existing preset, but ensures that there
|
||||
* is always a resolved react.
|
||||
*/
|
||||
export const resolvedReact = async (existing: any) => ({
|
||||
react: existing?.react ?? dirname(require.resolve('react/package.json')),
|
||||
|
@ -5,15 +5,18 @@ import * as ReactDomServer from 'react-dom/server';
|
||||
import { expect, within } from '@storybook/test';
|
||||
|
||||
/**
|
||||
* This component is used to display the resolved version of React and its related packages.
|
||||
* As long as `@storybook/addon-docs` is installed, `react` and `react-dom` should be available to import from and should resolve to the same version.
|
||||
* This component is used to display the resolved version of React and its related packages. As long
|
||||
* as `@storybook/addon-docs` is installed, `react` and `react-dom` should be available to import
|
||||
* from and should resolve to the same version.
|
||||
*
|
||||
* The autodocs here ensures that it also works in the generated documentation.
|
||||
*
|
||||
* - See the [MDX docs](/docs/addons-docs-docs2-resolvedreact--mdx) for how it resolves in MDX.
|
||||
* - See the [Story](/story/addons-docs-docs2-resolvedreact--story) for how it resolves in the actual story.
|
||||
* - See the [Story](/story/addons-docs-docs2-resolvedreact--story) for how it resolves in the actual
|
||||
* story.
|
||||
*
|
||||
* **Note: There appears to be a bug in the _production_ build of `react-dom`, where it reports version `18.2.0-next-9e3b772b8-20220608` while in fact version `18.2.0` is installed.**
|
||||
* **Note: There appears to be a bug in the _production_ build of `react-dom`, where it reports
|
||||
* version `18.2.0-next-9e3b772b8-20220608` while in fact version `18.2.0` is installed.**
|
||||
*/
|
||||
export default {
|
||||
title: 'Docs2/ResolvedReact',
|
||||
|
@ -8,24 +8,18 @@ export default {
|
||||
parameters: { chromatic: { disable: true } },
|
||||
};
|
||||
|
||||
/**
|
||||
* A basic button
|
||||
*/
|
||||
/** A basic button */
|
||||
export const Basic = {
|
||||
args: { label: 'Basic' },
|
||||
};
|
||||
|
||||
/**
|
||||
* Won't show up in DocsPage
|
||||
*/
|
||||
/** Won't show up in DocsPage */
|
||||
export const Disabled = {
|
||||
args: { label: 'Disabled in DocsPage' },
|
||||
parameters: { docs: { disable: true } },
|
||||
};
|
||||
|
||||
/**
|
||||
* Another button, just to show multiple stories
|
||||
*/
|
||||
/** Another button, just to show multiple stories */
|
||||
export const Another = {
|
||||
args: { label: 'Another' },
|
||||
parameters: {
|
||||
|
@ -7,9 +7,7 @@ export default {
|
||||
parameters: { chromatic: { disable: true } },
|
||||
};
|
||||
|
||||
/**
|
||||
* A story that throws
|
||||
*/
|
||||
/** A story that throws */
|
||||
export const ErrorStory = {
|
||||
decorators: [
|
||||
() => {
|
||||
|
@ -14,7 +14,7 @@
|
||||
```js
|
||||
import { setCustomElementsManifest } from '@storybook/web-components';
|
||||
import customElements from '../custom-elements.json';
|
||||
|
||||
|
||||
setCustomElementsManifest(customElements);
|
||||
```
|
||||
|
||||
|
@ -6,52 +6,60 @@ import { logger } from 'storybook/internal/node-logger';
|
||||
interface PresetOptions {
|
||||
/**
|
||||
* Allow to use @storybook/addon-actions
|
||||
* @see https://storybook.js.org/addons/@storybook/addon-actions
|
||||
*
|
||||
* @default true
|
||||
* @see https://storybook.js.org/addons/@storybook/addon-actions
|
||||
*/
|
||||
actions?: boolean;
|
||||
/**
|
||||
* Allow to use @storybook/addon-backgrounds
|
||||
* @see https://storybook.js.org/addons/@storybook/addon-backgrounds
|
||||
*
|
||||
* @default true
|
||||
* @see https://storybook.js.org/addons/@storybook/addon-backgrounds
|
||||
*/
|
||||
backgrounds?: boolean;
|
||||
configDir: string;
|
||||
/**
|
||||
* Allow to use @storybook/addon-controls
|
||||
* @see https://storybook.js.org/addons/@storybook/addon-controls
|
||||
*
|
||||
* @default true
|
||||
* @see https://storybook.js.org/addons/@storybook/addon-controls
|
||||
*/
|
||||
controls?: boolean;
|
||||
/**
|
||||
* Allow to use @storybook/addon-docs
|
||||
* @see https://storybook.js.org/addons/@storybook/addon-docs
|
||||
*
|
||||
* @default true
|
||||
* @see https://storybook.js.org/addons/@storybook/addon-docs
|
||||
*/
|
||||
docs?: boolean;
|
||||
/**
|
||||
* Allow to use @storybook/addon-measure
|
||||
* @see https://storybook.js.org/addons/@storybook/addon-measure
|
||||
*
|
||||
* @default true
|
||||
* @see https://storybook.js.org/addons/@storybook/addon-measure
|
||||
*/
|
||||
measure?: boolean;
|
||||
/**
|
||||
* Allow to use @storybook/addon-outline
|
||||
* @see https://storybook.js.org/addons/@storybook/addon-outline
|
||||
*
|
||||
* @default true
|
||||
* @see https://storybook.js.org/addons/@storybook/addon-outline
|
||||
*/
|
||||
outline?: boolean;
|
||||
themes?: boolean;
|
||||
/**
|
||||
* Allow to use @storybook/addon-toolbars
|
||||
* @see https://storybook.js.org/addons/@storybook/addon-toolbars
|
||||
*
|
||||
* @default true
|
||||
* @see https://storybook.js.org/addons/@storybook/addon-toolbars
|
||||
*/
|
||||
toolbars?: boolean;
|
||||
/**
|
||||
* Allow to use @storybook/addon-viewport
|
||||
* @see https://storybook.js.org/addons/@storybook/addon-viewport
|
||||
*
|
||||
* @default true
|
||||
* @see https://storybook.js.org/addons/@storybook/addon-viewport
|
||||
*/
|
||||
viewport?: boolean;
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ const highlightStyle = (color = '#FF4785', style: OutlineStyle = 'dashed') => `
|
||||
`;
|
||||
|
||||
interface HighlightInfo {
|
||||
/** html selector of the element */
|
||||
/** HTML selector of the element */
|
||||
elements: string[];
|
||||
color: string;
|
||||
style: OutlineStyle;
|
||||
|
@ -123,7 +123,10 @@ export const Panel = memo<{ storyId: string }>(function PanelMemoized({ storyId
|
||||
([end]: any) => setScrollTarget(end.isIntersecting ? undefined : end.target),
|
||||
{ root: global.document.querySelector('#panel-tab-content') }
|
||||
);
|
||||
if (endRef.current) observer.observe(endRef.current);
|
||||
|
||||
if (endRef.current) {
|
||||
observer.observe(endRef.current);
|
||||
}
|
||||
}
|
||||
return () => observer?.disconnect();
|
||||
}, []);
|
||||
|
@ -40,7 +40,9 @@ export const Empty = () => {
|
||||
return () => clearTimeout(load);
|
||||
}, []);
|
||||
|
||||
if (isLoading) return null;
|
||||
if (isLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EmptyTabContent
|
||||
|
@ -165,7 +165,9 @@ export const Interaction = ({
|
||||
const [isHovered, setIsHovered] = React.useState(false);
|
||||
const isInteractive = !controlStates.goto || !call.interceptable || !!call.ancestors.length;
|
||||
|
||||
if (isHidden) return null;
|
||||
if (isHidden) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<RowContainer call={call} pausedAt={pausedAt}>
|
||||
|
@ -60,7 +60,9 @@ export const Passing: Story = {
|
||||
interactions: getInteractions(CallStates.DONE),
|
||||
},
|
||||
play: async ({ args, canvasElement }) => {
|
||||
if (isChromatic()) return;
|
||||
if (isChromatic()) {
|
||||
return;
|
||||
}
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await waitFor(async () => {
|
||||
|
@ -6,9 +6,15 @@ import { Node } from './MethodCall';
|
||||
|
||||
const getParams = (line: string, fromIndex = 0): string => {
|
||||
for (let i = fromIndex, depth = 1; i < line.length; i += 1) {
|
||||
if (line[i] === '(') depth += 1;
|
||||
else if (line[i] === ')') depth -= 1;
|
||||
if (depth === 0) return line.slice(fromIndex, i);
|
||||
if (line[i] === '(') {
|
||||
depth += 1;
|
||||
} else if (line[i] === ')') {
|
||||
depth -= 1;
|
||||
}
|
||||
|
||||
if (depth === 0) {
|
||||
return line.slice(fromIndex, i);
|
||||
}
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
@ -69,7 +69,9 @@ const useThemeColors = () => {
|
||||
const special = /[^A-Z0-9]/i;
|
||||
const trimEnd = /[\s.,…]+$/gm;
|
||||
const ellipsize = (string: string, maxlength: number): string => {
|
||||
if (string.length <= maxlength) return string;
|
||||
if (string.length <= maxlength) {
|
||||
return string;
|
||||
}
|
||||
for (let i = maxlength - 1; i >= 0; i -= 1) {
|
||||
if (special.test(string[i]) && i > 10) {
|
||||
return `${string.slice(0, i).replace(trimEnd, '')}…`;
|
||||
@ -102,10 +104,7 @@ export const Node = ({
|
||||
}: {
|
||||
value: any;
|
||||
nested?: boolean;
|
||||
/**
|
||||
* Shows an object inspector instead of just printing the object.
|
||||
* Only available for Objects
|
||||
*/
|
||||
/** Shows an object inspector instead of just printing the object. Only available for Objects */
|
||||
showObjectInspector?: boolean;
|
||||
callsById?: Map<Call['id'], Call>;
|
||||
[props: string]: any;
|
||||
@ -422,7 +421,9 @@ export const MethodCall = ({
|
||||
callsById: Map<Call['id'], Call>;
|
||||
}) => {
|
||||
// Call might be undefined during initial render, can be safely ignored.
|
||||
if (!call) return null;
|
||||
if (!call) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (call.method === 'step' && call.path.length === 0) {
|
||||
return <StepNode label={call.args[0]} />;
|
||||
|
@ -1,7 +1,4 @@
|
||||
/**
|
||||
* Interaction Testing Theme
|
||||
* PLACEHOLDER until SB is updated <3
|
||||
*/
|
||||
/** Interaction Testing Theme PLACEHOLDER until SB is updated <3 */
|
||||
interface Colors {
|
||||
pure?: {
|
||||
gray?: any;
|
||||
|
@ -73,7 +73,9 @@ const colorizeText: (msg: string, type: string) => MsgElement[] = (msg: string,
|
||||
const getConvertedText: (msg: string) => MsgElement[] = (msg: string) => {
|
||||
let elementArray: MsgElement[] = [];
|
||||
|
||||
if (!msg) return elementArray;
|
||||
if (!msg) {
|
||||
return elementArray;
|
||||
}
|
||||
|
||||
const splitText = msg.split(/\[2m/).join('').split(/\[22m/);
|
||||
|
||||
|
@ -40,8 +40,13 @@ function roundedRect(context: CanvasRenderingContext2D, { x, y, w, h, r }: Round
|
||||
x = x - w / 2;
|
||||
y = y - h / 2;
|
||||
|
||||
if (w < 2 * r) r = w / 2;
|
||||
if (h < 2 * r) r = h / 2;
|
||||
if (w < 2 * r) {
|
||||
r = w / 2;
|
||||
}
|
||||
|
||||
if (h < 2 * r) {
|
||||
r = h / 2;
|
||||
}
|
||||
|
||||
context.beginPath();
|
||||
context.moveTo(x + r, y);
|
||||
@ -81,10 +86,8 @@ function positionCoordinate(
|
||||
}
|
||||
|
||||
/**
|
||||
* Offset the label based on how many layers appear before it
|
||||
* For example:
|
||||
* margin labels will shift further outwards if there are
|
||||
* padding labels
|
||||
* Offset the label based on how many layers appear before it For example: margin labels will shift
|
||||
* further outwards if there are padding labels
|
||||
*/
|
||||
function offset(
|
||||
type: LabelType,
|
||||
|
@ -1,6 +1,4 @@
|
||||
/**
|
||||
* Based on https://gist.github.com/awestbro/e668c12662ad354f02a413205b65fce7
|
||||
*/
|
||||
/** Based on https://gist.github.com/awestbro/e668c12662ad354f02a413205b65fce7 */
|
||||
import { global } from '@storybook/global';
|
||||
|
||||
import { draw } from './canvas';
|
||||
|
@ -85,7 +85,10 @@ export default function Onboarding({ api }: { api: API }) {
|
||||
(storyId: string) => {
|
||||
try {
|
||||
const { id, refId } = api.getCurrentStoryData() || {};
|
||||
if (id !== storyId || refId !== undefined) api.selectStory(storyId);
|
||||
|
||||
if (id !== storyId || refId !== undefined) {
|
||||
api.selectStory(storyId);
|
||||
}
|
||||
} catch (e) {}
|
||||
},
|
||||
[api]
|
||||
@ -132,17 +135,30 @@ export default function Onboarding({ api }: { api: API }) {
|
||||
|
||||
useEffect(() => {
|
||||
setStep((current) => {
|
||||
if (['1:Intro', '5:StoryCreated', '6:FinishedOnboarding'].includes(current)) return current;
|
||||
if (createNewStoryForm) return '4:CreateStory';
|
||||
if (saveFromControls) return '3:SaveFromControls';
|
||||
if (primaryControl) return '2:Controls';
|
||||
if (['1:Intro', '5:StoryCreated', '6:FinishedOnboarding'].includes(current)) {
|
||||
return current;
|
||||
}
|
||||
|
||||
if (createNewStoryForm) {
|
||||
return '4:CreateStory';
|
||||
}
|
||||
|
||||
if (saveFromControls) {
|
||||
return '3:SaveFromControls';
|
||||
}
|
||||
|
||||
if (primaryControl) {
|
||||
return '2:Controls';
|
||||
}
|
||||
return '1:Intro';
|
||||
});
|
||||
}, [createNewStoryForm, primaryControl, saveFromControls]);
|
||||
|
||||
useEffect(() => {
|
||||
return api.on(SAVE_STORY_RESPONSE, ({ payload, success }) => {
|
||||
if (!success || !payload?.newStoryName) return;
|
||||
if (!success || !payload?.newStoryName) {
|
||||
return;
|
||||
}
|
||||
setCreatedStory(payload);
|
||||
setShowConfetti(true);
|
||||
setStep('5:StoryCreated');
|
||||
|
@ -20,22 +20,49 @@ const StyledButton = styled.button<{ variant: ButtonProps['variant'] }>`
|
||||
justify-content: center;
|
||||
padding: 0 0.75rem;
|
||||
background: ${({ theme, variant }) => {
|
||||
if (variant === 'primary') return theme.color.secondary;
|
||||
if (variant === 'secondary') return theme.color.lighter;
|
||||
if (variant === 'outline') return 'transparent';
|
||||
if (variant === 'white') return theme.color.lightest;
|
||||
if (variant === 'primary') {
|
||||
return theme.color.secondary;
|
||||
}
|
||||
|
||||
if (variant === 'secondary') {
|
||||
return theme.color.lighter;
|
||||
}
|
||||
|
||||
if (variant === 'outline') {
|
||||
return 'transparent';
|
||||
}
|
||||
|
||||
if (variant === 'white') {
|
||||
return theme.color.lightest;
|
||||
}
|
||||
return theme.color.secondary;
|
||||
}};
|
||||
color: ${({ theme, variant }) => {
|
||||
if (variant === 'primary') return theme.color.lightest;
|
||||
if (variant === 'secondary') return theme.darkest;
|
||||
if (variant === 'outline') return theme.darkest;
|
||||
if (variant === 'white') return theme.color.secondary;
|
||||
if (variant === 'primary') {
|
||||
return theme.color.lightest;
|
||||
}
|
||||
|
||||
if (variant === 'secondary') {
|
||||
return theme.darkest;
|
||||
}
|
||||
|
||||
if (variant === 'outline') {
|
||||
return theme.darkest;
|
||||
}
|
||||
|
||||
if (variant === 'white') {
|
||||
return theme.color.secondary;
|
||||
}
|
||||
return theme.color.lightest;
|
||||
}};
|
||||
box-shadow: ${({ variant }) => {
|
||||
if (variant === 'secondary') return '#D9E8F2 0 0 0 1px inset';
|
||||
if (variant === 'outline') return '#D9E8F2 0 0 0 1px inset';
|
||||
if (variant === 'secondary') {
|
||||
return '#D9E8F2 0 0 0 1px inset';
|
||||
}
|
||||
|
||||
if (variant === 'outline') {
|
||||
return '#D9E8F2 0 0 0 1px inset';
|
||||
}
|
||||
return 'none';
|
||||
}};
|
||||
height: 32px;
|
||||
@ -49,27 +76,60 @@ const StyledButton = styled.button<{ variant: ButtonProps['variant'] }>`
|
||||
|
||||
&:hover {
|
||||
background-color: ${({ theme, variant }) => {
|
||||
if (variant === 'primary') return '#0b94eb';
|
||||
if (variant === 'secondary') return '#eef4f9';
|
||||
if (variant === 'outline') return 'transparent';
|
||||
if (variant === 'white') return theme.color.lightest;
|
||||
if (variant === 'primary') {
|
||||
return '#0b94eb';
|
||||
}
|
||||
|
||||
if (variant === 'secondary') {
|
||||
return '#eef4f9';
|
||||
}
|
||||
|
||||
if (variant === 'outline') {
|
||||
return 'transparent';
|
||||
}
|
||||
|
||||
if (variant === 'white') {
|
||||
return theme.color.lightest;
|
||||
}
|
||||
return '#0b94eb';
|
||||
}};
|
||||
color: ${({ theme, variant }) => {
|
||||
if (variant === 'primary') return theme.color.lightest;
|
||||
if (variant === 'secondary') return theme.darkest;
|
||||
if (variant === 'outline') return theme.darkest;
|
||||
if (variant === 'white') return theme.color.darkest;
|
||||
if (variant === 'primary') {
|
||||
return theme.color.lightest;
|
||||
}
|
||||
|
||||
if (variant === 'secondary') {
|
||||
return theme.darkest;
|
||||
}
|
||||
|
||||
if (variant === 'outline') {
|
||||
return theme.darkest;
|
||||
}
|
||||
|
||||
if (variant === 'white') {
|
||||
return theme.color.darkest;
|
||||
}
|
||||
return theme.color.lightest;
|
||||
}};
|
||||
}
|
||||
|
||||
&:focus {
|
||||
box-shadow: ${({ variant }) => {
|
||||
if (variant === 'primary') return 'inset 0 0 0 1px rgba(0, 0, 0, 0.2)';
|
||||
if (variant === 'secondary') return 'inset 0 0 0 1px #0b94eb';
|
||||
if (variant === 'outline') return 'inset 0 0 0 1px #0b94eb';
|
||||
if (variant === 'white') return 'none';
|
||||
if (variant === 'primary') {
|
||||
return 'inset 0 0 0 1px rgba(0, 0, 0, 0.2)';
|
||||
}
|
||||
|
||||
if (variant === 'secondary') {
|
||||
return 'inset 0 0 0 1px #0b94eb';
|
||||
}
|
||||
|
||||
if (variant === 'outline') {
|
||||
return 'inset 0 0 0 1px #0b94eb';
|
||||
}
|
||||
|
||||
if (variant === 'white') {
|
||||
return 'none';
|
||||
}
|
||||
return 'inset 0 0 0 2px rgba(0, 0, 0, 0.1)';
|
||||
}};
|
||||
}
|
||||
|
@ -26,15 +26,23 @@ export function GuidedTour({
|
||||
let timeout: NodeJS.Timeout;
|
||||
setStepIndex((current) => {
|
||||
const index = steps.findIndex(({ key }) => key === step);
|
||||
if (index === -1) return null;
|
||||
if (index === current) return current;
|
||||
|
||||
if (index === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (index === current) {
|
||||
return current;
|
||||
}
|
||||
timeout = setTimeout(setStepIndex, 500, index);
|
||||
return null;
|
||||
});
|
||||
return () => clearTimeout(timeout);
|
||||
}, [step, steps]);
|
||||
|
||||
if (stepIndex === null) return null;
|
||||
if (stepIndex === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Joyride
|
||||
@ -46,8 +54,13 @@ export function GuidedTour({
|
||||
disableOverlayClose
|
||||
disableScrolling
|
||||
callback={(data: CallBackProps) => {
|
||||
if (data.action === ACTIONS.CLOSE) onClose();
|
||||
if (data.action === ACTIONS.NEXT && data.index === data.size - 1) onComplete();
|
||||
if (data.action === ACTIONS.CLOSE) {
|
||||
onClose();
|
||||
}
|
||||
|
||||
if (data.action === ACTIONS.NEXT && data.index === data.size - 1) {
|
||||
onComplete();
|
||||
}
|
||||
}}
|
||||
floaterProps={{
|
||||
disableAnimation: true,
|
||||
|
@ -117,7 +117,10 @@ export const Tooltip: FC<TooltipProps> = ({
|
||||
document.head.appendChild(style);
|
||||
return () => {
|
||||
const styleElement = document.querySelector('#sb-onboarding-arrow-style');
|
||||
if (styleElement) styleElement.remove();
|
||||
|
||||
if (styleElement) {
|
||||
styleElement.remove();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
@ -194,7 +194,9 @@ export const SplashScreen = ({ onDismiss, duration = 6000 }: SplashScreenProps)
|
||||
}, [onDismiss]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!duration) return;
|
||||
if (!duration) {
|
||||
return;
|
||||
}
|
||||
const framelength = 1000 / 50; // 50 frames per second
|
||||
const increment = 100 / (duration / framelength); // 0-100% at 20ms intervals
|
||||
const interval = setInterval(() => setProgress((prev) => prev + increment), framelength);
|
||||
@ -202,7 +204,9 @@ export const SplashScreen = ({ onDismiss, duration = 6000 }: SplashScreenProps)
|
||||
}, [duration]);
|
||||
|
||||
useEffect(() => {
|
||||
if (ready) dismiss();
|
||||
if (ready) {
|
||||
dismiss();
|
||||
}
|
||||
}, [ready, dismiss]);
|
||||
|
||||
return (
|
||||
|
@ -5,7 +5,6 @@ import type { ThemeParameters } from '../constants';
|
||||
import { DEFAULT_THEME_PARAMETERS, GLOBAL_KEY, PARAM_KEY, THEMING_EVENTS } from '../constants';
|
||||
|
||||
/**
|
||||
*
|
||||
* @param StoryContext
|
||||
* @returns The global theme name set for your stories
|
||||
*/
|
||||
|
@ -8,9 +8,7 @@ import type { ToolbarArgType } from '../types';
|
||||
import { normalizeArgType } from '../utils/normalize-toolbar-arg-type';
|
||||
import { ToolbarMenuList } from './ToolbarMenuList';
|
||||
|
||||
/**
|
||||
* A smart component for handling manager-preview interactions.
|
||||
*/
|
||||
/** A smart component for handling manager-preview interactions. */
|
||||
export const ToolbarManager: FC = () => {
|
||||
const globalTypes = useGlobalTypes();
|
||||
const globalIds = Object.keys(globalTypes).filter((id) => !!globalTypes[id].toolbar);
|
||||
|
@ -1,25 +1,27 @@
|
||||
export type UserOptions = {
|
||||
/**
|
||||
* The directory where the Storybook configuration is located, relative to the vitest configuration file.
|
||||
* If not provided, the plugin will use '.storybook' in the current working directory.
|
||||
* @default '.storybook'
|
||||
* The directory where the Storybook configuration is located, relative to the vitest
|
||||
* configuration file. If not provided, the plugin will use '.storybook' in the current working
|
||||
* directory.
|
||||
*
|
||||
* @default '.storybook'
|
||||
*/
|
||||
configDir?: string;
|
||||
/**
|
||||
* Optional script to run Storybook.
|
||||
* If provided, Vitest will start Storybook using this script when ran in watch mode.
|
||||
* @default undefined
|
||||
* Optional script to run Storybook. If provided, Vitest will start Storybook using this script
|
||||
* when ran in watch mode.
|
||||
*
|
||||
* @default undefined
|
||||
*/
|
||||
storybookScript?: string;
|
||||
/**
|
||||
* The URL where Storybook is hosted.
|
||||
* This is used to provide a link to the story in the test output on failures.
|
||||
* @default 'http://localhost:6006'
|
||||
* The URL where Storybook is hosted. This is used to provide a link to the story in the test
|
||||
* output on failures.
|
||||
*
|
||||
* @default 'http://localhost:6006'
|
||||
*/
|
||||
storybookUrl?: string;
|
||||
/**
|
||||
* Tags to include, exclude, or skip. These tags are defined as annotations in your story or meta.
|
||||
*/
|
||||
/** Tags to include, exclude, or skip. These tags are defined as annotations in your story or meta. */
|
||||
tags?: {
|
||||
include?: string[];
|
||||
exclude?: string[];
|
||||
|
@ -16,7 +16,10 @@ interface ViewportsParam {
|
||||
|
||||
export const setViewport = async (viewportsParam: ViewportsParam = {} as ViewportsParam) => {
|
||||
const defaultViewport = viewportsParam.defaultViewport;
|
||||
if (!page || !globalThis.__vitest_browser__ || !defaultViewport) return null;
|
||||
|
||||
if (!page || !globalThis.__vitest_browser__ || !defaultViewport) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const viewports = {
|
||||
...INITIAL_VIEWPORTS,
|
||||
|
@ -5,13 +5,14 @@ import type { Options } from 'storybook/internal/types';
|
||||
import { listStories } from './list-stories';
|
||||
|
||||
/**
|
||||
* This file is largely based on https://github.com/storybookjs/storybook/blob/d1195cbd0c61687f1720fefdb772e2f490a46584/lib/core-common/src/utils/to-importFn.ts
|
||||
* This file is largely based on
|
||||
* https://github.com/storybookjs/storybook/blob/d1195cbd0c61687f1720fefdb772e2f490a46584/lib/core-common/src/utils/to-importFn.ts
|
||||
*/
|
||||
|
||||
/**
|
||||
* Paths get passed either with no leading './' - e.g. `src/Foo.stories.js`,
|
||||
* or with a leading `../` (etc), e.g. `../src/Foo.stories.js`.
|
||||
* We want to deal in importPaths relative to the working dir, so we normalize
|
||||
* Paths get passed either with no leading './' - e.g. `src/Foo.stories.js`, or with a leading `../`
|
||||
* (etc), e.g. `../src/Foo.stories.js`. We want to deal in importPaths relative to the working dir,
|
||||
* so we normalize
|
||||
*/
|
||||
function toImportPath(relativePath: string) {
|
||||
return relativePath.startsWith('../') ? relativePath : `./${relativePath}`;
|
||||
@ -19,9 +20,10 @@ function toImportPath(relativePath: string) {
|
||||
|
||||
/**
|
||||
* This function takes an array of stories and creates a mapping between the stories' relative paths
|
||||
* to the working directory and their dynamic imports. The import is done in an asynchronous function
|
||||
* to delay loading. It then creates a function, `importFn(path)`, which resolves a path to an import
|
||||
* function and this is called by Storybook to fetch a story dynamically when needed.
|
||||
* to the working directory and their dynamic imports. The import is done in an asynchronous
|
||||
* function to delay loading. It then creates a function, `importFn(path)`, which resolves a path to
|
||||
* an import function and this is called by Storybook to fetch a story dynamically when needed.
|
||||
*
|
||||
* @param stories An array of absolute story paths.
|
||||
*/
|
||||
async function toImportFn(stories: string[]) {
|
||||
|
@ -59,9 +59,11 @@ export async function generateModernIframeScriptCode(options: Options, projectRo
|
||||
};
|
||||
|
||||
/**
|
||||
* This code is largely taken from https://github.com/storybookjs/storybook/blob/d1195cbd0c61687f1720fefdb772e2f490a46584/builders/builder-webpack4/src/preview/virtualModuleModernEntry.js.handlebars
|
||||
* Some small tweaks were made to `getProjectAnnotations` (since `import()` needs to be resolved asynchronously)
|
||||
* and the HMR implementation has been tweaked to work with Vite.
|
||||
* This code is largely taken from
|
||||
* https://github.com/storybookjs/storybook/blob/d1195cbd0c61687f1720fefdb772e2f490a46584/builders/builder-webpack4/src/preview/virtualModuleModernEntry.js.handlebars
|
||||
* Some small tweaks were made to `getProjectAnnotations` (since `import()` needs to be resolved
|
||||
* asynchronously) and the HMR implementation has been tweaked to work with Vite.
|
||||
*
|
||||
* @todo Inline variable and remove `noinspection`
|
||||
*/
|
||||
const code = `
|
||||
|
@ -16,8 +16,8 @@ const allowedEnvVariables = [
|
||||
];
|
||||
|
||||
/**
|
||||
* Customized version of stringifyProcessEnvs from @storybook/core-common which
|
||||
* uses import.meta.env instead of process.env and checks for allowed variables.
|
||||
* Customized version of stringifyProcessEnvs from @storybook/core-common which uses import.meta.env
|
||||
* instead of process.env and checks for allowed variables.
|
||||
*/
|
||||
export function stringifyProcessEnvs(raw: Builder_EnvsRaw, envPrefix: ViteConfig['envPrefix']) {
|
||||
const updatedRaw: Builder_EnvsRaw = {};
|
||||
|
@ -111,7 +111,8 @@ const INCLUDE_CANDIDATES = [
|
||||
];
|
||||
|
||||
/**
|
||||
* Helper function which allows us to `filter` with an async predicate. Uses Promise.all for performance.
|
||||
* Helper function which allows us to `filter` with an async predicate. Uses Promise.all for
|
||||
* performance.
|
||||
*/
|
||||
const asyncFilter = async (arr: string[], predicate: (val: string) => Promise<boolean>) =>
|
||||
Promise.all(arr.map(predicate)).then((results) => arr.filter((_v, index) => results[index]));
|
||||
|
@ -18,23 +18,24 @@ const replacementMap = new Map([
|
||||
]);
|
||||
|
||||
/**
|
||||
* This plugin swaps out imports of pre-bundled storybook preview modules for destructured from global
|
||||
* variables that are added in runtime.js.
|
||||
* This plugin swaps out imports of pre-bundled storybook preview modules for destructured from
|
||||
* global variables that are added in runtime.js.
|
||||
*
|
||||
* For instance:
|
||||
* @example
|
||||
*
|
||||
* ```js
|
||||
* import { useMemo as useMemo2, useEffect as useEffect2 } from "@storybook/preview-api";
|
||||
* import { useMemo as useMemo2, useEffect as useEffect2 } from '@storybook/preview-api';
|
||||
* ```
|
||||
*
|
||||
* becomes
|
||||
* Becomes
|
||||
*
|
||||
* ```js
|
||||
* const { useMemo: useMemo2, useEffect: useEffect2 } = __STORYBOOK_MODULE_PREVIEW_API__;
|
||||
* ```
|
||||
*
|
||||
* It is based on existing plugins like https://github.com/crcong/vite-plugin-externals
|
||||
* and https://github.com/eight04/rollup-plugin-external-globals, but simplified to meet our simple needs.
|
||||
* It is based on existing plugins like https://github.com/crcong/vite-plugin-externals and
|
||||
* https://github.com/eight04/rollup-plugin-external-globals, but simplified to meet our simple
|
||||
* needs.
|
||||
*/
|
||||
export async function externalGlobalsPlugin(externals: Record<string, string>) {
|
||||
await init;
|
||||
@ -72,7 +73,10 @@ export async function externalGlobalsPlugin(externals: Record<string, string>) {
|
||||
// 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;
|
||||
|
||||
if (globalsList.every((glob) => !code.includes(glob))) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const [imports] = parse(code);
|
||||
const src = new MagicString(code);
|
||||
|
@ -12,7 +12,9 @@ export async function injectExportOrderPlugin() {
|
||||
// This should only run after the typescript has been transpiled
|
||||
enforce: 'post',
|
||||
async transform(code: string, id: string) {
|
||||
if (!filter(id)) return undefined;
|
||||
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`,
|
||||
|
@ -2,8 +2,8 @@ import MagicString from 'magic-string';
|
||||
import type { Plugin } from 'vite';
|
||||
|
||||
/**
|
||||
* This plugin removes HMR `accept` calls in story files. Stories should not be treated
|
||||
* as hmr boundaries, but vite has a bug which causes them to be treated as boundaries
|
||||
* This plugin removes HMR `accept` calls in story files. Stories should not be treated as hmr
|
||||
* boundaries, but vite has a bug which causes them to be treated as boundaries
|
||||
* (https://github.com/vitejs/vite/issues/9869).
|
||||
*/
|
||||
export async function stripStoryHMRBoundary(): Promise<Plugin> {
|
||||
@ -14,7 +14,9 @@ export async function stripStoryHMRBoundary(): Promise<Plugin> {
|
||||
name: 'storybook:strip-hmr-boundary-plugin',
|
||||
enforce: 'post',
|
||||
async transform(src: string, id: string) {
|
||||
if (!filter(id)) return undefined;
|
||||
if (!filter(id)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const s = new MagicString(src);
|
||||
s.replace(/import\.meta\.hot\.accept\(\);/, '');
|
||||
|
@ -25,15 +25,14 @@ type WebpackStatsPluginOptions = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Strips off query params added by rollup/vite to ids, to make paths compatible for comparison with git.
|
||||
* Strips off query params added by rollup/vite to ids, to make paths compatible for comparison with
|
||||
* git.
|
||||
*/
|
||||
function stripQueryParams(filePath: string): string {
|
||||
return filePath.split('?')[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* We only care about user code, not node_modules, vite files, or (most) virtual files.
|
||||
*/
|
||||
/** We only care about user code, not node_modules, vite files, or (most) virtual files. */
|
||||
function isUserCode(moduleName: string) {
|
||||
return Boolean(
|
||||
moduleName &&
|
||||
@ -48,9 +47,7 @@ function isUserCode(moduleName: string) {
|
||||
export type WebpackStatsPlugin = Plugin & { storybookGetStats: () => BuilderStats };
|
||||
|
||||
export function pluginWebpackStats({ workingDir }: WebpackStatsPluginOptions): WebpackStatsPlugin {
|
||||
/**
|
||||
* Convert an absolute path name to a path relative to the vite root, with a starting `./`
|
||||
*/
|
||||
/** Convert an absolute path name to a path relative to the vite root, with a starting `./` */
|
||||
function normalize(filename: string) {
|
||||
// Do not try to resolve virtual files
|
||||
if (filename.startsWith('/virtual:')) {
|
||||
@ -64,16 +61,12 @@ export function pluginWebpackStats({ workingDir }: WebpackStatsPluginOptions): W
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to create Reason objects out of a list of string paths
|
||||
*/
|
||||
/** Helper to create Reason objects out of a list of string paths */
|
||||
function createReasons(importers?: readonly string[]): Reason[] {
|
||||
return (importers || []).map((i) => ({ moduleName: normalize(i) }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to build a `Module` given a filename and list of files that import it
|
||||
*/
|
||||
/** Helper function to build a `Module` given a filename and list of files that import it */
|
||||
function createStatsMapModule(filename: string, importers?: readonly string[]): Module {
|
||||
return {
|
||||
id: filename,
|
||||
|
@ -19,8 +19,6 @@ export type StorybookConfigVite = {
|
||||
};
|
||||
|
||||
export type BuilderOptions = {
|
||||
/**
|
||||
* Path to vite.config file, relative to CWD.
|
||||
*/
|
||||
/** Path to `vite.config` file, relative to `process.cwd()`. */
|
||||
viteConfigPath?: string;
|
||||
};
|
||||
|
@ -7,8 +7,8 @@ function checkName(plugin: PluginOption, names: string[]) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if ANY of the plugins in the array have a name that matches one of the names in the names array.
|
||||
* Will resolve any promises in the array.
|
||||
* Returns true if ANY of the plugins in the array have a name that matches one of the names in the
|
||||
* names array. Will resolve any promises in the array.
|
||||
*/
|
||||
export async function hasVitePlugins(plugins: PluginOption[], names: string[]) {
|
||||
const resolvedPlugins = await Promise.all(plugins);
|
||||
|
@ -6,11 +6,10 @@ import type { PreviewAnnotation } from 'storybook/internal/types';
|
||||
import slash from 'slash';
|
||||
|
||||
/**
|
||||
* Preview annotations can take several forms, and vite needs them to be
|
||||
* a bit more restrained.
|
||||
* Preview annotations can take several forms, and vite needs them to be a bit more restrained.
|
||||
*
|
||||
* For node_modules, we want bare imports (so vite can process them),
|
||||
* and for files in the user's source, we want URLs absolute relative to project root.
|
||||
* For node_modules, we want bare imports (so vite can process them), and for files in the user's
|
||||
* source, we want URLs absolute relative to project root.
|
||||
*/
|
||||
export function processPreviewAnnotation(path: PreviewAnnotation | undefined, projectRoot: string) {
|
||||
// If entry is an object, take the first, which is the
|
||||
|
@ -1,9 +1,6 @@
|
||||
import type { PluginOption } from 'vite';
|
||||
|
||||
/**
|
||||
* Recursively removes all plugins with the names given
|
||||
* Resolves async plugins
|
||||
*/
|
||||
/** Recursively removes all plugins with the names given Resolves async plugins */
|
||||
export const withoutVitePlugins = async (
|
||||
plugins: PluginOption[] = [],
|
||||
namesToRemove: string[]
|
||||
|
@ -106,8 +106,8 @@ export const bail: WebpackBuilder['bail'] = async () => {
|
||||
};
|
||||
|
||||
/**
|
||||
* This function is a generator so that we can abort it mid process
|
||||
* in case of failure coming from other processes e.g. preview builder
|
||||
* This function is a generator so that we can abort it mid process in case of failure coming from
|
||||
* other processes e.g. preview builder
|
||||
*
|
||||
* I am sorry for making you read about generators today :')
|
||||
*/
|
||||
@ -231,8 +231,8 @@ function getWebpackStats({ config, stats }: { config: Configuration; stats: Stat
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is a generator so that we can abort it mid process
|
||||
* in case of failure coming from other processes e.g. manager builder
|
||||
* This function is a generator so that we can abort it mid process in case of failure coming from
|
||||
* other processes e.g. manager builder
|
||||
*
|
||||
* I am sorry for making you read about generators today :')
|
||||
*/
|
||||
|
@ -10,26 +10,20 @@ import type { Configuration, Stats } from 'webpack';
|
||||
|
||||
type TypeScriptOptionsBase = Partial<WebpackTypescriptOptions>;
|
||||
|
||||
/**
|
||||
* Options for TypeScript usage within Storybook.
|
||||
*/
|
||||
/** Options for TypeScript usage within Storybook. */
|
||||
export interface TypescriptOptions extends TypeScriptOptionsBase {
|
||||
/**
|
||||
* Configures `fork-ts-checker-webpack-plugin`
|
||||
*/
|
||||
/** Configures `fork-ts-checker-webpack-plugin` */
|
||||
checkOptions?: ConstructorParameters<typeof ForkTsCheckerWebpackPlugin>[0];
|
||||
}
|
||||
|
||||
export interface StorybookConfigWebpack extends Omit<StorybookConfig, 'webpack' | 'webpackFinal'> {
|
||||
/**
|
||||
* Modify or return a custom Webpack config after the Storybook's default configuration
|
||||
* has run (mostly used by addons).
|
||||
* Modify or return a custom Webpack config after the Storybook's default configuration has run
|
||||
* (mostly used by addons).
|
||||
*/
|
||||
webpack?: (config: Configuration, options: Options) => Configuration | Promise<Configuration>;
|
||||
|
||||
/**
|
||||
* Modify or return a custom Webpack config after every addon has run.
|
||||
*/
|
||||
/** Modify or return a custom Webpack config after every addon has run. */
|
||||
webpackFinal?: (
|
||||
config: Configuration,
|
||||
options: Options
|
||||
|
@ -13,8 +13,10 @@ export async function generatePackageJsonFile(entries: ReturnType<typeof getEntr
|
||||
const location = join(cwd, 'package.json');
|
||||
const pkgJson = await readJSON(location);
|
||||
|
||||
/** Re-create the `exports` field in `code/core/package.json`
|
||||
* This way we only need to update the `./scripts/entries.ts` file to ensure all things we create actually exist and are mapped to the correct path.
|
||||
/**
|
||||
* Re-create the `exports` field in `code/core/package.json` This way we only need to update the
|
||||
* `./scripts/entries.ts` file to ensure all things we create actually exist and are mapped to the
|
||||
* correct path.
|
||||
*/
|
||||
pkgJson.exports = entries.reduce<Record<string, Record<string, string>>>((acc, entry) => {
|
||||
let main = './' + slash(relative(cwd, entry.file).replace('src', 'dist'));
|
||||
@ -47,8 +49,11 @@ export async function generatePackageJsonFile(entries: ReturnType<typeof getEntr
|
||||
// Add the package.json file to the exports, so we can use it to `require.resolve` the package's root easily
|
||||
pkgJson.exports['./package.json'] = './package.json';
|
||||
|
||||
/** Add the `typesVersion` field to `code/core/package.json`, to make typescript respect and find the correct type annotation files, even when not configured with `"moduleResolution": "Bundler"`
|
||||
* If we even decide to only support `"moduleResolution": "Bundler"`, we should be able to remove this part, but that would be a breaking change.
|
||||
/**
|
||||
* Add the `typesVersion` field to `code/core/package.json`, to make typescript respect and find
|
||||
* the correct type annotation files, even when not configured with `"moduleResolution":
|
||||
* "Bundler"` If we even decide to only support `"moduleResolution": "Bundler"`, we should be able
|
||||
* to remove this part, but that would be a breaking change.
|
||||
*/
|
||||
pkgJson.typesVersions = {
|
||||
'*': {
|
||||
|
@ -20,12 +20,14 @@ async function generateTypesMapperContent(filePath: string) {
|
||||
}
|
||||
|
||||
export async function generateTypesMapperFiles(entries: ReturnType<typeof getEntries>) {
|
||||
/** Generate the type mapper files, which are used to map the types to the SOURCE location.
|
||||
* This would be for development builds ONLY, **HOWEVER**:
|
||||
* During a production build we ALSO run this, because we want to generate a `d.ts` file for each entry in parallel.
|
||||
* By generating these files (in parallel) first, we can then ensure we can compile the actual type definitions in parallel.
|
||||
* This is because the type definitions have interdependencies between them.
|
||||
* These interdependencies are MEGA complex, and this simplified approach immensely is the only way to ensure we can compile them in parallel.
|
||||
/**
|
||||
* Generate the type mapper files, which are used to map the types to the SOURCE location. This
|
||||
* would be for development builds ONLY, **HOWEVER**: During a production build we ALSO run this,
|
||||
* because we want to generate a `d.ts` file for each entry in parallel. By generating these files
|
||||
* (in parallel) first, we can then ensure we can compile the actual type definitions in parallel.
|
||||
* This is because the type definitions have interdependencies between them. These
|
||||
* interdependencies are MEGA complex, and this simplified approach immensely is the only way to
|
||||
* ensure we can compile them in parallel.
|
||||
*/
|
||||
const all = entries.filter((e) => e.dts).map((e) => e.file);
|
||||
|
||||
|
@ -5,9 +5,10 @@ import { dedent } from '../../../../scripts/prepare/tools';
|
||||
|
||||
export async function modifyThemeTypes() {
|
||||
/**
|
||||
* This is a unique hack (pre-existing the CPC project) because the only way to set a custom Theme interface with emotion, is by module enhancement.
|
||||
* This is not an option for us, because we pre-bundle emotion in.
|
||||
* The little hack work to ensure the `Theme` export is overloaded with our `StorybookTheme` interface. (in both development and production builds)
|
||||
* This is a unique hack (pre-existing the CPC project) because the only way to set a custom Theme
|
||||
* interface with emotion, is by module enhancement. This is not an option for us, because we
|
||||
* pre-bundle emotion in. The little hack work to ensure the `Theme` export is overloaded with our
|
||||
* `StorybookTheme` interface. (in both development and production builds)
|
||||
*/
|
||||
const target = join(__dirname, '..', '..', 'dist', 'theming', 'index.d.ts');
|
||||
const contents = await readFile(target, 'utf-8');
|
||||
|
@ -312,9 +312,10 @@ async function run() {
|
||||
await context.dispose();
|
||||
|
||||
/**
|
||||
* I'm leaving this in place, because I want to start utilizing it in the future.
|
||||
* I'm imagining a github action that shows the bundle analysis in the PR.
|
||||
* I didn't have the project-scope to make that happen now, but I want expose this very rich useful data accessible, for the next person investigating bundle size issues.
|
||||
* I'm leaving this in place, because I want to start utilizing it in the future. I'm
|
||||
* imagining a github action that shows the bundle analysis in the PR. I didn't have the
|
||||
* project-scope to make that happen now, but I want expose this very rich useful data
|
||||
* accessible, for the next person investigating bundle size issues.
|
||||
*/
|
||||
|
||||
// if (out.metafile) {
|
||||
|
@ -116,8 +116,8 @@ export const executor = {
|
||||
};
|
||||
|
||||
/**
|
||||
* This function is a generator so that we can abort it mid process
|
||||
* in case of failure coming from other processes e.g. preview builder
|
||||
* This function is a generator so that we can abort it mid process in case of failure coming from
|
||||
* other processes e.g. preview builder
|
||||
*
|
||||
* I am sorry for making you read about generators today :')
|
||||
*/
|
||||
@ -214,8 +214,8 @@ const starter: StarterFunction = async function* starterGeneratorFn({
|
||||
};
|
||||
|
||||
/**
|
||||
* This function is a generator so that we can abort it mid process
|
||||
* in case of failure coming from other processes e.g. preview builder
|
||||
* This function is a generator so that we can abort it mid process in case of failure coming from
|
||||
* other processes e.g. preview builder
|
||||
*
|
||||
* I am sorry for making you read about generators today :')
|
||||
*/
|
||||
|
@ -21,20 +21,23 @@ const sanitizeFinal = (path: string) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Manager entries should be **self-invoking** bits of code.
|
||||
* They can of-course import from modules, and ESbuild will bundle all of that into a single file.
|
||||
* But they should not export anything. However this can't be enforced, so what we do is wrap the given file, in a bit of code like this:
|
||||
* Manager entries should be **self-invoking** bits of code. They can of-course import from modules,
|
||||
* and ESbuild will bundle all of that into a single file. But they should not export anything.
|
||||
* However this can't be enforced, so what we do is wrap the given file, in a bit of code like
|
||||
* this:
|
||||
*
|
||||
* ```js
|
||||
* import '<<file>>';
|
||||
* ```
|
||||
*
|
||||
* That way we are indicating to ESbuild that we do not care about this files exports, and they will be dropped in the bundle.
|
||||
* That way we are indicating to ESbuild that we do not care about this files exports, and they will
|
||||
* be dropped in the bundle.
|
||||
*
|
||||
* We do all of that so we can wrap a try-catch around the code.
|
||||
* That would have been invalid syntax had the export statements been left in place.
|
||||
* We do all of that so we can wrap a try-catch around the code. That would have been invalid syntax
|
||||
* had the export statements been left in place.
|
||||
*
|
||||
* We need to wrap each managerEntry with a try-catch because if we do not, a failing managerEntry can stop execution of other managerEntries.
|
||||
* We need to wrap each managerEntry with a try-catch because if we do not, a failing managerEntry
|
||||
* can stop execution of other managerEntries.
|
||||
*/
|
||||
export async function wrapManagerEntries(entrypoints: string[], uniqueId?: string) {
|
||||
return Promise.all(
|
||||
|
@ -21,9 +21,11 @@ type Options = Config & {
|
||||
|
||||
/**
|
||||
* Creates a new browser channel instance.
|
||||
*
|
||||
* @param {Options} options - The options object.
|
||||
* @param {Page} options.page - page identifier.
|
||||
* @param {ChannelTransport[]} [options.extraTransports=[]] - An optional array of extra channel transports.
|
||||
* @param {Page} options.page - Page identifier.
|
||||
* @param {ChannelTransport[]} [options.extraTransports=[]] - An optional array of extra channel
|
||||
* transports. Default is `[]`
|
||||
* @returns {Channel} - The new channel instance.
|
||||
*/
|
||||
export function createBrowserChannel({ page, extraTransports = [] }: Options): Channel {
|
||||
|
@ -58,8 +58,9 @@ export class PostMessageTransport implements ChannelTransport {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends `event` to the associated window. If the window does not yet exist
|
||||
* the event will be stored in a buffer and sent when the window exists.
|
||||
* Sends `event` to the associated window. If the window does not yet exist the event will be
|
||||
* stored in a buffer and sent when the window exists.
|
||||
*
|
||||
* @param event
|
||||
*/
|
||||
send(event: ChannelEvent, options?: any): Promise<any> {
|
||||
|
@ -107,8 +107,8 @@ export function detectFrameworkPreset(
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to detect which builder to use, by searching for a vite config file or webpack installation.
|
||||
* If neither are found it will choose the default builder based on the project type.
|
||||
* Attempts to detect which builder to use, by searching for a vite config file or webpack
|
||||
* installation. If neither are found it will choose the default builder based on the project type.
|
||||
*
|
||||
* @returns CoreBuilder
|
||||
*/
|
||||
|
@ -51,9 +51,17 @@ export async function extractEslintInfo(packageManager: JsPackageManager): Promi
|
||||
}
|
||||
|
||||
export const normalizeExtends = (existingExtends: any): string[] => {
|
||||
if (!existingExtends) return [];
|
||||
if (typeof existingExtends === 'string') return [existingExtends];
|
||||
if (Array.isArray(existingExtends)) return existingExtends;
|
||||
if (!existingExtends) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (typeof existingExtends === 'string') {
|
||||
return [existingExtends];
|
||||
}
|
||||
|
||||
if (Array.isArray(existingExtends)) {
|
||||
return existingExtends;
|
||||
}
|
||||
throw new Error(`Invalid eslint extends ${existingExtends}`);
|
||||
};
|
||||
|
||||
|
@ -50,17 +50,25 @@ export const writeFileAsJson = (jsonPath: string, content: unknown) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Detect if any babel dependencies need to be added to the project
|
||||
* This is currently used by react-native generator
|
||||
* @param {Object} packageJson The current package.json so we can inspect its contents
|
||||
* @returns {Array} Contains the packages and versions that need to be installed
|
||||
* Detect if any babel dependencies need to be added to the project This is currently used by
|
||||
* react-native generator
|
||||
*
|
||||
* @example
|
||||
* const babelDependencies = await getBabelDependencies(packageManager, npmOptions, packageJson);
|
||||
* // you can then spread the result when using installDependencies
|
||||
*
|
||||
* ```ts
|
||||
* const babelDependencies = await getBabelDependencies(
|
||||
* packageManager,
|
||||
* npmOptions,
|
||||
* packageJson
|
||||
* ); // you can then spread the result when using installDependencies
|
||||
* installDependencies(npmOptions, [
|
||||
* `@storybook/react@${storybookVersion}`,
|
||||
* ...babelDependencies,
|
||||
* ]);
|
||||
* ```
|
||||
*
|
||||
* @param {Object} packageJson The current package.json so we can inspect its contents
|
||||
* @returns {Array} Contains the packages and versions that need to be installed
|
||||
*/
|
||||
export async function getBabelDependencies(
|
||||
packageManager: JsPackageManager,
|
||||
@ -133,9 +141,7 @@ type CopyTemplateFilesOptions = {
|
||||
destination?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated Please use `frameworkToRenderer` from `@storybook/core-common` instead
|
||||
*/
|
||||
/** @deprecated Please use `frameworkToRenderer` from `@storybook/core-common` instead */
|
||||
export const frameworkToRenderer = CoreFrameworkToRenderer;
|
||||
|
||||
export const frameworkToDefaultBuilder: Record<SupportedFrameworks, CoreBuilder> = {
|
||||
@ -224,8 +230,11 @@ export async function copyTemplateFiles({
|
||||
|
||||
if (commonAssetsDir) {
|
||||
let rendererType = frameworkToRenderer[renderer] || 'react';
|
||||
|
||||
// This is only used for docs links and the docs site uses `vue` for both `vue` & `vue3` renderers
|
||||
if (rendererType === 'vue3') rendererType = 'vue';
|
||||
if (rendererType === 'vue3') {
|
||||
rendererType = 'vue';
|
||||
}
|
||||
await adjustTemplate(join(destinationPath, 'Configure.mdx'), { renderer: rendererType });
|
||||
}
|
||||
}
|
||||
|
@ -26,9 +26,7 @@ export const externalFrameworks: ExternalFramework[] = [
|
||||
{ name: 'solid', frameworks: ['storybook-solidjs-vite'], renderer: 'storybook-solidjs' },
|
||||
];
|
||||
|
||||
/**
|
||||
* @deprecated Please use `SupportedFrameworks` from `@storybook/types` instead
|
||||
*/
|
||||
/** @deprecated Please use `SupportedFrameworks` from `@storybook/types` instead */
|
||||
export type SupportedRenderers = CoreSupportedFrameworks;
|
||||
|
||||
export const SUPPORTED_RENDERERS: SupportedRenderers[] = [
|
||||
@ -103,7 +101,7 @@ export type TemplateMatcher = {
|
||||
|
||||
export type TemplateConfiguration = {
|
||||
preset: ProjectType;
|
||||
/** will be checked both against dependencies and devDependencies */
|
||||
/** Will be checked both against dependencies and devDependencies */
|
||||
dependencies?: string[] | { [dependency: string]: (version: string) => boolean };
|
||||
peerDependencies?: string[] | { [dependency: string]: (version: string) => boolean };
|
||||
files?: string[];
|
||||
@ -113,9 +111,9 @@ export type TemplateConfiguration = {
|
||||
/**
|
||||
* Configuration to match a storybook preset template.
|
||||
*
|
||||
* This has to be an array sorted in order of specificity/priority.
|
||||
* Reason: both REACT and WEBPACK_REACT have react as dependency,
|
||||
* therefore WEBPACK_REACT has to come first, as it's more specific.
|
||||
* This has to be an array sorted in order of specificity/priority. Reason: both REACT and
|
||||
* WEBPACK_REACT have react as dependency, therefore WEBPACK_REACT has to come first, as it's more
|
||||
* specific.
|
||||
*/
|
||||
export const supportedTemplates: TemplateConfiguration[] = [
|
||||
{
|
||||
|
@ -57,7 +57,9 @@ const logged = new Set();
|
||||
export const once =
|
||||
(type: keyof typeof logger) =>
|
||||
(message: any, ...rest: any[]) => {
|
||||
if (logged.has(message)) return undefined;
|
||||
if (logged.has(message)) {
|
||||
return undefined;
|
||||
}
|
||||
logged.add(message);
|
||||
return logger[type](message, ...rest);
|
||||
};
|
||||
|
@ -24,7 +24,7 @@ type StorybookPackage = keyof typeof storybookPackagesVersions;
|
||||
* Extract package name and version from input
|
||||
*
|
||||
* @param pkg A string like `@storybook/cli`, `react` or `react@^16`
|
||||
* @return A tuple of 2 elements: [packageName, packageVersion]
|
||||
* @returns A tuple of 2 elements: [packageName, packageVersion]
|
||||
*/
|
||||
export function getPackageDetails(pkg: string): [string, string?] {
|
||||
const idx = pkg.lastIndexOf('@');
|
||||
@ -57,9 +57,7 @@ export abstract class JsPackageManager {
|
||||
basePath?: string
|
||||
): Promise<PackageJson | null>;
|
||||
|
||||
/**
|
||||
* Get the INSTALLED version of a package from the package.json file
|
||||
*/
|
||||
/** Get the INSTALLED version of a package from the package.json file */
|
||||
async getPackageVersion(packageName: string, basePath = this.cwd): Promise<string | null> {
|
||||
const packageJSON = await this.getPackageJSON(packageName, basePath);
|
||||
return packageJSON ? packageJSON.version ?? null : null;
|
||||
@ -69,9 +67,11 @@ export abstract class JsPackageManager {
|
||||
this.cwd = options?.cwd || process.cwd();
|
||||
}
|
||||
|
||||
/** Detect whether Storybook gets initialized in a monorepository/workspace environment
|
||||
* The cwd doesn't have to be the root of the monorepo, it can be a subdirectory
|
||||
* @returns true, if Storybook is initialized inside a monorepository/workspace
|
||||
/**
|
||||
* Detect whether Storybook gets initialized in a mono-repository/workspace environment The cwd
|
||||
* doesn't have to be the root of the monorepo, it can be a subdirectory
|
||||
*
|
||||
* @returns `true`, if Storybook is initialized inside a mono-repository/workspace
|
||||
*/
|
||||
public isStorybookInMonorepo() {
|
||||
let cwd = process.cwd();
|
||||
@ -113,9 +113,7 @@ export abstract class JsPackageManager {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Install dependencies listed in `package.json`
|
||||
*/
|
||||
/** Install dependencies listed in `package.json` */
|
||||
public async installDependencies() {
|
||||
logger.log('Installing dependencies...');
|
||||
logger.log();
|
||||
@ -172,8 +170,8 @@ export abstract class JsPackageManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the `package.json` file available in the directory the command was call from
|
||||
* If there is no `package.json` it will create one.
|
||||
* Read the `package.json` file available in the directory the command was call from If there is
|
||||
* no `package.json` it will create one.
|
||||
*/
|
||||
public async retrievePackageJson(): Promise<PackageJsonWithDepsAndDevDeps> {
|
||||
let packageJson;
|
||||
@ -215,15 +213,20 @@ export abstract class JsPackageManager {
|
||||
/**
|
||||
* Add dependencies to a project using `yarn add` or `npm install`.
|
||||
*
|
||||
* @param {Object} options contains `skipInstall`, `packageJson` and `installAsDevDependencies` which we use to determine how we install packages.
|
||||
* @param {Array} dependencies contains a list of packages to add.
|
||||
* @example
|
||||
*
|
||||
* ```ts
|
||||
* addDependencies(options, [
|
||||
* `@storybook/react@${storybookVersion}`,
|
||||
* `@storybook/addon-actions@${actionsVersion}`,
|
||||
* `@storybook/addon-links@${linksVersion}`,
|
||||
* `@storybook/preview-api@${addonsVersion}`,
|
||||
* ]);
|
||||
* ```
|
||||
*
|
||||
* @param {Object} options Contains `skipInstall`, `packageJson` and `installAsDevDependencies`
|
||||
* which we use to determine how we install packages.
|
||||
* @param {Array} dependencies Contains a list of packages to add.
|
||||
*/
|
||||
public async addDependencies(
|
||||
options: {
|
||||
@ -270,13 +273,15 @@ export abstract class JsPackageManager {
|
||||
/**
|
||||
* Remove dependencies from a project using `yarn remove` or `npm uninstall`.
|
||||
*
|
||||
* @param {Object} options contains `skipInstall`, `packageJson` and `installAsDevDependencies` which we use to determine how we install packages.
|
||||
* @param {Array} dependencies contains a list of packages to remove.
|
||||
* @example
|
||||
* removeDependencies(options, [
|
||||
* `@storybook/react`,
|
||||
* `@storybook/addon-actions`,
|
||||
* ]);
|
||||
*
|
||||
* ```ts
|
||||
* removeDependencies(options, [`@storybook/react`, `@storybook/addon-actions`]);
|
||||
* ```
|
||||
*
|
||||
* @param {Object} options Contains `skipInstall`, `packageJson` and `installAsDevDependencies`
|
||||
* which we use to determine how we install packages.
|
||||
* @param {Array} dependencies Contains a list of packages to remove.
|
||||
*/
|
||||
public async removeDependencies(
|
||||
options: {
|
||||
@ -315,10 +320,11 @@ export abstract class JsPackageManager {
|
||||
/**
|
||||
* Return an array of strings matching following format: `<package_name>@<package_latest_version>`
|
||||
*
|
||||
* For packages in the storybook monorepo, when the latest version is equal to the version of the current CLI
|
||||
* the version is not added to the string.
|
||||
* For packages in the storybook monorepo, when the latest version is equal to the version of the
|
||||
* current CLI the version is not added to the string.
|
||||
*
|
||||
* When a package is in the monorepo, and the version is not equal to the CLI version, the version is taken from the versions.ts file and added to the string.
|
||||
* When a package is in the monorepo, and the version is not equal to the CLI version, the version
|
||||
* is taken from the versions.ts file and added to the string.
|
||||
*
|
||||
* @param packages
|
||||
*/
|
||||
@ -343,8 +349,8 @@ export abstract class JsPackageManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array of string standing for the latest version of the input packages.
|
||||
* To be able to identify which version goes with which package the order of the input array is keep.
|
||||
* Return an array of string standing for the latest version of the input packages. To be able to
|
||||
* identify which version goes with which package the order of the input array is keep.
|
||||
*
|
||||
* @param packageNames
|
||||
*/
|
||||
@ -357,10 +363,11 @@ export abstract class JsPackageManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the latest version of the input package available on npmjs registry.
|
||||
* If constraint are provided it return the latest version matching the constraints.
|
||||
* Return the latest version of the input package available on npmjs registry. If constraint are
|
||||
* provided it return the latest version matching the constraints.
|
||||
*
|
||||
* For `@storybook/*` packages the latest version is retrieved from `cli/src/versions.json` file directly
|
||||
* For `@storybook/*` packages the latest version is retrieved from `cli/src/versions.json` file
|
||||
* directly
|
||||
*
|
||||
* @param packageName The name of the package
|
||||
* @param constraint A valid semver constraint, example: '1.x || >=2.5.0 || 5.0.0 - 7.2.3'
|
||||
@ -393,8 +400,8 @@ export abstract class JsPackageManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest version of the package available on npmjs.com.
|
||||
* If constraint is set then it returns a version satisfying it, otherwise the latest version available is returned.
|
||||
* Get the latest version of the package available on npmjs.com. If constraint is set then it
|
||||
* returns a version satisfying it, otherwise the latest version available is returned.
|
||||
*
|
||||
* @param packageName Name of the package
|
||||
* @param constraint Version range to use to constraint the returned version
|
||||
@ -526,9 +533,7 @@ export abstract class JsPackageManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the installed (within node_modules or pnp zip) version of a specified package
|
||||
*/
|
||||
/** Returns the installed (within node_modules or pnp zip) version of a specified package */
|
||||
public async getInstalledVersion(packageName: string): Promise<string | null> {
|
||||
const installations = await this.findInstallations([packageName]);
|
||||
if (!installations) {
|
||||
|
@ -88,9 +88,7 @@ export class JsPackageManagerFactory {
|
||||
throw new Error('Unable to find a usable package manager within NPM, PNPM, Yarn and Yarn 2');
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up map of package manager proxies by name
|
||||
*/
|
||||
/** Look up map of package manager proxies by name */
|
||||
private static PROXY_MAP: Record<PackageManagerName, PackageManagerProxy> = {
|
||||
npm: NPMProxy,
|
||||
pnpm: PNPMProxy,
|
||||
@ -99,8 +97,8 @@ export class JsPackageManagerFactory {
|
||||
};
|
||||
|
||||
/**
|
||||
* Infer the package manager based on the command the user is running.
|
||||
* Each package manager sets the `npm_config_user_agent` environment variable with its name and version e.g. "npm/7.24.0"
|
||||
* Infer the package manager based on the command the user is running. Each package manager sets
|
||||
* the `npm_config_user_agent` environment variable with its name and version e.g. "npm/7.24.0"
|
||||
* Which is really useful when invoking commands via npx/pnpx/yarn create/etc.
|
||||
*/
|
||||
private static inferPackageManagerFromUserAgent(): PackageManagerName | undefined {
|
||||
|
@ -264,7 +264,6 @@ export class NPMProxy extends JsPackageManager {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param input The output of `npm ls --json`
|
||||
* @param pattern A list of package names to filter the result. * can be used as a placeholder
|
||||
*/
|
||||
|
@ -68,17 +68,12 @@ function resolvePresetFunction<T = any>(
|
||||
* Parse an addon into either a managerEntries or a preset. Throw on invalid input.
|
||||
*
|
||||
* Valid inputs:
|
||||
* - '@storybook/addon-actions/manager'
|
||||
* => { type: 'virtual', item }
|
||||
*
|
||||
* - '@storybook/addon-docs/preset'
|
||||
* => { type: 'presets', item }
|
||||
*
|
||||
* - '@storybook/addon-docs'
|
||||
* => { type: 'presets', item: '@storybook/addon-docs/preset' }
|
||||
*
|
||||
* - { name: '@storybook/addon-docs(/preset)?', options: { ... } }
|
||||
* => { type: 'presets', item: { name: '@storybook/addon-docs/preset', options } }
|
||||
* - `'@storybook/addon-actions/manager' => { type: 'virtual', item }`
|
||||
* - `'@storybook/addon-docs/preset' => { type: 'presets', item }`
|
||||
* - `'@storybook/addon-docs' => { type: 'presets', item: '@storybook/addon-docs/preset' }`
|
||||
* - `{ name: '@storybook/addon-docs(/preset)?', options: { } } => { type: 'presets', item: { name:
|
||||
* '@storybook/addon-docs/preset', options } }`
|
||||
*/
|
||||
|
||||
export const resolveAddonName = (
|
||||
@ -111,16 +106,19 @@ export const resolveAddonName = (
|
||||
}
|
||||
|
||||
const checkExists = (exportName: string) => {
|
||||
if (resolve(`${name}${exportName}`)) return `${name}${exportName}`;
|
||||
if (resolve(`${name}${exportName}`)) {
|
||||
return `${name}${exportName}`;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
// This is used to maintain back-compat with community addons that do not
|
||||
// re-export their sub-addons but reference the sub-addon name directly.
|
||||
// We need to turn it into an absolute path so that webpack can
|
||||
// serve it up correctly when yarn pnp or pnpm is being used.
|
||||
// Vite will be broken in such cases, because it does not process absolute paths,
|
||||
// and it will try to import from the bare import, breaking in pnp/pnpm.
|
||||
/**
|
||||
* This is used to maintain back-compat with community addons that do not re-export their
|
||||
* sub-addons but reference the sub-addon name directly. We need to turn it into an absolute path
|
||||
* so that webpack can serve it up correctly when yarn pnp or pnpm is being used. Vite will be
|
||||
* broken in such cases, because it does not process absolute paths, and it will try to import
|
||||
* from the bare import, breaking in pnp/pnpm.
|
||||
*/
|
||||
const absolutizeExport = (exportName: string, preferMJS: boolean) => {
|
||||
const found = resolve(`${name}${exportName}`);
|
||||
|
||||
|
@ -4,6 +4,8 @@ export class HandledError extends Error {
|
||||
constructor(error: unknown) {
|
||||
super(String(error));
|
||||
|
||||
if (typeof error !== 'string') this.cause = error;
|
||||
if (typeof error !== 'string') {
|
||||
this.cause = error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -25,7 +25,9 @@ const essentialAddons = [
|
||||
|
||||
const pkgName = (entry: CoreCommon_AddonEntry): string => {
|
||||
if (typeof entry === 'string') {
|
||||
if (entry.includes('node_modules')) return entry;
|
||||
if (entry.includes('node_modules')) {
|
||||
return entry;
|
||||
}
|
||||
return `@storybook/addon-${entry}`;
|
||||
}
|
||||
return (entry as CoreCommon_OptionsEntry).name;
|
||||
|
@ -26,8 +26,14 @@ const isCorrectOrder = (
|
||||
const essentialsIndex = addons.findIndex(predicateFor('@storybook/addon-essentials'));
|
||||
let beforeIndex = addons.findIndex(predicateFor(before.name));
|
||||
let afterIndex = addons.findIndex(predicateFor(after.name));
|
||||
if (beforeIndex === -1 && before.inEssentials) beforeIndex = essentialsIndex;
|
||||
if (afterIndex === -1 && after.inEssentials) afterIndex = essentialsIndex;
|
||||
|
||||
if (beforeIndex === -1 && before.inEssentials) {
|
||||
beforeIndex = essentialsIndex;
|
||||
}
|
||||
|
||||
if (afterIndex === -1 && after.inEssentials) {
|
||||
afterIndex = essentialsIndex;
|
||||
}
|
||||
return beforeIndex !== -1 && afterIndex !== -1 && beforeIndex <= afterIndex;
|
||||
};
|
||||
|
||||
|
@ -24,22 +24,22 @@ export async function temporaryDirectory({ prefix = '' } = {}) {
|
||||
export type FileOptions = MergeExclusive<
|
||||
{
|
||||
/**
|
||||
File extension.
|
||||
|
||||
Mutually exclusive with the `name` option.
|
||||
|
||||
_You usually won't need this option. Specify it only when actually needed._
|
||||
*/
|
||||
* File extension.
|
||||
*
|
||||
* Mutually exclusive with the `name` option.
|
||||
*
|
||||
* _You usually won't need this option. Specify it only when actually needed._
|
||||
*/
|
||||
readonly extension?: string;
|
||||
},
|
||||
{
|
||||
/**
|
||||
Filename.
|
||||
|
||||
Mutually exclusive with the `extension` option.
|
||||
|
||||
_You usually won't need this option. Specify it only when actually needed._
|
||||
*/
|
||||
* Filename.
|
||||
*
|
||||
* Mutually exclusive with the `extension` option.
|
||||
*
|
||||
* _You usually won't need this option. Specify it only when actually needed._
|
||||
*/
|
||||
readonly name?: string;
|
||||
}
|
||||
>;
|
||||
@ -67,10 +67,10 @@ export function parseList(str: string): string[] {
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a package manager, returns the coerced version of Storybook.
|
||||
* It tries to find renderer packages in the project and returns the coerced version of the first one found.
|
||||
* Example:
|
||||
* If @storybook/react version 8.0.0-alpha.14 is installed, it returns the coerced version 8.0.0
|
||||
* Given a package manager, returns the coerced version of Storybook. It tries to find renderer
|
||||
* packages in the project and returns the coerced version of the first one found. Example: If
|
||||
*
|
||||
* @storybook/react version 8.0.0-alpha.14 is installed, it returns the coerced version 8.0.0
|
||||
*/
|
||||
export async function getCoercedStorybookVersion(packageManager: JsPackageManager) {
|
||||
const packages = (
|
||||
@ -96,37 +96,38 @@ export function getEnvConfig(program: Record<string, any>, configEnv: Record<str
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a file name, creates an object with utilities to manage a log file.
|
||||
* It creates a temporary log file which you can manage with the returned functions.
|
||||
* You can then decide whether to move the log file to the users project, or remove it.
|
||||
* Given a file name, creates an object with utilities to manage a log file. It creates a temporary
|
||||
* log file which you can manage with the returned functions. You can then decide whether to move
|
||||
* the log file to the users project, or remove it.
|
||||
*
|
||||
* @example
|
||||
* ```
|
||||
* const { logStream, moveLogFile, removeLogFile, clearLogFile, readLogFile } = await createLogStream('my-log-file.log');
|
||||
*
|
||||
* // SCENARIO 1:
|
||||
* // you can write custom messages to generate a log file
|
||||
* logStream.write('my log message');
|
||||
* await moveLogFile();
|
||||
* ```ts
|
||||
* const { logStream, moveLogFile, removeLogFile, clearLogFile, readLogFile } =
|
||||
* await createLogStream('my-log-file.log');
|
||||
*
|
||||
* // SCENARIO 2:
|
||||
* // or you can pass it to stdio and capture the output of that command
|
||||
* try {
|
||||
* await this.executeCommand({
|
||||
* command: 'pnpm',
|
||||
* args: ['info', packageName, ...args],
|
||||
* // do not output to the user, and send stdio and stderr to log file
|
||||
* stdio: ['ignore', logStream, logStream]
|
||||
* });
|
||||
* } catch (err) {
|
||||
* // do something with the log file content
|
||||
* const output = await readLogFile();
|
||||
* // move the log file to the users project
|
||||
* await moveLogFile();
|
||||
* }
|
||||
* // success, no need to keep the log file
|
||||
* await removeLogFile();
|
||||
* // SCENARIO 1:
|
||||
* // you can write custom messages to generate a log file
|
||||
* logStream.write('my log message');
|
||||
* await moveLogFile();
|
||||
*
|
||||
* // SCENARIO 2:
|
||||
* // or you can pass it to stdio and capture the output of that command
|
||||
* try {
|
||||
* await this.executeCommand({
|
||||
* command: 'pnpm',
|
||||
* args: ['info', packageName, ...args],
|
||||
* // do not output to the user, and send stdio and stderr to log file
|
||||
* stdio: ['ignore', logStream, logStream],
|
||||
* });
|
||||
* } catch (err) {
|
||||
* // do something with the log file content
|
||||
* const output = await readLogFile();
|
||||
* // move the log file to the users project
|
||||
* await moveLogFile();
|
||||
* }
|
||||
* // success, no need to keep the log file
|
||||
* await removeLogFile();
|
||||
* ```
|
||||
*/
|
||||
export const createLogStream = async (
|
||||
|
@ -36,8 +36,9 @@ const getPrettier = async (): Promise<
|
||||
};
|
||||
|
||||
/**
|
||||
* Format the content of a file using prettier.
|
||||
* If prettier is not available in the user's project, it will fallback to use editorconfig settings if available and formats the file by a prettier-fallback.
|
||||
* Format the content of a file using prettier. If prettier is not available in the user's project,
|
||||
* it will fallback to use editorconfig settings if available and formats the file by a
|
||||
* prettier-fallback.
|
||||
*/
|
||||
export async function formatFileContent(filePath: string, content: string): Promise<string> {
|
||||
try {
|
||||
|
@ -5,9 +5,7 @@ import { dedent } from 'ts-dedent';
|
||||
import { frameworkPackages } from './get-storybook-info';
|
||||
import { normalizePath } from './normalize-path';
|
||||
|
||||
/**
|
||||
* Framework can be a string or an object. This utility always returns the string name.
|
||||
*/
|
||||
/** Framework can be a string or an object. This utility always returns the string name. */
|
||||
export async function getFrameworkName(options: Options) {
|
||||
const framework = await options.presets.apply('framework', '', options);
|
||||
|
||||
@ -23,11 +21,15 @@ export async function getFrameworkName(options: Options) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the proper framework name from the given framework field.
|
||||
* The framework field can be the framework package name or a path to the framework package.
|
||||
* Extracts the proper framework name from the given framework field. The framework field can be the
|
||||
* framework package name or a path to the framework package.
|
||||
*
|
||||
* @example
|
||||
* extractProperFrameworkName('/path/to/@storybook/angular') // => '@storybook/angular'
|
||||
* extractProperFrameworkName('@third-party/framework') // => '@third-party/framework'
|
||||
*
|
||||
* ```ts
|
||||
* ExtractProperFrameworkName('/path/to/@storybook/angular'); // => '@storybook/angular'
|
||||
* extractProperFrameworkName('@third-party/framework'); // => '@third-party/framework'
|
||||
* ```
|
||||
*/
|
||||
export const extractProperFrameworkName = (framework: string) => {
|
||||
const normalizedPath = normalizePath(framework);
|
||||
|
@ -5,8 +5,8 @@ import { extractProperFrameworkName, getFrameworkName } from './get-framework-na
|
||||
import { frameworkPackages } from './get-storybook-info';
|
||||
|
||||
/**
|
||||
* Render is set as a string on core. It must be set by the framework
|
||||
* It falls back to the framework name if not set
|
||||
* Render is set as a string on core. It must be set by the framework It falls back to the framework
|
||||
* name if not set
|
||||
*/
|
||||
export async function getRendererName(options: Options) {
|
||||
const core = await options.presets.apply('core', {}, options);
|
||||
@ -22,12 +22,17 @@ export async function getRendererName(options: Options) {
|
||||
|
||||
/**
|
||||
* Extracts the proper renderer name from the given framework name.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```ts
|
||||
* extractProperRendererNameFromFramework('@storybook/react'); // => 'react'
|
||||
* extractProperRendererNameFromFramework('@storybook/angular'); // => 'angular'
|
||||
* extractProperRendererNameFromFramework('@third-party/framework'); // => null
|
||||
* ```
|
||||
*
|
||||
* @param frameworkName The name of the framework.
|
||||
* @returns The name of the renderer.
|
||||
* @example
|
||||
* extractProperRendererNameFromFramework('@storybook/react') // => 'react'
|
||||
* extractProperRendererNameFromFramework('@storybook/angular') // => 'angular'
|
||||
* extractProperRendererNameFromFramework('@third-party/framework') // => null
|
||||
*/
|
||||
export async function extractProperRendererNameFromFramework(frameworkName: string) {
|
||||
const extractedFrameworkName = extractProperFrameworkName(frameworkName);
|
||||
|
@ -23,9 +23,7 @@ export const rendererPackages: Record<string, string> = {
|
||||
'storybook-framework-qwik': 'qwik',
|
||||
'storybook-solidjs': 'solid',
|
||||
|
||||
/**
|
||||
* @deprecated This is deprecated.
|
||||
*/
|
||||
/** @deprecated This is deprecated. */
|
||||
'@storybook/vue': 'vue',
|
||||
};
|
||||
|
||||
@ -103,7 +101,10 @@ export const getConfigInfo = (packageJson: PackageJson, configDir?: string) => {
|
||||
const storybookScript = packageJson.scripts?.storybook;
|
||||
if (storybookScript && !configDir) {
|
||||
const configParam = getStorybookConfiguration(storybookScript, '-c', '--config-dir');
|
||||
if (configParam) storybookConfigDir = configParam;
|
||||
|
||||
if (configParam) {
|
||||
storybookConfigDir = configParam;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -1,9 +1,10 @@
|
||||
/**
|
||||
* Return a string corresponding to template filled with bindings using following pattern:
|
||||
* For each (key, value) of `bindings` replace, in template, `{{key}}` by escaped version of `value`
|
||||
* Return a string corresponding to template filled with bindings using following pattern: For each
|
||||
* (key, value) of `bindings` replace, in template, `{{key}}` by escaped version of `value`
|
||||
*
|
||||
* @param template {String} Template with `{{binding}}`
|
||||
* @param bindings {Object} key-value object use to fill the template, `{{key}}` will be replaced by `escaped(value)`
|
||||
* @param bindings {Object} key-value object use to fill the template, `{{key}}` will be replaced by
|
||||
* `escaped(value)`
|
||||
* @returns {String} Filled template
|
||||
*/
|
||||
export const interpolate = (template: string, bindings: Record<string, string>) => {
|
||||
|
@ -2,12 +2,17 @@ import { posix } from 'node:path';
|
||||
|
||||
/**
|
||||
* Normalize a path to use forward slashes and remove .. and .
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```ts
|
||||
* normalizePath('path/to/../file'); // => 'path/file'
|
||||
* normalizePath('path/to/./file'); // => 'path/to/file'
|
||||
* normalizePath('path\\to\\file'); // => 'path/to/file'
|
||||
* ```
|
||||
*
|
||||
* @param p The path to normalize
|
||||
* @returns The normalized path
|
||||
* @example
|
||||
* normalizePath('path/to/../file') // => 'path/file'
|
||||
* normalizePath('path/to/./file') // => 'path/to/file'
|
||||
* normalizePath('path\\to\\file') // => 'path/to/file'
|
||||
*/
|
||||
export function normalizePath(p: string) {
|
||||
return posix.normalize(p.replace(/\\/g, '/'));
|
||||
|
@ -60,11 +60,11 @@ export const nodePathsToArray = (nodePath: string) =>
|
||||
.map((p) => resolve('./', p));
|
||||
|
||||
const relativePattern = /^\.{1,2}([/\\]|$)/;
|
||||
/**
|
||||
* Ensures that a path starts with `./` or `../`, or is entirely `.` or `..`
|
||||
*/
|
||||
/** Ensures that a path starts with `./` or `../`, or is entirely `.` or `..` */
|
||||
export function normalizeStoryPath(filename: string) {
|
||||
if (relativePattern.test(filename)) return filename;
|
||||
if (relativePattern.test(filename)) {
|
||||
return filename;
|
||||
}
|
||||
|
||||
return `.${sep}${filename}`;
|
||||
}
|
||||
|
@ -1,7 +1,5 @@
|
||||
import { posix as posixPath, sep } from 'node:path';
|
||||
|
||||
/**
|
||||
* Replaces the path separator with forward slashes
|
||||
*/
|
||||
/** Replaces the path separator with forward slashes */
|
||||
export const posix = (localPath: string, seperator: string = sep) =>
|
||||
localPath.split(seperator).filter(Boolean).join(posixPath.sep);
|
||||
|
@ -11,8 +11,11 @@ const logger = console;
|
||||
/**
|
||||
* Remove the given addon package and remove it from main.js
|
||||
*
|
||||
* Usage:
|
||||
* - sb remove @storybook/addon-links
|
||||
* @example
|
||||
*
|
||||
* ```sh
|
||||
* sb remove @storybook/addon-links
|
||||
* ```
|
||||
*/
|
||||
export async function removeAddon(
|
||||
addon: string,
|
||||
|
@ -4,11 +4,12 @@ import findCacheDirectory from 'find-cache-dir';
|
||||
|
||||
/**
|
||||
* Get the path of the file or directory with input name inside the Storybook cache directory:
|
||||
* - `node_modules/.cache/storybook/{directoryName}` in a Node.js project or npm package
|
||||
* - `.cache/storybook/{directoryName}` otherwise
|
||||
*
|
||||
* - `node_modules/.cache/storybook/{directoryName}` in a Node.js project or npm package
|
||||
* - `.cache/storybook/{directoryName}` otherwise
|
||||
*
|
||||
* @param fileOrDirectoryName {string} Name of the file or directory
|
||||
* @return {string} Absolute path to the file or directory
|
||||
* @returns {string} Absolute path to the file or directory
|
||||
*/
|
||||
export function resolvePathInStorybookCache(fileOrDirectoryName: string, sub = 'default'): string {
|
||||
let cacheDirectory = findCacheDirectory({ name: 'storybook' });
|
||||
|
@ -1,6 +1,4 @@
|
||||
/**
|
||||
* Mimicking the satisfies operator until we can upgrade to TS4.9
|
||||
*/
|
||||
/** Mimicking the satisfies operator until we can upgrade to TS4.9 */
|
||||
export function satisfies<A>() {
|
||||
return <T extends A>(x: T) => x;
|
||||
}
|
||||
|
@ -54,22 +54,35 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
ref
|
||||
) => {
|
||||
let Comp: 'button' | 'a' | typeof Slot = 'button';
|
||||
if (props.isLink) Comp = 'a';
|
||||
if (asChild) Comp = Slot;
|
||||
|
||||
if (props.isLink) {
|
||||
Comp = 'a';
|
||||
}
|
||||
|
||||
if (asChild) {
|
||||
Comp = Slot;
|
||||
}
|
||||
let localVariant = variant;
|
||||
let localSize = size;
|
||||
|
||||
const [isAnimating, setIsAnimating] = useState(false);
|
||||
|
||||
const handleClick = (event: SyntheticEvent) => {
|
||||
if (onClick) onClick(event);
|
||||
if (animation === 'none') return;
|
||||
if (onClick) {
|
||||
onClick(event);
|
||||
}
|
||||
|
||||
if (animation === 'none') {
|
||||
return;
|
||||
}
|
||||
setIsAnimating(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
if (isAnimating) setIsAnimating(false);
|
||||
if (isAnimating) {
|
||||
setIsAnimating(false);
|
||||
}
|
||||
}, 1000);
|
||||
return () => clearTimeout(timer);
|
||||
}, [isAnimating]);
|
||||
@ -146,10 +159,21 @@ const StyledButton = styled('button', {
|
||||
justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
padding: (() => {
|
||||
if (padding === 'small' && size === 'small') return '0 7px';
|
||||
if (padding === 'small' && size === 'medium') return '0 9px';
|
||||
if (size === 'small') return '0 10px';
|
||||
if (size === 'medium') return '0 12px';
|
||||
if (padding === 'small' && size === 'small') {
|
||||
return '0 7px';
|
||||
}
|
||||
|
||||
if (padding === 'small' && size === 'medium') {
|
||||
return '0 9px';
|
||||
}
|
||||
|
||||
if (size === 'small') {
|
||||
return '0 10px';
|
||||
}
|
||||
|
||||
if (size === 'medium') {
|
||||
return '0 12px';
|
||||
}
|
||||
return 0;
|
||||
})(),
|
||||
height: size === 'small' ? '28px' : '32px',
|
||||
@ -168,9 +192,17 @@ const StyledButton = styled('button', {
|
||||
fontWeight: theme.typography.weight.bold,
|
||||
lineHeight: '1',
|
||||
background: (() => {
|
||||
if (variant === 'solid') return theme.color.secondary;
|
||||
if (variant === 'outline') return theme.button.background;
|
||||
if (variant === 'ghost' && active) return theme.background.hoverable;
|
||||
if (variant === 'solid') {
|
||||
return theme.color.secondary;
|
||||
}
|
||||
|
||||
if (variant === 'outline') {
|
||||
return theme.button.background;
|
||||
}
|
||||
|
||||
if (variant === 'ghost' && active) {
|
||||
return theme.background.hoverable;
|
||||
}
|
||||
return 'transparent';
|
||||
})(),
|
||||
...(variant === 'ghost'
|
||||
@ -179,11 +211,15 @@ const StyledButton = styled('button', {
|
||||
// It is a temporary solution until we have implemented Theming 2.0.
|
||||
'.sb-bar &': {
|
||||
background: (() => {
|
||||
if (active) return transparentize(0.9, theme.barTextColor);
|
||||
if (active) {
|
||||
return transparentize(0.9, theme.barTextColor);
|
||||
}
|
||||
return 'transparent';
|
||||
})(),
|
||||
color: (() => {
|
||||
if (active) return theme.barSelectedColor;
|
||||
if (active) {
|
||||
return theme.barSelectedColor;
|
||||
}
|
||||
return theme.barTextColor;
|
||||
})(),
|
||||
'&:hover': {
|
||||
@ -204,10 +240,21 @@ const StyledButton = styled('button', {
|
||||
}
|
||||
: {}),
|
||||
color: (() => {
|
||||
if (variant === 'solid') return theme.color.lightest;
|
||||
if (variant === 'outline') return theme.input.color;
|
||||
if (variant === 'ghost' && active) return theme.color.secondary;
|
||||
if (variant === 'ghost') return theme.color.mediumdark;
|
||||
if (variant === 'solid') {
|
||||
return theme.color.lightest;
|
||||
}
|
||||
|
||||
if (variant === 'outline') {
|
||||
return theme.input.color;
|
||||
}
|
||||
|
||||
if (variant === 'ghost' && active) {
|
||||
return theme.color.secondary;
|
||||
}
|
||||
|
||||
if (variant === 'ghost') {
|
||||
return theme.color.mediumdark;
|
||||
}
|
||||
return theme.input.color;
|
||||
})(),
|
||||
boxShadow: variant === 'outline' ? `${theme.button.border} 0 0 0 1px inset` : 'none',
|
||||
@ -219,10 +266,18 @@ const StyledButton = styled('button', {
|
||||
color: variant === 'ghost' ? theme.color.secondary : undefined,
|
||||
background: (() => {
|
||||
let bgColor = theme.color.secondary;
|
||||
if (variant === 'solid') bgColor = theme.color.secondary;
|
||||
if (variant === 'outline') bgColor = theme.button.background;
|
||||
|
||||
if (variant === 'ghost') return transparentize(0.86, theme.color.secondary);
|
||||
if (variant === 'solid') {
|
||||
bgColor = theme.color.secondary;
|
||||
}
|
||||
|
||||
if (variant === 'outline') {
|
||||
bgColor = theme.button.background;
|
||||
}
|
||||
|
||||
if (variant === 'ghost') {
|
||||
return transparentize(0.86, theme.color.secondary);
|
||||
}
|
||||
return theme.base === 'light' ? darken(0.02, bgColor) : lighten(0.03, bgColor);
|
||||
})(),
|
||||
},
|
||||
@ -231,10 +286,18 @@ const StyledButton = styled('button', {
|
||||
color: variant === 'ghost' ? theme.color.secondary : undefined,
|
||||
background: (() => {
|
||||
let bgColor = theme.color.secondary;
|
||||
if (variant === 'solid') bgColor = theme.color.secondary;
|
||||
if (variant === 'outline') bgColor = theme.button.background;
|
||||
|
||||
if (variant === 'ghost') return theme.background.hoverable;
|
||||
if (variant === 'solid') {
|
||||
bgColor = theme.color.secondary;
|
||||
}
|
||||
|
||||
if (variant === 'outline') {
|
||||
bgColor = theme.button.background;
|
||||
}
|
||||
|
||||
if (variant === 'ghost') {
|
||||
return theme.background.hoverable;
|
||||
}
|
||||
return theme.base === 'light' ? darken(0.02, bgColor) : lighten(0.03, bgColor);
|
||||
})(),
|
||||
},
|
||||
|
@ -119,7 +119,10 @@ export const Loader = ({ progress, error, size, ...props }: LoaderProps) => {
|
||||
if (progress) {
|
||||
const { value, modules } = progress;
|
||||
let { message } = progress;
|
||||
if (modules) message += ` ${modules.complete} / ${modules.total} modules`;
|
||||
|
||||
if (modules) {
|
||||
message += ` ${modules.complete} / ${modules.total} modules`;
|
||||
}
|
||||
return (
|
||||
<ProgressWrapper
|
||||
aria-label="Content is loading..."
|
||||
|
@ -146,10 +146,7 @@ const IconButtonSkeletonWrapper = styled.div(() => ({
|
||||
height: 28,
|
||||
}));
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* This component will be removed in Storybook 9.0
|
||||
* */
|
||||
/** @deprecated This component will be removed in Storybook 9.0 */
|
||||
export const IconButtonSkeleton = () => (
|
||||
<IconButtonSkeletonWrapper>
|
||||
<IconPlaceholder />
|
||||
|
@ -7,9 +7,9 @@ import { styled } from '@storybook/core/theming';
|
||||
import TextareaAutoResize from 'react-textarea-autosize';
|
||||
|
||||
/**
|
||||
* these types are copied from `react-textarea-autosize`.
|
||||
* I copied them because of https://github.com/storybookjs/storybook/issues/18734
|
||||
* Maybe there's some bug in `tsup` or `react-textarea-autosize`?
|
||||
* These types are copied from `react-textarea-autosize`. I copied them because of
|
||||
* https://github.com/storybookjs/storybook/issues/18734 Maybe there's some bug in `tsup` or
|
||||
* `react-textarea-autosize`?
|
||||
*/
|
||||
type TextareaPropsRaw = React.TextareaHTMLAttributes<HTMLTextAreaElement>;
|
||||
type Style = Omit<NonNullable<TextareaPropsRaw['style']>, 'maxHeight' | 'minHeight'> & {
|
||||
|
@ -29,9 +29,9 @@ export interface IconsProps extends ComponentProps<typeof Svg> {
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated No longer used, will be removed in Storybook 9.0
|
||||
* Please use the `@storybook/icons` package instead.
|
||||
* */
|
||||
* @deprecated No longer used, will be removed in Storybook 9.0 Please use the `@storybook/icons`
|
||||
* package instead.
|
||||
*/
|
||||
export const Icons = ({
|
||||
icon,
|
||||
useSymbol,
|
||||
@ -66,9 +66,9 @@ export interface SymbolsProps {
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated No longer used, will be removed in Storybook 9.0
|
||||
* Please use the `@storybook/icons` package instead.
|
||||
* */
|
||||
* @deprecated No longer used, will be removed in Storybook 9.0 Please use the `@storybook/icons`
|
||||
* package instead.
|
||||
*/
|
||||
export const Symbols = memo<SymbolsProps>(function Symbols({ icons: keys = Object.keys(icons) }) {
|
||||
return (
|
||||
<Svg
|
||||
|
@ -16,7 +16,9 @@ const LazySyntaxHighlighter = lazy(async () => {
|
||||
languages = [];
|
||||
}
|
||||
|
||||
if (Comp === null) Comp = SyntaxHighlighter;
|
||||
if (Comp === null) {
|
||||
Comp = SyntaxHighlighter;
|
||||
}
|
||||
|
||||
return {
|
||||
default: (props: ComponentProps<typeof SyntaxHighlighter>) => <SyntaxHighlighter {...props} />,
|
||||
|
@ -163,8 +163,8 @@ const processLineNumber = (row: any) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* A custom renderer for handling `span.linenumber` element in each line of code,
|
||||
* which is enabled by default if no renderer is passed in from the parent component
|
||||
* A custom renderer for handling `span.linenumber` element in each line of code, which is enabled
|
||||
* by default if no renderer is passed in from the parent component
|
||||
*/
|
||||
const defaultRenderer: SyntaxHighlighterRenderer = ({ rows, stylesheet, useInlineStyles }) => {
|
||||
return rows.map((node: any, i: number) => {
|
||||
|
@ -42,6 +42,7 @@ export interface WithTooltipPureProps
|
||||
onDoubleClick?: () => void;
|
||||
/**
|
||||
* If `true`, a click outside the trigger element closes the tooltip
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
closeOnOutsideClick?: boolean;
|
||||
@ -157,7 +158,9 @@ const WithToolTipState = ({
|
||||
const [tooltipShown, setTooltipShown] = useState(startOpen);
|
||||
const onVisibilityChange = useCallback(
|
||||
(visibility: boolean) => {
|
||||
if (onChange && onChange(visibility) === false) return;
|
||||
if (onChange && onChange(visibility) === false) {
|
||||
return;
|
||||
}
|
||||
setTooltipShown(visibility);
|
||||
},
|
||||
[onChange]
|
||||
|
@ -5,9 +5,8 @@ import { withReset } from './lib/common';
|
||||
/**
|
||||
* This is a "local" reset to style subtrees with Storybook styles
|
||||
*
|
||||
* We can't style individual elements (e.g. h1, h2, etc.) in here
|
||||
* because the CSS specificity is too high, so those styles can too
|
||||
* easily override child elements that are not expecting it.
|
||||
* We can't style individual elements (e.g. h1, h2, etc.) in here because the CSS specificity is too
|
||||
* high, so those styles can too easily override child elements that are not expecting it.
|
||||
*/
|
||||
|
||||
export const ResetWrapper = styled.div(withReset);
|
||||
|
@ -201,10 +201,10 @@ export const experimental_serverAPI = (extension: Record<string, Function>, opti
|
||||
};
|
||||
|
||||
/**
|
||||
* If for some reason this config is not applied, the reason is that
|
||||
* likely there is an addon that does `export core = () => ({ someConfig })`,
|
||||
* instead of `export core = (existing) => ({ ...existing, someConfig })`,
|
||||
* just overwriting everything and not merging with the existing values.
|
||||
* If for some reason this config is not applied, the reason is that likely there is an addon that
|
||||
* does `export core = () => ({ someConfig })`, instead of `export core = (existing) => ({
|
||||
* ...existing, someConfig })`, just overwriting everything and not merging with the existing
|
||||
* values.
|
||||
*/
|
||||
export const core = async (existing: CoreConfig, options: Options): Promise<CoreConfig> => ({
|
||||
...existing,
|
||||
@ -283,10 +283,10 @@ export const experimental_serverChannel = async (
|
||||
};
|
||||
|
||||
/**
|
||||
* Try to resolve react and react-dom from the root node_modules of the project
|
||||
* addon-docs uses this to alias react and react-dom to the project's version when possible
|
||||
* If the user doesn't have an explicit dependency on react this will return the existing values
|
||||
* Which will be the versions shipped with addon-docs
|
||||
* Try to resolve react and react-dom from the root node_modules of the project addon-docs uses this
|
||||
* to alias react and react-dom to the project's version when possible If the user doesn't have an
|
||||
* explicit dependency on react this will return the existing values Which will be the versions
|
||||
* shipped with addon-docs
|
||||
*/
|
||||
export const resolvedReact = async (existing: any) => {
|
||||
try {
|
||||
@ -300,9 +300,7 @@ export const resolvedReact = async (existing: any) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Set up `dev-only`, `docs-only`, `test-only` tags out of the box
|
||||
*/
|
||||
/** Set up `dev-only`, `docs-only`, `test-only` tags out of the box */
|
||||
export const tags = async (existing: any) => {
|
||||
return {
|
||||
...existing,
|
||||
|
@ -26,9 +26,7 @@ export function initCreateNewStoryChannel(
|
||||
options: Options,
|
||||
coreOptions: CoreConfig
|
||||
) {
|
||||
/**
|
||||
* Listens for events to create a new storyfile
|
||||
*/
|
||||
/** Listens for events to create a new storyfile */
|
||||
channel.on(
|
||||
CREATE_NEW_STORYFILE_REQUEST,
|
||||
async (data: RequestData<CreateNewStoryRequestPayload>) => {
|
||||
|
@ -30,9 +30,7 @@ export async function initFileSearchChannel(
|
||||
options: Options,
|
||||
coreOptions: CoreConfig
|
||||
) {
|
||||
/**
|
||||
* Listens for a search query event and searches for files in the project
|
||||
*/
|
||||
/** Listens for a search query event and searches for files in the project */
|
||||
channel.on(
|
||||
FILE_COMPONENT_SEARCH_REQUEST,
|
||||
async (data: RequestData<FileComponentSearchRequestPayload>) => {
|
||||
@ -106,9 +104,7 @@ export async function initFileSearchChannel(
|
||||
error: null,
|
||||
} satisfies ResponseData<FileComponentSearchResponsePayload>);
|
||||
} catch (e: any) {
|
||||
/**
|
||||
* Emits the search result event with an error message
|
||||
*/
|
||||
/** Emits the search result event with an error message */
|
||||
channel.emit(FILE_COMPONENT_SEARCH_RESPONSE, {
|
||||
success: false,
|
||||
id: searchQuery ?? '',
|
||||
|
@ -29,7 +29,9 @@ export class MultipleIndexingError extends Error {
|
||||
constructor(public indexingErrors: IndexingError[]) {
|
||||
super();
|
||||
|
||||
if (this.indexingErrors.length === 0) throw new Error('Unexpected empty error list');
|
||||
if (this.indexingErrors.length === 0) {
|
||||
throw new Error('Unexpected empty error list');
|
||||
}
|
||||
|
||||
if (this.indexingErrors.length === 1) {
|
||||
const [err] = this.indexingErrors;
|
||||
|
@ -39,7 +39,7 @@ type StoryIndexEntryWithExtra = StoryIndexEntry & {
|
||||
};
|
||||
/** A .mdx file will produce a docs entry */
|
||||
type DocsCacheEntry = DocsIndexEntry;
|
||||
/** A *.stories.* file will produce a list of stories and possibly a docs entry */
|
||||
/** A `_.stories._` file will produce a list of stories and possibly a docs entry */
|
||||
type StoriesCacheEntry = {
|
||||
entries: (StoryIndexEntryWithExtra | DocsIndexEntry)[];
|
||||
dependents: Path[];
|
||||
@ -76,25 +76,26 @@ const makeAbsolute = (otherImport: Path, normalizedPath: Path, workingDir: Path)
|
||||
: otherImport;
|
||||
|
||||
/**
|
||||
* The StoryIndexGenerator extracts stories and docs entries for each file matching
|
||||
* (one or more) stories "specifiers", as defined in main.js.
|
||||
* The StoryIndexGenerator extracts stories and docs entries for each file matching (one or more)
|
||||
* stories "specifiers", as defined in main.js.
|
||||
*
|
||||
* The output is a set of entries (see above for the types).
|
||||
*
|
||||
* Each file is treated as a stories or a (modern) docs file.
|
||||
*
|
||||
* A stories file is indexed by an indexer (passed in), which produces a list of stories.
|
||||
* - If the stories have the `parameters.docsOnly` setting, they are disregarded.
|
||||
* - If the stories have autodocs enabled, a docs entry is added pointing to the story file.
|
||||
*
|
||||
* - If the stories have the `parameters.docsOnly` setting, they are disregarded.
|
||||
* - If the stories have `autodocs` enabled, a docs entry is added pointing to the story file.
|
||||
*
|
||||
* A (modern) docs (.mdx) file is indexed, a docs entry is added.
|
||||
*
|
||||
* In the preview, a docs entry with the `autodocs` tag will be rendered
|
||||
* as a CSF file that exports an MDX template on the `docs.page` parameter, whereas
|
||||
* other docs entries are rendered as MDX files directly.
|
||||
* In the preview, a docs entry with the `autodocs` tag will be rendered as a CSF file that exports
|
||||
* an MDX template on the `docs.page` parameter, whereas other docs entries are rendered as MDX
|
||||
* files directly.
|
||||
*
|
||||
* The entries are "uniq"-ed and sorted. Stories entries are preferred to docs entries and
|
||||
* MDX docs entries are preferred to CSF templates (with warnings).
|
||||
* The entries are "uniq"-ed and sorted. Stories entries are preferred to docs entries and MDX docs
|
||||
* entries are preferred to CSF templates (with warnings).
|
||||
*/
|
||||
export class StoryIndexGenerator {
|
||||
// An internal cache mapping specifiers to a set of path=><set of stories>
|
||||
@ -172,9 +173,7 @@ export class StoryIndexGenerator {
|
||||
await this.ensureExtracted({ projectTags });
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the updater function over all the empty cache entries
|
||||
*/
|
||||
/** Run the updater function over all the empty cache entries */
|
||||
async updateExtracted(
|
||||
updater: (
|
||||
specifier: NormalizedStoriesSpecifier,
|
||||
@ -194,7 +193,9 @@ export class StoryIndexGenerator {
|
||||
);
|
||||
return Promise.all(
|
||||
Object.keys(entry).map(async (absolutePath) => {
|
||||
if (entry[absolutePath] && !overwrite) return;
|
||||
if (entry[absolutePath] && !overwrite) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
entry[absolutePath] = await updater(specifier, absolutePath, entry[absolutePath]);
|
||||
@ -249,12 +250,22 @@ export class StoryIndexGenerator {
|
||||
)}`
|
||||
);
|
||||
return Object.values(cache).flatMap((entry): (IndexEntry | ErrorEntry)[] => {
|
||||
if (!entry) return [];
|
||||
if (entry.type === 'docs') return [entry];
|
||||
if (entry.type === 'error') return [entry];
|
||||
if (!entry) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (entry.type === 'docs') {
|
||||
return [entry];
|
||||
}
|
||||
|
||||
if (entry.type === 'error') {
|
||||
return [entry];
|
||||
}
|
||||
|
||||
return entry.entries.map((item) => {
|
||||
if (item.type === 'docs') return item;
|
||||
if (item.type === 'docs') {
|
||||
return item;
|
||||
}
|
||||
|
||||
addStats(item.extra.stats, statsSummary);
|
||||
|
||||
@ -272,11 +283,17 @@ export class StoryIndexGenerator {
|
||||
return [...this.specifierToCache.values()].flatMap((cache: SpecifierStoriesCache) =>
|
||||
Object.entries(cache)
|
||||
.filter(([fileName, cacheEntry]) => {
|
||||
// We are only interested in stories cache entries (and assume they've been processed already)
|
||||
// If we found a match in the cache that's still null or not a stories file,
|
||||
// it is a docs file and it isn't a dependency / storiesImport.
|
||||
// See https://github.com/storybookjs/storybook/issues/20958
|
||||
if (!cacheEntry || cacheEntry.type !== 'stories') return false;
|
||||
/**
|
||||
* We are only interested in stories cache entries (and assume they've been processed
|
||||
* already) If we found a match in the cache that's still null or not a stories file, it
|
||||
* is a docs file and it isn't a dependency / storiesImport.
|
||||
*
|
||||
* @see
|
||||
* https://github.com/storybookjs/storybook/issues/20958
|
||||
*/
|
||||
if (!cacheEntry || cacheEntry.type !== 'stories') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !!absoluteImports.find((storyImport) =>
|
||||
fileName.match(
|
||||
@ -289,11 +306,10 @@ export class StoryIndexGenerator {
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to find the component path from a raw import string and return it in
|
||||
* the same format as `importPath`. Respect tsconfig paths if available.
|
||||
* Try to find the component path from a raw import string and return it in the same format as
|
||||
* `importPath`. Respect tsconfig paths if available.
|
||||
*
|
||||
* If no such file exists, assume that the import is from a package and
|
||||
* return the raw
|
||||
* If no such file exists, assume that the import is from a package and return the raw
|
||||
*/
|
||||
resolveComponentPath(
|
||||
rawComponentPath: Path,
|
||||
@ -422,7 +438,9 @@ export class StoryIndexGenerator {
|
||||
const result = await analyze(content);
|
||||
|
||||
// Templates are not indexed
|
||||
if (result.isTemplate) return false;
|
||||
if (result.isTemplate) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const absoluteImports = (result.imports as string[]).map((p) =>
|
||||
makeAbsolute(p, normalizedPath, this.options.workingDir)
|
||||
@ -535,11 +553,12 @@ export class StoryIndexGenerator {
|
||||
const changeDocsName = 'Use `<Meta of={} name="Other Name">` to distinguish them.';
|
||||
|
||||
// This shouldn't be possible, but double check and use for typing
|
||||
if (worseEntry.type === 'story')
|
||||
if (worseEntry.type === 'story') {
|
||||
throw new IndexingError(`Duplicate stories with id: ${firstEntry.id}`, [
|
||||
firstEntry.importPath,
|
||||
secondEntry.importPath,
|
||||
]);
|
||||
}
|
||||
|
||||
if (betterEntry.type === 'story') {
|
||||
const worseDescriptor = isMdxEntry(worseEntry)
|
||||
@ -569,11 +588,12 @@ export class StoryIndexGenerator {
|
||||
if (
|
||||
worseEntry.tags?.includes(AUTODOCS_TAG) &&
|
||||
!(this.options.docs.autodocs === true || projectTags?.includes(AUTODOCS_TAG))
|
||||
)
|
||||
) {
|
||||
throw new IndexingError(
|
||||
`You created a component docs page for '${worseEntry.title}', but also tagged the CSF file with '${AUTODOCS_TAG}'. This is probably a mistake.`,
|
||||
[betterEntry.importPath, worseEntry.importPath]
|
||||
);
|
||||
}
|
||||
|
||||
// Otherwise the existing entry is created by project-level autodocs which is allowed to be overridden.
|
||||
} else {
|
||||
@ -613,9 +633,13 @@ export class StoryIndexGenerator {
|
||||
}
|
||||
|
||||
async getIndexAndStats(): Promise<{ storyIndex: StoryIndex; stats: IndexStatsSummary }> {
|
||||
if (this.lastIndex && this.lastStats)
|
||||
if (this.lastIndex && this.lastStats) {
|
||||
return { storyIndex: this.lastIndex, stats: this.lastStats };
|
||||
if (this.lastError) throw this.lastError;
|
||||
}
|
||||
|
||||
if (this.lastError) {
|
||||
throw this.lastError;
|
||||
}
|
||||
|
||||
const previewCode = await this.getPreviewCode();
|
||||
const projectTags = this.getProjectTags(previewCode);
|
||||
@ -627,8 +651,9 @@ export class StoryIndexGenerator {
|
||||
try {
|
||||
const errorEntries = storiesList.filter((entry) => entry.type === 'error');
|
||||
|
||||
if (errorEntries.length)
|
||||
if (errorEntries.length) {
|
||||
throw new MultipleIndexingError(errorEntries.map((entry) => (entry as ErrorEntry).err));
|
||||
}
|
||||
|
||||
const duplicateErrors: IndexingError[] = [];
|
||||
const indexEntries: StoryIndex['entries'] = {};
|
||||
@ -641,10 +666,15 @@ export class StoryIndexGenerator {
|
||||
indexEntries[entry.id] = entry;
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof IndexingError) duplicateErrors.push(err);
|
||||
if (err instanceof IndexingError) {
|
||||
duplicateErrors.push(err);
|
||||
}
|
||||
}
|
||||
});
|
||||
if (duplicateErrors.length) throw new MultipleIndexingError(duplicateErrors);
|
||||
|
||||
if (duplicateErrors.length) {
|
||||
throw new MultipleIndexingError(duplicateErrors);
|
||||
}
|
||||
|
||||
const sorted = await this.sortStories(
|
||||
indexEntries,
|
||||
|
@ -5,12 +5,12 @@ import type { Path } from '@storybook/core/types';
|
||||
/**
|
||||
* Calculate a name to use for a docs entry if not specified. The rule is:
|
||||
*
|
||||
* 1. If the name of the MDX file is the "same" as the CSF file
|
||||
* (e.g. Button.mdx, Button.stories.jsx) use the default name.
|
||||
* 1. If the name of the MDX file is the "same" as the CSF file (e.g. Button.mdx, Button.stories.jsx)
|
||||
* use the default name.
|
||||
* 2. Else use the (ext-less) name of the MDX file
|
||||
*
|
||||
* @param mdxImportPath importPath of the MDX file with of={}
|
||||
* @param csfImportPath importPath of the of CSF file
|
||||
* @param mdxImportPath ImportPath of the MDX file with of={}
|
||||
* @param csfImportPath ImportPath of the of CSF file
|
||||
*/
|
||||
export function autoName(mdxImportPath: Path, csfImportPath: Path, defaultName: string) {
|
||||
const mdxBasename = basename(mdxImportPath);
|
||||
@ -19,7 +19,9 @@ export function autoName(mdxImportPath: Path, csfImportPath: Path, defaultName:
|
||||
const [mdxFilename] = mdxBasename.split('.');
|
||||
const [csfFilename] = csfBasename.split('.');
|
||||
|
||||
if (mdxFilename === csfFilename) return defaultName;
|
||||
if (mdxFilename === csfFilename) {
|
||||
return defaultName;
|
||||
}
|
||||
|
||||
return mdxFilename;
|
||||
}
|
||||
|
@ -32,7 +32,9 @@ export async function copyAllStaticFiles(staticDirs: any[] | undefined, outputDi
|
||||
filter: (_, dest) => !skipPaths.includes(dest),
|
||||
});
|
||||
} catch (e) {
|
||||
if (e instanceof Error) logger.error(e.message);
|
||||
if (e instanceof Error) {
|
||||
logger.error(e.message);
|
||||
}
|
||||
process.exit(-1);
|
||||
}
|
||||
})
|
||||
|
@ -23,7 +23,9 @@ export async function doTelemetry(
|
||||
} catch (err) {
|
||||
// If we fail to get the index, treat it as a recoverable error, but send it up to telemetry
|
||||
// as if we crashed. In the future we will revisit this to send a distinct error
|
||||
if (!(err instanceof Error)) throw new Error('encountered a non-recoverable error');
|
||||
if (!(err instanceof Error)) {
|
||||
throw new Error('encountered a non-recoverable error');
|
||||
}
|
||||
sendTelemetryError(err, 'dev', {
|
||||
cliOptions: options,
|
||||
presetOptions: { ...options, corePresets: [], overridePresets: [] },
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user