mirror of
https://github.com/storybookjs/storybook.git
synced 2025-04-08 10:01:47 +08:00
Merge pull request #18536 from storybookjs/tom/sb-447-use-strict-types-in-core-preview
Turn on strict types in store + preview-web
This commit is contained in:
commit
533fd71be6
@ -12,6 +12,7 @@ import type {
|
|||||||
StoryKind,
|
StoryKind,
|
||||||
StoryName,
|
StoryName,
|
||||||
Args,
|
Args,
|
||||||
|
ComponentTitle,
|
||||||
} from '@storybook/csf';
|
} from '@storybook/csf';
|
||||||
|
|
||||||
import { Addon } from './index';
|
import { Addon } from './index';
|
||||||
@ -50,18 +51,29 @@ export interface StorySortObjectParameter {
|
|||||||
includeNames?: boolean;
|
includeNames?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StoryIndexEntry {
|
type Path = string;
|
||||||
|
interface BaseIndexEntry {
|
||||||
id: StoryId;
|
id: StoryId;
|
||||||
name: StoryName;
|
name: StoryName;
|
||||||
title: string;
|
title: ComponentTitle;
|
||||||
importPath: string;
|
importPath: Path;
|
||||||
}
|
}
|
||||||
|
export type StoryIndexEntry = BaseIndexEntry & {
|
||||||
|
type: 'story';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DocsIndexEntry = BaseIndexEntry & {
|
||||||
|
storiesImports: Path[];
|
||||||
|
type: 'docs';
|
||||||
|
legacy?: boolean;
|
||||||
|
};
|
||||||
|
export type IndexEntry = StoryIndexEntry | DocsIndexEntry;
|
||||||
|
|
||||||
// The `any` here is the story store's `StoreItem` record. Ideally we should probably only
|
// The `any` here is the story store's `StoreItem` record. Ideally we should probably only
|
||||||
// pass a defined subset of that full data, but we pass it all so far :shrug:
|
// pass a defined subset of that full data, but we pass it all so far :shrug:
|
||||||
export type StorySortComparator = Comparator<[StoryId, any, Parameters, Parameters]>;
|
export type StorySortComparator = Comparator<[StoryId, any, Parameters, Parameters]>;
|
||||||
export type StorySortParameter = StorySortComparator | StorySortObjectParameter;
|
export type StorySortParameter = StorySortComparator | StorySortObjectParameter;
|
||||||
export type StorySortComparatorV7 = Comparator<StoryIndexEntry>;
|
export type StorySortComparatorV7 = Comparator<IndexEntry>;
|
||||||
export type StorySortParameterV7 = StorySortComparatorV7 | StorySortObjectParameter;
|
export type StorySortParameterV7 = StorySortComparatorV7 | StorySortObjectParameter;
|
||||||
|
|
||||||
export interface OptionsParameter extends Object {
|
export interface OptionsParameter extends Object {
|
||||||
|
@ -16,6 +16,7 @@ import { start } from './start';
|
|||||||
|
|
||||||
jest.mock('@storybook/preview-web/dist/cjs/WebView');
|
jest.mock('@storybook/preview-web/dist/cjs/WebView');
|
||||||
jest.spyOn(WebView.prototype, 'prepareForDocs').mockReturnValue('docs-root');
|
jest.spyOn(WebView.prototype, 'prepareForDocs').mockReturnValue('docs-root');
|
||||||
|
jest.spyOn(WebView.prototype, 'prepareForStory').mockReturnValue('story-root');
|
||||||
|
|
||||||
jest.mock('global', () => ({
|
jest.mock('global', () => ({
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@ -156,7 +157,7 @@ describe('start', () => {
|
|||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: 'component-a--story-one',
|
id: 'component-a--story-one',
|
||||||
}),
|
}),
|
||||||
undefined
|
'story-root'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -328,7 +329,7 @@ describe('start', () => {
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
undefined
|
'story-root'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -365,7 +366,7 @@ describe('start', () => {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
undefined
|
'story-root'
|
||||||
);
|
);
|
||||||
|
|
||||||
expect((window as any).IS_STORYBOOK).toBe(true);
|
expect((window as any).IS_STORYBOOK).toBe(true);
|
||||||
@ -707,7 +708,7 @@ describe('start', () => {
|
|||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: 'component-c--story-one',
|
id: 'component-c--story-one',
|
||||||
}),
|
}),
|
||||||
undefined
|
'story-root'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1184,7 +1185,7 @@ describe('start', () => {
|
|||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: 'component-a--story-one',
|
id: 'component-a--story-one',
|
||||||
}),
|
}),
|
||||||
undefined
|
'story-root'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -34,7 +34,9 @@ export class DocsRender<TFramework extends AnyFramework> implements Render<TFram
|
|||||||
|
|
||||||
public disableKeyListeners = false;
|
public disableKeyListeners = false;
|
||||||
|
|
||||||
public teardown: (options: { viewModeChanged?: boolean }) => Promise<void>;
|
public teardown?: (options: { viewModeChanged?: boolean }) => Promise<void>;
|
||||||
|
|
||||||
|
public torndown = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private channel: Channel,
|
private channel: Channel,
|
||||||
@ -42,7 +44,7 @@ export class DocsRender<TFramework extends AnyFramework> implements Render<TFram
|
|||||||
public entry: IndexEntry
|
public entry: IndexEntry
|
||||||
) {
|
) {
|
||||||
this.id = entry.id;
|
this.id = entry.id;
|
||||||
this.legacy = entry.type !== 'docs' || entry.legacy;
|
this.legacy = entry.type !== 'docs' || !!entry.legacy;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The two story "renders" are equal and have both loaded the same story
|
// The two story "renders" are equal and have both loaded the same story
|
||||||
@ -105,6 +107,8 @@ export class DocsRender<TFramework extends AnyFramework> implements Render<TFram
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.csfFiles) throw new Error('getDocsContext called before prepare');
|
||||||
|
|
||||||
let metaCsfFile: ModuleExports;
|
let metaCsfFile: ModuleExports;
|
||||||
const exportToStoryId = new Map<ModuleExport, StoryId>();
|
const exportToStoryId = new Map<ModuleExport, StoryId>();
|
||||||
const storyIdToCSFFile = new Map<StoryId, CSFFile<TFramework>>();
|
const storyIdToCSFFile = new Map<StoryId, CSFFile<TFramework>>();
|
||||||
@ -126,16 +130,18 @@ export class DocsRender<TFramework extends AnyFramework> implements Render<TFram
|
|||||||
return {
|
return {
|
||||||
...base,
|
...base,
|
||||||
storyIdByModuleExport: (moduleExport) => {
|
storyIdByModuleExport: (moduleExport) => {
|
||||||
if (exportToStoryId.has(moduleExport)) return exportToStoryId.get(moduleExport);
|
const storyId = exportToStoryId.get(moduleExport);
|
||||||
|
if (storyId) return storyId;
|
||||||
|
|
||||||
throw new Error(`No story found with that export: ${moduleExport}`);
|
throw new Error(`No story found with that export: ${moduleExport}`);
|
||||||
},
|
},
|
||||||
storyById,
|
storyById,
|
||||||
componentStories: () => {
|
componentStories: () => {
|
||||||
return Object.entries(metaCsfFile)
|
return (
|
||||||
.map(([_, moduleExport]) => exportToStoryId.get(moduleExport))
|
Object.entries(metaCsfFile)
|
||||||
.filter(Boolean)
|
.map(([_, moduleExport]) => exportToStoryId.get(moduleExport))
|
||||||
.map(storyById);
|
.filter(Boolean) as StoryId[]
|
||||||
|
).map(storyById);
|
||||||
},
|
},
|
||||||
setMeta(m: ModuleExports) {
|
setMeta(m: ModuleExports) {
|
||||||
metaCsfFile = m;
|
metaCsfFile = m;
|
||||||
@ -154,10 +160,15 @@ export class DocsRender<TFramework extends AnyFramework> implements Render<TFram
|
|||||||
}
|
}
|
||||||
|
|
||||||
async render() {
|
async render() {
|
||||||
if (!(this.story || this.exports) || !this.docsContext || !this.canvasElement)
|
if (
|
||||||
|
!(this.story || this.exports) ||
|
||||||
|
!this.docsContext ||
|
||||||
|
!this.canvasElement ||
|
||||||
|
!this.store.projectAnnotations
|
||||||
|
)
|
||||||
throw new Error('DocsRender not ready to render');
|
throw new Error('DocsRender not ready to render');
|
||||||
|
|
||||||
const { docs } = this.story?.parameters || this.store.projectAnnotations.parameters;
|
const { docs } = this.story?.parameters || this.store.projectAnnotations.parameters || {};
|
||||||
|
|
||||||
if (!docs) {
|
if (!docs) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@ -170,7 +181,8 @@ export class DocsRender<TFramework extends AnyFramework> implements Render<TFram
|
|||||||
this.docsContext,
|
this.docsContext,
|
||||||
{
|
{
|
||||||
...docs,
|
...docs,
|
||||||
...(!this.legacy && { page: this.exports.default }),
|
// exports must be defined in non-legacy mode (see check at top)
|
||||||
|
...(!this.legacy && { page: this.exports!.default }),
|
||||||
},
|
},
|
||||||
this.canvasElement,
|
this.canvasElement,
|
||||||
() => this.channel.emit(DOCS_RENDERED, this.id)
|
() => this.channel.emit(DOCS_RENDERED, this.id)
|
||||||
@ -178,6 +190,7 @@ export class DocsRender<TFramework extends AnyFramework> implements Render<TFram
|
|||||||
this.teardown = async ({ viewModeChanged }: { viewModeChanged?: boolean } = {}) => {
|
this.teardown = async ({ viewModeChanged }: { viewModeChanged?: boolean } = {}) => {
|
||||||
if (!viewModeChanged || !this.canvasElement) return;
|
if (!viewModeChanged || !this.canvasElement) return;
|
||||||
renderer.unmount(this.canvasElement);
|
renderer.unmount(this.canvasElement);
|
||||||
|
this.torndown = true;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,7 +46,7 @@ export class Preview<TFramework extends AnyFramework> {
|
|||||||
|
|
||||||
importFn?: ModuleImportFn;
|
importFn?: ModuleImportFn;
|
||||||
|
|
||||||
renderToDOM: RenderToDOM<TFramework>;
|
renderToDOM?: RenderToDOM<TFramework>;
|
||||||
|
|
||||||
storyRenders: StoryRender<TFramework>[] = [];
|
storyRenders: StoryRender<TFramework>[] = [];
|
||||||
|
|
||||||
@ -156,6 +156,8 @@ export class Preview<TFramework extends AnyFramework> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
emitGlobals() {
|
emitGlobals() {
|
||||||
|
if (!this.storyStore.globals || !this.storyStore.projectAnnotations)
|
||||||
|
throw new Error(`Cannot emit before initialization`);
|
||||||
this.channel.emit(SET_GLOBALS, {
|
this.channel.emit(SET_GLOBALS, {
|
||||||
globals: this.storyStore.globals.get() || {},
|
globals: this.storyStore.globals.get() || {},
|
||||||
globalTypes: this.storyStore.projectAnnotations.globalTypes || {},
|
globalTypes: this.storyStore.projectAnnotations.globalTypes || {},
|
||||||
@ -171,6 +173,9 @@ export class Preview<TFramework extends AnyFramework> {
|
|||||||
|
|
||||||
// If initialization gets as far as the story index, this function runs.
|
// If initialization gets as far as the story index, this function runs.
|
||||||
initializeWithStoryIndex(storyIndex: StoryIndex): PromiseLike<void> {
|
initializeWithStoryIndex(storyIndex: StoryIndex): PromiseLike<void> {
|
||||||
|
if (!this.importFn)
|
||||||
|
throw new Error(`Cannot call initializeWithStoryIndex before initialization`);
|
||||||
|
|
||||||
return this.storyStore.initialize({
|
return this.storyStore.initialize({
|
||||||
storyIndex,
|
storyIndex,
|
||||||
importFn: this.importFn,
|
importFn: this.importFn,
|
||||||
@ -218,7 +223,7 @@ export class Preview<TFramework extends AnyFramework> {
|
|||||||
// Update the store with the new stories.
|
// Update the store with the new stories.
|
||||||
await this.onStoriesChanged({ storyIndex });
|
await this.onStoriesChanged({ storyIndex });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.renderPreviewEntryError('Error loading story index:', err);
|
this.renderPreviewEntryError('Error loading story index:', err as Error);
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -235,6 +240,8 @@ export class Preview<TFramework extends AnyFramework> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async onUpdateGlobals({ globals }: { globals: Globals }) {
|
async onUpdateGlobals({ globals }: { globals: Globals }) {
|
||||||
|
if (!this.storyStore.globals)
|
||||||
|
throw new Error(`Cannot call onUpdateGlobals before initialization`);
|
||||||
this.storyStore.globals.update(globals);
|
this.storyStore.globals.update(globals);
|
||||||
|
|
||||||
await Promise.all(this.storyRenders.map((r) => r.rerender()));
|
await Promise.all(this.storyRenders.map((r) => r.rerender()));
|
||||||
@ -295,6 +302,9 @@ export class Preview<TFramework extends AnyFramework> {
|
|||||||
// we will change it to go ahead and load the story, which will end up being
|
// we will change it to go ahead and load the story, which will end up being
|
||||||
// "instant", although async.
|
// "instant", although async.
|
||||||
renderStoryToElement(story: Story<TFramework>, element: HTMLElement) {
|
renderStoryToElement(story: Story<TFramework>, element: HTMLElement) {
|
||||||
|
if (!this.renderToDOM)
|
||||||
|
throw new Error(`Cannot call renderStoryToElement before initialization`);
|
||||||
|
|
||||||
const render = new StoryRender<TFramework>(
|
const render = new StoryRender<TFramework>(
|
||||||
this.channel,
|
this.channel,
|
||||||
this.storyStore,
|
this.storyStore,
|
||||||
@ -318,7 +328,7 @@ export class Preview<TFramework extends AnyFramework> {
|
|||||||
{ viewModeChanged }: { viewModeChanged?: boolean } = {}
|
{ viewModeChanged }: { viewModeChanged?: boolean } = {}
|
||||||
) {
|
) {
|
||||||
this.storyRenders = this.storyRenders.filter((r) => r !== render);
|
this.storyRenders = this.storyRenders.filter((r) => r !== render);
|
||||||
await render?.teardown({ viewModeChanged });
|
await render?.teardown?.({ viewModeChanged });
|
||||||
}
|
}
|
||||||
|
|
||||||
// API
|
// API
|
||||||
|
@ -3,8 +3,11 @@ import global from 'global';
|
|||||||
import { RenderContext } from '@storybook/store';
|
import { RenderContext } from '@storybook/store';
|
||||||
import addons, { mockChannel as createMockChannel } from '@storybook/addons';
|
import addons, { mockChannel as createMockChannel } from '@storybook/addons';
|
||||||
import { DocsRenderer } from '@storybook/addon-docs';
|
import { DocsRenderer } from '@storybook/addon-docs';
|
||||||
|
import { mocked } from 'ts-jest/utils';
|
||||||
|
import { expect } from '@jest/globals';
|
||||||
|
|
||||||
import { PreviewWeb } from './PreviewWeb';
|
import { PreviewWeb } from './PreviewWeb';
|
||||||
|
import { WebView } from './WebView';
|
||||||
import {
|
import {
|
||||||
componentOneExports,
|
componentOneExports,
|
||||||
importFn,
|
importFn,
|
||||||
@ -56,6 +59,9 @@ beforeEach(() => {
|
|||||||
|
|
||||||
addons.setChannel(mockChannel as any);
|
addons.setChannel(mockChannel as any);
|
||||||
addons.setServerChannel(createMockChannel());
|
addons.setServerChannel(createMockChannel());
|
||||||
|
|
||||||
|
mocked(WebView.prototype).prepareForDocs.mockReturnValue('docs-element' as any);
|
||||||
|
mocked(WebView.prototype).prepareForStory.mockReturnValue('story-element' as any);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('PreviewWeb', () => {
|
describe('PreviewWeb', () => {
|
||||||
|
@ -28,6 +28,8 @@ import { logger } from '@storybook/client-logger';
|
|||||||
import { addons, mockChannel as createMockChannel } from '@storybook/addons';
|
import { addons, mockChannel as createMockChannel } from '@storybook/addons';
|
||||||
import type { AnyFramework } from '@storybook/csf';
|
import type { AnyFramework } from '@storybook/csf';
|
||||||
import type { ModuleImportFn, WebProjectAnnotations } from '@storybook/store';
|
import type { ModuleImportFn, WebProjectAnnotations } from '@storybook/store';
|
||||||
|
import { expect } from '@jest/globals';
|
||||||
|
import { mocked } from 'ts-jest/utils';
|
||||||
|
|
||||||
import { PreviewWeb } from './PreviewWeb';
|
import { PreviewWeb } from './PreviewWeb';
|
||||||
import {
|
import {
|
||||||
@ -48,8 +50,8 @@ import {
|
|||||||
modernDocsExports,
|
modernDocsExports,
|
||||||
teardownRenderToDOM,
|
teardownRenderToDOM,
|
||||||
} from './PreviewWeb.mockdata';
|
} from './PreviewWeb.mockdata';
|
||||||
|
import { WebView } from './WebView';
|
||||||
|
|
||||||
jest.mock('./WebView');
|
|
||||||
const { history, document } = global;
|
const { history, document } = global;
|
||||||
|
|
||||||
const mockStoryIndex = jest.fn(() => storyIndex);
|
const mockStoryIndex = jest.fn(() => storyIndex);
|
||||||
@ -79,6 +81,7 @@ jest.mock('global', () => ({
|
|||||||
|
|
||||||
jest.mock('@storybook/client-logger');
|
jest.mock('@storybook/client-logger');
|
||||||
jest.mock('react-dom');
|
jest.mock('react-dom');
|
||||||
|
jest.mock('./WebView');
|
||||||
|
|
||||||
const createGate = (): [Promise<any | undefined>, (_?: any) => void] => {
|
const createGate = (): [Promise<any | undefined>, (_?: any) => void] => {
|
||||||
let openGate = (_?: any) => {};
|
let openGate = (_?: any) => {};
|
||||||
@ -104,9 +107,6 @@ async function createAndRenderPreview({
|
|||||||
getProjectAnnotations?: () => WebProjectAnnotations<AnyFramework>;
|
getProjectAnnotations?: () => WebProjectAnnotations<AnyFramework>;
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const preview = new PreviewWeb();
|
const preview = new PreviewWeb();
|
||||||
(
|
|
||||||
preview.view.prepareForDocs as jest.MockedFunction<typeof preview.view.prepareForDocs>
|
|
||||||
).mockReturnValue('docs-element' as any);
|
|
||||||
await preview.initialize({
|
await preview.initialize({
|
||||||
importFn: inputImportFn,
|
importFn: inputImportFn,
|
||||||
getProjectAnnotations: inputGetProjectAnnotations,
|
getProjectAnnotations: inputGetProjectAnnotations,
|
||||||
@ -134,6 +134,9 @@ beforeEach(() => {
|
|||||||
addons.setChannel(mockChannel as any);
|
addons.setChannel(mockChannel as any);
|
||||||
addons.setServerChannel(createMockChannel());
|
addons.setServerChannel(createMockChannel());
|
||||||
mockFetchResult = { status: 200, json: mockStoryIndex, text: () => 'error text' };
|
mockFetchResult = { status: 200, json: mockStoryIndex, text: () => 'error text' };
|
||||||
|
|
||||||
|
mocked(WebView.prototype).prepareForDocs.mockReturnValue('docs-element' as any);
|
||||||
|
mocked(WebView.prototype).prepareForStory.mockReturnValue('story-element' as any);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('PreviewWeb', () => {
|
describe('PreviewWeb', () => {
|
||||||
@ -172,7 +175,7 @@ describe('PreviewWeb', () => {
|
|||||||
|
|
||||||
const preview = await createAndRenderPreview();
|
const preview = await createAndRenderPreview();
|
||||||
|
|
||||||
expect(preview.storyStore.globals.get()).toEqual({ a: 'c' });
|
expect(preview.storyStore.globals!.get()).toEqual({ a: 'c' });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('emits the SET_GLOBALS event', async () => {
|
it('emits the SET_GLOBALS event', async () => {
|
||||||
@ -233,7 +236,7 @@ describe('PreviewWeb', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(preview.storyStore.globals.get()).toEqual({ a: 'b' });
|
expect(preview.storyStore.globals!.get()).toEqual({ a: 'b' });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -456,7 +459,7 @@ describe('PreviewWeb', () => {
|
|||||||
loaded: { l: 7 },
|
loaded: { l: 7 },
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
undefined // this is coming from view.prepareForStory, not super important
|
'story-element'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -487,26 +490,27 @@ describe('PreviewWeb', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('renders helpful message if renderToDOM is undefined', async () => {
|
it('renders helpful message if renderToDOM is undefined', async () => {
|
||||||
const originalRenderToDOM = projectAnnotations.renderToDOM;
|
document.location.search = '?id=component-one--a';
|
||||||
try {
|
const preview = new PreviewWeb();
|
||||||
projectAnnotations.renderToDOM = undefined;
|
await expect(
|
||||||
|
preview.initialize({
|
||||||
|
importFn,
|
||||||
|
getProjectAnnotations: () => ({
|
||||||
|
...getProjectAnnotations,
|
||||||
|
renderToDOM: undefined,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
).rejects.toThrow();
|
||||||
|
|
||||||
document.location.search = '?id=component-one--a';
|
expect(preview.view.showErrorDisplay).toHaveBeenCalled();
|
||||||
const preview = new PreviewWeb();
|
expect((preview.view.showErrorDisplay as jest.Mock).mock.calls[0][0])
|
||||||
await expect(preview.initialize({ importFn, getProjectAnnotations })).rejects.toThrow();
|
.toMatchInlineSnapshot(`
|
||||||
|
|
||||||
expect(preview.view.showErrorDisplay).toHaveBeenCalled();
|
|
||||||
expect((preview.view.showErrorDisplay as jest.Mock).mock.calls[0][0])
|
|
||||||
.toMatchInlineSnapshot(`
|
|
||||||
[Error: Expected your framework's preset to export a \`renderToDOM\` field.
|
[Error: Expected your framework's preset to export a \`renderToDOM\` field.
|
||||||
|
|
||||||
Perhaps it needs to be upgraded for Storybook 6.4?
|
Perhaps it needs to be upgraded for Storybook 6.4?
|
||||||
|
|
||||||
More info: https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#mainjs-framework-field ]
|
More info: https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#mainjs-framework-field ]
|
||||||
`);
|
`);
|
||||||
} finally {
|
|
||||||
projectAnnotations.renderToDOM = originalRenderToDOM;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders exception if the play function throws', async () => {
|
it('renders exception if the play function throws', async () => {
|
||||||
@ -705,7 +709,7 @@ describe('PreviewWeb', () => {
|
|||||||
|
|
||||||
emitter.emit(UPDATE_GLOBALS, { globals: { foo: 'bar' } });
|
emitter.emit(UPDATE_GLOBALS, { globals: { foo: 'bar' } });
|
||||||
|
|
||||||
expect(preview.storyStore.globals.get()).toEqual({ a: 'b', foo: 'bar' });
|
expect(preview.storyStore.globals!.get()).toEqual({ a: 'b', foo: 'bar' });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('passes new globals in context to renderToDOM', async () => {
|
it('passes new globals in context to renderToDOM', async () => {
|
||||||
@ -724,7 +728,7 @@ describe('PreviewWeb', () => {
|
|||||||
globals: { a: 'b', foo: 'bar' },
|
globals: { a: 'b', foo: 'bar' },
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
undefined // this is coming from view.prepareForStory, not super important
|
'story-element'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -807,7 +811,7 @@ describe('PreviewWeb', () => {
|
|||||||
args: { foo: 'a', new: 'arg' },
|
args: { foo: 'a', new: 'arg' },
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
undefined // this is coming from view.prepareForStory, not super important
|
'story-element'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -864,7 +868,7 @@ describe('PreviewWeb', () => {
|
|||||||
args: { foo: 'a', new: 'arg' },
|
args: { foo: 'a', new: 'arg' },
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
undefined // this is coming from view.prepareForStory, not super important
|
'story-element'
|
||||||
);
|
);
|
||||||
|
|
||||||
// Now let the first loader call resolve
|
// Now let the first loader call resolve
|
||||||
@ -883,7 +887,7 @@ describe('PreviewWeb', () => {
|
|||||||
args: { foo: 'a', new: 'arg' },
|
args: { foo: 'a', new: 'arg' },
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
undefined // this is coming from view.prepareForStory, not super important
|
'story-element'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -914,7 +918,7 @@ describe('PreviewWeb', () => {
|
|||||||
args: { foo: 'a' },
|
args: { foo: 'a' },
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
undefined // this is coming from view.prepareForStory, not super important
|
'story-element'
|
||||||
);
|
);
|
||||||
expect(projectAnnotations.renderToDOM).toHaveBeenCalledWith(
|
expect(projectAnnotations.renderToDOM).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@ -924,7 +928,7 @@ describe('PreviewWeb', () => {
|
|||||||
args: { foo: 'a', new: 'arg' },
|
args: { foo: 'a', new: 'arg' },
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
undefined // this is coming from view.prepareForStory, not super important
|
'story-element'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -947,7 +951,7 @@ describe('PreviewWeb', () => {
|
|||||||
args: { foo: 'a' },
|
args: { foo: 'a' },
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
undefined // this is coming from view.prepareForStory, not super important
|
'story-element'
|
||||||
);
|
);
|
||||||
expect(projectAnnotations.renderToDOM).toHaveBeenCalledWith(
|
expect(projectAnnotations.renderToDOM).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@ -957,7 +961,7 @@ describe('PreviewWeb', () => {
|
|||||||
args: { foo: 'a', new: 'arg' },
|
args: { foo: 'a', new: 'arg' },
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
undefined // this is coming from view.prepareForStory, not super important
|
'story-element'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -985,7 +989,7 @@ describe('PreviewWeb', () => {
|
|||||||
args: { foo: 'a' },
|
args: { foo: 'a' },
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
undefined // this is coming from view.prepareForStory, not super important
|
'story-element'
|
||||||
);
|
);
|
||||||
|
|
||||||
emitter.emit(UPDATE_STORY_ARGS, {
|
emitter.emit(UPDATE_STORY_ARGS, {
|
||||||
@ -1005,7 +1009,7 @@ describe('PreviewWeb', () => {
|
|||||||
args: { foo: 'a', new: 'arg' },
|
args: { foo: 'a', new: 'arg' },
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
undefined // this is coming from view.prepareForStory, not super important
|
'story-element'
|
||||||
);
|
);
|
||||||
|
|
||||||
// Now let the playFunction call resolve
|
// Now let the playFunction call resolve
|
||||||
@ -1172,7 +1176,7 @@ describe('PreviewWeb', () => {
|
|||||||
args: { foo: 'a', new: 'value' },
|
args: { foo: 'a', new: 'value' },
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
undefined // this is coming from view.prepareForStory, not super important
|
'story-element'
|
||||||
);
|
);
|
||||||
|
|
||||||
await waitForEvents([STORY_ARGS_UPDATED]);
|
await waitForEvents([STORY_ARGS_UPDATED]);
|
||||||
@ -1213,7 +1217,7 @@ describe('PreviewWeb', () => {
|
|||||||
args: { foo: 'a' },
|
args: { foo: 'a' },
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
undefined // this is coming from view.prepareForStory, not super important
|
'story-element'
|
||||||
);
|
);
|
||||||
|
|
||||||
await waitForEvents([STORY_ARGS_UPDATED]);
|
await waitForEvents([STORY_ARGS_UPDATED]);
|
||||||
@ -1254,7 +1258,7 @@ describe('PreviewWeb', () => {
|
|||||||
args: { foo: 'a' },
|
args: { foo: 'a' },
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
undefined // this is coming from view.prepareForStory, not super important
|
'story-element'
|
||||||
);
|
);
|
||||||
|
|
||||||
await waitForEvents([STORY_ARGS_UPDATED]);
|
await waitForEvents([STORY_ARGS_UPDATED]);
|
||||||
@ -1295,7 +1299,7 @@ describe('PreviewWeb', () => {
|
|||||||
args: { foo: 'a' },
|
args: { foo: 'a' },
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
undefined // this is coming from view.prepareForStory, not super important
|
'story-element'
|
||||||
);
|
);
|
||||||
|
|
||||||
await waitForEvents([STORY_ARGS_UPDATED]);
|
await waitForEvents([STORY_ARGS_UPDATED]);
|
||||||
@ -1323,7 +1327,7 @@ describe('PreviewWeb', () => {
|
|||||||
|
|
||||||
expect(projectAnnotations.renderToDOM).toHaveBeenCalledWith(
|
expect(projectAnnotations.renderToDOM).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({ forceRemount: false }),
|
expect.objectContaining({ forceRemount: false }),
|
||||||
undefined // this is coming from view.prepareForStory, not super important
|
'story-element'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -1347,7 +1351,7 @@ describe('PreviewWeb', () => {
|
|||||||
|
|
||||||
expect(projectAnnotations.renderToDOM).toHaveBeenCalledWith(
|
expect(projectAnnotations.renderToDOM).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({ forceRemount: true }),
|
expect.objectContaining({ forceRemount: true }),
|
||||||
undefined // this is coming from view.prepareForStory, not super important
|
'story-element'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1367,7 +1371,7 @@ describe('PreviewWeb', () => {
|
|||||||
loaded: { l: 7 },
|
loaded: { l: 7 },
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
undefined // this is coming from view.prepareForStory, not super important
|
'story-element'
|
||||||
);
|
);
|
||||||
|
|
||||||
mockChannel.emit.mockClear();
|
mockChannel.emit.mockClear();
|
||||||
@ -1723,7 +1727,7 @@ describe('PreviewWeb', () => {
|
|||||||
loaded: { l: 7 },
|
loaded: { l: 7 },
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
undefined // this is coming from view.prepareForStory, not super important
|
'story-element'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1888,7 +1892,7 @@ describe('PreviewWeb', () => {
|
|||||||
loaded: { l: 7 },
|
loaded: { l: 7 },
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
undefined // this is coming from view.prepareForStory, not super important
|
'story-element'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1942,7 +1946,7 @@ describe('PreviewWeb', () => {
|
|||||||
loaded: { l: 7 },
|
loaded: { l: 7 },
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
undefined // this is coming from view.prepareForStory, not super important
|
'story-element'
|
||||||
);
|
);
|
||||||
|
|
||||||
mockChannel.emit.mockClear();
|
mockChannel.emit.mockClear();
|
||||||
@ -1967,7 +1971,7 @@ describe('PreviewWeb', () => {
|
|||||||
loaded: { l: 7 },
|
loaded: { l: 7 },
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
undefined // this is coming from view.prepareForStory, not super important
|
'story-element'
|
||||||
);
|
);
|
||||||
|
|
||||||
await waitForRenderPhase('playing');
|
await waitForRenderPhase('playing');
|
||||||
@ -1995,7 +1999,7 @@ describe('PreviewWeb', () => {
|
|||||||
loaded: { l: 7 },
|
loaded: { l: 7 },
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
undefined // this is coming from view.prepareForStory, not super important
|
'story-element'
|
||||||
);
|
);
|
||||||
|
|
||||||
mockChannel.emit.mockClear();
|
mockChannel.emit.mockClear();
|
||||||
@ -2284,7 +2288,7 @@ describe('PreviewWeb', () => {
|
|||||||
loaded: { l: 7 },
|
loaded: { l: 7 },
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
undefined // this is coming from view.prepareForStory, not super important
|
'story-element'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -2557,7 +2561,7 @@ describe('PreviewWeb', () => {
|
|||||||
loaded: { l: 7 },
|
loaded: { l: 7 },
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
undefined // this is coming from view.prepareForStory, not super important
|
'story-element'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -2585,7 +2589,7 @@ describe('PreviewWeb', () => {
|
|||||||
args: { foo: 'updated' },
|
args: { foo: 'updated' },
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
undefined // this is coming from view.prepareForStory, not super important
|
'story-element'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -2846,7 +2850,7 @@ describe('PreviewWeb', () => {
|
|||||||
args: { foo: 'updated', bar: 'edited' },
|
args: { foo: 'updated', bar: 'edited' },
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
undefined // this is coming from view.prepareForStory, not super important
|
'story-element'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -3071,7 +3075,7 @@ describe('PreviewWeb', () => {
|
|||||||
globals: { a: 'edited' },
|
globals: { a: 'edited' },
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
undefined // this is coming from view.prepareForStory, not super important
|
'story-element'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -20,7 +20,7 @@ import {
|
|||||||
UPDATE_QUERY_PARAMS,
|
UPDATE_QUERY_PARAMS,
|
||||||
} from '@storybook/core-events';
|
} from '@storybook/core-events';
|
||||||
import { logger } from '@storybook/client-logger';
|
import { logger } from '@storybook/client-logger';
|
||||||
import { AnyFramework, StoryId, ProjectAnnotations, Args, Globals } from '@storybook/csf';
|
import { AnyFramework, StoryId, ProjectAnnotations, Args, Globals, ViewMode } from '@storybook/csf';
|
||||||
import type {
|
import type {
|
||||||
ModuleImportFn,
|
ModuleImportFn,
|
||||||
Selection,
|
Selection,
|
||||||
@ -54,9 +54,9 @@ export class PreviewWeb<TFramework extends AnyFramework> extends Preview<TFramew
|
|||||||
|
|
||||||
previewEntryError?: Error;
|
previewEntryError?: Error;
|
||||||
|
|
||||||
currentSelection: Selection;
|
currentSelection?: Selection;
|
||||||
|
|
||||||
currentRender: StoryRender<TFramework> | DocsRender<TFramework>;
|
currentRender?: StoryRender<TFramework> | DocsRender<TFramework>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
@ -93,6 +93,9 @@ export class PreviewWeb<TFramework extends AnyFramework> extends Preview<TFramew
|
|||||||
}
|
}
|
||||||
|
|
||||||
async setInitialGlobals() {
|
async setInitialGlobals() {
|
||||||
|
if (!this.storyStore.globals)
|
||||||
|
throw new Error(`Cannot call setInitialGlobals before initialization`);
|
||||||
|
|
||||||
const { globals } = this.urlStore.selectionSpecifier || {};
|
const { globals } = this.urlStore.selectionSpecifier || {};
|
||||||
if (globals) {
|
if (globals) {
|
||||||
this.storyStore.globals.updateFromPersisted(globals);
|
this.storyStore.globals.updateFromPersisted(globals);
|
||||||
@ -113,6 +116,9 @@ export class PreviewWeb<TFramework extends AnyFramework> extends Preview<TFramew
|
|||||||
|
|
||||||
// Use the selection specifier to choose a story, then render it
|
// Use the selection specifier to choose a story, then render it
|
||||||
async selectSpecifiedStory() {
|
async selectSpecifiedStory() {
|
||||||
|
if (!this.storyStore.storyIndex)
|
||||||
|
throw new Error(`Cannot call selectSpecifiedStory before initialization`);
|
||||||
|
|
||||||
if (!this.urlStore.selectionSpecifier) {
|
if (!this.urlStore.selectionSpecifier) {
|
||||||
this.renderMissingStory();
|
this.renderMissingStory();
|
||||||
return;
|
return;
|
||||||
@ -199,7 +205,7 @@ export class PreviewWeb<TFramework extends AnyFramework> extends Preview<TFramew
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onSetCurrentStory(selection: Selection) {
|
onSetCurrentStory(selection: { storyId: StoryId; viewMode?: ViewMode }) {
|
||||||
this.urlStore.setSelection({ viewMode: 'story', ...selection });
|
this.urlStore.setSelection({ viewMode: 'story', ...selection });
|
||||||
this.channel.emit(CURRENT_STORY_WAS_SET, this.urlStore.selection);
|
this.channel.emit(CURRENT_STORY_WAS_SET, this.urlStore.selection);
|
||||||
this.renderSelection();
|
this.renderSelection();
|
||||||
@ -240,18 +246,19 @@ export class PreviewWeb<TFramework extends AnyFramework> extends Preview<TFramew
|
|||||||
// - a story selected in "docs" viewMode,
|
// - a story selected in "docs" viewMode,
|
||||||
// in which case we render the docsPage for that story
|
// in which case we render the docsPage for that story
|
||||||
async renderSelection({ persistedArgs }: { persistedArgs?: Args } = {}) {
|
async renderSelection({ persistedArgs }: { persistedArgs?: Args } = {}) {
|
||||||
|
const { renderToDOM } = this;
|
||||||
|
if (!renderToDOM) throw new Error('Cannot call renderSelection before initialization');
|
||||||
const { selection } = this.urlStore;
|
const { selection } = this.urlStore;
|
||||||
if (!selection) {
|
if (!selection) throw new Error('Cannot call renderSelection as no selection was made');
|
||||||
throw new Error('Cannot render story as no selection was made');
|
|
||||||
}
|
|
||||||
|
|
||||||
const { storyId } = selection;
|
const { storyId } = selection;
|
||||||
let entry;
|
let entry;
|
||||||
try {
|
try {
|
||||||
entry = await this.storyStore.storyIdToEntry(storyId);
|
entry = await this.storyStore.storyIdToEntry(storyId);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
await this.teardownRender(this.currentRender);
|
if (this.currentRender) await this.teardownRender(this.currentRender);
|
||||||
this.renderStoryLoadingException(storyId, err);
|
this.renderStoryLoadingException(storyId, err as Error);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Docs entries cannot be rendered in 'story' viewMode.
|
// Docs entries cannot be rendered in 'story' viewMode.
|
||||||
@ -268,18 +275,14 @@ export class PreviewWeb<TFramework extends AnyFramework> extends Preview<TFramew
|
|||||||
this.view.showPreparingDocs();
|
this.view.showPreparingDocs();
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastSelection = this.currentSelection;
|
|
||||||
let lastRender = this.currentRender;
|
|
||||||
|
|
||||||
// If the last render is still preparing, let's drop it right now. Either
|
// If the last render is still preparing, let's drop it right now. Either
|
||||||
// (a) it is a different story, which means we would drop it later, OR
|
// (a) it is a different story, which means we would drop it later, OR
|
||||||
// (b) it is the *same* story, in which case we will resolve our own .prepare() at the
|
// (b) it is the *same* story, in which case we will resolve our own .prepare() at the
|
||||||
// same moment anyway, and we should just "take over" the rendering.
|
// same moment anyway, and we should just "take over" the rendering.
|
||||||
// (We can't tell which it is yet, because it is possible that an HMR is going on and
|
// (We can't tell which it is yet, because it is possible that an HMR is going on and
|
||||||
// even though the storyId is the same, the story itself is not).
|
// even though the storyId is the same, the story itself is not).
|
||||||
if (lastRender?.isPreparing()) {
|
if (this.currentRender?.isPreparing()) {
|
||||||
await this.teardownRender(lastRender);
|
await this.teardownRender(this.currentRender);
|
||||||
lastRender = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let render;
|
let render;
|
||||||
@ -290,7 +293,7 @@ export class PreviewWeb<TFramework extends AnyFramework> extends Preview<TFramew
|
|||||||
(...args) => {
|
(...args) => {
|
||||||
// At the start of renderToDOM we make the story visible (see note in WebView)
|
// At the start of renderToDOM we make the story visible (see note in WebView)
|
||||||
this.view.showStoryDuringRender();
|
this.view.showStoryDuringRender();
|
||||||
return this.renderToDOM(...args);
|
return renderToDOM(...args);
|
||||||
},
|
},
|
||||||
this.mainStoryCallbacks(storyId),
|
this.mainStoryCallbacks(storyId),
|
||||||
storyId,
|
storyId,
|
||||||
@ -302,7 +305,10 @@ export class PreviewWeb<TFramework extends AnyFramework> extends Preview<TFramew
|
|||||||
|
|
||||||
// We need to store this right away, so if the story changes during
|
// We need to store this right away, so if the story changes during
|
||||||
// the async `.prepare()` below, we can (potentially) cancel it
|
// the async `.prepare()` below, we can (potentially) cancel it
|
||||||
|
const lastSelection = this.currentSelection;
|
||||||
this.currentSelection = selection;
|
this.currentSelection = selection;
|
||||||
|
|
||||||
|
const lastRender = this.currentRender;
|
||||||
this.currentRender = render;
|
this.currentRender = render;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -311,18 +317,26 @@ export class PreviewWeb<TFramework extends AnyFramework> extends Preview<TFramew
|
|||||||
if (err !== PREPARE_ABORTED) {
|
if (err !== PREPARE_ABORTED) {
|
||||||
// We are about to render an error so make sure the previous story is
|
// We are about to render an error so make sure the previous story is
|
||||||
// no longer rendered.
|
// no longer rendered.
|
||||||
await this.teardownRender(lastRender);
|
if (lastRender) await this.teardownRender(lastRender);
|
||||||
this.renderStoryLoadingException(storyId, err);
|
this.renderStoryLoadingException(storyId, err as Error);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const implementationChanged = !storyIdChanged && !render.isEqual(lastRender);
|
const implementationChanged = !storyIdChanged && lastRender && !render.isEqual(lastRender);
|
||||||
|
|
||||||
if (persistedArgs && entry.type !== 'docs')
|
if (persistedArgs && entry.type !== 'docs') {
|
||||||
|
if (!render.story) throw new Error('Render has not been prepared!');
|
||||||
this.storyStore.args.updateFromPersisted(render.story, persistedArgs);
|
this.storyStore.args.updateFromPersisted(render.story, persistedArgs);
|
||||||
|
}
|
||||||
|
|
||||||
// Don't re-render the story if nothing has changed to justify it
|
// Don't re-render the story if nothing has changed to justify it
|
||||||
if (lastRender && !storyIdChanged && !implementationChanged && !viewModeChanged) {
|
if (
|
||||||
|
lastRender &&
|
||||||
|
!lastRender.torndown &&
|
||||||
|
!storyIdChanged &&
|
||||||
|
!implementationChanged &&
|
||||||
|
!viewModeChanged
|
||||||
|
) {
|
||||||
this.currentRender = lastRender;
|
this.currentRender = lastRender;
|
||||||
this.channel.emit(STORY_UNCHANGED, storyId);
|
this.channel.emit(STORY_UNCHANGED, storyId);
|
||||||
this.view.showMain();
|
this.view.showMain();
|
||||||
@ -331,7 +345,7 @@ export class PreviewWeb<TFramework extends AnyFramework> extends Preview<TFramew
|
|||||||
|
|
||||||
// Wait for the previous render to leave the page. NOTE: this will wait to ensure anything async
|
// Wait for the previous render to leave the page. NOTE: this will wait to ensure anything async
|
||||||
// is properly aborted, which (in some cases) can lead to the whole screen being refreshed.
|
// is properly aborted, which (in some cases) can lead to the whole screen being refreshed.
|
||||||
await this.teardownRender(lastRender, { viewModeChanged });
|
if (lastRender) await this.teardownRender(lastRender, { viewModeChanged });
|
||||||
|
|
||||||
// If we are rendering something new (as opposed to re-rendering the same or first story), emit
|
// If we are rendering something new (as opposed to re-rendering the same or first story), emit
|
||||||
if (lastSelection && (storyIdChanged || viewModeChanged)) {
|
if (lastSelection && (storyIdChanged || viewModeChanged)) {
|
||||||
@ -339,6 +353,7 @@ export class PreviewWeb<TFramework extends AnyFramework> extends Preview<TFramew
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (entry.type !== 'docs') {
|
if (entry.type !== 'docs') {
|
||||||
|
if (!render.story) throw new Error('Render has not been prepared!');
|
||||||
const { parameters, initialArgs, argTypes, args } = this.storyStore.getStoryContext(
|
const { parameters, initialArgs, argTypes, args } = this.storyStore.getStoryContext(
|
||||||
render.story
|
render.story
|
||||||
);
|
);
|
||||||
@ -367,6 +382,7 @@ export class PreviewWeb<TFramework extends AnyFramework> extends Preview<TFramew
|
|||||||
this.renderStoryToElement.bind(this)
|
this.renderStoryToElement.bind(this)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
if (!render.story) throw new Error('Render has not been prepared!');
|
||||||
this.storyRenders.push(render as StoryRender<TFramework>);
|
this.storyRenders.push(render as StoryRender<TFramework>);
|
||||||
(this.currentRender as StoryRender<TFramework>).renderToElement(
|
(this.currentRender as StoryRender<TFramework>).renderToElement(
|
||||||
this.view.prepareForStory(render.story)
|
this.view.prepareForStory(render.story)
|
||||||
@ -380,6 +396,9 @@ export class PreviewWeb<TFramework extends AnyFramework> extends Preview<TFramew
|
|||||||
// we will change it to go ahead and load the story, which will end up being
|
// we will change it to go ahead and load the story, which will end up being
|
||||||
// "instant", although async.
|
// "instant", although async.
|
||||||
renderStoryToElement(story: Story<TFramework>, element: HTMLElement) {
|
renderStoryToElement(story: Story<TFramework>, element: HTMLElement) {
|
||||||
|
if (!this.renderToDOM)
|
||||||
|
throw new Error(`Cannot call renderStoryToElement before initialization`);
|
||||||
|
|
||||||
const render = new StoryRender<TFramework>(
|
const render = new StoryRender<TFramework>(
|
||||||
this.channel,
|
this.channel,
|
||||||
this.storyStore,
|
this.storyStore,
|
||||||
@ -400,10 +419,10 @@ export class PreviewWeb<TFramework extends AnyFramework> extends Preview<TFramew
|
|||||||
|
|
||||||
async teardownRender(
|
async teardownRender(
|
||||||
render: Render<TFramework>,
|
render: Render<TFramework>,
|
||||||
{ viewModeChanged }: { viewModeChanged?: boolean } = {}
|
{ viewModeChanged = false }: { viewModeChanged?: boolean } = {}
|
||||||
) {
|
) {
|
||||||
this.storyRenders = this.storyRenders.filter((r) => r !== render);
|
this.storyRenders = this.storyRenders.filter((r) => r !== render);
|
||||||
await render?.teardown({ viewModeChanged });
|
await render?.teardown?.({ viewModeChanged });
|
||||||
}
|
}
|
||||||
|
|
||||||
// API
|
// API
|
||||||
|
@ -54,7 +54,8 @@ export interface Render<TFramework extends AnyFramework> {
|
|||||||
story?: Story<TFramework>;
|
story?: Story<TFramework>;
|
||||||
isPreparing: () => boolean;
|
isPreparing: () => boolean;
|
||||||
disableKeyListeners: boolean;
|
disableKeyListeners: boolean;
|
||||||
teardown: (options: { viewModeChanged: boolean }) => Promise<void>;
|
teardown?: (options: { viewModeChanged: boolean }) => Promise<void>;
|
||||||
|
torndown: boolean;
|
||||||
renderToElement: (canvasElement: HTMLElement, renderStoryToElement?: any) => Promise<void>;
|
renderToElement: (canvasElement: HTMLElement, renderStoryToElement?: any) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,6 +76,8 @@ export class StoryRender<TFramework extends AnyFramework> implements Render<TFra
|
|||||||
|
|
||||||
private teardownRender: TeardownRenderToDOM = () => {};
|
private teardownRender: TeardownRenderToDOM = () => {};
|
||||||
|
|
||||||
|
public torndown = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public channel: Channel,
|
public channel: Channel,
|
||||||
public store: StoryStore<TFramework>,
|
public store: StoryStore<TFramework>,
|
||||||
@ -143,6 +146,7 @@ export class StoryRender<TFramework extends AnyFramework> implements Render<TFra
|
|||||||
}
|
}
|
||||||
|
|
||||||
private storyContext() {
|
private storyContext() {
|
||||||
|
if (!this.story) throw new Error(`Cannot call storyContext before preparing`);
|
||||||
return this.store.getStoryContext(this.story);
|
return this.store.getStoryContext(this.story);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -153,7 +157,10 @@ export class StoryRender<TFramework extends AnyFramework> implements Render<TFra
|
|||||||
initial?: boolean;
|
initial?: boolean;
|
||||||
forceRemount?: boolean;
|
forceRemount?: boolean;
|
||||||
} = {}) {
|
} = {}) {
|
||||||
|
const { canvasElement } = this;
|
||||||
if (!this.story) throw new Error('cannot render when not prepared');
|
if (!this.story) throw new Error('cannot render when not prepared');
|
||||||
|
if (!canvasElement) throw new Error('cannot render when canvasElement is unset');
|
||||||
|
|
||||||
const { id, componentId, title, name, applyLoaders, unboundStoryFn, playFunction } = this.story;
|
const { id, componentId, title, name, applyLoaders, unboundStoryFn, playFunction } = this.story;
|
||||||
|
|
||||||
if (forceRemount && !initial) {
|
if (forceRemount && !initial) {
|
||||||
@ -169,7 +176,7 @@ export class StoryRender<TFramework extends AnyFramework> implements Render<TFra
|
|||||||
const abortSignal = (this.abortController as AbortController).signal;
|
const abortSignal = (this.abortController as AbortController).signal;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let loadedContext: StoryContext<TFramework>;
|
let loadedContext: Awaited<ReturnType<typeof applyLoaders>>;
|
||||||
await this.runPhase(abortSignal, 'loading', async () => {
|
await this.runPhase(abortSignal, 'loading', async () => {
|
||||||
loadedContext = await applyLoaders({
|
loadedContext = await applyLoaders({
|
||||||
...this.storyContext(),
|
...this.storyContext(),
|
||||||
@ -181,13 +188,12 @@ export class StoryRender<TFramework extends AnyFramework> implements Render<TFra
|
|||||||
}
|
}
|
||||||
|
|
||||||
const renderStoryContext: StoryContext<TFramework> = {
|
const renderStoryContext: StoryContext<TFramework> = {
|
||||||
// @ts-ignore
|
...loadedContext!,
|
||||||
...loadedContext,
|
|
||||||
// By this stage, it is possible that new args/globals have been received for this story
|
// By this stage, it is possible that new args/globals have been received for this story
|
||||||
// and we need to ensure we render it with the new values
|
// and we need to ensure we render it with the new values
|
||||||
...this.storyContext(),
|
...this.storyContext(),
|
||||||
abortSignal,
|
abortSignal,
|
||||||
canvasElement: this.canvasElement as HTMLElement,
|
canvasElement,
|
||||||
};
|
};
|
||||||
const renderContext: RenderContext<TFramework> = {
|
const renderContext: RenderContext<TFramework> = {
|
||||||
componentId,
|
componentId,
|
||||||
@ -205,7 +211,7 @@ export class StoryRender<TFramework extends AnyFramework> implements Render<TFra
|
|||||||
|
|
||||||
await this.runPhase(abortSignal, 'rendering', async () => {
|
await this.runPhase(abortSignal, 'rendering', async () => {
|
||||||
this.teardownRender =
|
this.teardownRender =
|
||||||
(await this.renderToScreen(renderContext, this.canvasElement)) || (() => {});
|
(await this.renderToScreen(renderContext, canvasElement)) || (() => {});
|
||||||
});
|
});
|
||||||
this.notYetRendered = false;
|
this.notYetRendered = false;
|
||||||
if (abortSignal.aborted) return;
|
if (abortSignal.aborted) return;
|
||||||
@ -242,12 +248,11 @@ export class StoryRender<TFramework extends AnyFramework> implements Render<TFra
|
|||||||
// as a method to abort them, ASAP, but this is not foolproof as we cannot control what
|
// as a method to abort them, ASAP, but this is not foolproof as we cannot control what
|
||||||
// happens inside the user's code.
|
// happens inside the user's code.
|
||||||
cancelRender() {
|
cancelRender() {
|
||||||
if (this.abortController) {
|
this.abortController?.abort();
|
||||||
this.abortController.abort();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async teardown(options: {} = {}) {
|
async teardown(options: {} = {}) {
|
||||||
|
this.torndown = true;
|
||||||
this.cancelRender();
|
this.cancelRender();
|
||||||
|
|
||||||
// If the story has loaded, we need to cleanup
|
// If the story has loaded, we need to cleanup
|
||||||
|
@ -61,8 +61,7 @@ const getFirstString = (v: ValueOf<qs.ParsedQs>): string | void => {
|
|||||||
return getFirstString(v[0]);
|
return getFirstString(v[0]);
|
||||||
}
|
}
|
||||||
if (isObject(v)) {
|
if (isObject(v)) {
|
||||||
// @ts-ignore
|
return getFirstString(Object.values(v).filter(Boolean) as string[]);
|
||||||
return getFirstString(Object.values(v));
|
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
@ -74,7 +73,7 @@ Use \`id=$storyId\` instead.
|
|||||||
See https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#new-url-structure`
|
See https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#new-url-structure`
|
||||||
);
|
);
|
||||||
|
|
||||||
export const getSelectionSpecifierFromPath: () => SelectionSpecifier = () => {
|
export const getSelectionSpecifierFromPath: () => SelectionSpecifier | null = () => {
|
||||||
const query = qs.parse(document.location.search, { ignoreQueryPrefix: true });
|
const query = qs.parse(document.location.search, { ignoreQueryPrefix: true });
|
||||||
const args = typeof query.args === 'string' ? parseArgsParam(query.args) : undefined;
|
const args = typeof query.args === 'string' ? parseArgsParam(query.args) : undefined;
|
||||||
const globals = typeof query.globals === 'string' ? parseArgsParam(query.globals) : undefined;
|
const globals = typeof query.globals === 'string' ? parseArgsParam(query.globals) : undefined;
|
||||||
@ -103,9 +102,9 @@ export const getSelectionSpecifierFromPath: () => SelectionSpecifier = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export class UrlStore {
|
export class UrlStore {
|
||||||
selectionSpecifier: SelectionSpecifier;
|
selectionSpecifier: SelectionSpecifier | null;
|
||||||
|
|
||||||
selection: Selection;
|
selection?: Selection;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.selectionSpecifier = getSelectionSpecifierFromPath();
|
this.selectionSpecifier = getSelectionSpecifierFromPath();
|
||||||
|
@ -41,7 +41,7 @@ export class WebView {
|
|||||||
|
|
||||||
testing = false;
|
testing = false;
|
||||||
|
|
||||||
preparingTimeout: ReturnType<typeof setTimeout> = null;
|
preparingTimeout?: ReturnType<typeof setTimeout>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
// Special code for testing situations
|
// Special code for testing situations
|
||||||
|
@ -25,7 +25,8 @@ const validateArgs = (key = '', value: unknown): boolean => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (Array.isArray(value)) return value.every((v) => validateArgs(key, v));
|
if (Array.isArray(value)) return value.every((v) => validateArgs(key, v));
|
||||||
if (isPlainObject(value)) return Object.entries(value).every(([k, v]) => validateArgs(k, v));
|
if (isPlainObject(value))
|
||||||
|
return Object.entries(value as object).every(([k, v]) => validateArgs(k, v));
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../tsconfig.json",
|
"extends": "../../tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"strict": true
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src/**/*"
|
"src/**/*"
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { expect } from '@jest/globals';
|
||||||
|
|
||||||
import { ArgsStore } from './ArgsStore';
|
import { ArgsStore } from './ArgsStore';
|
||||||
|
|
||||||
jest.mock('@storybook/client-logger');
|
jest.mock('@storybook/client-logger');
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
|
import { expect } from '@jest/globals';
|
||||||
import { GlobalsStore } from './GlobalsStore';
|
import { GlobalsStore } from './GlobalsStore';
|
||||||
|
|
||||||
describe('GlobalsStore', () => {
|
describe('GlobalsStore', () => {
|
||||||
it('is initialized to the value in globals', () => {
|
it('is initialized to the value in globals', () => {
|
||||||
const store = new GlobalsStore();
|
const store = new GlobalsStore({
|
||||||
store.set({
|
|
||||||
globals: {
|
globals: {
|
||||||
arg1: 'arg1',
|
arg1: 'arg1',
|
||||||
arg2: 2,
|
arg2: 2,
|
||||||
@ -20,8 +20,7 @@ describe('GlobalsStore', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('is initialized to the default values from globalTypes if global is unset', () => {
|
it('is initialized to the default values from globalTypes if global is unset', () => {
|
||||||
const store = new GlobalsStore();
|
const store = new GlobalsStore({
|
||||||
store.set({
|
|
||||||
globals: {
|
globals: {
|
||||||
arg1: 'arg1',
|
arg1: 'arg1',
|
||||||
arg2: 2,
|
arg2: 2,
|
||||||
@ -42,8 +41,7 @@ describe('GlobalsStore', () => {
|
|||||||
|
|
||||||
describe('update', () => {
|
describe('update', () => {
|
||||||
it('changes the global args', () => {
|
it('changes the global args', () => {
|
||||||
const store = new GlobalsStore();
|
const store = new GlobalsStore({ globals: { foo: 'old' }, globalTypes: { baz: {} } });
|
||||||
store.set({ globals: { foo: 'old' }, globalTypes: { baz: {} } });
|
|
||||||
|
|
||||||
store.update({ foo: 'bar' });
|
store.update({ foo: 'bar' });
|
||||||
expect(store.get()).toEqual({ foo: 'bar' });
|
expect(store.get()).toEqual({ foo: 'bar' });
|
||||||
@ -57,8 +55,7 @@ describe('GlobalsStore', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('does not merge objects', () => {
|
it('does not merge objects', () => {
|
||||||
const store = new GlobalsStore();
|
const store = new GlobalsStore({ globals: {}, globalTypes: {} });
|
||||||
store.set({ globals: {}, globalTypes: {} });
|
|
||||||
|
|
||||||
store.update({ obj: { foo: 'bar' } });
|
store.update({ obj: { foo: 'bar' } });
|
||||||
expect(store.get()).toEqual({ obj: { foo: 'bar' } });
|
expect(store.get()).toEqual({ obj: { foo: 'bar' } });
|
||||||
@ -70,8 +67,7 @@ describe('GlobalsStore', () => {
|
|||||||
|
|
||||||
describe('updateFromPersisted', () => {
|
describe('updateFromPersisted', () => {
|
||||||
it('only sets values for which globals or globalArgs exist', () => {
|
it('only sets values for which globals or globalArgs exist', () => {
|
||||||
const store = new GlobalsStore();
|
const store = new GlobalsStore({
|
||||||
store.set({
|
|
||||||
globals: {
|
globals: {
|
||||||
arg1: 'arg1',
|
arg1: 'arg1',
|
||||||
},
|
},
|
||||||
@ -92,8 +88,7 @@ describe('GlobalsStore', () => {
|
|||||||
|
|
||||||
describe('second call to set', () => {
|
describe('second call to set', () => {
|
||||||
it('is initialized to the (new) default values from globalTypes if the (new) global is unset', () => {
|
it('is initialized to the (new) default values from globalTypes if the (new) global is unset', () => {
|
||||||
const store = new GlobalsStore();
|
const store = new GlobalsStore({ globals: {}, globalTypes: {} });
|
||||||
store.set({ globals: {}, globalTypes: {} });
|
|
||||||
|
|
||||||
expect(store.get()).toEqual({});
|
expect(store.get()).toEqual({});
|
||||||
|
|
||||||
@ -118,8 +113,7 @@ describe('GlobalsStore', () => {
|
|||||||
|
|
||||||
describe('when underlying globals have not changed', () => {
|
describe('when underlying globals have not changed', () => {
|
||||||
it('retains updated values, but not if they are undeclared', () => {
|
it('retains updated values, but not if they are undeclared', () => {
|
||||||
const store = new GlobalsStore();
|
const store = new GlobalsStore({
|
||||||
store.set({
|
|
||||||
globals: {
|
globals: {
|
||||||
arg1: 'arg1',
|
arg1: 'arg1',
|
||||||
},
|
},
|
||||||
@ -152,8 +146,7 @@ describe('GlobalsStore', () => {
|
|||||||
|
|
||||||
describe('when underlying globals have changed', () => {
|
describe('when underlying globals have changed', () => {
|
||||||
it('retains a the same delta', () => {
|
it('retains a the same delta', () => {
|
||||||
const store = new GlobalsStore();
|
const store = new GlobalsStore({
|
||||||
store.set({
|
|
||||||
globals: {
|
globals: {
|
||||||
arg1: 'arg1',
|
arg1: 'arg1',
|
||||||
arg4: 'arg4',
|
arg4: 'arg4',
|
||||||
|
@ -14,11 +14,22 @@ const setUndeclaredWarning = deprecate(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export class GlobalsStore {
|
export class GlobalsStore {
|
||||||
allowedGlobalNames: Set<string>;
|
// We use ! here because TS doesn't analyse the .set() function to see if it actually get set
|
||||||
|
allowedGlobalNames!: Set<string>;
|
||||||
|
|
||||||
initialGlobals: Globals;
|
initialGlobals!: Globals;
|
||||||
|
|
||||||
globals: Globals = {};
|
globals!: Globals;
|
||||||
|
|
||||||
|
constructor({
|
||||||
|
globals = {},
|
||||||
|
globalTypes = {},
|
||||||
|
}: {
|
||||||
|
globals?: Globals;
|
||||||
|
globalTypes?: GlobalTypes;
|
||||||
|
}) {
|
||||||
|
this.set({ globals, globalTypes });
|
||||||
|
}
|
||||||
|
|
||||||
set({ globals = {}, globalTypes = {} }: { globals?: Globals; globalTypes?: GlobalTypes }) {
|
set({ globals = {}, globalTypes = {} }: { globals?: Globals; globalTypes?: GlobalTypes }) {
|
||||||
const delta = this.initialGlobals && deepDiff(this.initialGlobals, this.globals);
|
const delta = this.initialGlobals && deepDiff(this.initialGlobals, this.globals);
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { expect } from '@jest/globals';
|
||||||
|
|
||||||
import { StoryIndexStore } from './StoryIndexStore';
|
import { StoryIndexStore } from './StoryIndexStore';
|
||||||
import { StoryIndex } from './types';
|
import { StoryIndex } from './types';
|
||||||
|
|
||||||
@ -7,18 +9,21 @@ const storyIndex: StoryIndex = {
|
|||||||
v: 4,
|
v: 4,
|
||||||
entries: {
|
entries: {
|
||||||
'component-one--a': {
|
'component-one--a': {
|
||||||
|
type: 'story',
|
||||||
id: 'component-one--a',
|
id: 'component-one--a',
|
||||||
title: 'Component One',
|
title: 'Component One',
|
||||||
name: 'A',
|
name: 'A',
|
||||||
importPath: './src/ComponentOne.stories.js',
|
importPath: './src/ComponentOne.stories.js',
|
||||||
},
|
},
|
||||||
'component-one--b': {
|
'component-one--b': {
|
||||||
|
type: 'story',
|
||||||
id: 'component-one--b',
|
id: 'component-one--b',
|
||||||
title: 'Component One',
|
title: 'Component One',
|
||||||
name: 'B',
|
name: 'B',
|
||||||
importPath: './src/ComponentOne.stories.js',
|
importPath: './src/ComponentOne.stories.js',
|
||||||
},
|
},
|
||||||
'component-two--c': {
|
'component-two--c': {
|
||||||
|
type: 'story',
|
||||||
id: 'component-one--c',
|
id: 'component-one--c',
|
||||||
title: 'Component Two',
|
title: 'Component Two',
|
||||||
name: 'C',
|
name: 'C',
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import dedent from 'ts-dedent';
|
import dedent from 'ts-dedent';
|
||||||
import { Channel } from '@storybook/addons';
|
|
||||||
import type { StoryId } from '@storybook/csf';
|
import type { StoryId } from '@storybook/csf';
|
||||||
import memoize from 'memoizerific';
|
import memoize from 'memoizerific';
|
||||||
|
|
||||||
@ -13,8 +12,6 @@ const getImportPathMap = memoize(1)((entries: StoryIndex['entries']) =>
|
|||||||
);
|
);
|
||||||
|
|
||||||
export class StoryIndexStore {
|
export class StoryIndexStore {
|
||||||
channel: Channel;
|
|
||||||
|
|
||||||
entries: StoryIndex['entries'];
|
entries: StoryIndex['entries'];
|
||||||
|
|
||||||
constructor({ entries }: StoryIndex = { v: 4, entries: {} }) {
|
constructor({ entries }: StoryIndex = { v: 4, entries: {} }) {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import type { AnyFramework, ProjectAnnotations } from '@storybook/csf';
|
import type { AnyFramework, ProjectAnnotations } from '@storybook/csf';
|
||||||
import global from 'global';
|
import global from 'global';
|
||||||
|
import { expect } from '@jest/globals';
|
||||||
|
|
||||||
import { prepareStory } from './csf/prepareStory';
|
import { prepareStory } from './csf/prepareStory';
|
||||||
import { processCSFFile } from './csf/processCSFFile';
|
import { processCSFFile } from './csf/processCSFFile';
|
||||||
@ -76,10 +77,10 @@ describe('StoryStore', () => {
|
|||||||
store.setProjectAnnotations(projectAnnotations);
|
store.setProjectAnnotations(projectAnnotations);
|
||||||
store.initialize({ storyIndex, importFn, cache: false });
|
store.initialize({ storyIndex, importFn, cache: false });
|
||||||
|
|
||||||
expect(store.projectAnnotations.globalTypes).toEqual({
|
expect(store.projectAnnotations!.globalTypes).toEqual({
|
||||||
a: { name: 'a', type: { name: 'string' } },
|
a: { name: 'a', type: { name: 'string' } },
|
||||||
});
|
});
|
||||||
expect(store.projectAnnotations.argTypes).toEqual({
|
expect(store.projectAnnotations!.argTypes).toEqual({
|
||||||
a: { name: 'a', type: { name: 'string' } },
|
a: { name: 'a', type: { name: 'string' } },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -90,10 +91,10 @@ describe('StoryStore', () => {
|
|||||||
store.initialize({ storyIndex, importFn, cache: false });
|
store.initialize({ storyIndex, importFn, cache: false });
|
||||||
|
|
||||||
store.setProjectAnnotations(projectAnnotations);
|
store.setProjectAnnotations(projectAnnotations);
|
||||||
expect(store.projectAnnotations.globalTypes).toEqual({
|
expect(store.projectAnnotations!.globalTypes).toEqual({
|
||||||
a: { name: 'a', type: { name: 'string' } },
|
a: { name: 'a', type: { name: 'string' } },
|
||||||
});
|
});
|
||||||
expect(store.projectAnnotations.argTypes).toEqual({
|
expect(store.projectAnnotations!.argTypes).toEqual({
|
||||||
a: { name: 'a', type: { name: 'string' } },
|
a: { name: 'a', type: { name: 'string' } },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -408,7 +409,7 @@ describe('StoryStore', () => {
|
|||||||
const story = await store.loadStory({ storyId: 'component-one--a' });
|
const story = await store.loadStory({ storyId: 'component-one--a' });
|
||||||
|
|
||||||
store.args.update(story.id, { foo: 'bar' });
|
store.args.update(story.id, { foo: 'bar' });
|
||||||
store.globals.update({ a: 'c' });
|
store.globals!.update({ a: 'c' });
|
||||||
|
|
||||||
expect(store.getStoryContext(story)).toMatchObject({
|
expect(store.getStoryContext(story)).toMatchObject({
|
||||||
args: { foo: 'bar' },
|
args: { foo: 'bar' },
|
||||||
@ -455,8 +456,9 @@ describe('StoryStore', () => {
|
|||||||
|
|
||||||
importFn.mockClear();
|
importFn.mockClear();
|
||||||
const csfFiles = await store.loadAllCSFFiles();
|
const csfFiles = await store.loadAllCSFFiles();
|
||||||
|
expect(csfFiles).not.toBeUndefined();
|
||||||
|
|
||||||
expect(Object.keys(csfFiles)).toEqual([
|
expect(Object.keys(csfFiles!)).toEqual([
|
||||||
'./src/ComponentOne.stories.js',
|
'./src/ComponentOne.stories.js',
|
||||||
'./src/ComponentTwo.stories.js',
|
'./src/ComponentTwo.stories.js',
|
||||||
]);
|
]);
|
||||||
|
@ -38,13 +38,13 @@ const CSF_CACHE_SIZE = 1000;
|
|||||||
const STORY_CACHE_SIZE = 10000;
|
const STORY_CACHE_SIZE = 10000;
|
||||||
|
|
||||||
export class StoryStore<TFramework extends AnyFramework> {
|
export class StoryStore<TFramework extends AnyFramework> {
|
||||||
storyIndex: StoryIndexStore;
|
storyIndex?: StoryIndexStore;
|
||||||
|
|
||||||
importFn: ModuleImportFn;
|
importFn?: ModuleImportFn;
|
||||||
|
|
||||||
projectAnnotations: NormalizedProjectAnnotations<TFramework>;
|
projectAnnotations?: NormalizedProjectAnnotations<TFramework>;
|
||||||
|
|
||||||
globals: GlobalsStore;
|
globals?: GlobalsStore;
|
||||||
|
|
||||||
args: ArgsStore;
|
args: ArgsStore;
|
||||||
|
|
||||||
@ -58,10 +58,10 @@ export class StoryStore<TFramework extends AnyFramework> {
|
|||||||
|
|
||||||
initializationPromise: SynchronousPromise<void>;
|
initializationPromise: SynchronousPromise<void>;
|
||||||
|
|
||||||
resolveInitializationPromise: () => void;
|
// This *does* get set in the constructor but the semantics of `new SynchronousPromise` trip up TS
|
||||||
|
resolveInitializationPromise!: () => void;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.globals = new GlobalsStore();
|
|
||||||
this.args = new ArgsStore();
|
this.args = new ArgsStore();
|
||||||
this.hooks = {};
|
this.hooks = {};
|
||||||
|
|
||||||
@ -82,7 +82,11 @@ export class StoryStore<TFramework extends AnyFramework> {
|
|||||||
this.projectAnnotations = normalizeProjectAnnotations(projectAnnotations);
|
this.projectAnnotations = normalizeProjectAnnotations(projectAnnotations);
|
||||||
const { globals, globalTypes } = projectAnnotations;
|
const { globals, globalTypes } = projectAnnotations;
|
||||||
|
|
||||||
this.globals.set({ globals, globalTypes });
|
if (this.globals) {
|
||||||
|
this.globals.set({ globals, globalTypes });
|
||||||
|
} else {
|
||||||
|
this.globals = new GlobalsStore({ globals, globalTypes });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
initialize({
|
initialize({
|
||||||
@ -114,6 +118,8 @@ export class StoryStore<TFramework extends AnyFramework> {
|
|||||||
importFn?: ModuleImportFn;
|
importFn?: ModuleImportFn;
|
||||||
storyIndex?: StoryIndex;
|
storyIndex?: StoryIndex;
|
||||||
}) {
|
}) {
|
||||||
|
if (!this.storyIndex) throw new Error(`onStoriesChanged called before initialization`);
|
||||||
|
|
||||||
if (importFn) this.importFn = importFn;
|
if (importFn) this.importFn = importFn;
|
||||||
if (storyIndex) this.storyIndex.entries = storyIndex.entries;
|
if (storyIndex) this.storyIndex.entries = storyIndex.entries;
|
||||||
if (this.cachedCSFFiles) await this.cacheAllCSFFiles();
|
if (this.cachedCSFFiles) await this.cacheAllCSFFiles();
|
||||||
@ -122,11 +128,15 @@ export class StoryStore<TFramework extends AnyFramework> {
|
|||||||
// Get an entry from the index, waiting on initialization if necessary
|
// Get an entry from the index, waiting on initialization if necessary
|
||||||
async storyIdToEntry(storyId: StoryId) {
|
async storyIdToEntry(storyId: StoryId) {
|
||||||
await this.initializationPromise;
|
await this.initializationPromise;
|
||||||
return this.storyIndex.storyIdToEntry(storyId);
|
// The index will always be set before the initialization promise returns
|
||||||
|
return this.storyIndex!.storyIdToEntry(storyId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// To load a single CSF file to service a story we need to look up the importPath in the index
|
// To load a single CSF file to service a story we need to look up the importPath in the index
|
||||||
loadCSFFileByStoryId(storyId: StoryId): PromiseLike<CSFFile<TFramework>> {
|
loadCSFFileByStoryId(storyId: StoryId): PromiseLike<CSFFile<TFramework>> {
|
||||||
|
if (!this.storyIndex || !this.importFn)
|
||||||
|
throw new Error(`loadCSFFileByStoryId called before initialization`);
|
||||||
|
|
||||||
const { importPath, title } = this.storyIndex.storyIdToEntry(storyId);
|
const { importPath, title } = this.storyIndex.storyIdToEntry(storyId);
|
||||||
return this.importFn(importPath).then((moduleExports) =>
|
return this.importFn(importPath).then((moduleExports) =>
|
||||||
// We pass the title in here as it may have been generated by autoTitle on the server.
|
// We pass the title in here as it may have been generated by autoTitle on the server.
|
||||||
@ -135,6 +145,8 @@ export class StoryStore<TFramework extends AnyFramework> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
loadAllCSFFiles(): PromiseLike<StoryStore<TFramework>['cachedCSFFiles']> {
|
loadAllCSFFiles(): PromiseLike<StoryStore<TFramework>['cachedCSFFiles']> {
|
||||||
|
if (!this.storyIndex) throw new Error(`loadAllCSFFiles called before initialization`);
|
||||||
|
|
||||||
const importPaths: Record<Path, StoryId> = {};
|
const importPaths: Record<Path, StoryId> = {};
|
||||||
Object.entries(this.storyIndex.entries).forEach(([storyId, { importPath }]) => {
|
Object.entries(this.storyIndex.entries).forEach(([storyId, { importPath }]) => {
|
||||||
importPaths[importPath] = storyId;
|
importPaths[importPath] = storyId;
|
||||||
@ -179,6 +191,8 @@ export class StoryStore<TFramework extends AnyFramework> {
|
|||||||
storyId: StoryId;
|
storyId: StoryId;
|
||||||
csfFile: CSFFile<TFramework>;
|
csfFile: CSFFile<TFramework>;
|
||||||
}): Story<TFramework> {
|
}): Story<TFramework> {
|
||||||
|
if (!this.projectAnnotations) throw new Error(`storyFromCSFFile called before initialization`);
|
||||||
|
|
||||||
const storyAnnotations = csfFile.stories[storyId];
|
const storyAnnotations = csfFile.stories[storyId];
|
||||||
if (!storyAnnotations) {
|
if (!storyAnnotations) {
|
||||||
throw new Error(`Didn't find '${storyId}' in CSF file, this is unexpected`);
|
throw new Error(`Didn't find '${storyId}' in CSF file, this is unexpected`);
|
||||||
@ -197,6 +211,9 @@ export class StoryStore<TFramework extends AnyFramework> {
|
|||||||
|
|
||||||
// If we have a CSF file we can get all the stories from it synchronously
|
// If we have a CSF file we can get all the stories from it synchronously
|
||||||
componentStoriesFromCSFFile({ csfFile }: { csfFile: CSFFile<TFramework> }): Story<TFramework>[] {
|
componentStoriesFromCSFFile({ csfFile }: { csfFile: CSFFile<TFramework> }): Story<TFramework>[] {
|
||||||
|
if (!this.storyIndex)
|
||||||
|
throw new Error(`componentStoriesFromCSFFile called before initialization`);
|
||||||
|
|
||||||
return Object.keys(this.storyIndex.entries)
|
return Object.keys(this.storyIndex.entries)
|
||||||
.filter((storyId: StoryId) => !!csfFile.stories[storyId])
|
.filter((storyId: StoryId) => !!csfFile.stories[storyId])
|
||||||
.map((storyId: StoryId) => this.storyFromCSFFile({ storyId, csfFile }));
|
.map((storyId: StoryId) => this.storyFromCSFFile({ storyId, csfFile }));
|
||||||
@ -205,6 +222,10 @@ export class StoryStore<TFramework extends AnyFramework> {
|
|||||||
async loadDocsFileById(
|
async loadDocsFileById(
|
||||||
docsId: StoryId
|
docsId: StoryId
|
||||||
): Promise<{ docsExports: ModuleExports; csfFiles: CSFFile<TFramework>[] }> {
|
): Promise<{ docsExports: ModuleExports; csfFiles: CSFFile<TFramework>[] }> {
|
||||||
|
const { storyIndex } = this;
|
||||||
|
if (!storyIndex || !this.importFn)
|
||||||
|
throw new Error(`componentStoriesFromCSFFile called before initialization`);
|
||||||
|
|
||||||
const entry = await this.storyIdToEntry(docsId);
|
const entry = await this.storyIdToEntry(docsId);
|
||||||
if (entry.type !== 'docs') throw new Error(`Cannot load docs file for id ${docsId}`);
|
if (entry.type !== 'docs') throw new Error(`Cannot load docs file for id ${docsId}`);
|
||||||
|
|
||||||
@ -213,7 +234,7 @@ export class StoryStore<TFramework extends AnyFramework> {
|
|||||||
const [docsExports, ...csfFiles] = (await Promise.all([
|
const [docsExports, ...csfFiles] = (await Promise.all([
|
||||||
this.importFn(importPath),
|
this.importFn(importPath),
|
||||||
...storiesImports.map((storyImportPath) => {
|
...storiesImports.map((storyImportPath) => {
|
||||||
const firstStoryEntry = this.storyIndex.importPathToEntry(storyImportPath);
|
const firstStoryEntry = storyIndex.importPathToEntry(storyImportPath);
|
||||||
return this.loadCSFFileByStoryId(firstStoryEntry.id);
|
return this.loadCSFFileByStoryId(firstStoryEntry.id);
|
||||||
}),
|
}),
|
||||||
])) as [ModuleExports, ...CSFFile<TFramework>[]];
|
])) as [ModuleExports, ...CSFFile<TFramework>[]];
|
||||||
@ -232,6 +253,8 @@ export class StoryStore<TFramework extends AnyFramework> {
|
|||||||
// A prepared story does not include args, globals or hooks. These are stored in the story store
|
// A prepared story does not include args, globals or hooks. These are stored in the story store
|
||||||
// and updated separtely to the (immutable) story.
|
// and updated separtely to the (immutable) story.
|
||||||
getStoryContext(story: Story<TFramework>): Omit<StoryContextForLoaders<TFramework>, 'viewMode'> {
|
getStoryContext(story: Story<TFramework>): Omit<StoryContextForLoaders<TFramework>, 'viewMode'> {
|
||||||
|
if (!this.globals) throw new Error(`getStoryContext called before initialization`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...story,
|
...story,
|
||||||
args: this.args.get(story.id),
|
args: this.args.get(story.id),
|
||||||
@ -247,15 +270,17 @@ export class StoryStore<TFramework extends AnyFramework> {
|
|||||||
extract(
|
extract(
|
||||||
options: ExtractOptions = { includeDocsOnly: false }
|
options: ExtractOptions = { includeDocsOnly: false }
|
||||||
): Record<StoryId, StoryContextForEnhancers<TFramework>> {
|
): Record<StoryId, StoryContextForEnhancers<TFramework>> {
|
||||||
if (!this.cachedCSFFiles) {
|
if (!this.storyIndex) throw new Error(`extract called before initialization`);
|
||||||
|
|
||||||
|
const { cachedCSFFiles } = this;
|
||||||
|
if (!cachedCSFFiles)
|
||||||
throw new Error('Cannot call extract() unless you call cacheAllCSFFiles() first.');
|
throw new Error('Cannot call extract() unless you call cacheAllCSFFiles() first.');
|
||||||
}
|
|
||||||
|
|
||||||
return Object.entries(this.storyIndex.entries).reduce(
|
return Object.entries(this.storyIndex.entries).reduce(
|
||||||
(acc, [storyId, { type, importPath }]) => {
|
(acc, [storyId, { type, importPath }]) => {
|
||||||
if (type === 'docs') return acc;
|
if (type === 'docs') return acc;
|
||||||
|
|
||||||
const csfFile = this.cachedCSFFiles[importPath];
|
const csfFile = cachedCSFFiles[importPath];
|
||||||
const story = this.storyFromCSFFile({ storyId, csfFile });
|
const story = this.storyFromCSFFile({ storyId, csfFile });
|
||||||
|
|
||||||
if (!options.includeDocsOnly && story.parameters.docsOnly) {
|
if (!options.includeDocsOnly && story.parameters.docsOnly) {
|
||||||
@ -282,6 +307,8 @@ export class StoryStore<TFramework extends AnyFramework> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getSetStoriesPayload() {
|
getSetStoriesPayload() {
|
||||||
|
if (!this.globals) throw new Error(`getSetStoriesPayload called before initialization`);
|
||||||
|
|
||||||
const stories = this.extract({ includeDocsOnly: true });
|
const stories = this.extract({ includeDocsOnly: true });
|
||||||
|
|
||||||
const kindParameters: Parameters = Object.values(stories).reduce(
|
const kindParameters: Parameters = Object.values(stories).reduce(
|
||||||
@ -305,11 +332,14 @@ export class StoryStore<TFramework extends AnyFramework> {
|
|||||||
// It is used to allow v7 Storybooks to be composed in v6 Storybooks, which expect a
|
// It is used to allow v7 Storybooks to be composed in v6 Storybooks, which expect a
|
||||||
// `stories.json` file with legacy fields (`kind` etc).
|
// `stories.json` file with legacy fields (`kind` etc).
|
||||||
getStoriesJsonData = (): StoryIndexV3 => {
|
getStoriesJsonData = (): StoryIndexV3 => {
|
||||||
|
const { storyIndex } = this;
|
||||||
|
if (!storyIndex) throw new Error(`getStoriesJsonData called before initialization`);
|
||||||
|
|
||||||
const value = this.getSetStoriesPayload();
|
const value = this.getSetStoriesPayload();
|
||||||
const allowedParameters = ['fileName', 'docsOnly', 'framework', '__id', '__isArgsStory'];
|
const allowedParameters = ['fileName', 'docsOnly', 'framework', '__id', '__isArgsStory'];
|
||||||
|
|
||||||
const stories: Record<StoryId, V2CompatIndexEntry> = mapValues(value.stories, (story) => {
|
const stories: Record<StoryId, V2CompatIndexEntry> = mapValues(value.stories, (story) => {
|
||||||
const { importPath } = this.storyIndex.entries[story.id];
|
const { importPath } = storyIndex.entries[story.id];
|
||||||
return {
|
return {
|
||||||
...pick(story, ['id', 'name', 'title']),
|
...pick(story, ['id', 'name', 'title']),
|
||||||
importPath,
|
importPath,
|
||||||
@ -332,13 +362,16 @@ export class StoryStore<TFramework extends AnyFramework> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
raw(): BoundStory<TFramework>[] {
|
raw(): BoundStory<TFramework>[] {
|
||||||
return Object.values(this.extract()).map(({ id }: { id: StoryId }) => this.fromId(id));
|
return Object.values(this.extract())
|
||||||
|
.map(({ id }: { id: StoryId }) => this.fromId(id))
|
||||||
|
.filter(Boolean) as BoundStory<TFramework>[];
|
||||||
}
|
}
|
||||||
|
|
||||||
fromId(storyId: StoryId): BoundStory<TFramework> {
|
fromId(storyId: StoryId): BoundStory<TFramework> | null {
|
||||||
if (!this.cachedCSFFiles) {
|
if (!this.storyIndex) throw new Error(`fromId called before initialization`);
|
||||||
|
|
||||||
|
if (!this.cachedCSFFiles)
|
||||||
throw new Error('Cannot call fromId/raw() unless you call cacheAllCSFFiles() first.');
|
throw new Error('Cannot call fromId/raw() unless you call cacheAllCSFFiles() first.');
|
||||||
}
|
|
||||||
|
|
||||||
let importPath;
|
let importPath;
|
||||||
try {
|
try {
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
import { once } from '@storybook/client-logger';
|
import { once } from '@storybook/client-logger';
|
||||||
|
import { expect } from '@jest/globals';
|
||||||
|
import { SBType } from '@storybook/csf';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
combineArgs,
|
combineArgs,
|
||||||
groupArgsByTarget,
|
groupArgsByTarget,
|
||||||
@ -7,13 +10,13 @@ import {
|
|||||||
validateOptions,
|
validateOptions,
|
||||||
} from './args';
|
} from './args';
|
||||||
|
|
||||||
const stringType = { name: 'string' };
|
const stringType: SBType = { name: 'string' };
|
||||||
const numberType = { name: 'number' };
|
const numberType: SBType = { name: 'number' };
|
||||||
const booleanType = { name: 'boolean' };
|
const booleanType: SBType = { name: 'boolean' };
|
||||||
const enumType = { name: 'enum' };
|
const enumType: SBType = { name: 'enum', value: [1, 2, 3] };
|
||||||
const functionType = { name: 'function' };
|
const functionType: SBType = { name: 'function' };
|
||||||
const numArrayType = { name: 'array', value: numberType };
|
const numArrayType: SBType = { name: 'array', value: numberType };
|
||||||
const boolObjectType = { name: 'object', value: { bool: booleanType } };
|
const boolObjectType: SBType = { name: 'object', value: { bool: booleanType } };
|
||||||
|
|
||||||
jest.mock('@storybook/client-logger');
|
jest.mock('@storybook/client-logger');
|
||||||
|
|
||||||
|
@ -151,7 +151,7 @@ export function groupArgsByTarget<TArgs = Args>({
|
|||||||
}: StoryContext<AnyFramework, TArgs>) {
|
}: StoryContext<AnyFramework, TArgs>) {
|
||||||
const groupedArgs: Record<string, Partial<TArgs>> = {};
|
const groupedArgs: Record<string, Partial<TArgs>> = {};
|
||||||
(Object.entries(args) as [keyof TArgs, any][]).forEach(([name, value]) => {
|
(Object.entries(args) as [keyof TArgs, any][]).forEach(([name, value]) => {
|
||||||
const { target = NO_TARGET_NAME } = (argTypes[name] || {}) as { target: string };
|
const { target = NO_TARGET_NAME } = (argTypes[name] || {}) as { target?: string };
|
||||||
|
|
||||||
groupedArgs[target] = groupedArgs[target] || {};
|
groupedArgs[target] = groupedArgs[target] || {};
|
||||||
groupedArgs[target][name] = value;
|
groupedArgs[target][name] = value;
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { normalizeStoriesEntry } from '@storybook/core-common';
|
import { normalizeStoriesEntry } from '@storybook/core-common';
|
||||||
|
import { expect } from '@jest/globals';
|
||||||
|
|
||||||
import { userOrAutoTitleFromSpecifier as userOrAuto } from './autoTitle';
|
import { userOrAutoTitleFromSpecifier as userOrAuto } from './autoTitle';
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { expect } from '@jest/globals';
|
||||||
|
|
||||||
import { composeConfigs } from './composeConfigs';
|
import { composeConfigs } from './composeConfigs';
|
||||||
|
|
||||||
describe('composeConfigs', () => {
|
describe('composeConfigs', () => {
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { expect } from '@jest/globals';
|
||||||
|
|
||||||
import { normalizeInputType, normalizeInputTypes } from './normalizeInputTypes';
|
import { normalizeInputType, normalizeInputTypes } from './normalizeInputTypes';
|
||||||
|
|
||||||
describe('normalizeInputType', () => {
|
describe('normalizeInputType', () => {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import type { AnyFramework, ProjectAnnotations } from '@storybook/csf';
|
import type { AnyFramework, ArgTypes, ProjectAnnotations } from '@storybook/csf';
|
||||||
|
|
||||||
import { inferArgTypes } from '../inferArgTypes';
|
import { inferArgTypes } from '../inferArgTypes';
|
||||||
import { inferControls } from '../inferControls';
|
import { inferControls } from '../inferControls';
|
||||||
@ -12,7 +12,7 @@ export function normalizeProjectAnnotations<TFramework extends AnyFramework>({
|
|||||||
...annotations
|
...annotations
|
||||||
}: ProjectAnnotations<TFramework>): NormalizedProjectAnnotations<TFramework> {
|
}: ProjectAnnotations<TFramework>): NormalizedProjectAnnotations<TFramework> {
|
||||||
return {
|
return {
|
||||||
...(argTypes && { argTypes: normalizeInputTypes(argTypes) }),
|
...(argTypes && { argTypes: normalizeInputTypes(argTypes as ArgTypes) }),
|
||||||
...(globalTypes && { globalTypes: normalizeInputTypes(globalTypes) }),
|
...(globalTypes && { globalTypes: normalizeInputTypes(globalTypes) }),
|
||||||
argTypesEnhancers: [
|
argTypesEnhancers: [
|
||||||
...(argTypesEnhancers || []),
|
...(argTypesEnhancers || []),
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
|
import { expect } from '@jest/globals';
|
||||||
import { AnyFramework, StoryAnnotationsOrFn } from '@storybook/csf';
|
import { AnyFramework, StoryAnnotationsOrFn } from '@storybook/csf';
|
||||||
|
|
||||||
import { normalizeStory } from './normalizeStory';
|
import { normalizeStory } from './normalizeStory';
|
||||||
|
|
||||||
describe('normalizeStory', () => {
|
describe('normalizeStory', () => {
|
||||||
describe('id generation', () => {
|
describe('id generation', () => {
|
||||||
it('combines title and export name', () => {
|
|
||||||
expect(normalizeStory('name', {}, { title: 'title' }).id).toEqual('title--name');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('respects component id', () => {
|
it('respects component id', () => {
|
||||||
expect(normalizeStory('name', {}, { title: 'title', id: 'component-id' }).id).toEqual(
|
expect(normalizeStory('name', {}, { title: 'title', id: 'component-id' }).id).toEqual(
|
||||||
'component-id--name'
|
'component-id--name'
|
||||||
@ -27,22 +25,28 @@ describe('normalizeStory', () => {
|
|||||||
describe('name', () => {
|
describe('name', () => {
|
||||||
it('preferences story.name over story.storyName', () => {
|
it('preferences story.name over story.storyName', () => {
|
||||||
expect(
|
expect(
|
||||||
normalizeStory('export', { name: 'name', storyName: 'storyName' }, { title: 'title' }).name
|
normalizeStory(
|
||||||
|
'export',
|
||||||
|
{ name: 'name', storyName: 'storyName' },
|
||||||
|
{ id: 'title', title: 'title' }
|
||||||
|
).name
|
||||||
).toEqual('name');
|
).toEqual('name');
|
||||||
expect(normalizeStory('export', { storyName: 'storyName' }, { title: 'title' }).name).toEqual(
|
expect(
|
||||||
'storyName'
|
normalizeStory('export', { storyName: 'storyName' }, { id: 'title', title: 'title' }).name
|
||||||
);
|
).toEqual('storyName');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('falls back to capitalized export name', () => {
|
it('falls back to capitalized export name', () => {
|
||||||
expect(normalizeStory('exportOne', {}, { title: 'title' }).name).toEqual('Export One');
|
expect(normalizeStory('exportOne', {}, { id: 'title', title: 'title' }).name).toEqual(
|
||||||
|
'Export One'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('user-provided story function', () => {
|
describe('user-provided story function', () => {
|
||||||
it('should normalize into an object', () => {
|
it('should normalize into an object', () => {
|
||||||
const storyFn = () => {};
|
const storyFn = () => {};
|
||||||
const meta = { title: 'title' };
|
const meta = { id: 'title', title: 'title' };
|
||||||
expect(normalizeStory('storyExport', storyFn, meta)).toMatchInlineSnapshot(`
|
expect(normalizeStory('storyExport', storyFn, meta)).toMatchInlineSnapshot(`
|
||||||
Object {
|
Object {
|
||||||
"argTypes": Object {},
|
"argTypes": Object {},
|
||||||
@ -63,21 +67,21 @@ describe('normalizeStory', () => {
|
|||||||
describe('render function', () => {
|
describe('render function', () => {
|
||||||
it('implicit render function', () => {
|
it('implicit render function', () => {
|
||||||
const storyObj = {};
|
const storyObj = {};
|
||||||
const meta = { title: 'title' };
|
const meta = { id: 'title', title: 'title' };
|
||||||
const normalized = normalizeStory('storyExport', storyObj, meta);
|
const normalized = normalizeStory('storyExport', storyObj, meta);
|
||||||
expect(normalized.render).toBeUndefined();
|
expect(normalized.render).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('user-provided story render function', () => {
|
it('user-provided story render function', () => {
|
||||||
const storyObj = { render: jest.fn() };
|
const storyObj = { render: jest.fn() };
|
||||||
const meta = { title: 'title', render: jest.fn() };
|
const meta = { id: 'title', title: 'title', render: jest.fn() };
|
||||||
const normalized = normalizeStory('storyExport', storyObj, meta);
|
const normalized = normalizeStory('storyExport', storyObj, meta);
|
||||||
expect(normalized.render).toBe(storyObj.render);
|
expect(normalized.render).toBe(storyObj.render);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('user-provided meta render function', () => {
|
it('user-provided meta render function', () => {
|
||||||
const storyObj = {};
|
const storyObj = {};
|
||||||
const meta = { title: 'title', render: jest.fn() };
|
const meta = { id: 'title', title: 'title', render: jest.fn() };
|
||||||
const normalized = normalizeStory('storyExport', storyObj, meta);
|
const normalized = normalizeStory('storyExport', storyObj, meta);
|
||||||
expect(normalized.render).toBeUndefined();
|
expect(normalized.render).toBeUndefined();
|
||||||
});
|
});
|
||||||
@ -86,21 +90,21 @@ describe('normalizeStory', () => {
|
|||||||
describe('play function', () => {
|
describe('play function', () => {
|
||||||
it('no render function', () => {
|
it('no render function', () => {
|
||||||
const storyObj = {};
|
const storyObj = {};
|
||||||
const meta = { title: 'title' };
|
const meta = { id: 'title', title: 'title' };
|
||||||
const normalized = normalizeStory('storyExport', storyObj, meta);
|
const normalized = normalizeStory('storyExport', storyObj, meta);
|
||||||
expect(normalized.play).toBeUndefined();
|
expect(normalized.play).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('user-provided story render function', () => {
|
it('user-provided story render function', () => {
|
||||||
const storyObj = { play: jest.fn() };
|
const storyObj = { play: jest.fn() };
|
||||||
const meta = { title: 'title', play: jest.fn() };
|
const meta = { id: 'title', title: 'title', play: jest.fn() };
|
||||||
const normalized = normalizeStory('storyExport', storyObj, meta);
|
const normalized = normalizeStory('storyExport', storyObj, meta);
|
||||||
expect(normalized.play).toBe(storyObj.play);
|
expect(normalized.play).toBe(storyObj.play);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('user-provided meta render function', () => {
|
it('user-provided meta render function', () => {
|
||||||
const storyObj = {};
|
const storyObj = {};
|
||||||
const meta = { title: 'title', play: jest.fn() };
|
const meta = { id: 'title', title: 'title', play: jest.fn() };
|
||||||
const normalized = normalizeStory('storyExport', storyObj, meta);
|
const normalized = normalizeStory('storyExport', storyObj, meta);
|
||||||
expect(normalized.play).toBeUndefined();
|
expect(normalized.play).toBeUndefined();
|
||||||
});
|
});
|
||||||
@ -109,7 +113,7 @@ describe('normalizeStory', () => {
|
|||||||
describe('annotations', () => {
|
describe('annotations', () => {
|
||||||
it('empty annotations', () => {
|
it('empty annotations', () => {
|
||||||
const storyObj = {};
|
const storyObj = {};
|
||||||
const meta = { title: 'title' };
|
const meta = { id: 'title', title: 'title' };
|
||||||
const normalized = normalizeStory('storyExport', storyObj, meta);
|
const normalized = normalizeStory('storyExport', storyObj, meta);
|
||||||
expect(normalized).toMatchInlineSnapshot(`
|
expect(normalized).toMatchInlineSnapshot(`
|
||||||
Object {
|
Object {
|
||||||
@ -135,7 +139,7 @@ describe('normalizeStory', () => {
|
|||||||
args: { storyArg: 'val' },
|
args: { storyArg: 'val' },
|
||||||
argTypes: { storyArgType: { type: 'string' } },
|
argTypes: { storyArgType: { type: 'string' } },
|
||||||
};
|
};
|
||||||
const meta = { title: 'title' };
|
const meta = { id: 'title', title: 'title' };
|
||||||
const { moduleExport, ...normalized } = normalizeStory('storyExport', storyObj, meta);
|
const { moduleExport, ...normalized } = normalizeStory('storyExport', storyObj, meta);
|
||||||
expect(normalized).toMatchInlineSnapshot(`
|
expect(normalized).toMatchInlineSnapshot(`
|
||||||
Object {
|
Object {
|
||||||
@ -182,7 +186,7 @@ describe('normalizeStory', () => {
|
|||||||
argTypes: { storyArgType2: { type: 'string' } },
|
argTypes: { storyArgType2: { type: 'string' } },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const meta = { title: 'title' };
|
const meta = { id: 'title', title: 'title' };
|
||||||
const { moduleExport, ...normalized } = normalizeStory('storyExport', storyObj, meta);
|
const { moduleExport, ...normalized } = normalizeStory('storyExport', storyObj, meta);
|
||||||
expect(normalized).toMatchInlineSnapshot(`
|
expect(normalized).toMatchInlineSnapshot(`
|
||||||
Object {
|
Object {
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
import type {
|
import type {
|
||||||
ComponentAnnotations,
|
|
||||||
AnyFramework,
|
AnyFramework,
|
||||||
LegacyStoryAnnotationsOrFn,
|
LegacyStoryAnnotationsOrFn,
|
||||||
StoryId,
|
StoryId,
|
||||||
StoryAnnotations,
|
StoryAnnotations,
|
||||||
StoryFn,
|
StoryFn,
|
||||||
|
ArgTypes,
|
||||||
} from '@storybook/csf';
|
} from '@storybook/csf';
|
||||||
import { storyNameFromExport, toId } from '@storybook/csf';
|
import { storyNameFromExport, toId } from '@storybook/csf';
|
||||||
import dedent from 'ts-dedent';
|
import dedent from 'ts-dedent';
|
||||||
import { logger } from '@storybook/client-logger';
|
import { logger } from '@storybook/client-logger';
|
||||||
import deprecate from 'util-deprecate';
|
import deprecate from 'util-deprecate';
|
||||||
import type { NormalizedStoryAnnotations } from '../types';
|
import type { NormalizedComponentAnnotations, NormalizedStoryAnnotations } from '../types';
|
||||||
import { normalizeInputTypes } from './normalizeInputTypes';
|
import { normalizeInputTypes } from './normalizeInputTypes';
|
||||||
|
|
||||||
const deprecatedStoryAnnotation = dedent`
|
const deprecatedStoryAnnotation = dedent`
|
||||||
@ -25,16 +25,11 @@ const deprecatedStoryAnnotationWarning = deprecate(() => {}, deprecatedStoryAnno
|
|||||||
export function normalizeStory<TFramework extends AnyFramework>(
|
export function normalizeStory<TFramework extends AnyFramework>(
|
||||||
key: StoryId,
|
key: StoryId,
|
||||||
storyAnnotations: LegacyStoryAnnotationsOrFn<TFramework>,
|
storyAnnotations: LegacyStoryAnnotationsOrFn<TFramework>,
|
||||||
meta: ComponentAnnotations<TFramework>
|
meta: NormalizedComponentAnnotations<TFramework>
|
||||||
): NormalizedStoryAnnotations<TFramework> {
|
): NormalizedStoryAnnotations<TFramework> {
|
||||||
let userStoryFn: StoryFn<TFramework>;
|
const storyObject: StoryAnnotations<TFramework> = storyAnnotations;
|
||||||
let storyObject: StoryAnnotations<TFramework>;
|
const userStoryFn: StoryFn<TFramework> | null =
|
||||||
if (typeof storyAnnotations === 'function') {
|
typeof storyAnnotations === 'function' ? storyAnnotations : null;
|
||||||
userStoryFn = storyAnnotations;
|
|
||||||
storyObject = storyAnnotations;
|
|
||||||
} else {
|
|
||||||
storyObject = storyAnnotations;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { story } = storyObject;
|
const { story } = storyObject;
|
||||||
if (story) {
|
if (story) {
|
||||||
@ -51,12 +46,12 @@ export function normalizeStory<TFramework extends AnyFramework>(
|
|||||||
const decorators = [...(storyObject.decorators || []), ...(story?.decorators || [])];
|
const decorators = [...(storyObject.decorators || []), ...(story?.decorators || [])];
|
||||||
const parameters = { ...story?.parameters, ...storyObject.parameters };
|
const parameters = { ...story?.parameters, ...storyObject.parameters };
|
||||||
const args = { ...story?.args, ...storyObject.args };
|
const args = { ...story?.args, ...storyObject.args };
|
||||||
const argTypes = { ...story?.argTypes, ...storyObject.argTypes };
|
const argTypes = { ...(story?.argTypes as ArgTypes), ...(storyObject.argTypes as ArgTypes) };
|
||||||
const loaders = [...(storyObject.loaders || []), ...(story?.loaders || [])];
|
const loaders = [...(storyObject.loaders || []), ...(story?.loaders || [])];
|
||||||
const { render, play } = storyObject;
|
const { render, play } = storyObject;
|
||||||
|
|
||||||
// eslint-disable-next-line no-underscore-dangle
|
// eslint-disable-next-line no-underscore-dangle
|
||||||
const id = parameters.__id || toId(meta.id || meta.title, exportName);
|
const id = parameters.__id || toId(meta.id, exportName);
|
||||||
return {
|
return {
|
||||||
moduleExport: storyAnnotations,
|
moduleExport: storyAnnotations,
|
||||||
id,
|
id,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import global from 'global';
|
import global from 'global';
|
||||||
|
import { expect } from '@jest/globals';
|
||||||
import { addons, HooksContext } from '@storybook/addons';
|
import { addons, HooksContext } from '@storybook/addons';
|
||||||
import type {
|
import type {
|
||||||
AnyFramework,
|
AnyFramework,
|
||||||
@ -7,6 +8,7 @@ import type {
|
|||||||
SBScalarType,
|
SBScalarType,
|
||||||
StoryContext,
|
StoryContext,
|
||||||
} from '@storybook/csf';
|
} from '@storybook/csf';
|
||||||
|
|
||||||
import { NO_TARGET_NAME } from '../args';
|
import { NO_TARGET_NAME } from '../args';
|
||||||
import { prepareStory } from './prepareStory';
|
import { prepareStory } from './prepareStory';
|
||||||
|
|
||||||
@ -193,7 +195,7 @@ describe('prepareStory', () => {
|
|||||||
|
|
||||||
describe('argsEnhancers', () => {
|
describe('argsEnhancers', () => {
|
||||||
it('are applied in the right order', () => {
|
it('are applied in the right order', () => {
|
||||||
const run = [];
|
const run: number[] = [];
|
||||||
const enhancerOne: ArgsEnhancer<AnyFramework> = () => {
|
const enhancerOne: ArgsEnhancer<AnyFramework> = () => {
|
||||||
run.push(1);
|
run.push(1);
|
||||||
return {};
|
return {};
|
||||||
@ -351,7 +353,7 @@ describe('prepareStory', () => {
|
|||||||
it('awaits the result of a loader', async () => {
|
it('awaits the result of a loader', async () => {
|
||||||
const loader = jest.fn(async () => new Promise((r) => setTimeout(() => r({ foo: 7 }), 100)));
|
const loader = jest.fn(async () => new Promise((r) => setTimeout(() => r({ foo: 7 }), 100)));
|
||||||
const { applyLoaders } = prepareStory(
|
const { applyLoaders } = prepareStory(
|
||||||
{ id, name, loaders: [loader], moduleExport },
|
{ id, name, loaders: [loader as any], moduleExport },
|
||||||
{ id, title },
|
{ id, title },
|
||||||
{ render }
|
{ render }
|
||||||
);
|
);
|
||||||
@ -387,7 +389,7 @@ describe('prepareStory', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('later loaders override earlier loaders', async () => {
|
it('later loaders override earlier loaders', async () => {
|
||||||
const loaders = [
|
const loaders: any[] = [
|
||||||
async () => new Promise((r) => setTimeout(() => r({ foo: 7 }), 100)),
|
async () => new Promise((r) => setTimeout(() => r({ foo: 7 }), 100)),
|
||||||
async () => new Promise((r) => setTimeout(() => r({ foo: 3 }), 50)),
|
async () => new Promise((r) => setTimeout(() => r({ foo: 3 }), 50)),
|
||||||
];
|
];
|
||||||
|
@ -11,6 +11,7 @@ import type {
|
|||||||
StoryContext,
|
StoryContext,
|
||||||
AnyFramework,
|
AnyFramework,
|
||||||
StrictArgTypes,
|
StrictArgTypes,
|
||||||
|
StoryContextForLoaders,
|
||||||
} from '@storybook/csf';
|
} from '@storybook/csf';
|
||||||
import { includeConditionalArg } from '@storybook/csf';
|
import { includeConditionalArg } from '@storybook/csf';
|
||||||
|
|
||||||
@ -84,6 +85,8 @@ export function prepareStory<TFramework extends AnyFramework>(
|
|||||||
componentAnnotations.render ||
|
componentAnnotations.render ||
|
||||||
projectAnnotations.render;
|
projectAnnotations.render;
|
||||||
|
|
||||||
|
if (!render) throw new Error(`No render function available for storyId '${id}'`);
|
||||||
|
|
||||||
const passedArgTypes: StrictArgTypes = combineParameters(
|
const passedArgTypes: StrictArgTypes = combineParameters(
|
||||||
projectAnnotations.argTypes,
|
projectAnnotations.argTypes,
|
||||||
componentAnnotations.argTypes,
|
componentAnnotations.argTypes,
|
||||||
@ -154,7 +157,7 @@ export function prepareStory<TFramework extends AnyFramework>(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const applyLoaders = async (context: StoryContext<TFramework>) => {
|
const applyLoaders = async (context: StoryContextForLoaders<TFramework>) => {
|
||||||
const loadResults = await Promise.all(loaders.map((loader) => loader(context)));
|
const loadResults = await Promise.all(loaders.map((loader) => loader(context)));
|
||||||
const loaded = Object.assign({}, ...loadResults);
|
const loaded = Object.assign({}, ...loadResults);
|
||||||
return { ...context, loaded };
|
return { ...context, loaded };
|
||||||
@ -183,7 +186,7 @@ export function prepareStory<TFramework extends AnyFramework>(
|
|||||||
const unboundStoryFn = (context: StoryContext<TFramework>) => {
|
const unboundStoryFn = (context: StoryContext<TFramework>) => {
|
||||||
let finalContext: StoryContext<TFramework> = context;
|
let finalContext: StoryContext<TFramework> = context;
|
||||||
if (global.FEATURES?.argTypeTargetsV7) {
|
if (global.FEATURES?.argTypeTargetsV7) {
|
||||||
const argsByTarget = groupArgsByTarget({ args: context.args, ...context });
|
const argsByTarget = groupArgsByTarget(context);
|
||||||
finalContext = {
|
finalContext = {
|
||||||
...context,
|
...context,
|
||||||
allArgs: context.args,
|
allArgs: context.args,
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { expect } from '@jest/globals';
|
||||||
|
|
||||||
import { processCSFFile } from './processCSFFile';
|
import { processCSFFile } from './processCSFFile';
|
||||||
|
|
||||||
describe('processCSFFile', () => {
|
describe('processCSFFile', () => {
|
||||||
|
@ -24,10 +24,9 @@ const checkStorySort = (parameters: Parameters) => {
|
|||||||
if (options?.storySort) logger.error('The storySort option parameter can only be set globally');
|
if (options?.storySort) logger.error('The storySort option parameter can only be set globally');
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkDisallowedParameters = (parameters: Parameters) => {
|
const checkDisallowedParameters = (parameters?: Parameters) => {
|
||||||
if (!parameters) {
|
if (!parameters) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
checkGlobals(parameters);
|
checkGlobals(parameters);
|
||||||
checkStorySort(parameters);
|
checkStorySort(parameters);
|
||||||
};
|
};
|
||||||
|
@ -8,6 +8,7 @@ import {
|
|||||||
Args,
|
Args,
|
||||||
StoryContext,
|
StoryContext,
|
||||||
Parameters,
|
Parameters,
|
||||||
|
LegacyStoryAnnotationsOrFn,
|
||||||
} from '@storybook/csf';
|
} from '@storybook/csf';
|
||||||
|
|
||||||
import { composeConfigs } from '../composeConfigs';
|
import { composeConfigs } from '../composeConfigs';
|
||||||
@ -50,7 +51,7 @@ export function composeStory<
|
|||||||
TFramework extends AnyFramework = AnyFramework,
|
TFramework extends AnyFramework = AnyFramework,
|
||||||
TArgs extends Args = Args
|
TArgs extends Args = Args
|
||||||
>(
|
>(
|
||||||
storyAnnotations: AnnotatedStoryFn<TFramework, TArgs> | StoryAnnotations<TFramework, TArgs>,
|
storyAnnotations: LegacyStoryAnnotationsOrFn<TFramework>,
|
||||||
componentAnnotations: ComponentAnnotations<TFramework, TArgs>,
|
componentAnnotations: ComponentAnnotations<TFramework, TArgs>,
|
||||||
projectAnnotations: ProjectAnnotations<TFramework> = GLOBAL_STORYBOOK_PROJECT_ANNOTATIONS,
|
projectAnnotations: ProjectAnnotations<TFramework> = GLOBAL_STORYBOOK_PROJECT_ANNOTATIONS,
|
||||||
defaultConfig: ProjectAnnotations<TFramework> = {},
|
defaultConfig: ProjectAnnotations<TFramework> = {},
|
||||||
@ -63,15 +64,17 @@ export function composeStory<
|
|||||||
// @TODO: Support auto title
|
// @TODO: Support auto title
|
||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
componentAnnotations.title = componentAnnotations.title ?? 'ComposedStory';
|
componentAnnotations.title = componentAnnotations.title ?? 'ComposedStory';
|
||||||
const normalizedComponentAnnotations = normalizeComponentAnnotations(componentAnnotations);
|
const normalizedComponentAnnotations =
|
||||||
|
normalizeComponentAnnotations<TFramework>(componentAnnotations);
|
||||||
|
|
||||||
const storyName =
|
const storyName =
|
||||||
exportsName ||
|
exportsName ||
|
||||||
storyAnnotations.storyName ||
|
storyAnnotations.storyName ||
|
||||||
storyAnnotations.story?.name ||
|
storyAnnotations.story?.name ||
|
||||||
storyAnnotations.name;
|
storyAnnotations.name ||
|
||||||
|
'unknown';
|
||||||
|
|
||||||
const normalizedStory = normalizeStory(
|
const normalizedStory = normalizeStory<TFramework>(
|
||||||
storyName,
|
storyName,
|
||||||
storyAnnotations,
|
storyAnnotations,
|
||||||
normalizedComponentAnnotations
|
normalizedComponentAnnotations
|
||||||
@ -121,7 +124,12 @@ export function composeStories<TModule extends CSFExports>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result = Object.assign(storiesMap, {
|
const result = Object.assign(storiesMap, {
|
||||||
[exportsName]: composeStoryFn(story, meta, globalConfig, exportsName),
|
[exportsName]: composeStoryFn(
|
||||||
|
story as LegacyStoryAnnotationsOrFn,
|
||||||
|
meta,
|
||||||
|
globalConfig,
|
||||||
|
exportsName
|
||||||
|
),
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
}, {});
|
}, {});
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { expect } from '@jest/globals';
|
||||||
import type { AnyFramework, StoryContext } from '@storybook/csf';
|
import type { AnyFramework, StoryContext } from '@storybook/csf';
|
||||||
|
|
||||||
import { defaultDecorateStory } from './decorators';
|
import { defaultDecorateStory } from './decorators';
|
||||||
@ -15,7 +16,7 @@ function makeContext(input: Record<string, any> = {}): StoryContext<AnyFramework
|
|||||||
|
|
||||||
describe('client-api.decorators', () => {
|
describe('client-api.decorators', () => {
|
||||||
it('calls decorators in out to in order', () => {
|
it('calls decorators in out to in order', () => {
|
||||||
const order = [];
|
const order: number[] = [];
|
||||||
const decorators = [
|
const decorators = [
|
||||||
(s) => order.push(1) && s(),
|
(s) => order.push(1) && s(),
|
||||||
(s) => order.push(2) && s(),
|
(s) => order.push(2) && s(),
|
||||||
@ -29,7 +30,7 @@ describe('client-api.decorators', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('passes context through to sub decorators', () => {
|
it('passes context through to sub decorators', () => {
|
||||||
const contexts = [];
|
const contexts: StoryContext[] = [];
|
||||||
const decorators = [
|
const decorators = [
|
||||||
(s, c) => contexts.push(c) && s({ args: { k: 1 } }),
|
(s, c) => contexts.push(c) && s({ args: { k: 1 } }),
|
||||||
(s, c) => contexts.push(c) && s({ args: { k: 2 } }),
|
(s, c) => contexts.push(c) && s({ args: { k: 2 } }),
|
||||||
@ -43,7 +44,7 @@ describe('client-api.decorators', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('passes context through to sub decorators additively', () => {
|
it('passes context through to sub decorators additively', () => {
|
||||||
const contexts = [];
|
const contexts: StoryContext[] = [];
|
||||||
const decorators = [
|
const decorators = [
|
||||||
(s, c) => contexts.push(c) && s({ args: { a: 1 } }),
|
(s, c) => contexts.push(c) && s({ args: { a: 1 } }),
|
||||||
(s, c) => contexts.push(c) && s({ globals: { g: 2 } }),
|
(s, c) => contexts.push(c) && s({ globals: { g: 2 } }),
|
||||||
@ -78,7 +79,7 @@ describe('client-api.decorators', () => {
|
|||||||
// both story functions would receive {story: 2}. The assumption here is that we'll never render
|
// both story functions would receive {story: 2}. The assumption here is that we'll never render
|
||||||
// the same story twice at the same time.
|
// the same story twice at the same time.
|
||||||
it('does not interleave contexts if two decorated stories are call simultaneously', async () => {
|
it('does not interleave contexts if two decorated stories are call simultaneously', async () => {
|
||||||
const contexts = [];
|
const contexts: StoryContext[] = [];
|
||||||
let resolve;
|
let resolve;
|
||||||
const fence = new Promise((r) => {
|
const fence = new Promise((r) => {
|
||||||
resolve = r;
|
resolve = r;
|
||||||
@ -104,7 +105,7 @@ describe('client-api.decorators', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('DOES NOT merge core metadata or pass through core metadata keys in context', () => {
|
it('DOES NOT merge core metadata or pass through core metadata keys in context', () => {
|
||||||
const contexts = [];
|
const contexts: StoryContext[] = [];
|
||||||
const decorators = [
|
const decorators = [
|
||||||
(s, c) =>
|
(s, c) =>
|
||||||
contexts.push(c) &&
|
contexts.push(c) &&
|
||||||
|
@ -69,6 +69,9 @@ export function defaultDecorateStory<TFramework extends AnyFramework>(
|
|||||||
const bindWithContext =
|
const bindWithContext =
|
||||||
(decoratedStoryFn: LegacyStoryFn<TFramework>): PartialStoryFn<TFramework> =>
|
(decoratedStoryFn: LegacyStoryFn<TFramework>): PartialStoryFn<TFramework> =>
|
||||||
(update) => {
|
(update) => {
|
||||||
|
// This code path isn't possible because we always set `contextStore.value` before calling
|
||||||
|
// `decoratedWithContextStore`, but TS doesn't know that.
|
||||||
|
if (!contextStore.value) throw new Error('Decorated function called without init');
|
||||||
contextStore.value = {
|
contextStore.value = {
|
||||||
...contextStore.value,
|
...contextStore.value,
|
||||||
...sanitizeStoryContextUpdate(update),
|
...sanitizeStoryContextUpdate(update),
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { expect } from '@jest/globals';
|
||||||
import {
|
import {
|
||||||
FORCE_RE_RENDER,
|
FORCE_RE_RENDER,
|
||||||
STORY_RENDERED,
|
STORY_RENDERED,
|
||||||
@ -6,6 +7,8 @@ import {
|
|||||||
UPDATE_GLOBALS,
|
UPDATE_GLOBALS,
|
||||||
} from '@storybook/core-events';
|
} from '@storybook/core-events';
|
||||||
import { addons } from '@storybook/addons';
|
import { addons } from '@storybook/addons';
|
||||||
|
import { DecoratorFunction, StoryContext } from '@storybook/csf';
|
||||||
|
|
||||||
import { defaultDecorateStory } from './decorators';
|
import { defaultDecorateStory } from './decorators';
|
||||||
import {
|
import {
|
||||||
applyHooks,
|
applyHooks,
|
||||||
@ -60,8 +63,8 @@ beforeEach(() => {
|
|||||||
|
|
||||||
const decorateStory = applyHooks(defaultDecorateStory);
|
const decorateStory = applyHooks(defaultDecorateStory);
|
||||||
|
|
||||||
const run = (storyFn, decorators = [], context) =>
|
const run = (storyFn, decorators: DecoratorFunction[] = [], context = {}) =>
|
||||||
decorateStory(storyFn, decorators)({ ...context, hooks });
|
decorateStory(storyFn, decorators)({ ...context, hooks } as Partial<StoryContext>);
|
||||||
|
|
||||||
describe('Preview hooks', () => {
|
describe('Preview hooks', () => {
|
||||||
describe('useEffect', () => {
|
describe('useEffect', () => {
|
||||||
@ -321,7 +324,7 @@ describe('Preview hooks', () => {
|
|||||||
expect(result).toBe(callback);
|
expect(result).toBe(callback);
|
||||||
});
|
});
|
||||||
it('returns the previous callback reference if deps are unchanged', () => {
|
it('returns the previous callback reference if deps are unchanged', () => {
|
||||||
const callbacks = [];
|
const callbacks: (() => void)[] = [];
|
||||||
const storyFn = () => {
|
const storyFn = () => {
|
||||||
const callback = useCallback(() => {}, []);
|
const callback = useCallback(() => {}, []);
|
||||||
callbacks.push(callback);
|
callbacks.push(callback);
|
||||||
@ -331,7 +334,7 @@ describe('Preview hooks', () => {
|
|||||||
expect(callbacks[0]).toBe(callbacks[1]);
|
expect(callbacks[0]).toBe(callbacks[1]);
|
||||||
});
|
});
|
||||||
it('creates new callback reference if deps are changed', () => {
|
it('creates new callback reference if deps are changed', () => {
|
||||||
const callbacks = [];
|
const callbacks: (() => void)[] = [];
|
||||||
let counter = 0;
|
let counter = 0;
|
||||||
const storyFn = () => {
|
const storyFn = () => {
|
||||||
counter += 1;
|
counter += 1;
|
@ -1,4 +1,5 @@
|
|||||||
import { logger } from '@storybook/client-logger';
|
import { logger } from '@storybook/client-logger';
|
||||||
|
import { expect } from '@jest/globals';
|
||||||
|
|
||||||
import { inferArgTypes } from './inferArgTypes';
|
import { inferArgTypes } from './inferArgTypes';
|
||||||
|
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import { StoryContextForEnhancers } from '@storybook/store';
|
import { expect } from '@jest/globals';
|
||||||
import { logger } from '@storybook/client-logger';
|
import { logger } from '@storybook/client-logger';
|
||||||
|
import { StoryContextForEnhancers } from '@storybook/csf';
|
||||||
|
|
||||||
import { argTypesEnhancers } from './inferControls';
|
import { argTypesEnhancers } from './inferControls';
|
||||||
|
|
||||||
const getStoryContext = (overrides: any = {}): StoryContextForEnhancers => ({
|
const getStoryContext = (overrides: any = {}): StoryContextForEnhancers => ({
|
||||||
|
@ -11,13 +11,13 @@ type ControlsMatchers = {
|
|||||||
|
|
||||||
const inferControl = (argType: StrictInputType, name: string, matchers: ControlsMatchers): any => {
|
const inferControl = (argType: StrictInputType, name: string, matchers: ControlsMatchers): any => {
|
||||||
const { type, options } = argType;
|
const { type, options } = argType;
|
||||||
if (!type && !options) {
|
if (!type) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// args that end with background or color e.g. iconColor
|
// args that end with background or color e.g. iconColor
|
||||||
if (matchers.color && matchers.color.test(name)) {
|
if (matchers.color && matchers.color.test(name)) {
|
||||||
const controlType = argType.type.name;
|
const controlType = type.name;
|
||||||
|
|
||||||
if (controlType === 'string') {
|
if (controlType === 'string') {
|
||||||
return { control: { type: 'color' } };
|
return { control: { type: 'color' } };
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { expect } from '@jest/globals';
|
||||||
import { combineParameters } from './parameters';
|
import { combineParameters } from './parameters';
|
||||||
|
|
||||||
describe('client-api.parameters', () => {
|
describe('client-api.parameters', () => {
|
||||||
|
@ -10,8 +10,9 @@ import isPlainObject from 'lodash/isPlainObject';
|
|||||||
*/
|
*/
|
||||||
export const combineParameters = (...parameterSets: (Parameters | undefined)[]) => {
|
export const combineParameters = (...parameterSets: (Parameters | undefined)[]) => {
|
||||||
const mergeKeys: Record<string, boolean> = {};
|
const mergeKeys: Record<string, boolean> = {};
|
||||||
const combined = parameterSets.filter(Boolean).reduce((acc, p) => {
|
const definedParametersSets = parameterSets.filter(Boolean) as Parameters[];
|
||||||
Object.entries(p).forEach(([key, value]) => {
|
const combined = definedParametersSets.reduce((acc, parameters) => {
|
||||||
|
Object.entries(parameters).forEach(([key, value]) => {
|
||||||
const existing = acc[key];
|
const existing = acc[key];
|
||||||
if (Array.isArray(value) || typeof existing === 'undefined') {
|
if (Array.isArray(value) || typeof existing === 'undefined') {
|
||||||
acc[key] = value;
|
acc[key] = value;
|
||||||
@ -26,7 +27,7 @@ export const combineParameters = (...parameterSets: (Parameters | undefined)[])
|
|||||||
}, {} as Parameters);
|
}, {} as Parameters);
|
||||||
|
|
||||||
Object.keys(mergeKeys).forEach((key) => {
|
Object.keys(mergeKeys).forEach((key) => {
|
||||||
const mergeValues = parameterSets
|
const mergeValues = definedParametersSets
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.map((p) => p[key])
|
.map((p) => p[key])
|
||||||
.filter((value) => typeof value !== 'undefined');
|
.filter((value) => typeof value !== 'undefined');
|
||||||
|
@ -37,7 +37,7 @@ export const sortStoriesV7 = (
|
|||||||
throw new Error(dedent`
|
throw new Error(dedent`
|
||||||
Error sorting stories with sort parameter ${storySortParameter}:
|
Error sorting stories with sort parameter ${storySortParameter}:
|
||||||
|
|
||||||
> ${err.message}
|
> ${(err as Error).message}
|
||||||
|
|
||||||
Are you using a V6-style sort function in V7 mode?
|
Are you using a V6-style sort function in V7 mode?
|
||||||
|
|
||||||
|
@ -1,25 +1,31 @@
|
|||||||
|
import { expect } from '@jest/globals';
|
||||||
|
import { StoryId } from '@storybook/csf';
|
||||||
|
import { StoryIndexEntry } from '@storybook/store';
|
||||||
|
|
||||||
import { storySort } from './storySort';
|
import { storySort } from './storySort';
|
||||||
|
|
||||||
describe('preview.storySort', () => {
|
describe('preview.storySort', () => {
|
||||||
const fixture = {
|
const fixture: Record<StoryId, StoryIndexEntry> = Object.fromEntries(
|
||||||
a: { title: 'a' },
|
Object.entries({
|
||||||
á: { title: 'á' },
|
a: { title: 'a' },
|
||||||
A: { title: 'A' },
|
á: { title: 'á' },
|
||||||
b: { title: 'b' },
|
A: { title: 'A' },
|
||||||
a_a: { title: 'a / a' },
|
b: { title: 'b' },
|
||||||
a_b: { title: 'a / b' },
|
a_a: { title: 'a / a' },
|
||||||
a_c: { title: 'a / c' },
|
a_b: { title: 'a / b' },
|
||||||
b_a_a: { title: 'b / a / a' },
|
a_c: { title: 'a / c' },
|
||||||
b_b: { title: 'b / b' },
|
b_a_a: { title: 'b / a / a' },
|
||||||
c: { title: 'c' },
|
b_b: { title: 'b / b' },
|
||||||
locale1: { title: 'Б' },
|
c: { title: 'c' },
|
||||||
locale2: { title: 'Г' },
|
locale1: { title: 'Б' },
|
||||||
c__a: { title: 'c', name: 'a' },
|
locale2: { title: 'Г' },
|
||||||
c_b__a: { title: 'c / b', name: 'a' },
|
c__a: { title: 'c', name: 'a' },
|
||||||
c_b__b: { title: 'c / b', name: 'b' },
|
c_b__a: { title: 'c / b', name: 'a' },
|
||||||
c_b__c: { title: 'c / b', name: 'c' },
|
c_b__b: { title: 'c / b', name: 'b' },
|
||||||
c__c: { title: 'c', name: 'c' },
|
c_b__c: { title: 'c / b', name: 'c' },
|
||||||
};
|
c__c: { title: 'c', name: 'c' },
|
||||||
|
}).map(([id, entry]) => [id, { type: 'story', name: 'name', ...entry, id, importPath: id }])
|
||||||
|
);
|
||||||
|
|
||||||
it('uses configure order by default', () => {
|
it('uses configure order by default', () => {
|
||||||
const sortFn = storySort();
|
const sortFn = storySort();
|
||||||
|
@ -22,7 +22,9 @@ import type {
|
|||||||
PartialStoryFn,
|
PartialStoryFn,
|
||||||
Parameters,
|
Parameters,
|
||||||
} from '@storybook/csf';
|
} from '@storybook/csf';
|
||||||
|
import type { StoryIndexEntry, DocsIndexEntry, IndexEntry } from '@storybook/addons';
|
||||||
|
|
||||||
|
export type { StoryIndexEntry, DocsIndexEntry, IndexEntry };
|
||||||
export type { StoryId, Parameters };
|
export type { StoryId, Parameters };
|
||||||
export type Path = string;
|
export type Path = string;
|
||||||
export type ModuleExport = any;
|
export type ModuleExport = any;
|
||||||
@ -51,8 +53,9 @@ export type NormalizedProjectAnnotations<TFramework extends AnyFramework = AnyFr
|
|||||||
|
|
||||||
export type NormalizedComponentAnnotations<TFramework extends AnyFramework = AnyFramework> =
|
export type NormalizedComponentAnnotations<TFramework extends AnyFramework = AnyFramework> =
|
||||||
ComponentAnnotations<TFramework> & {
|
ComponentAnnotations<TFramework> & {
|
||||||
// Useful to guarantee that id exists
|
// Useful to guarantee that id & title exists
|
||||||
id: ComponentId;
|
id: ComponentId;
|
||||||
|
title: ComponentTitle;
|
||||||
argTypes?: StrictArgTypes;
|
argTypes?: StrictArgTypes;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -64,6 +67,7 @@ export type NormalizedStoryAnnotations<TFramework extends AnyFramework = AnyFram
|
|||||||
// You cannot actually set id on story annotations, but we normalize it to be there for convience
|
// You cannot actually set id on story annotations, but we normalize it to be there for convience
|
||||||
id: StoryId;
|
id: StoryId;
|
||||||
argTypes?: StrictArgTypes;
|
argTypes?: StrictArgTypes;
|
||||||
|
name: StoryName;
|
||||||
userStoryFn?: StoryFn<TFramework>;
|
userStoryFn?: StoryFn<TFramework>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -80,8 +84,10 @@ export type Story<TFramework extends AnyFramework = AnyFramework> =
|
|||||||
unboundStoryFn: LegacyStoryFn<TFramework>;
|
unboundStoryFn: LegacyStoryFn<TFramework>;
|
||||||
applyLoaders: (
|
applyLoaders: (
|
||||||
context: StoryContextForLoaders<TFramework>
|
context: StoryContextForLoaders<TFramework>
|
||||||
) => Promise<StoryContext<TFramework>>;
|
) => Promise<
|
||||||
playFunction: (context: StoryContext<TFramework>) => Promise<void> | void;
|
StoryContextForLoaders<TFramework> & { loaded: StoryContext<TFramework>['loaded'] }
|
||||||
|
>;
|
||||||
|
playFunction?: (context: StoryContext<TFramework>) => Promise<void> | void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type BoundStory<TFramework extends AnyFramework = AnyFramework> = Story<TFramework> & {
|
export type BoundStory<TFramework extends AnyFramework = AnyFramework> = Story<TFramework> & {
|
||||||
@ -99,23 +105,6 @@ export declare type RenderContext<TFramework extends AnyFramework = AnyFramework
|
|||||||
unboundStoryFn: LegacyStoryFn<TFramework>;
|
unboundStoryFn: LegacyStoryFn<TFramework>;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface BaseIndexEntry {
|
|
||||||
id: StoryId;
|
|
||||||
name: StoryName;
|
|
||||||
title: ComponentTitle;
|
|
||||||
importPath: Path;
|
|
||||||
}
|
|
||||||
export type StoryIndexEntry = BaseIndexEntry & {
|
|
||||||
type: 'story';
|
|
||||||
};
|
|
||||||
|
|
||||||
export type DocsIndexEntry = BaseIndexEntry & {
|
|
||||||
storiesImports: Path[];
|
|
||||||
type: 'docs';
|
|
||||||
legacy?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type IndexEntry = StoryIndexEntry | DocsIndexEntry;
|
|
||||||
export interface V2CompatIndexEntry extends Omit<StoryIndexEntry, 'type'> {
|
export interface V2CompatIndexEntry extends Omit<StoryIndexEntry, 'type'> {
|
||||||
kind: StoryIndexEntry['title'];
|
kind: StoryIndexEntry['title'];
|
||||||
story: StoryIndexEntry['name'];
|
story: StoryIndexEntry['name'];
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../tsconfig.json",
|
"extends": "../../tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"strict": true
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src/**/*"
|
"src/**/*"
|
||||||
|
@ -83,7 +83,7 @@ export function composeStory<TArgs = Args>(
|
|||||||
exportsName?: string
|
exportsName?: string
|
||||||
) {
|
) {
|
||||||
return originalComposeStory<ReactFramework, TArgs>(
|
return originalComposeStory<ReactFramework, TArgs>(
|
||||||
story,
|
story as ComposedStory<ReactFramework, Args>,
|
||||||
componentAnnotations,
|
componentAnnotations,
|
||||||
projectAnnotations,
|
projectAnnotations,
|
||||||
defaultProjectAnnotations,
|
defaultProjectAnnotations,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user