mirror of
https://github.com/storybookjs/storybook.git
synced 2025-04-05 04:11:06 +08:00
Merge branch 'tom/remove-ssv6' into norbert/remove-storystorev7
This commit is contained in:
commit
dc02dd38ad
@ -2,9 +2,6 @@
|
||||
|
||||
import './globals';
|
||||
|
||||
// eslint-disable-next-line import/export
|
||||
export * from './public-api';
|
||||
// eslint-disable-next-line import/export
|
||||
export * from './public-types';
|
||||
|
||||
export type { StoryFnAngularReturnType as IStory } from './types';
|
||||
|
@ -1 +0,0 @@
|
||||
export * from './public-types';
|
@ -1 +0,0 @@
|
||||
import './globals';
|
@ -156,7 +156,7 @@ export async function buildStaticStandalone(options: BuildStaticStandaloneOption
|
||||
|
||||
let initializedStoryIndexGenerator: Promise<StoryIndexGenerator | undefined> =
|
||||
Promise.resolve(undefined);
|
||||
if (features?.buildStoriesJson && !options.ignorePreview) {
|
||||
if (!options.ignorePreview) {
|
||||
const workingDir = process.cwd();
|
||||
const directories = {
|
||||
configDir: options.configDir,
|
||||
|
@ -190,7 +190,6 @@ export const features = async (
|
||||
): Promise<StorybookConfig['features']> => ({
|
||||
...existing,
|
||||
warnOnLegacyHierarchySeparator: true,
|
||||
buildStoriesJson: false,
|
||||
argTypeTargetsV7: true,
|
||||
legacyDecoratorFileOrder: false,
|
||||
});
|
||||
|
@ -7,7 +7,6 @@ import { router } from './router';
|
||||
|
||||
export async function getStoryIndexGenerator(
|
||||
features: {
|
||||
buildStoriesJson?: boolean;
|
||||
argTypeTargetsV7?: boolean;
|
||||
warnOnLegacyHierarchySeparator?: boolean;
|
||||
},
|
||||
|
@ -40,7 +40,6 @@ jest.mock('@storybook/global', () => ({
|
||||
global: {
|
||||
...globalThis,
|
||||
fetch: jest.fn(() => ({ json: () => ({ v: 4, entries: mockGetEntries() }) })),
|
||||
FEATURES: {},
|
||||
CONFIG_TYPE: 'DEVELOPMENT',
|
||||
},
|
||||
}));
|
||||
|
@ -10,34 +10,14 @@ The preview's job is:
|
||||
|
||||
3. Render the current selection to the web view in either story or docs mode.
|
||||
|
||||
## V7 Store vs Legacy (V6)
|
||||
|
||||
The story store is designed to load stories 'on demand', and will operate in this fashion if the `storyStoreV7` feature is enabled.
|
||||
|
||||
However, for back-compat reasons, in v6 mode, we need to load all stories, synchronously on bootup, emitting the `SET_STORIES` event.
|
||||
|
||||
In V7 mode we do not emit that event, instead preferring the `STORY_PREPARED` event, with the data for the single story being rendered.
|
||||
|
||||
## Initialization
|
||||
|
||||
The preview is `initialized` in two ways.
|
||||
|
||||
### V7 Mode:
|
||||
|
||||
- `importFn` - is an async `import()` function
|
||||
|
||||
- `getProjectAnnotations` - is a simple function that evaluations `preview.js` and addon config files and combines them. If it errors, the Preview will show the error.
|
||||
|
||||
- No `getStoryIndex` function is passed, instead the preview creates a `StoryIndexClient` that pulls `stories.json` from node and watches the event stream for invalidation events.
|
||||
|
||||
### V6 Mode
|
||||
|
||||
- `importFn` - is a simulated `import()` function, that is synchronous, see `client-api` for details.
|
||||
- `getProjectAnnotations` - also evaluates `preview.js` et al, but watches for calls to `setStories`, and passes them to the `ClientApi`
|
||||
- `getStoryIndex` is a local function (that must be called _after_ `getProjectAnnotations`) that gets the list of stories added.
|
||||
|
||||
See `client-api` for more details on this process.
|
||||
|
||||
## Story Rendering and interruptions
|
||||
|
||||
The Preview is split into three parts responsible for state management:
|
||||
|
@ -54,7 +54,6 @@
|
||||
"lodash": "^4.17.21",
|
||||
"memoizerific": "^1.11.3",
|
||||
"qs": "^6.10.0",
|
||||
"synchronous-promise": "^2.0.15",
|
||||
"ts-dedent": "^2.0.0",
|
||||
"util-deprecate": "^1.0.2"
|
||||
},
|
||||
|
@ -1,4 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/triple-slash-reference */
|
||||
/// <reference path="typings.d.ts" />
|
||||
|
||||
export * from './modules/client-api';
|
@ -1,4 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/triple-slash-reference */
|
||||
/// <reference path="typings.d.ts" />
|
||||
|
||||
export * from './modules/core-client';
|
@ -41,22 +41,6 @@ export { DocsContext } from './preview-web';
|
||||
*/
|
||||
export { simulatePageLoad, simulateDOMContentLoaded } from './preview-web';
|
||||
|
||||
/**
|
||||
* STORIES API
|
||||
*/
|
||||
export {
|
||||
addArgTypes,
|
||||
addArgTypesEnhancer,
|
||||
addArgs,
|
||||
addArgsEnhancer,
|
||||
addDecorator,
|
||||
addLoader,
|
||||
addParameters,
|
||||
addStepRunner,
|
||||
} from './client-api';
|
||||
export { getQueryParam, getQueryParams } from './client-api';
|
||||
export { setGlobalRender } from './client-api';
|
||||
|
||||
export {
|
||||
combineArgs,
|
||||
combineParameters,
|
||||
@ -83,7 +67,5 @@ export type { PropDescriptor } from './store';
|
||||
/**
|
||||
* STORIES API
|
||||
*/
|
||||
export { ClientApi } from './client-api';
|
||||
export { StoryStore } from './store';
|
||||
export { Preview, PreviewWeb } from './preview-web';
|
||||
export { start } from './core-client';
|
||||
|
@ -1,203 +0,0 @@
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
|
||||
import { global } from '@storybook/global';
|
||||
import type {
|
||||
Args,
|
||||
StepRunner,
|
||||
ArgTypes,
|
||||
Renderer,
|
||||
DecoratorFunction,
|
||||
Parameters,
|
||||
ArgTypesEnhancer,
|
||||
ArgsEnhancer,
|
||||
LoaderFunction,
|
||||
Globals,
|
||||
GlobalTypes,
|
||||
Path,
|
||||
ModuleImportFn,
|
||||
ModuleExports,
|
||||
} from '@storybook/types';
|
||||
import type { StoryStore } from '../../store';
|
||||
import { combineParameters, composeStepRunners, normalizeInputTypes } from '../../store';
|
||||
|
||||
import { StoryStoreFacade } from './StoryStoreFacade';
|
||||
|
||||
const warningAlternatives = {
|
||||
addDecorator: `Instead, use \`export const decorators = [];\` in your \`preview.js\`.`,
|
||||
addParameters: `Instead, use \`export const parameters = {};\` in your \`preview.js\`.`,
|
||||
addLoader: `Instead, use \`export const loaders = [];\` in your \`preview.js\`.`,
|
||||
addArgs: '',
|
||||
addArgTypes: '',
|
||||
addArgsEnhancer: '',
|
||||
addArgTypesEnhancer: '',
|
||||
addStepRunner: '',
|
||||
getGlobalRender: '',
|
||||
setGlobalRender: '',
|
||||
};
|
||||
|
||||
const checkMethod = (method: keyof typeof warningAlternatives) => {
|
||||
if (!global.__STORYBOOK_CLIENT_API__) {
|
||||
throw new Error(`Singleton client API not yet initialized, cannot call \`${method}\`.`);
|
||||
}
|
||||
};
|
||||
|
||||
export const addDecorator = (decorator: DecoratorFunction<Renderer>) => {
|
||||
checkMethod('addDecorator');
|
||||
global.__STORYBOOK_CLIENT_API__?.addDecorator(decorator);
|
||||
};
|
||||
|
||||
export const addParameters = (parameters: Parameters) => {
|
||||
checkMethod('addParameters');
|
||||
global.__STORYBOOK_CLIENT_API__?.addParameters(parameters);
|
||||
};
|
||||
|
||||
export const addLoader = (loader: LoaderFunction<Renderer>) => {
|
||||
checkMethod('addLoader');
|
||||
global.__STORYBOOK_CLIENT_API__?.addLoader(loader);
|
||||
};
|
||||
|
||||
export const addArgs = (args: Args) => {
|
||||
checkMethod('addArgs');
|
||||
global.__STORYBOOK_CLIENT_API__?.addArgs(args);
|
||||
};
|
||||
|
||||
export const addArgTypes = (argTypes: ArgTypes) => {
|
||||
checkMethod('addArgTypes');
|
||||
global.__STORYBOOK_CLIENT_API__?.addArgTypes(argTypes);
|
||||
};
|
||||
|
||||
export const addArgsEnhancer = (enhancer: ArgsEnhancer<Renderer>) => {
|
||||
checkMethod('addArgsEnhancer');
|
||||
global.__STORYBOOK_CLIENT_API__?.addArgsEnhancer(enhancer);
|
||||
};
|
||||
|
||||
export const addArgTypesEnhancer = (enhancer: ArgTypesEnhancer<Renderer>) => {
|
||||
checkMethod('addArgTypesEnhancer');
|
||||
global.__STORYBOOK_CLIENT_API__?.addArgTypesEnhancer(enhancer);
|
||||
};
|
||||
|
||||
export const addStepRunner = (stepRunner: StepRunner) => {
|
||||
checkMethod('addStepRunner');
|
||||
global.__STORYBOOK_CLIENT_API__?.addStepRunner(stepRunner);
|
||||
};
|
||||
|
||||
export const getGlobalRender = () => {
|
||||
checkMethod('getGlobalRender');
|
||||
return global.__STORYBOOK_CLIENT_API__?.facade.projectAnnotations.render;
|
||||
};
|
||||
|
||||
export const setGlobalRender = (render: StoryStoreFacade<any>['projectAnnotations']['render']) => {
|
||||
checkMethod('setGlobalRender');
|
||||
if (global.__STORYBOOK_CLIENT_API__) {
|
||||
global.__STORYBOOK_CLIENT_API__.facade.projectAnnotations.render = render;
|
||||
}
|
||||
};
|
||||
|
||||
export class ClientApi<TRenderer extends Renderer> {
|
||||
facade: StoryStoreFacade<TRenderer>;
|
||||
|
||||
storyStore?: StoryStore<TRenderer>;
|
||||
|
||||
onImportFnChanged?: ({ importFn }: { importFn: ModuleImportFn }) => void;
|
||||
|
||||
// If we don't get passed modules so don't know filenames, we can
|
||||
// just use numeric indexes
|
||||
|
||||
constructor({ storyStore }: { storyStore?: StoryStore<TRenderer> } = {}) {
|
||||
this.facade = new StoryStoreFacade();
|
||||
|
||||
this.storyStore = storyStore;
|
||||
}
|
||||
|
||||
importFn(path: Path) {
|
||||
return this.facade.importFn(path);
|
||||
}
|
||||
|
||||
getStoryIndex() {
|
||||
if (!this.storyStore) {
|
||||
throw new Error('Cannot get story index before setting storyStore');
|
||||
}
|
||||
return this.facade.getStoryIndex(this.storyStore);
|
||||
}
|
||||
|
||||
addDecorator = (decorator: DecoratorFunction<TRenderer>) => {
|
||||
this.facade.projectAnnotations.decorators?.push(decorator);
|
||||
};
|
||||
|
||||
addParameters = ({
|
||||
globals,
|
||||
globalTypes,
|
||||
...parameters
|
||||
}: Parameters & { globals?: Globals; globalTypes?: GlobalTypes }) => {
|
||||
this.facade.projectAnnotations.parameters = combineParameters(
|
||||
this.facade.projectAnnotations.parameters,
|
||||
parameters
|
||||
);
|
||||
if (globals) {
|
||||
this.facade.projectAnnotations.globals = {
|
||||
...this.facade.projectAnnotations.globals,
|
||||
...globals,
|
||||
};
|
||||
}
|
||||
if (globalTypes) {
|
||||
this.facade.projectAnnotations.globalTypes = {
|
||||
...this.facade.projectAnnotations.globalTypes,
|
||||
...normalizeInputTypes(globalTypes),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
addStepRunner = (stepRunner: StepRunner) => {
|
||||
this.facade.projectAnnotations.runStep = composeStepRunners(
|
||||
[this.facade.projectAnnotations.runStep, stepRunner].filter(Boolean) as StepRunner[]
|
||||
);
|
||||
};
|
||||
|
||||
addLoader = (loader: LoaderFunction<TRenderer>) => {
|
||||
this.facade.projectAnnotations.loaders?.push(loader);
|
||||
};
|
||||
|
||||
addArgs = (args: Args) => {
|
||||
this.facade.projectAnnotations.args = {
|
||||
...this.facade.projectAnnotations.args,
|
||||
...args,
|
||||
};
|
||||
};
|
||||
|
||||
addArgTypes = (argTypes: ArgTypes) => {
|
||||
this.facade.projectAnnotations.argTypes = {
|
||||
...this.facade.projectAnnotations.argTypes,
|
||||
...normalizeInputTypes(argTypes),
|
||||
};
|
||||
};
|
||||
|
||||
addArgsEnhancer = (enhancer: ArgsEnhancer<TRenderer>) => {
|
||||
this.facade.projectAnnotations.argsEnhancers?.push(enhancer);
|
||||
};
|
||||
|
||||
addArgTypesEnhancer = (enhancer: ArgTypesEnhancer<TRenderer>) => {
|
||||
this.facade.projectAnnotations.argTypesEnhancers?.push(enhancer);
|
||||
};
|
||||
|
||||
// Because of the API of `storiesOf().add()` we don't have a good "end" call for a
|
||||
// storiesOf file to finish adding stories, and us to load it into the facade as a
|
||||
// single psuedo-CSF file. So instead we just keep collecting the CSF files and load
|
||||
// them all into the facade at the end.
|
||||
_addedExports = {} as Record<Path, ModuleExports>;
|
||||
|
||||
_loadAddedExports() {
|
||||
Object.entries(this._addedExports).forEach(([fileName, fileExports]) =>
|
||||
this.facade.addStoriesFromExports(fileName, fileExports)
|
||||
);
|
||||
}
|
||||
|
||||
// @deprecated
|
||||
raw = () => {
|
||||
return this.storyStore?.raw();
|
||||
};
|
||||
|
||||
// @deprecated
|
||||
get _storyStore() {
|
||||
return this.storyStore;
|
||||
}
|
||||
}
|
@ -1,253 +0,0 @@
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
import { global } from '@storybook/global';
|
||||
import { dedent } from 'ts-dedent';
|
||||
import { SynchronousPromise } from 'synchronous-promise';
|
||||
import { toId, isExportStory, storyNameFromExport } from '@storybook/csf';
|
||||
import type {
|
||||
IndexEntry,
|
||||
Renderer,
|
||||
ComponentId,
|
||||
DocsOptions,
|
||||
Parameters,
|
||||
Path,
|
||||
ModuleExports,
|
||||
NormalizedProjectAnnotations,
|
||||
NormalizedStoriesSpecifier,
|
||||
PreparedStory,
|
||||
StoryIndex,
|
||||
StoryId,
|
||||
} from '@storybook/types';
|
||||
import { logger } from '@storybook/client-logger';
|
||||
import type { StoryStore } from '../../store';
|
||||
import { userOrAutoTitle, sortStoriesV6 } from '../../store';
|
||||
|
||||
export const AUTODOCS_TAG = 'autodocs';
|
||||
export const STORIES_MDX_TAG = 'stories-mdx';
|
||||
|
||||
export class StoryStoreFacade<TRenderer extends Renderer> {
|
||||
projectAnnotations: NormalizedProjectAnnotations<TRenderer>;
|
||||
|
||||
entries: Record<StoryId, IndexEntry & { componentId?: ComponentId }>;
|
||||
|
||||
csfExports: Record<Path, ModuleExports>;
|
||||
|
||||
constructor() {
|
||||
this.projectAnnotations = {
|
||||
loaders: [],
|
||||
decorators: [],
|
||||
parameters: {},
|
||||
argsEnhancers: [],
|
||||
argTypesEnhancers: [],
|
||||
args: {},
|
||||
argTypes: {},
|
||||
};
|
||||
|
||||
this.entries = {};
|
||||
|
||||
this.csfExports = {};
|
||||
}
|
||||
|
||||
// This doesn't actually import anything because the client-api loads fully
|
||||
// on startup, but this is a shim after all.
|
||||
importFn(path: Path) {
|
||||
return SynchronousPromise.resolve().then(() => {
|
||||
const moduleExports = this.csfExports[path];
|
||||
if (!moduleExports) throw new Error(`Unknown path: ${path}`);
|
||||
return moduleExports;
|
||||
});
|
||||
}
|
||||
|
||||
getStoryIndex(store: StoryStore<TRenderer>) {
|
||||
const fileNameOrder = Object.keys(this.csfExports);
|
||||
const storySortParameter = this.projectAnnotations.parameters?.options?.storySort;
|
||||
|
||||
const storyEntries = Object.entries(this.entries);
|
||||
// Add the kind parameters and global parameters to each entry
|
||||
const sortableV6 = storyEntries.map(([storyId, { type, importPath, ...entry }]) => {
|
||||
const exports = this.csfExports[importPath];
|
||||
const csfFile = store.processCSFFileWithCache<TRenderer>(
|
||||
exports,
|
||||
importPath,
|
||||
exports.default.title
|
||||
);
|
||||
|
||||
let storyLike: PreparedStory<TRenderer>;
|
||||
if (type === 'story') {
|
||||
storyLike = store.storyFromCSFFile({ storyId, csfFile });
|
||||
} else {
|
||||
storyLike = {
|
||||
...entry,
|
||||
story: entry.name,
|
||||
kind: entry.title,
|
||||
componentId: toId(entry.componentId || entry.title),
|
||||
parameters: { fileName: importPath },
|
||||
} as any;
|
||||
}
|
||||
return [
|
||||
storyId,
|
||||
storyLike,
|
||||
csfFile.meta.parameters,
|
||||
this.projectAnnotations.parameters || {},
|
||||
] as [StoryId, PreparedStory<TRenderer>, Parameters, Parameters];
|
||||
});
|
||||
|
||||
// NOTE: the sortStoriesV6 version returns the v7 data format. confusing but more convenient!
|
||||
let sortedV7: IndexEntry[];
|
||||
|
||||
try {
|
||||
sortedV7 = sortStoriesV6(sortableV6, storySortParameter, fileNameOrder);
|
||||
} catch (err: any) {
|
||||
if (typeof storySortParameter === 'function') {
|
||||
throw new Error(dedent`
|
||||
Error sorting stories with sort parameter ${storySortParameter}:
|
||||
|
||||
> ${err.message}
|
||||
|
||||
Are you using a V7-style sort function in V6 compatibility mode?
|
||||
|
||||
More info: https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#v7-style-story-sort
|
||||
`);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
const entries = sortedV7.reduce((acc, s) => {
|
||||
// We use the original entry we stored in `this.stories` because it is possible that the CSF file itself
|
||||
// exports a `parameters.fileName` which can be different and mess up our `importFn`.
|
||||
// NOTE: this doesn't actually change the story object, just the index.
|
||||
acc[s.id] = this.entries[s.id];
|
||||
return acc;
|
||||
}, {} as StoryIndex['entries']);
|
||||
|
||||
return { v: 4, entries };
|
||||
}
|
||||
|
||||
clearFilenameExports(fileName: Path) {
|
||||
if (!this.csfExports[fileName]) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear this module's stories from the storyList and existing exports
|
||||
Object.entries(this.entries).forEach(([id, { importPath }]) => {
|
||||
if (importPath === fileName) {
|
||||
delete this.entries[id];
|
||||
}
|
||||
});
|
||||
|
||||
// We keep this as an empty record so we can use it to maintain component order
|
||||
this.csfExports[fileName] = {};
|
||||
}
|
||||
|
||||
// NOTE: we could potentially share some of this code with the stories.json generation
|
||||
addStoriesFromExports(fileName: Path, fileExports: ModuleExports) {
|
||||
if (fileName.match(/\.mdx$/) && !fileName.match(/\.stories\.mdx$/)) {
|
||||
if (global.FEATURES?.storyStoreV7MdxErrors !== false) {
|
||||
throw new Error(dedent`
|
||||
Cannot index \`.mdx\` file (\`${fileName}\`) in SB8.
|
||||
|
||||
The legacy story store does not support new-style \`.mdx\` files. If the file above
|
||||
is not intended to be indexed (i.e. displayed as an entry in the sidebar), either
|
||||
exclude it from your \`stories\` glob, or add <Meta isTemplate /> to it.`);
|
||||
}
|
||||
}
|
||||
|
||||
// if the export haven't changed since last time we added them, this is a no-op
|
||||
if (this.csfExports[fileName] === fileExports) {
|
||||
return;
|
||||
}
|
||||
// OTOH, if they have changed, let's clear them out first
|
||||
this.clearFilenameExports(fileName);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const { default: defaultExport, __namedExportsOrder, ...namedExports } = fileExports;
|
||||
// eslint-disable-next-line prefer-const
|
||||
let { id: componentId, title, tags: componentTags = [] } = defaultExport || {};
|
||||
|
||||
const specifiers = (global.STORIES || []).map(
|
||||
(specifier: NormalizedStoriesSpecifier & { importPathMatcher: string }) => ({
|
||||
...specifier,
|
||||
importPathMatcher: new RegExp(specifier.importPathMatcher),
|
||||
})
|
||||
);
|
||||
|
||||
title = userOrAutoTitle(fileName, specifiers, title);
|
||||
|
||||
if (!title) {
|
||||
logger.info(
|
||||
`Unexpected default export without title in '${fileName}': ${JSON.stringify(
|
||||
fileExports.default
|
||||
)}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.csfExports[fileName] = {
|
||||
...fileExports,
|
||||
default: { ...defaultExport, title },
|
||||
};
|
||||
|
||||
let sortedExports = namedExports;
|
||||
|
||||
// prefer a user/loader provided `__namedExportsOrder` array if supplied
|
||||
// we do this as es module exports are always ordered alphabetically
|
||||
// see https://github.com/storybookjs/storybook/issues/9136
|
||||
if (Array.isArray(__namedExportsOrder)) {
|
||||
sortedExports = {};
|
||||
__namedExportsOrder.forEach((name) => {
|
||||
const namedExport = namedExports[name];
|
||||
if (namedExport) sortedExports[name] = namedExport;
|
||||
});
|
||||
}
|
||||
|
||||
const storyExports = Object.entries(sortedExports).filter(([key]) =>
|
||||
isExportStory(key, defaultExport)
|
||||
);
|
||||
|
||||
// NOTE: this logic is equivalent to the `extractStories` function of `StoryIndexGenerator`
|
||||
const docsOptions = (global.DOCS_OPTIONS || {}) as DocsOptions;
|
||||
const { autodocs } = docsOptions;
|
||||
const componentAutodocs = componentTags.includes(AUTODOCS_TAG);
|
||||
const autodocsOptedIn = autodocs === true || (autodocs === 'tag' && componentAutodocs);
|
||||
if (storyExports.length) {
|
||||
if (componentTags.includes(STORIES_MDX_TAG) || autodocsOptedIn) {
|
||||
const name = docsOptions.defaultName;
|
||||
const docsId = toId(componentId || title, name);
|
||||
this.entries[docsId] = {
|
||||
type: 'docs',
|
||||
id: docsId,
|
||||
title,
|
||||
name,
|
||||
importPath: fileName,
|
||||
...(componentId && { componentId }),
|
||||
tags: [
|
||||
...componentTags,
|
||||
'docs',
|
||||
...(autodocsOptedIn && !componentAutodocs ? [AUTODOCS_TAG] : []),
|
||||
],
|
||||
storiesImports: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
storyExports.forEach(([key, storyExport]: [string, any]) => {
|
||||
const exportName = storyNameFromExport(key);
|
||||
const id = storyExport.parameters?.__id || toId(componentId || title, exportName);
|
||||
const name =
|
||||
(typeof storyExport !== 'function' && storyExport.name) ||
|
||||
storyExport.storyName ||
|
||||
storyExport.story?.name ||
|
||||
exportName;
|
||||
|
||||
if (!storyExport.parameters?.docsOnly) {
|
||||
this.entries[id] = {
|
||||
type: 'story',
|
||||
id,
|
||||
name,
|
||||
title,
|
||||
importPath: fileName,
|
||||
...(componentId && { componentId }),
|
||||
tags: [...(storyExport.tags || componentTags), 'story'],
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
export {
|
||||
addArgs,
|
||||
addArgsEnhancer,
|
||||
addArgTypes,
|
||||
addArgTypesEnhancer,
|
||||
addDecorator,
|
||||
addLoader,
|
||||
addParameters,
|
||||
addStepRunner,
|
||||
ClientApi,
|
||||
setGlobalRender,
|
||||
} from './ClientApi';
|
||||
|
||||
export * from '../../store';
|
||||
|
||||
export * from './queryparams';
|
@ -1,17 +0,0 @@
|
||||
import { global } from '@storybook/global';
|
||||
import { parse } from 'qs';
|
||||
|
||||
export const getQueryParams = () => {
|
||||
const { document } = global;
|
||||
// document.location is not defined in react-native
|
||||
if (document && document.location && document.location.search) {
|
||||
return parse(document.location.search, { ignoreQueryPrefix: true });
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
export const getQueryParam = (key: string) => {
|
||||
const params = getQueryParams();
|
||||
|
||||
return params[key];
|
||||
};
|
@ -1,110 +0,0 @@
|
||||
/// <reference types="node" />
|
||||
/// <reference types="webpack-env" />
|
||||
|
||||
import { logger } from '@storybook/client-logger';
|
||||
import type { Path, ModuleExports } from '@storybook/types';
|
||||
|
||||
export interface RequireContext {
|
||||
keys: () => string[];
|
||||
(id: string): any;
|
||||
resolve(id: string): string;
|
||||
}
|
||||
|
||||
export type LoaderFunction = () => void | any[];
|
||||
|
||||
export type Loadable = RequireContext | RequireContext[] | LoaderFunction;
|
||||
|
||||
/**
|
||||
* Executes a Loadable (function that returns exports or require context(s))
|
||||
* and returns a map of filename => module exports
|
||||
*
|
||||
* @param loadable Loadable
|
||||
* @returns Map<Path, ModuleExports>
|
||||
*/
|
||||
export function executeLoadable(loadable: Loadable) {
|
||||
let reqs = null;
|
||||
// todo discuss / improve type check
|
||||
if (Array.isArray(loadable)) {
|
||||
reqs = loadable;
|
||||
} else if ((loadable as RequireContext).keys) {
|
||||
reqs = [loadable as RequireContext];
|
||||
}
|
||||
|
||||
let exportsMap = new Map<Path, ModuleExports>();
|
||||
if (reqs) {
|
||||
reqs.forEach((req) => {
|
||||
req.keys().forEach((filename: string) => {
|
||||
try {
|
||||
const fileExports = req(filename) as ModuleExports;
|
||||
exportsMap.set(
|
||||
typeof req.resolve === 'function' ? req.resolve(filename) : filename,
|
||||
fileExports
|
||||
);
|
||||
} catch (error: any) {
|
||||
const errorString =
|
||||
error.message && error.stack ? `${error.message}\n ${error.stack}` : error.toString();
|
||||
logger.error(`Unexpected error while loading ${filename}: ${errorString}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
const exported = (loadable as LoaderFunction)();
|
||||
if (Array.isArray(exported) && exported.every((obj) => obj.default != null)) {
|
||||
exportsMap = new Map(
|
||||
exported.map((fileExports, index) => [`exports-map-${index}`, fileExports])
|
||||
);
|
||||
} else if (exported) {
|
||||
logger.warn(
|
||||
`Loader function passed to 'configure' should return void or an array of module exports that all contain a 'default' export. Received: ${JSON.stringify(
|
||||
exported
|
||||
)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return exportsMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a Loadable (function that returns exports or require context(s))
|
||||
* and compares it's output to the last time it was run (as stored on a node module)
|
||||
*
|
||||
* @param loadable Loadable
|
||||
* @param m NodeModule
|
||||
* @returns { added: Map<Path, ModuleExports>, removed: Map<Path, ModuleExports> }
|
||||
*/
|
||||
export function executeLoadableForChanges(loadable: Loadable, m?: NodeModule) {
|
||||
let lastExportsMap: ReturnType<typeof executeLoadable> =
|
||||
m?.hot?.data?.lastExportsMap || new Map();
|
||||
if (m?.hot?.dispose) {
|
||||
m.hot.accept();
|
||||
m.hot.dispose((data) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
data.lastExportsMap = lastExportsMap;
|
||||
});
|
||||
}
|
||||
|
||||
const exportsMap = executeLoadable(loadable);
|
||||
const added = new Map<Path, ModuleExports>();
|
||||
Array.from(exportsMap.entries())
|
||||
// Ignore files that do not have a default export
|
||||
.filter(([, fileExports]) => !!fileExports.default)
|
||||
// Ignore exports that are equal (by reference) to last time, this means the file hasn't changed
|
||||
.filter(([fileName, fileExports]) => lastExportsMap.get(fileName) !== fileExports)
|
||||
.forEach(([fileName, fileExports]) => added.set(fileName, fileExports));
|
||||
|
||||
const removed = new Map<Path, ModuleExports>();
|
||||
Array.from(lastExportsMap.keys())
|
||||
.filter((fileName) => !exportsMap.has(fileName))
|
||||
.forEach((fileName) => {
|
||||
const value = lastExportsMap.get(fileName);
|
||||
if (value) {
|
||||
removed.set(fileName, value);
|
||||
}
|
||||
});
|
||||
|
||||
// Save the value for the dispose() call above
|
||||
lastExportsMap = exportsMap;
|
||||
|
||||
return { added, removed };
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
import { ClientApi } from '../../client-api';
|
||||
import { StoryStore } from '../../store';
|
||||
import { start } from './start';
|
||||
|
||||
export { start, ClientApi, StoryStore };
|
@ -1,625 +0,0 @@
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
// import { describe, it, beforeAll, beforeEach, afterAll, afterEach, jest } from '@jest/globals';
|
||||
import { STORY_RENDERED, STORY_UNCHANGED, SET_INDEX, CONFIG_ERROR } from '@storybook/core-events';
|
||||
|
||||
import type { ModuleExports, Path } from '@storybook/types';
|
||||
import { global } from '@storybook/global';
|
||||
import { setGlobalRender } from '../../client-api';
|
||||
import {
|
||||
waitForRender,
|
||||
waitForEvents,
|
||||
waitForQuiescence,
|
||||
emitter,
|
||||
mockChannel,
|
||||
} from '../preview-web/PreviewWeb.mockdata';
|
||||
|
||||
import { start as realStart } from './start';
|
||||
import type { Loadable } from './executeLoadable';
|
||||
|
||||
jest.mock('@storybook/global', () => ({
|
||||
global: {
|
||||
...globalThis,
|
||||
window: globalThis,
|
||||
history: { replaceState: jest.fn() },
|
||||
document: {
|
||||
location: {
|
||||
pathname: 'pathname',
|
||||
search: '?id=*',
|
||||
},
|
||||
},
|
||||
DOCS_OPTIONS: {},
|
||||
},
|
||||
}));
|
||||
|
||||
// console.log(global);
|
||||
|
||||
jest.mock('@storybook/channels', () => ({
|
||||
createBrowserChannel: () => mockChannel,
|
||||
}));
|
||||
jest.mock('@storybook/client-logger');
|
||||
jest.mock('react-dom');
|
||||
|
||||
// for the auto-title test
|
||||
jest.mock('../../store', () => {
|
||||
const actualStore = jest.requireActual('../../store');
|
||||
return {
|
||||
...actualStore,
|
||||
userOrAutoTitle: (importPath: Path, specifier: any, userTitle?: string) =>
|
||||
userTitle || 'auto-title',
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../preview-web', () => {
|
||||
const actualPreviewWeb = jest.requireActual('../../preview-web');
|
||||
|
||||
class OverloadPreviewWeb extends actualPreviewWeb.PreviewWeb {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.view = {
|
||||
...Object.fromEntries(
|
||||
Object.getOwnPropertyNames(this.view.constructor.prototype).map((key) => [key, jest.fn()])
|
||||
),
|
||||
prepareForDocs: jest.fn().mockReturnValue('docs-root'),
|
||||
prepareForStory: jest.fn().mockReturnValue('story-root'),
|
||||
};
|
||||
}
|
||||
}
|
||||
return {
|
||||
...actualPreviewWeb,
|
||||
PreviewWeb: OverloadPreviewWeb,
|
||||
};
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockChannel.emit.mockClear();
|
||||
// Preview doesn't clean itself up as it isn't designed to ever be stopped :shrug:
|
||||
emitter.removeAllListeners();
|
||||
});
|
||||
|
||||
const start: typeof realStart = (...args) => {
|
||||
const result = realStart(...args);
|
||||
|
||||
const configure: typeof result['configure'] = (
|
||||
framework: string,
|
||||
loadable: Loadable,
|
||||
m?: NodeModule,
|
||||
disableBackwardCompatibility = false
|
||||
) => result.configure(framework, loadable, m, disableBackwardCompatibility);
|
||||
|
||||
return {
|
||||
...result,
|
||||
configure,
|
||||
};
|
||||
};
|
||||
afterEach(() => {
|
||||
// I'm not sure why this is required (it seems just afterEach is required really)
|
||||
mockChannel.emit.mockClear();
|
||||
});
|
||||
|
||||
function makeRequireContext(importMap: Record<Path, ModuleExports>) {
|
||||
const req = (path: Path) => importMap[path];
|
||||
req.keys = () => Object.keys(importMap);
|
||||
return req;
|
||||
}
|
||||
|
||||
describe.skip('start', () => {
|
||||
beforeEach(() => {
|
||||
global.DOCS_OPTIONS = {};
|
||||
// @ts-expect-error (setting this to undefined is indeed what we want to do)
|
||||
global.__STORYBOOK_CLIENT_API__ = undefined;
|
||||
// @ts-expect-error (setting this to undefined is indeed what we want to do)
|
||||
global.__STORYBOOK_PREVIEW__ = undefined;
|
||||
// @ts-expect-error (setting this to undefined is indeed what we want to do)
|
||||
global.IS_STORYBOOK = undefined;
|
||||
});
|
||||
|
||||
const componentCExports = {
|
||||
default: {
|
||||
title: 'Component C',
|
||||
tags: ['component-tag', 'autodocs'],
|
||||
},
|
||||
StoryOne: {
|
||||
render: jest.fn(),
|
||||
tags: ['story-tag'],
|
||||
},
|
||||
StoryTwo: jest.fn(),
|
||||
};
|
||||
|
||||
describe('when configure is called with CSF only', () => {
|
||||
it('loads and renders the first story correctly', async () => {
|
||||
const renderToCanvas = jest.fn();
|
||||
|
||||
const { configure } = start(renderToCanvas);
|
||||
configure('test', () => [componentCExports]);
|
||||
|
||||
await waitForRender();
|
||||
expect(mockChannel.emit.mock.calls.find((call) => call[0] === SET_INDEX)?.[1])
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"entries": Object {
|
||||
"component-c--story-one": Object {
|
||||
"argTypes": Object {},
|
||||
"args": Object {},
|
||||
"id": "component-c--story-one",
|
||||
"importPath": "exports-map-0",
|
||||
"initialArgs": Object {},
|
||||
"name": "Story One",
|
||||
"parameters": Object {
|
||||
"__isArgsStory": false,
|
||||
"fileName": "exports-map-0",
|
||||
"renderer": "test",
|
||||
},
|
||||
"tags": Array [
|
||||
"story-tag",
|
||||
"story",
|
||||
],
|
||||
"title": "Component C",
|
||||
"type": "story",
|
||||
},
|
||||
"component-c--story-two": Object {
|
||||
"argTypes": Object {},
|
||||
"args": Object {},
|
||||
"id": "component-c--story-two",
|
||||
"importPath": "exports-map-0",
|
||||
"initialArgs": Object {},
|
||||
"name": "Story Two",
|
||||
"parameters": Object {
|
||||
"__isArgsStory": false,
|
||||
"fileName": "exports-map-0",
|
||||
"renderer": "test",
|
||||
},
|
||||
"tags": Array [
|
||||
"component-tag",
|
||||
"autodocs",
|
||||
"story",
|
||||
],
|
||||
"title": "Component C",
|
||||
"type": "story",
|
||||
},
|
||||
},
|
||||
"v": 4,
|
||||
}
|
||||
`);
|
||||
|
||||
await waitForRender();
|
||||
expect(mockChannel.emit).toHaveBeenCalledWith(STORY_RENDERED, 'component-c--story-one');
|
||||
|
||||
expect(renderToCanvas).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: 'component-c--story-one',
|
||||
}),
|
||||
'story-root'
|
||||
);
|
||||
});
|
||||
|
||||
it('supports HMR when a story file changes', async () => {
|
||||
const renderToCanvas = jest.fn(({ storyFn }) => storyFn());
|
||||
|
||||
let disposeCallback: (data: object) => void = () => {};
|
||||
const module = {
|
||||
id: 'file1',
|
||||
hot: {
|
||||
data: {},
|
||||
accept: jest.fn(),
|
||||
dispose(cb: () => void) {
|
||||
disposeCallback = cb;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const { configure } = start(renderToCanvas);
|
||||
configure('test', () => [componentCExports], module as any);
|
||||
|
||||
await waitForRender();
|
||||
expect(mockChannel.emit).toHaveBeenCalledWith(STORY_RENDERED, 'component-c--story-one');
|
||||
expect(componentCExports.StoryOne.render).toHaveBeenCalled();
|
||||
expect(module.hot.accept).toHaveBeenCalled();
|
||||
expect(disposeCallback).toBeDefined();
|
||||
|
||||
mockChannel.emit.mockClear();
|
||||
disposeCallback(module.hot.data);
|
||||
const secondImplementation = jest.fn();
|
||||
configure(
|
||||
'test',
|
||||
() => [{ ...componentCExports, StoryOne: secondImplementation }],
|
||||
module as any
|
||||
);
|
||||
|
||||
await waitForRender();
|
||||
expect(mockChannel.emit).toHaveBeenCalledWith(STORY_RENDERED, 'component-c--story-one');
|
||||
expect(secondImplementation).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('re-emits SET_INDEX when a story is added', async () => {
|
||||
const renderToCanvas = jest.fn(({ storyFn }) => storyFn());
|
||||
|
||||
let disposeCallback: (data: object) => void = () => {};
|
||||
const module = {
|
||||
id: 'file1',
|
||||
hot: {
|
||||
data: {},
|
||||
accept: jest.fn(),
|
||||
dispose(cb: () => void) {
|
||||
disposeCallback = cb;
|
||||
},
|
||||
},
|
||||
};
|
||||
const { configure } = start(renderToCanvas);
|
||||
configure('test', () => [componentCExports], module as any);
|
||||
|
||||
await waitForRender();
|
||||
|
||||
mockChannel.emit.mockClear();
|
||||
disposeCallback(module.hot.data);
|
||||
configure('test', () => [{ ...componentCExports, StoryThree: jest.fn() }], module as any);
|
||||
|
||||
await waitForEvents([SET_INDEX]);
|
||||
expect(mockChannel.emit.mock.calls.find((call) => call[0] === SET_INDEX)?.[1])
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"entries": Object {
|
||||
"component-c--story-one": Object {
|
||||
"argTypes": Object {},
|
||||
"args": Object {},
|
||||
"id": "component-c--story-one",
|
||||
"importPath": "exports-map-0",
|
||||
"initialArgs": Object {},
|
||||
"name": "Story One",
|
||||
"parameters": Object {
|
||||
"__isArgsStory": false,
|
||||
"fileName": "exports-map-0",
|
||||
"renderer": "test",
|
||||
},
|
||||
"tags": Array [
|
||||
"story-tag",
|
||||
"story",
|
||||
],
|
||||
"title": "Component C",
|
||||
"type": "story",
|
||||
},
|
||||
"component-c--story-three": Object {
|
||||
"argTypes": Object {},
|
||||
"args": Object {},
|
||||
"id": "component-c--story-three",
|
||||
"importPath": "exports-map-0",
|
||||
"initialArgs": Object {},
|
||||
"name": "Story Three",
|
||||
"parameters": Object {
|
||||
"__isArgsStory": false,
|
||||
"fileName": "exports-map-0",
|
||||
"renderer": "test",
|
||||
},
|
||||
"tags": Array [
|
||||
"component-tag",
|
||||
"autodocs",
|
||||
"story",
|
||||
],
|
||||
"title": "Component C",
|
||||
"type": "story",
|
||||
},
|
||||
"component-c--story-two": Object {
|
||||
"argTypes": Object {},
|
||||
"args": Object {},
|
||||
"id": "component-c--story-two",
|
||||
"importPath": "exports-map-0",
|
||||
"initialArgs": Object {},
|
||||
"name": "Story Two",
|
||||
"parameters": Object {
|
||||
"__isArgsStory": false,
|
||||
"fileName": "exports-map-0",
|
||||
"renderer": "test",
|
||||
},
|
||||
"tags": Array [
|
||||
"component-tag",
|
||||
"autodocs",
|
||||
"story",
|
||||
],
|
||||
"title": "Component C",
|
||||
"type": "story",
|
||||
},
|
||||
},
|
||||
"v": 4,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('re-emits SET_INDEX when a story file is removed', async () => {
|
||||
const renderToCanvas = jest.fn(({ storyFn }) => storyFn());
|
||||
|
||||
let disposeCallback: (data: object) => void = () => {};
|
||||
const module = {
|
||||
id: 'file1',
|
||||
hot: {
|
||||
data: {},
|
||||
accept: jest.fn(),
|
||||
dispose(cb: () => void) {
|
||||
disposeCallback = cb;
|
||||
},
|
||||
},
|
||||
};
|
||||
const { configure } = start(renderToCanvas);
|
||||
configure(
|
||||
'test',
|
||||
() => [componentCExports, { default: { title: 'Component D' }, StoryFour: jest.fn() }],
|
||||
module as any
|
||||
);
|
||||
|
||||
await waitForEvents([SET_INDEX]);
|
||||
expect(mockChannel.emit.mock.calls.find((call) => call[0] === SET_INDEX)?.[1])
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"entries": Object {
|
||||
"component-c--story-one": Object {
|
||||
"argTypes": Object {},
|
||||
"args": Object {},
|
||||
"id": "component-c--story-one",
|
||||
"importPath": "exports-map-0",
|
||||
"initialArgs": Object {},
|
||||
"name": "Story One",
|
||||
"parameters": Object {
|
||||
"__isArgsStory": false,
|
||||
"fileName": "exports-map-0",
|
||||
"renderer": "test",
|
||||
},
|
||||
"tags": Array [
|
||||
"story-tag",
|
||||
"story",
|
||||
],
|
||||
"title": "Component C",
|
||||
"type": "story",
|
||||
},
|
||||
"component-c--story-two": Object {
|
||||
"argTypes": Object {},
|
||||
"args": Object {},
|
||||
"id": "component-c--story-two",
|
||||
"importPath": "exports-map-0",
|
||||
"initialArgs": Object {},
|
||||
"name": "Story Two",
|
||||
"parameters": Object {
|
||||
"__isArgsStory": false,
|
||||
"fileName": "exports-map-0",
|
||||
"renderer": "test",
|
||||
},
|
||||
"tags": Array [
|
||||
"component-tag",
|
||||
"autodocs",
|
||||
"story",
|
||||
],
|
||||
"title": "Component C",
|
||||
"type": "story",
|
||||
},
|
||||
"component-d--story-four": Object {
|
||||
"argTypes": Object {},
|
||||
"args": Object {},
|
||||
"id": "component-d--story-four",
|
||||
"importPath": "exports-map-1",
|
||||
"initialArgs": Object {},
|
||||
"name": "Story Four",
|
||||
"parameters": Object {
|
||||
"__isArgsStory": false,
|
||||
"fileName": "exports-map-1",
|
||||
"renderer": "test",
|
||||
},
|
||||
"tags": Array [
|
||||
"story",
|
||||
],
|
||||
"title": "Component D",
|
||||
"type": "story",
|
||||
},
|
||||
},
|
||||
"v": 4,
|
||||
}
|
||||
`);
|
||||
await waitForRender();
|
||||
|
||||
mockChannel.emit.mockClear();
|
||||
disposeCallback(module.hot.data);
|
||||
configure('test', () => [componentCExports], module as any);
|
||||
|
||||
await waitForEvents([SET_INDEX]);
|
||||
expect(mockChannel.emit.mock.calls.find((call) => call[0] === SET_INDEX)?.[1])
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"entries": Object {
|
||||
"component-c--story-one": Object {
|
||||
"argTypes": Object {},
|
||||
"args": Object {},
|
||||
"id": "component-c--story-one",
|
||||
"importPath": "exports-map-0",
|
||||
"initialArgs": Object {},
|
||||
"name": "Story One",
|
||||
"parameters": Object {
|
||||
"__isArgsStory": false,
|
||||
"fileName": "exports-map-0",
|
||||
"renderer": "test",
|
||||
},
|
||||
"tags": Array [
|
||||
"story-tag",
|
||||
"story",
|
||||
],
|
||||
"title": "Component C",
|
||||
"type": "story",
|
||||
},
|
||||
"component-c--story-two": Object {
|
||||
"argTypes": Object {},
|
||||
"args": Object {},
|
||||
"id": "component-c--story-two",
|
||||
"importPath": "exports-map-0",
|
||||
"initialArgs": Object {},
|
||||
"name": "Story Two",
|
||||
"parameters": Object {
|
||||
"__isArgsStory": false,
|
||||
"fileName": "exports-map-0",
|
||||
"renderer": "test",
|
||||
},
|
||||
"tags": Array [
|
||||
"component-tag",
|
||||
"autodocs",
|
||||
"story",
|
||||
],
|
||||
"title": "Component C",
|
||||
"type": "story",
|
||||
},
|
||||
},
|
||||
"v": 4,
|
||||
}
|
||||
`);
|
||||
|
||||
await waitForEvents([STORY_UNCHANGED]);
|
||||
});
|
||||
|
||||
it('allows you to override the render function in project annotations', async () => {
|
||||
const renderToCanvas = jest.fn(({ storyFn }) => storyFn());
|
||||
const frameworkRender = jest.fn();
|
||||
|
||||
const { configure } = start(renderToCanvas, { render: frameworkRender });
|
||||
|
||||
const projectRender = jest.fn();
|
||||
setGlobalRender(projectRender);
|
||||
configure('test', () => {
|
||||
return [
|
||||
{
|
||||
default: {
|
||||
title: 'Component A',
|
||||
component: jest.fn(),
|
||||
},
|
||||
StoryOne: {},
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
await waitForRender();
|
||||
expect(mockChannel.emit).toHaveBeenCalledWith(STORY_RENDERED, 'component-a--story-one');
|
||||
|
||||
expect(frameworkRender).not.toHaveBeenCalled();
|
||||
expect(projectRender).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('docs', () => {
|
||||
beforeEach(() => {
|
||||
global.DOCS_OPTIONS = {};
|
||||
});
|
||||
|
||||
// NOTE: MDX files are only ever passed as CSF
|
||||
it('sends over docs only stories as entries', async () => {
|
||||
const renderToCanvas = jest.fn();
|
||||
|
||||
const { configure } = start(renderToCanvas);
|
||||
|
||||
configure(
|
||||
'test',
|
||||
makeRequireContext({
|
||||
'./Introduction.stories.mdx': {
|
||||
default: { title: 'Introduction', tags: ['stories-mdx'] },
|
||||
_Page: { name: 'Page', parameters: { docsOnly: true } },
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
await waitForEvents([SET_INDEX]);
|
||||
expect(mockChannel.emit.mock.calls.find((call) => call[0] === SET_INDEX)?.[1])
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"entries": Object {
|
||||
"introduction": Object {
|
||||
"id": "introduction",
|
||||
"importPath": "./Introduction.stories.mdx",
|
||||
"name": undefined,
|
||||
"parameters": Object {
|
||||
"fileName": "./Introduction.stories.mdx",
|
||||
"renderer": "test",
|
||||
},
|
||||
"storiesImports": Array [],
|
||||
"tags": Array [
|
||||
"stories-mdx",
|
||||
"docs",
|
||||
],
|
||||
"title": "Introduction",
|
||||
"type": "docs",
|
||||
},
|
||||
},
|
||||
"v": 4,
|
||||
}
|
||||
`);
|
||||
|
||||
// Wait a second to let the docs "render" finish (and maybe throw)
|
||||
await waitForQuiescence();
|
||||
});
|
||||
|
||||
it('errors on .mdx files', async () => {
|
||||
const renderToCanvas = jest.fn();
|
||||
|
||||
const { configure } = start(renderToCanvas);
|
||||
|
||||
configure(
|
||||
'test',
|
||||
makeRequireContext({
|
||||
'./Introduction.mdx': {
|
||||
default: () => 'some mdx function',
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
await waitForEvents([CONFIG_ERROR]);
|
||||
expect(mockChannel.emit.mock.calls.find((call) => call[0] === CONFIG_ERROR)?.[1])
|
||||
.toMatchInlineSnapshot(`
|
||||
[Error: Cannot index \`.mdx\` file (\`./Introduction.mdx\`) in SB8.
|
||||
|
||||
The legacy story store does not support new-style \`.mdx\` files. If the file above
|
||||
is not intended to be indexed (i.e. displayed as an entry in the sidebar), either
|
||||
exclude it from your \`stories\` glob, or add <Meta isTemplate /> to it.]
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('auto-title', () => {
|
||||
const componentDExports = {
|
||||
default: {
|
||||
component: 'Component D',
|
||||
},
|
||||
StoryOne: jest.fn(),
|
||||
};
|
||||
it.skip('loads and renders the first story correctly', async () => {
|
||||
const renderToCanvas = jest.fn();
|
||||
|
||||
const { configure } = start(renderToCanvas);
|
||||
configure('test', () => [componentDExports]);
|
||||
|
||||
await waitForEvents([SET_INDEX]);
|
||||
expect(mockChannel.emit.mock.calls.find((call) => call[0] === SET_INDEX)?.[1])
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"entries": Object {
|
||||
"auto-title--story-one": Object {
|
||||
"argTypes": Object {},
|
||||
"args": Object {},
|
||||
"id": "auto-title--story-one",
|
||||
"importPath": "exports-map-0",
|
||||
"initialArgs": Object {},
|
||||
"name": "Story One",
|
||||
"parameters": Object {
|
||||
"__isArgsStory": false,
|
||||
"fileName": "exports-map-0",
|
||||
"renderer": "test",
|
||||
},
|
||||
"tags": Array [
|
||||
"story",
|
||||
],
|
||||
"title": "auto-title",
|
||||
"type": "story",
|
||||
},
|
||||
},
|
||||
"v": 4,
|
||||
}
|
||||
`);
|
||||
|
||||
await waitForRender();
|
||||
});
|
||||
});
|
||||
});
|
@ -1,79 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
import { global } from '@storybook/global';
|
||||
import type { Renderer, ArgsStoryFn, ProjectAnnotations } from '@storybook/types';
|
||||
import type { ClientApi } from '../../client-api';
|
||||
|
||||
const removedApi = (name: string) => () => {
|
||||
throw new Error(`@storybook/client-api:${name} was removed in SB8.`);
|
||||
};
|
||||
|
||||
interface CoreClient_RendererImplementation<TRenderer extends Renderer> {
|
||||
/**
|
||||
* A function that applies decorators to a story.
|
||||
* @template TRenderer The type of renderer used by the Storybook client API.
|
||||
* @type {ProjectAnnotations<TRenderer>['applyDecorators']}
|
||||
*/
|
||||
decorateStory?: ProjectAnnotations<TRenderer>['applyDecorators'];
|
||||
/**
|
||||
* A function that renders a story with args.
|
||||
* @template TRenderer The type of renderer used by the Storybook client API.
|
||||
* @type {ArgsStoryFn<TRenderer>}
|
||||
*/
|
||||
render?: ArgsStoryFn<TRenderer>;
|
||||
}
|
||||
|
||||
interface CoreClient_ClientAPIFacade {
|
||||
/**
|
||||
* The old way of retrieving the list of stories at runtime.
|
||||
* @deprecated This method is deprecated and will be removed in a future version.
|
||||
*/
|
||||
raw: (...args: any[]) => never;
|
||||
}
|
||||
|
||||
interface CoreClient_StartReturnValue<TRenderer extends Renderer> {
|
||||
/**
|
||||
* Forces a re-render of all stories in the Storybook preview.
|
||||
* This function emits the `FORCE_RE_RENDER` event to the Storybook channel.
|
||||
* @deprecated This method is deprecated and will be removed in a future version.
|
||||
* @returns {void}
|
||||
*/
|
||||
forceReRender: () => void;
|
||||
/**
|
||||
* The old way of setting up storybook with runtime configuration.
|
||||
* @deprecated This method is deprecated and will be removed in a future version.
|
||||
* @returns {void}
|
||||
*/
|
||||
configure: any;
|
||||
/**
|
||||
* @deprecated This property is deprecated and will be removed in a future version.
|
||||
* @type {ClientApi<TRenderer> | CoreClient_ClientAPIFacade}
|
||||
*/
|
||||
clientApi: ClientApi<TRenderer> | CoreClient_ClientAPIFacade;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the Storybook preview API.
|
||||
* @template TRenderer The type of renderer used by the Storybook client API.
|
||||
* @param {ProjectAnnotations<TRenderer>['renderToCanvas']} renderToCanvas A function that renders a story to a canvas.
|
||||
* @param {CoreClient_RendererImplementation<TRenderer>} [options] Optional configuration options for the renderer implementation.
|
||||
* @param {ProjectAnnotations<TRenderer>['applyDecorators']} [options.decorateStory] A function that applies decorators to a story.
|
||||
* @param {ArgsStoryFn<TRenderer>} [options.render] A function that renders a story with arguments.
|
||||
* @returns {CoreClient_StartReturnValue<TRenderer>} An object containing functions and objects related to the Storybook preview API.
|
||||
*/
|
||||
export function start<TRenderer extends Renderer>(
|
||||
renderToCanvas: ProjectAnnotations<TRenderer>['renderToCanvas'],
|
||||
{ decorateStory, render }: CoreClient_RendererImplementation<TRenderer> = {}
|
||||
): CoreClient_StartReturnValue<TRenderer> {
|
||||
if (global) {
|
||||
// To enable user code to detect if it is running in Storybook
|
||||
global.IS_STORYBOOK = true;
|
||||
}
|
||||
|
||||
return {
|
||||
forceReRender: removedApi('forceReRender'),
|
||||
configure: removedApi('configure'),
|
||||
clientApi: {
|
||||
raw: removedApi('raw'),
|
||||
},
|
||||
};
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
import { dedent } from 'ts-dedent';
|
||||
import { global } from '@storybook/global';
|
||||
import { SynchronousPromise } from 'synchronous-promise';
|
||||
import {
|
||||
CONFIG_ERROR,
|
||||
FORCE_REMOUNT,
|
||||
@ -13,7 +12,7 @@ import {
|
||||
UPDATE_GLOBALS,
|
||||
UPDATE_STORY_ARGS,
|
||||
} from '@storybook/core-events';
|
||||
import { logger, deprecate } from '@storybook/client-logger';
|
||||
import { logger } from '@storybook/client-logger';
|
||||
import type { Channel } from '@storybook/channels';
|
||||
import type {
|
||||
Renderer,
|
||||
@ -68,14 +67,7 @@ export class Preview<TRenderer extends Renderer> {
|
||||
}
|
||||
|
||||
// INITIALIZATION
|
||||
|
||||
// NOTE: the reason that the preview and store's initialization code is written in a promise
|
||||
// style and not `async-await`, and the use of `SynchronousPromise`s is in order to allow
|
||||
// storyshots to immediately call `raw()` on the store without waiting for a later tick.
|
||||
// (Even simple things like `Promise.resolve()` and `await` involve the callback happening
|
||||
// in the next promise "tick").
|
||||
// See the comment in `storyshots-core/src/api/index.ts` for more detail.
|
||||
initialize({
|
||||
async initialize({
|
||||
getStoryIndex,
|
||||
importFn,
|
||||
getProjectAnnotations,
|
||||
@ -93,9 +85,8 @@ export class Preview<TRenderer extends Renderer> {
|
||||
|
||||
this.setupListeners();
|
||||
|
||||
return this.getProjectAnnotationsOrRenderError(getProjectAnnotations).then(
|
||||
(projectAnnotations) => this.initializeWithProjectAnnotations(projectAnnotations)
|
||||
);
|
||||
const projectAnnotations = await this.getProjectAnnotationsOrRenderError(getProjectAnnotations);
|
||||
return this.initializeWithProjectAnnotations(projectAnnotations);
|
||||
}
|
||||
|
||||
setupListeners() {
|
||||
@ -107,49 +98,44 @@ export class Preview<TRenderer extends Renderer> {
|
||||
this.channel.on(FORCE_REMOUNT, this.onForceRemount.bind(this));
|
||||
}
|
||||
|
||||
getProjectAnnotationsOrRenderError(
|
||||
async getProjectAnnotationsOrRenderError(
|
||||
getProjectAnnotations: () => MaybePromise<ProjectAnnotations<TRenderer>>
|
||||
): Promise<ProjectAnnotations<TRenderer>> {
|
||||
return SynchronousPromise.resolve()
|
||||
.then(getProjectAnnotations)
|
||||
.then((projectAnnotations) => {
|
||||
if (projectAnnotations.renderToDOM)
|
||||
deprecate(`\`renderToDOM\` is deprecated, please rename to \`renderToCanvas\``);
|
||||
try {
|
||||
const projectAnnotations = await getProjectAnnotations();
|
||||
|
||||
this.renderToCanvas = projectAnnotations.renderToCanvas || projectAnnotations.renderToDOM;
|
||||
if (!this.renderToCanvas) {
|
||||
throw new Error(dedent`
|
||||
this.renderToCanvas = projectAnnotations.renderToCanvas;
|
||||
if (!this.renderToCanvas) {
|
||||
throw new Error(dedent`
|
||||
Expected your framework's preset to export a \`renderToCanvas\` field.
|
||||
|
||||
Perhaps it needs to be upgraded for Storybook 6.4?
|
||||
|
||||
More info: https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#mainjs-framework-field
|
||||
`);
|
||||
}
|
||||
return projectAnnotations;
|
||||
})
|
||||
.catch((err) => {
|
||||
// This is an error extracting the projectAnnotations (i.e. evaluating the previewEntries) and
|
||||
// needs to be show to the user as a simple error
|
||||
this.renderPreviewEntryError('Error reading preview.js:', err);
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
return projectAnnotations;
|
||||
} catch (err) {
|
||||
// This is an error extracting the projectAnnotations (i.e. evaluating the previewEntries) and
|
||||
// needs to be show to the user as a simple error
|
||||
this.renderPreviewEntryError('Error reading preview.js:', err as Error);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// If initialization gets as far as project annotations, this function runs.
|
||||
initializeWithProjectAnnotations(projectAnnotations: ProjectAnnotations<TRenderer>) {
|
||||
async initializeWithProjectAnnotations(projectAnnotations: ProjectAnnotations<TRenderer>) {
|
||||
this.storyStore.setProjectAnnotations(projectAnnotations);
|
||||
|
||||
this.setInitialGlobals();
|
||||
|
||||
const storyIndexPromise: Promise<StoryIndex> = this.getStoryIndexFromServer();
|
||||
|
||||
return storyIndexPromise
|
||||
.then((storyIndex: StoryIndex) => this.initializeWithStoryIndex(storyIndex))
|
||||
.catch((err) => {
|
||||
this.renderPreviewEntryError('Error loading story index:', err);
|
||||
throw err;
|
||||
});
|
||||
try {
|
||||
const storyIndex = await this.getStoryIndexFromServer();
|
||||
return this.initializeWithStoryIndex(storyIndex);
|
||||
} catch (err) {
|
||||
this.renderPreviewEntryError('Error loading story index:', err as Error);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async setInitialGlobals() {
|
||||
@ -177,15 +163,11 @@ export class Preview<TRenderer extends Renderer> {
|
||||
}
|
||||
|
||||
// If initialization gets as far as the story index, this function runs.
|
||||
initializeWithStoryIndex(storyIndex: StoryIndex): PromiseLike<void> {
|
||||
initializeWithStoryIndex(storyIndex: StoryIndex): void {
|
||||
if (!this.importFn)
|
||||
throw new Error(`Cannot call initializeWithStoryIndex before initialization`);
|
||||
|
||||
return this.storyStore.initialize({
|
||||
storyIndex,
|
||||
importFn: this.importFn,
|
||||
cache: false,
|
||||
});
|
||||
this.storyStore.initialize({ storyIndex, importFn: this.importFn });
|
||||
}
|
||||
|
||||
// EVENT HANDLERS
|
||||
|
@ -44,7 +44,6 @@ jest.mock('@storybook/global', () => ({
|
||||
search: '?id=*',
|
||||
},
|
||||
},
|
||||
FEATURES: {},
|
||||
fetch: async () => ({ status: 200, json: async () => mockStoryIndex }),
|
||||
},
|
||||
}));
|
||||
|
@ -69,7 +69,6 @@ jest.mock('@storybook/global', () => ({
|
||||
search: '?id=*',
|
||||
},
|
||||
},
|
||||
FEATURES: {},
|
||||
fetch: async () => mockFetchResult,
|
||||
},
|
||||
}));
|
||||
|
@ -5,7 +5,6 @@ import {
|
||||
PRELOAD_ENTRIES,
|
||||
PREVIEW_KEYDOWN,
|
||||
SET_CURRENT_STORY,
|
||||
SET_INDEX,
|
||||
STORY_ARGS_UPDATED,
|
||||
STORY_CHANGED,
|
||||
STORY_ERRORED,
|
||||
@ -114,12 +113,10 @@ export class PreviewWithSelection<TFramework extends Renderer> extends Preview<T
|
||||
}
|
||||
|
||||
// If initialization gets as far as the story index, this function runs.
|
||||
initializeWithStoryIndex(storyIndex: StoryIndex): PromiseLike<void> {
|
||||
return super.initializeWithStoryIndex(storyIndex).then(() => {
|
||||
this.channel.emit(SET_INDEX, this.storyStore.getSetIndexPayload());
|
||||
async initializeWithStoryIndex(storyIndex: StoryIndex): Promise<void> {
|
||||
await super.initializeWithStoryIndex(storyIndex);
|
||||
|
||||
return this.selectSpecifiedStory();
|
||||
});
|
||||
return this.selectSpecifiedStory();
|
||||
}
|
||||
|
||||
// Use the selection specifier to choose a story, then render it
|
||||
@ -201,8 +198,6 @@ export class PreviewWithSelection<TFramework extends Renderer> extends Preview<T
|
||||
}) {
|
||||
await super.onStoriesChanged({ importFn, storyIndex });
|
||||
|
||||
this.channel.emit(SET_INDEX, await this.storyStore.getSetIndexPayload());
|
||||
|
||||
if (this.selectionStore.selection) {
|
||||
await this.renderSelection();
|
||||
} else {
|
||||
|
@ -2,7 +2,6 @@ import memoize from 'memoizerific';
|
||||
import type {
|
||||
IndexEntry,
|
||||
Renderer,
|
||||
API_PreparedStoryIndex,
|
||||
ComponentTitle,
|
||||
Parameters,
|
||||
Path,
|
||||
@ -24,7 +23,6 @@ import type {
|
||||
} from '@storybook/types';
|
||||
import mapValues from 'lodash/mapValues.js';
|
||||
import pick from 'lodash/pick.js';
|
||||
import { SynchronousPromise } from 'synchronous-promise';
|
||||
|
||||
import { HooksContext } from '../addons';
|
||||
import { StoryIndexStore } from './StoryIndexStore';
|
||||
@ -63,9 +61,9 @@ export class StoryStore<TRenderer extends Renderer> {
|
||||
|
||||
prepareStoryWithCache: typeof prepareStory;
|
||||
|
||||
initializationPromise: SynchronousPromise<void>;
|
||||
initializationPromise: Promise<void>;
|
||||
|
||||
// This *does* get set in the constructor but the semantics of `new SynchronousPromise` trip up TS
|
||||
// This *does* get set in the constructor but the semantics of `new Promise` trip up TS
|
||||
resolveInitializationPromise!: () => void;
|
||||
|
||||
constructor() {
|
||||
@ -80,7 +78,7 @@ export class StoryStore<TRenderer extends Renderer> {
|
||||
this.prepareStoryWithCache = memoize(STORY_CACHE_SIZE)(prepareStory) as typeof prepareStory;
|
||||
|
||||
// We cannot call `loadStory()` until we've been initialized properly. But we can wait for it.
|
||||
this.initializationPromise = new SynchronousPromise((resolve) => {
|
||||
this.initializationPromise = new Promise((resolve) => {
|
||||
this.resolveInitializationPromise = resolve;
|
||||
});
|
||||
}
|
||||
@ -100,19 +98,15 @@ export class StoryStore<TRenderer extends Renderer> {
|
||||
initialize({
|
||||
storyIndex,
|
||||
importFn,
|
||||
cache = false,
|
||||
}: {
|
||||
storyIndex?: StoryIndex;
|
||||
importFn: ModuleImportFn;
|
||||
cache?: boolean;
|
||||
}): Promise<void> {
|
||||
}): void {
|
||||
this.storyIndex = new StoryIndexStore(storyIndex);
|
||||
this.importFn = importFn;
|
||||
|
||||
// We don't need the cache to be loaded to call `loadStory`, we just need the index ready
|
||||
this.resolveInitializationPromise();
|
||||
|
||||
return cache ? this.cacheAllCSFFiles() : SynchronousPromise.resolve();
|
||||
}
|
||||
|
||||
// This means that one of the CSF files has changed.
|
||||
@ -142,18 +136,18 @@ export class StoryStore<TRenderer extends Renderer> {
|
||||
}
|
||||
|
||||
// To load a single CSF file to service a story we need to look up the importPath in the index
|
||||
loadCSFFileByStoryId(storyId: StoryId): Promise<CSFFile<TRenderer>> {
|
||||
async loadCSFFileByStoryId(storyId: StoryId): Promise<CSFFile<TRenderer>> {
|
||||
if (!this.storyIndex || !this.importFn)
|
||||
throw new Error(`loadCSFFileByStoryId called before initialization`);
|
||||
|
||||
const { importPath, title } = this.storyIndex.storyIdToEntry(storyId);
|
||||
return this.importFn(importPath).then((moduleExports) =>
|
||||
// We pass the title in here as it may have been generated by autoTitle on the server.
|
||||
this.processCSFFileWithCache(moduleExports, importPath, title)
|
||||
);
|
||||
const moduleExports = await this.importFn(importPath);
|
||||
|
||||
// We pass the title in here as it may have been generated by autoTitle on the server.
|
||||
return this.processCSFFileWithCache(moduleExports, importPath, title);
|
||||
}
|
||||
|
||||
loadAllCSFFiles({ batchSize = EXTRACT_BATCH_SIZE } = {}): Promise<
|
||||
async loadAllCSFFiles({ batchSize = EXTRACT_BATCH_SIZE } = {}): Promise<
|
||||
StoryStore<TRenderer>['cachedCSFFiles']
|
||||
> {
|
||||
if (!this.storyIndex) throw new Error(`loadAllCSFFiles called before initialization`);
|
||||
@ -163,41 +157,33 @@ export class StoryStore<TRenderer extends Renderer> {
|
||||
storyId,
|
||||
]);
|
||||
|
||||
const loadInBatches = (
|
||||
const loadInBatches = async (
|
||||
remainingImportPaths: typeof importPaths
|
||||
): Promise<{ importPath: Path; csfFile: CSFFile<TRenderer> }[]> => {
|
||||
if (remainingImportPaths.length === 0) return SynchronousPromise.resolve([]);
|
||||
if (remainingImportPaths.length === 0) return Promise.resolve([]);
|
||||
|
||||
const csfFilePromiseList = remainingImportPaths
|
||||
.slice(0, batchSize)
|
||||
.map(([importPath, storyId]) =>
|
||||
this.loadCSFFileByStoryId(storyId).then((csfFile) => ({
|
||||
importPath,
|
||||
csfFile,
|
||||
}))
|
||||
);
|
||||
.map(async ([importPath, storyId]) => ({
|
||||
importPath,
|
||||
csfFile: await this.loadCSFFileByStoryId(storyId),
|
||||
}));
|
||||
|
||||
return SynchronousPromise.all(csfFilePromiseList).then((firstResults) =>
|
||||
loadInBatches(remainingImportPaths.slice(batchSize)).then((restResults) =>
|
||||
firstResults.concat(restResults)
|
||||
)
|
||||
);
|
||||
const firstResults = await Promise.all(csfFilePromiseList);
|
||||
const restResults = await loadInBatches(remainingImportPaths.slice(batchSize));
|
||||
return firstResults.concat(restResults);
|
||||
};
|
||||
|
||||
return loadInBatches(importPaths).then((list) =>
|
||||
list.reduce((acc, { importPath, csfFile }) => {
|
||||
acc[importPath] = csfFile;
|
||||
return acc;
|
||||
}, {} as Record<Path, CSFFile<TRenderer>>)
|
||||
);
|
||||
const list = await loadInBatches(importPaths);
|
||||
return list.reduce((acc, { importPath, csfFile }) => {
|
||||
acc[importPath] = csfFile;
|
||||
return acc;
|
||||
}, {} as Record<Path, CSFFile<TRenderer>>);
|
||||
}
|
||||
|
||||
cacheAllCSFFiles(): Promise<void> {
|
||||
return this.initializationPromise.then(() =>
|
||||
this.loadAllCSFFiles().then((csfFiles) => {
|
||||
this.cachedCSFFiles = csfFiles;
|
||||
})
|
||||
);
|
||||
async cacheAllCSFFiles(): Promise<void> {
|
||||
await this.initializationPromise;
|
||||
this.cachedCSFFiles = await this.loadAllCSFFiles();
|
||||
}
|
||||
|
||||
preparedMetaFromCSFFile({ csfFile }: { csfFile: CSFFile<TRenderer> }): PreparedMeta<TRenderer> {
|
||||
@ -393,38 +379,6 @@ export class StoryStore<TRenderer extends Renderer> {
|
||||
};
|
||||
};
|
||||
|
||||
getSetIndexPayload(): API_PreparedStoryIndex {
|
||||
if (!this.storyIndex) throw new Error('getSetIndexPayload called before initialization');
|
||||
if (!this.cachedCSFFiles)
|
||||
throw new Error('Cannot call getSetIndexPayload() unless you call cacheAllCSFFiles() first');
|
||||
const { cachedCSFFiles } = this;
|
||||
|
||||
const stories = this.extract({ includeDocsOnly: true });
|
||||
|
||||
return {
|
||||
v: 4,
|
||||
entries: Object.fromEntries(
|
||||
Object.entries(this.storyIndex.entries).map(([id, entry]) => [
|
||||
id,
|
||||
stories[id]
|
||||
? {
|
||||
...entry,
|
||||
args: stories[id].initialArgs,
|
||||
initialArgs: stories[id].initialArgs,
|
||||
argTypes: stories[id].argTypes,
|
||||
parameters: stories[id].parameters,
|
||||
}
|
||||
: {
|
||||
...entry,
|
||||
parameters: this.preparedMetaFromCSFFile({
|
||||
csfFile: cachedCSFFiles[entry.importPath],
|
||||
}).parameters,
|
||||
},
|
||||
])
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
raw(): BoundStory<TRenderer>[] {
|
||||
return Object.values(this.extract())
|
||||
.map(({ id }: { id: StoryId }) => this.fromId(id))
|
||||
|
@ -10,34 +10,14 @@ The preview's job is:
|
||||
|
||||
3. Render the current selection to the web view in either story or docs mode.
|
||||
|
||||
## V7 Store vs Legacy (V6)
|
||||
|
||||
The story store is designed to load stories 'on demand', and will operate in this fashion if the `storyStoreV7` feature is enabled.
|
||||
|
||||
However, for back-compat reasons, in v6 mode, we need to load all stories, synchronously on bootup, emitting the `SET_STORIES` event.
|
||||
|
||||
In V7 mode we do not emit that event, instead preferring the `STORY_PREPARED` event, with the data for the single story being rendered.
|
||||
|
||||
## Initialization
|
||||
|
||||
The preview is `initialized` in two ways.
|
||||
|
||||
### V7 Mode:
|
||||
|
||||
- `importFn` - is an async `import()` function
|
||||
|
||||
- `getProjectAnnotations` - is a simple function that evaluations `preview.js` and addon config files and combines them. If it errors, the Preview will show the error.
|
||||
|
||||
- No `getStoryIndex` function is passed, instead the preview creates a `StoryIndexClient` that pulls `stories.json` from node and watches the event stream for invalidation events.
|
||||
|
||||
### V6 Mode
|
||||
|
||||
- `importFn` - is a simulated `import()` function, that is synchronous, see `client-api` for details.
|
||||
- `getProjectAnnotations` - also evaluates `preview.js` et al, but watches for calls to `setStories`, and passes them to the `ClientApi`
|
||||
- `getStoryIndex` is a local function (that must be called _after_ `getProjectAnnotations`) that gets the list of stories added.
|
||||
|
||||
See `client-api` for more details on this process.
|
||||
|
||||
## Story Rendering and interruptions
|
||||
|
||||
The Preview is split into three parts responsible for state management:
|
||||
|
@ -274,11 +274,6 @@ export interface StorybookConfig {
|
||||
staticDirs?: (DirectoryMapping | string)[];
|
||||
logLevel?: string;
|
||||
features?: {
|
||||
/**
|
||||
* Build stories.json automatically on start/build
|
||||
*/
|
||||
buildStoriesJson?: boolean;
|
||||
|
||||
/**
|
||||
* Do not throw errors if using `.mdx` files in SSv7
|
||||
* (for internal use in sandboxes)
|
||||
|
@ -2,8 +2,8 @@
|
||||
|
||||
import './globals';
|
||||
|
||||
export * from './public-api';
|
||||
export * from './public-types';
|
||||
export { setup } from './render';
|
||||
|
||||
// optimization: stop HMR propagation in webpack
|
||||
try {
|
||||
|
@ -1 +0,0 @@
|
||||
export { setup } from './render';
|
@ -433,7 +433,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/core@npm:7.23.2, @babel/core@npm:^7.11.6, @babel/core@npm:^7.12.0, @babel/core@npm:^7.12.3, @babel/core@npm:^7.13.16, @babel/core@npm:^7.18.9, @babel/core@npm:^7.19.6, @babel/core@npm:^7.20.12, @babel/core@npm:^7.22.1, @babel/core@npm:^7.23.2, @babel/core@npm:^7.3.4, @babel/core@npm:^7.7.5":
|
||||
"@babel/core@npm:7.23.2, @babel/core@npm:^7.23.2":
|
||||
version: 7.23.2
|
||||
resolution: "@babel/core@npm:7.23.2"
|
||||
dependencies:
|
||||
@ -456,6 +456,29 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.0, @babel/core@npm:^7.12.3, @babel/core@npm:^7.13.16, @babel/core@npm:^7.18.9, @babel/core@npm:^7.19.6, @babel/core@npm:^7.20.12, @babel/core@npm:^7.22.1, @babel/core@npm:^7.3.4, @babel/core@npm:^7.7.5":
|
||||
version: 7.23.0
|
||||
resolution: "@babel/core@npm:7.23.0"
|
||||
dependencies:
|
||||
"@ampproject/remapping": "npm:^2.2.0"
|
||||
"@babel/code-frame": "npm:^7.22.13"
|
||||
"@babel/generator": "npm:^7.23.0"
|
||||
"@babel/helper-compilation-targets": "npm:^7.22.15"
|
||||
"@babel/helper-module-transforms": "npm:^7.23.0"
|
||||
"@babel/helpers": "npm:^7.23.0"
|
||||
"@babel/parser": "npm:^7.23.0"
|
||||
"@babel/template": "npm:^7.22.15"
|
||||
"@babel/traverse": "npm:^7.23.0"
|
||||
"@babel/types": "npm:^7.23.0"
|
||||
convert-source-map: "npm:^2.0.0"
|
||||
debug: "npm:^4.1.0"
|
||||
gensync: "npm:^1.0.0-beta.2"
|
||||
json5: "npm:^2.2.3"
|
||||
semver: "npm:^6.3.1"
|
||||
checksum: ba3604b28de28cdb07d7829f67127b03ad2e826c4e28a0560a037c8bbe16b8dc8cdb8baf344e916ad3c28c63aab88c1a1a38f5e3df6047ab79c910b41bb3a4e8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/generator@npm:7.22.9":
|
||||
version: 7.22.9
|
||||
resolution: "@babel/generator@npm:7.22.9"
|
||||
@ -718,7 +741,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/helpers@npm:^7.22.6, @babel/helpers@npm:^7.23.2":
|
||||
"@babel/helpers@npm:^7.22.6, @babel/helpers@npm:^7.23.0, @babel/helpers@npm:^7.23.2":
|
||||
version: 7.23.2
|
||||
resolution: "@babel/helpers@npm:7.23.2"
|
||||
dependencies:
|
||||
@ -2221,7 +2244,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/traverse@npm:^7.1.6, @babel/traverse@npm:^7.18.9, @babel/traverse@npm:^7.22.8, @babel/traverse@npm:^7.23.2, @babel/traverse@npm:^7.4.5, @babel/traverse@npm:^7.7.0":
|
||||
"@babel/traverse@npm:^7.1.6, @babel/traverse@npm:^7.18.9, @babel/traverse@npm:^7.22.8, @babel/traverse@npm:^7.23.0, @babel/traverse@npm:^7.23.2, @babel/traverse@npm:^7.4.5, @babel/traverse@npm:^7.7.0":
|
||||
version: 7.23.2
|
||||
resolution: "@babel/traverse@npm:7.23.2"
|
||||
dependencies:
|
||||
@ -6933,7 +6956,6 @@ __metadata:
|
||||
qs: "npm:^6.10.0"
|
||||
react: "npm:^18.2.0"
|
||||
slash: "npm:^5.0.0"
|
||||
synchronous-promise: "npm:^2.0.15"
|
||||
ts-dedent: "npm:^2.0.0"
|
||||
util-deprecate: "npm:^1.0.2"
|
||||
languageName: unknown
|
||||
@ -18783,7 +18805,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"is-core-module@npm:^2.11.0, is-core-module@npm:^2.13.0, is-core-module@npm:^2.13.1, is-core-module@npm:^2.5.0, is-core-module@npm:^2.8.1":
|
||||
"is-core-module@npm:^2.11.0, is-core-module@npm:^2.13.0, is-core-module@npm:^2.5.0, is-core-module@npm:^2.8.1":
|
||||
version: 2.13.0
|
||||
resolution: "is-core-module@npm:2.13.0"
|
||||
dependencies:
|
||||
has: "npm:^1.0.3"
|
||||
checksum: a8e7f46f8cefd7c9f6f5d54f3dbf1c40bf79467b6612d6023421ec6ea7e8e4c22593b3963ff7a3f770db07bc19fccbe7987a550a8bc1a4d6ec4115db5e4c5dca
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"is-core-module@npm:^2.13.1":
|
||||
version: 2.13.1
|
||||
resolution: "is-core-module@npm:2.13.1"
|
||||
dependencies:
|
||||
@ -21468,7 +21499,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"make-fetch-happen@npm:^10.0.3, make-fetch-happen@npm:^10.0.6":
|
||||
"make-fetch-happen@npm:^10.0.6":
|
||||
version: 10.2.1
|
||||
resolution: "make-fetch-happen@npm:10.2.1"
|
||||
dependencies:
|
||||
@ -21492,7 +21523,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"make-fetch-happen@npm:^11.0.0, make-fetch-happen@npm:^11.0.1, make-fetch-happen@npm:^11.1.1":
|
||||
"make-fetch-happen@npm:^11.0.0, make-fetch-happen@npm:^11.0.1, make-fetch-happen@npm:^11.0.3, make-fetch-happen@npm:^11.1.1":
|
||||
version: 11.1.1
|
||||
resolution: "make-fetch-happen@npm:11.1.1"
|
||||
dependencies:
|
||||
@ -23241,14 +23272,14 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"node-gyp@npm:^9.0.0, node-gyp@npm:^9.3.1":
|
||||
version: 9.4.1
|
||||
resolution: "node-gyp@npm:9.4.1"
|
||||
version: 9.4.0
|
||||
resolution: "node-gyp@npm:9.4.0"
|
||||
dependencies:
|
||||
env-paths: "npm:^2.2.0"
|
||||
exponential-backoff: "npm:^3.1.1"
|
||||
glob: "npm:^7.1.4"
|
||||
graceful-fs: "npm:^4.2.6"
|
||||
make-fetch-happen: "npm:^10.0.3"
|
||||
make-fetch-happen: "npm:^11.0.3"
|
||||
nopt: "npm:^6.0.0"
|
||||
npmlog: "npm:^6.0.0"
|
||||
rimraf: "npm:^3.0.2"
|
||||
@ -23257,7 +23288,7 @@ __metadata:
|
||||
which: "npm:^2.0.2"
|
||||
bin:
|
||||
node-gyp: bin/node-gyp.js
|
||||
checksum: f7d676cfa79f27d35edf17fe9c80064123670362352d19729e5dc9393d7e99f1397491c3107eddc0c0e8941442a6244a7ba6c860cfbe4b433b4cae248a55fe10
|
||||
checksum: e8dfbe2b02f23d056f69e01c409381963e92c71cafba6c9cfbf63b038f65ca19ab8183bb6891d080e59c4eb2cc425fc736f42e90afc0f0030ecd97bfc64fb7ad
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -29210,13 +29241,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"synchronous-promise@npm:^2.0.15":
|
||||
version: 2.0.17
|
||||
resolution: "synchronous-promise@npm:2.0.17"
|
||||
checksum: 1babe643d8417789ef6e5a2f3d4b8abcda2de236acd09bbe2c98f6be82c0a2c92ed21a6e4f934845fa8de18b1435a9cba1e8c3d945032e8a532f076224c024b1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tapable@npm:^2.0.0, tapable@npm:^2.1.1, tapable@npm:^2.2.0, tapable@npm:^2.2.1":
|
||||
version: 2.2.1
|
||||
resolution: "tapable@npm:2.2.1"
|
||||
@ -31793,7 +31817,20 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"which-typed-array@npm:^1.1.11, which-typed-array@npm:^1.1.13, which-typed-array@npm:^1.1.2, which-typed-array@npm:^1.1.9":
|
||||
"which-typed-array@npm:^1.1.11, which-typed-array@npm:^1.1.2, which-typed-array@npm:^1.1.9":
|
||||
version: 1.1.11
|
||||
resolution: "which-typed-array@npm:1.1.11"
|
||||
dependencies:
|
||||
available-typed-arrays: "npm:^1.0.5"
|
||||
call-bind: "npm:^1.0.2"
|
||||
for-each: "npm:^0.3.3"
|
||||
gopd: "npm:^1.0.1"
|
||||
has-tostringtag: "npm:^1.0.0"
|
||||
checksum: 2cf4ce417beb50ae0ec3b1b479ea6d72d3e71986462ebd77344ca6398f77c7c59804eebe88f4126ce79f85edbcaa6c7783f54b0a5bf34f785eab7cbb35c30499
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"which-typed-array@npm:^1.1.13":
|
||||
version: 1.1.13
|
||||
resolution: "which-typed-array@npm:1.1.13"
|
||||
dependencies:
|
||||
|
@ -9,34 +9,13 @@ Type:
|
||||
```ts
|
||||
{
|
||||
argTypeTargetsV7?: boolean;
|
||||
buildStoriesJson?: boolean;
|
||||
legacyDecoratorFileOrder?: boolean;
|
||||
legacyMdx1?: boolean;
|
||||
storyStoreV7?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
Enables Storybook's additional features.
|
||||
|
||||
## `buildStoriesJson`
|
||||
|
||||
Type: `boolean`
|
||||
|
||||
Default: `true`, when [`storyStoreV7`](#storystorev7) is `true`
|
||||
|
||||
Generates a `index.json` and `stories.json` files to help story loading with the on-demand mode.
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
|
||||
<CodeSnippets
|
||||
paths={[
|
||||
'common/main-config-features-build-stories-json.js.mdx',
|
||||
'common/main-config-features-build-stories-json.ts.mdx',
|
||||
]}
|
||||
/>
|
||||
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
## `legacyDecoratorFileOrder`
|
||||
|
||||
Type: `boolean`
|
||||
@ -71,25 +50,6 @@ Enables support for MDX version 1 as a fallback. Requires [@storybook/mdx1-csf](
|
||||
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
## `storyStoreV7`
|
||||
|
||||
Type: `boolean`
|
||||
|
||||
Default: `true`
|
||||
|
||||
Opts out of [on-demand story loading](#on-demand-story-loading); loads all stories at build time.
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
|
||||
<CodeSnippets
|
||||
paths={[
|
||||
'common/main-config-features-story-store-v7.js.mdx',
|
||||
'common/main-config-features-story-store-v7.ts.mdx',
|
||||
]}
|
||||
/>
|
||||
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
## `argTypeTargetsV7`
|
||||
|
||||
(⚠️ **Experimental**)
|
||||
@ -107,26 +67,4 @@ Filter args with a "target" on the type from the render function.
|
||||
]}
|
||||
/>
|
||||
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
## On-demand story loading
|
||||
|
||||
As your Storybook grows, it gets challenging to load all of your stories performantly, slowing down the loading times and yielding a large bundle. Out of the box, Storybook loads your stories on demand rather than during boot-up to improve the performance of your Storybook. If you need to load all of your stories during boot-up, you can disable this feature by setting the `storyStoreV7` feature flag to `false` in your configuration as follows:
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
|
||||
<CodeSnippets
|
||||
paths={[
|
||||
'common/main-config-features-story-store-v7.js.mdx',
|
||||
'common/main-config-features-story-store-v7.ts.mdx',
|
||||
]}
|
||||
/>
|
||||
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
### Known limitations
|
||||
|
||||
Because of the way stories are currently indexed in Storybook, loading stories on demand with `storyStoreV7` has a couple of minor limitations at the moment:
|
||||
|
||||
- [CSF formats](../api/csf.md) from version 1 to version 3 are supported. The `storiesOf` construct is not.
|
||||
- Custom [`storySort` functions](../writing-stories/naming-components-and-hierarchy.md#sorting-stories) receive more limited arguments.
|
||||
<!-- prettier-ignore-end -->
|
@ -117,7 +117,7 @@ When [auto-titling](../configure/sidebar-and-urls.md#csf-30-auto-titles), prefix
|
||||
|
||||
<div class="aside">
|
||||
|
||||
💡 With [`storyStoreV7`](./main-config-features.md#storystorev7) (the default in Storybook 7), Storybook now statically analyzes the configuration file to improve performance. Loading stories with a custom implementation may de-optimize or break this ability.
|
||||
💡 Storybook now statically analyzes the configuration file to improve performance. Loading stories with a custom implementation may de-optimize or break this ability.
|
||||
|
||||
</div>
|
||||
|
||||
|
@ -10,7 +10,6 @@ By default, Storybook provides zero-config support for Webpack and automatically
|
||||
|
||||
| Option | Description |
|
||||
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `storyStoreV7` | Enabled by default.<br/> Configures Webpack's [code splitting](https://webpack.js.org/guides/code-splitting/) feature<br/> `features: { storyStoreV7: false }` |
|
||||
| `lazyCompilation` | Enables Webpack's experimental [`lazy compilation`](https://webpack.js.org/configuration/experiments/#experimentslazycompilation)<br/>`core: { builder: { options: { lazyCompilation: true } } }` |
|
||||
| `fsCache` | Configures Webpack's filesystem [caching](https://webpack.js.org/configuration/cache/#cachetype) feature<br/> `core: { builder: { options: { fsCache: true } } }` |
|
||||
|
||||
|
@ -20,7 +20,6 @@ const config = {
|
||||
channelOptions: { allowFunction: false, maxDepth: 10 },
|
||||
},
|
||||
features: {
|
||||
buildStoriesJson: true,
|
||||
warnOnLegacyHierarchySeparator: false,
|
||||
previewMdx2: true,
|
||||
},
|
||||
|
Loading…
x
Reference in New Issue
Block a user