/* eslint no-underscore-dangle: 0 */ import deprecate from 'util-deprecate'; import dedent from 'ts-dedent'; import { logger } from '@storybook/client-logger'; import { StoryFn, Parameters, LoaderFunction, DecorateStoryFunction } from '@storybook/addons'; import { toId } from '@storybook/csf'; import { ClientApiParams, DecoratorFunction, ClientApiAddons, StoryApi, ArgTypesEnhancer, } from './types'; import { applyHooks } from './hooks'; import StoryStore from './story_store'; import { defaultDecorateStory } from './decorators'; // ClientApi (and StoreStore) are really singletons. However they are not created until the // relevant framework instanciates them via `start.js`. The good news is this happens right away. let singleton: ClientApi; const addDecoratorDeprecationWarning = deprecate( () => {}, `\`addDecorator\` is deprecated, and will be removed in Storybook 7.0. Instead, use \`export const decorators = [];\` in your \`preview.js\`. Read more at https://github.com/storybookjs/storybook/MIGRATION.md#deprecated-addparameters-and-adddecorator).` ); export const addDecorator = (decorator: DecoratorFunction, deprecationWarning = true) => { if (!singleton) throw new Error(`Singleton client API not yet initialized, cannot call addDecorator`); if (deprecationWarning) addDecoratorDeprecationWarning(); singleton.addDecorator(decorator); }; const addParametersDeprecationWarning = deprecate( () => {}, `\`addParameters\` is deprecated, and will be removed in Storybook 7.0. Instead, use \`export const parameters = {};\` in your \`preview.js\`. Read more at https://github.com/storybookjs/storybook/MIGRATION.md#deprecated-addparameters-and-adddecorator).` ); export const addParameters = (parameters: Parameters, deprecationWarning = true) => { if (!singleton) throw new Error(`Singleton client API not yet initialized, cannot call addParameters`); if (deprecationWarning) addParametersDeprecationWarning(); singleton.addParameters(parameters); }; export const addArgTypesEnhancer = (enhancer: ArgTypesEnhancer) => { if (!singleton) throw new Error(`Singleton client API not yet initialized, cannot call addArgTypesEnhancer`); singleton.addArgTypesEnhancer(enhancer); }; export default class ClientApi { private _storyStore: StoryStore; private _addons: ClientApiAddons<unknown>; private _decorateStory: DecorateStoryFunction; // React Native Fast refresh doesn't allow multiple dispose calls private _noStoryModuleAddMethodHotDispose: boolean; constructor({ storyStore, decorateStory = defaultDecorateStory, noStoryModuleAddMethodHotDispose, }: ClientApiParams) { this._storyStore = storyStore; this._addons = {}; this._noStoryModuleAddMethodHotDispose = noStoryModuleAddMethodHotDispose || false; this._decorateStory = decorateStory; if (!storyStore) throw new Error('storyStore is required'); singleton = this; } setAddon = deprecate( (addon: any) => { this._addons = { ...this._addons, ...addon, }; }, dedent` \`setAddon\` is deprecated and will be removed in Storybook 7.0. https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#deprecated-setaddon ` ); addDecorator = (decorator: DecoratorFunction) => { this._storyStore.addGlobalMetadata({ decorators: [decorator] }); }; clearDecorators = deprecate( () => { this._storyStore.clearGlobalDecorators(); }, dedent` \`clearDecorators\` is deprecated and will be removed in Storybook 7.0. https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#deprecated-cleardecorators ` ); addParameters = (parameters: Parameters) => { this._storyStore.addGlobalMetadata({ parameters }); }; addArgTypesEnhancer = (enhancer: ArgTypesEnhancer) => { this._storyStore.addArgTypesEnhancer(enhancer); }; // what are the occasions that "m" is a boolean vs an obj storiesOf = <StoryFnReturnType = unknown>( kind: string, m: NodeModule ): StoryApi<StoryFnReturnType> => { if (!kind && typeof kind !== 'string') { throw new Error('Invalid or missing kind provided for stories, should be a string'); } if (!m) { logger.warn( `Missing 'module' parameter for story with a kind of '${kind}'. It will break your HMR` ); } if (m) { const proto = Object.getPrototypeOf(m); if (proto.exports && proto.exports.default) { // FIXME: throw an error in SB6.0 logger.error( `Illegal mix of CSF default export and storiesOf calls in a single file: ${proto.i}` ); } } if (m && m.hot && m.hot.dispose) { m.hot.dispose(() => { const { _storyStore } = this; // If HMR dispose happens in a story file, we know that HMR will pass up to the configuration file (preview.js) // and be handled by the HMR.allow in config_api, leading to a re-run of configuration. // So configuration is about to happen--we can skip the safety check. _storyStore.removeStoryKind(kind, { allowUnsafe: true }); }); } let hasAdded = false; const api: StoryApi<StoryFnReturnType> = { kind: kind.toString(), add: () => api, addDecorator: () => api, addLoader: () => api, addParameters: () => api, }; // apply addons Object.keys(this._addons).forEach((name) => { const addon = this._addons[name]; api[name] = (...args: any[]) => { addon.apply(api, args); return api; }; }); api.add = ( storyName: string, storyFn: StoryFn<StoryFnReturnType>, parameters: Parameters = {} ) => { hasAdded = true; const id = parameters.__id || toId(kind, storyName); if (typeof storyName !== 'string') { throw new Error(`Invalid or missing storyName provided for a "${kind}" story.`); } if (!this._noStoryModuleAddMethodHotDispose && m && m.hot && m.hot.dispose) { m.hot.dispose(() => { const { _storyStore } = this; // See note about allowUnsafe above _storyStore.remove(id, { allowUnsafe: true }); }); } const fileName = m && m.id ? `${m.id}` : undefined; const { decorators, ...storyParameters } = parameters; this._storyStore.addStory( { id, kind, name: storyName, storyFn, parameters: { fileName, ...storyParameters }, decorators, }, { applyDecorators: applyHooks(this._decorateStory), } ); return api; }; api.addDecorator = (decorator: DecoratorFunction<StoryFnReturnType>) => { if (hasAdded) throw new Error(`You cannot add a decorator after the first story for a kind. Read more here: https://github.com/storybookjs/storybook/blob/master/MIGRATION.md#can-no-longer-add-decorators-parameters-after-stories`); this._storyStore.addKindMetadata(kind, { decorators: [decorator] }); return api; }; api.addLoader = (loader: LoaderFunction) => { if (hasAdded) throw new Error(`You cannot add a loader after the first story for a kind.`); this._storyStore.addKindMetadata(kind, { loaders: [loader] }); return api; }; api.addParameters = (parameters: Parameters) => { if (hasAdded) throw new Error(`You cannot add parameters after the first story for a kind. Read more here: https://github.com/storybookjs/storybook/blob/master/MIGRATION.md#can-no-longer-add-decorators-parameters-after-stories`); this._storyStore.addKindMetadata(kind, { parameters }); return api; }; return api; }; getStorybook = () => this._storyStore.getStorybook(); raw = () => this._storyStore.raw(); // FIXME: temporary expose the store for react-native // Longer term react-native should use the Provider/Consumer api store = () => this._storyStore; }