UNPKG

12.3 kBJavaScriptView Raw
1function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }
2
3import deprecate from 'util-deprecate';
4import dedent from 'ts-dedent';
5import global from 'global';
6import { logger } from '@storybook/client-logger';
7import { toId, sanitize } from '@storybook/csf';
8import { combineParameters, normalizeInputTypes } from '@storybook/store';
9import { StoryStoreFacade } from './StoryStoreFacade';
10// ClientApi (and StoreStore) are really singletons. However they are not created until the
11// relevant framework instanciates them via `start.js`. The good news is this happens right away.
12let singleton;
13const warningAlternatives = {
14 addDecorator: `Instead, use \`export const decorators = [];\` in your \`preview.js\`.`,
15 addParameters: `Instead, use \`export const parameters = {};\` in your \`preview.js\`.`,
16 addLoaders: `Instead, use \`export const loaders = [];\` in your \`preview.js\`.`
17};
18
19const warningMessage = method => deprecate(() => {}, dedent`
20 \`${method}\` is deprecated, and will be removed in Storybook 7.0.
21
22 ${warningAlternatives[method]}
23
24 Read more at https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#deprecated-addparameters-and-adddecorator).`);
25
26const warnings = {
27 addDecorator: warningMessage('addDecorator'),
28 addParameters: warningMessage('addParameters'),
29 addLoaders: warningMessage('addLoaders')
30};
31
32const checkMethod = (method, deprecationWarning) => {
33 var _global$FEATURES;
34
35 if ((_global$FEATURES = global.FEATURES) !== null && _global$FEATURES !== void 0 && _global$FEATURES.storyStoreV7) {
36 throw new Error(dedent`You cannot use \`${method}\` with the new Story Store.
37
38 ${warningAlternatives[method]}`);
39 }
40
41 if (!singleton) {
42 throw new Error(`Singleton client API not yet initialized, cannot call \`${method}\`.`);
43 }
44
45 if (deprecationWarning) {
46 warnings[method]();
47 }
48};
49
50export const addDecorator = (decorator, deprecationWarning = true) => {
51 checkMethod('addDecorator', deprecationWarning);
52 singleton.addDecorator(decorator);
53};
54export const addParameters = (parameters, deprecationWarning = true) => {
55 checkMethod('addParameters', deprecationWarning);
56 singleton.addParameters(parameters);
57};
58export const addLoader = (loader, deprecationWarning = true) => {
59 checkMethod('addLoader', deprecationWarning);
60 singleton.addLoader(loader);
61};
62export const addArgsEnhancer = enhancer => {
63 checkMethod('addArgsEnhancer', false);
64 singleton.addArgsEnhancer(enhancer);
65};
66export const addArgTypesEnhancer = enhancer => {
67 checkMethod('addArgTypesEnhancer', false);
68 singleton.addArgTypesEnhancer(enhancer);
69};
70export const getGlobalRender = () => {
71 checkMethod('getGlobalRender', false);
72 return singleton.facade.projectAnnotations.render;
73};
74export const setGlobalRender = render => {
75 checkMethod('setGlobalRender', false);
76 singleton.facade.projectAnnotations.render = render;
77};
78const invalidStoryTypes = new Set(['string', 'number', 'boolean', 'symbol']);
79export class ClientApi {
80 // If we don't get passed modules so don't know filenames, we can
81 // just use numeric indexes
82 constructor({
83 storyStore
84 } = {}) {
85 this.facade = void 0;
86 this.storyStore = void 0;
87 this.addons = void 0;
88 this.onImportFnChanged = void 0;
89 this.lastFileName = 0;
90 this.setAddon = deprecate(addon => {
91 this.addons = Object.assign({}, this.addons, addon);
92 }, dedent`
93 \`setAddon\` is deprecated and will be removed in Storybook 7.0.
94
95 https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#deprecated-setaddon
96 `);
97
98 this.addDecorator = decorator => {
99 this.facade.projectAnnotations.decorators.push(decorator);
100 };
101
102 this.clearDecorators = deprecate(() => {
103 this.facade.projectAnnotations.decorators = [];
104 }, dedent`
105 \`clearDecorators\` is deprecated and will be removed in Storybook 7.0.
106
107 https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#deprecated-cleardecorators
108 `);
109
110 this.addParameters = (_ref) => {
111 let {
112 globals,
113 globalTypes
114 } = _ref,
115 parameters = _objectWithoutPropertiesLoose(_ref, ["globals", "globalTypes"]);
116
117 this.facade.projectAnnotations.parameters = combineParameters(this.facade.projectAnnotations.parameters, parameters);
118
119 if (globals) {
120 this.facade.projectAnnotations.globals = Object.assign({}, this.facade.projectAnnotations.globals, globals);
121 }
122
123 if (globalTypes) {
124 this.facade.projectAnnotations.globalTypes = Object.assign({}, this.facade.projectAnnotations.globalTypes, normalizeInputTypes(globalTypes));
125 }
126 };
127
128 this.addLoader = loader => {
129 this.facade.projectAnnotations.loaders.push(loader);
130 };
131
132 this.addArgsEnhancer = enhancer => {
133 this.facade.projectAnnotations.argsEnhancers.push(enhancer);
134 };
135
136 this.addArgTypesEnhancer = enhancer => {
137 this.facade.projectAnnotations.argTypesEnhancers.push(enhancer);
138 };
139
140 this.storiesOf = (kind, m) => {
141 if (!kind && typeof kind !== 'string') {
142 throw new Error('Invalid or missing kind provided for stories, should be a string');
143 }
144
145 if (!m) {
146 logger.warn(`Missing 'module' parameter for story with a kind of '${kind}'. It will break your HMR`);
147 }
148
149 if (m) {
150 const proto = Object.getPrototypeOf(m);
151
152 if (proto.exports && proto.exports.default) {
153 // FIXME: throw an error in SB6.0
154 logger.error(`Illegal mix of CSF default export and storiesOf calls in a single file: ${proto.i}`);
155 }
156 } // eslint-disable-next-line no-plusplus
157
158
159 const baseFilename = m && m.id ? `${m.id}` : (this.lastFileName++).toString();
160 let fileName = baseFilename;
161 let i = 1; // Deal with `storiesOf()` being called twice in the same file.
162 // On HMR, `this.csfExports[fileName]` will be reset to `{}`, so an empty object is due
163 // to this export, not a second call of `storiesOf()`.
164
165 while (this.facade.csfExports[fileName] && Object.keys(this.facade.csfExports[fileName]).length > 0) {
166 i += 1;
167 fileName = `${baseFilename}-${i}`;
168 }
169
170 if (m && m.hot && m.hot.accept) {
171 // This module used storiesOf(), so when it re-runs on HMR, it will reload
172 // itself automatically without us needing to look at our imports
173 m.hot.accept();
174 m.hot.dispose(() => {
175 this.facade.clearFilenameExports(fileName); // We need to update the importFn as soon as the module re-evaluates
176 // (and calls storiesOf() again, etc). We could call `onImportFnChanged()`
177 // at the end of every setStories call (somehow), but then we'd need to
178 // debounce it somehow for initial startup. Instead, we'll take advantage of
179 // the fact that the evaluation of the module happens immediately in the same tick
180
181 setTimeout(() => {
182 var _this$onImportFnChang;
183
184 (_this$onImportFnChang = this.onImportFnChanged) === null || _this$onImportFnChang === void 0 ? void 0 : _this$onImportFnChang.call(this, {
185 importFn: this.importFn.bind(this)
186 });
187 }, 0);
188 });
189 }
190
191 let hasAdded = false;
192 const api = {
193 kind: kind.toString(),
194 add: () => api,
195 addDecorator: () => api,
196 addLoader: () => api,
197 addParameters: () => api
198 }; // apply addons
199
200 Object.keys(this.addons).forEach(name => {
201 const addon = this.addons[name];
202
203 api[name] = (...args) => {
204 addon.apply(api, args);
205 return api;
206 };
207 });
208 const meta = {
209 id: sanitize(kind),
210 title: kind,
211 decorators: [],
212 loaders: [],
213 parameters: {}
214 }; // We map these back to a simple default export, even though we have type guarantees at this point
215
216 this.facade.csfExports[fileName] = {
217 default: meta
218 };
219 let counter = 0;
220
221 api.add = (storyName, storyFn, parameters = {}) => {
222 hasAdded = true;
223
224 if (typeof storyName !== 'string') {
225 throw new Error(`Invalid or missing storyName provided for a "${kind}" story.`);
226 }
227
228 if (!storyFn || Array.isArray(storyFn) || invalidStoryTypes.has(typeof storyFn)) {
229 throw new Error(`Cannot load story "${storyName}" in "${kind}" due to invalid format. Storybook expected a function/object but received ${typeof storyFn} instead.`);
230 }
231
232 const {
233 decorators,
234 loaders,
235 component,
236 args,
237 argTypes
238 } = parameters,
239 storyParameters = _objectWithoutPropertiesLoose(parameters, ["decorators", "loaders", "component", "args", "argTypes"]); // eslint-disable-next-line no-underscore-dangle
240
241
242 const storyId = parameters.__id || toId(kind, storyName);
243 const csfExports = this.facade.csfExports[fileName]; // Whack a _ on the front incase it is "default"
244
245 csfExports[`story${counter}`] = {
246 name: storyName,
247 parameters: Object.assign({
248 fileName,
249 __id: storyId
250 }, storyParameters),
251 decorators,
252 loaders,
253 args,
254 argTypes,
255 component,
256 render: storyFn
257 };
258 counter += 1;
259 this.facade.stories[storyId] = {
260 id: storyId,
261 title: csfExports.default.title,
262 name: storyName,
263 importPath: fileName
264 };
265 return api;
266 };
267
268 api.addDecorator = decorator => {
269 if (hasAdded) throw new Error(`You cannot add a decorator after the first story for a kind.
270Read more here: https://github.com/storybookjs/storybook/blob/master/MIGRATION.md#can-no-longer-add-decoratorsparameters-after-stories`);
271 meta.decorators.push(decorator);
272 return api;
273 };
274
275 api.addLoader = loader => {
276 if (hasAdded) throw new Error(`You cannot add a loader after the first story for a kind.`);
277 meta.loaders.push(loader);
278 return api;
279 };
280
281 api.addParameters = (_ref2) => {
282 let {
283 component,
284 args,
285 argTypes
286 } = _ref2,
287 parameters = _objectWithoutPropertiesLoose(_ref2, ["component", "args", "argTypes"]);
288
289 if (hasAdded) throw new Error(`You cannot add parameters after the first story for a kind.
290Read more here: https://github.com/storybookjs/storybook/blob/master/MIGRATION.md#can-no-longer-add-decoratorsparameters-after-stories`);
291 meta.parameters = combineParameters(meta.parameters, parameters);
292 if (component) meta.component = component;
293 if (args) meta.args = Object.assign({}, meta.args, args);
294 if (argTypes) meta.argTypes = Object.assign({}, meta.argTypes, argTypes);
295 return api;
296 };
297
298 return api;
299 };
300
301 this.getStorybook = () => {
302 const {
303 stories
304 } = this.storyStore.storyIndex;
305 const kinds = {};
306 Object.entries(stories).forEach(([storyId, {
307 title,
308 name,
309 importPath
310 }]) => {
311 if (!kinds[title]) {
312 kinds[title] = {
313 kind: title,
314 fileName: importPath,
315 stories: []
316 };
317 }
318
319 const {
320 storyFn
321 } = this.storyStore.fromId(storyId);
322 kinds[title].stories.push({
323 name,
324 render: storyFn
325 });
326 });
327 return Object.values(kinds);
328 };
329
330 this.raw = () => {
331 return this.storyStore.raw();
332 };
333
334 this.facade = new StoryStoreFacade();
335 this.addons = {};
336 this.storyStore = storyStore;
337 singleton = this;
338 }
339
340 importFn(path) {
341 return this.facade.importFn(path);
342 }
343
344 getStoryIndex() {
345 if (!this.storyStore) {
346 throw new Error('Cannot get story index before setting storyStore');
347 }
348
349 return this.facade.getStoryIndex(this.storyStore);
350 }
351
352 // @deprecated
353 get _storyStore() {
354 return this.storyStore;
355 }
356
357}
\No newline at end of file