storybook/lib/client-api/src/client_api.ts
Michael Shilman 6a0002b29e Small fixes
2020-10-07 13:35:53 +08:00

253 lines
7.8 KiB
TypeScript

/* 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;
}