UNPKG

25.4 kBJavaScriptView Raw
1import "core-js/modules/es.array.reduce.js";
2
3/* eslint no-underscore-dangle: 0 */
4import memoize from 'memoizerific';
5import dedent from 'ts-dedent';
6import stable from 'stable';
7import mapValues from 'lodash/mapValues';
8import pick from 'lodash/pick';
9import deprecate from 'util-deprecate';
10import Events from '@storybook/core-events';
11import { logger } from '@storybook/client-logger';
12import { sanitize, toId } from '@storybook/csf';
13import { combineArgs, mapArgsToTypes, validateOptions } from './args';
14import { HooksContext } from './hooks';
15import { storySort } from './storySort';
16import { combineParameters } from './parameters';
17import { ensureArgTypes } from './ensureArgTypes';
18import { inferArgTypes } from './inferArgTypes';
19import { inferControls } from './inferControls';
20
21function extractSanitizedKindNameFromStorySpecifier(storySpecifier) {
22 if (typeof storySpecifier === 'string') {
23 return storySpecifier.split('--').shift();
24 }
25
26 return sanitize(storySpecifier.kind);
27}
28
29function extractIdFromStorySpecifier(storySpecifier) {
30 if (typeof storySpecifier === 'string') {
31 return storySpecifier;
32 }
33
34 return toId(storySpecifier.kind, storySpecifier.name);
35}
36
37const isStoryDocsOnly = parameters => {
38 return parameters && parameters.docsOnly;
39};
40
41const includeStory = (story, options = {
42 includeDocsOnly: false
43}) => {
44 if (options.includeDocsOnly) {
45 return true;
46 }
47
48 return !isStoryDocsOnly(story.parameters);
49};
50
51const checkGlobals = parameters => {
52 const {
53 globals,
54 globalTypes
55 } = parameters;
56
57 if (globals || globalTypes) {
58 logger.error('Global args/argTypes can only be set globally', JSON.stringify({
59 globals,
60 globalTypes
61 }));
62 }
63};
64
65const checkStorySort = parameters => {
66 const {
67 options
68 } = parameters;
69 if (options !== null && options !== void 0 && options.storySort) logger.error('The storySort option parameter can only be set globally');
70};
71
72const storyFnWarning = deprecate(() => {}, dedent`
73 \`storyFn\` is deprecated and will be removed in Storybook 7.0.
74
75 https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#deprecated-storyfn`);
76const argTypeDefaultValueWarning = deprecate(() => {}, dedent`
77 \`argType.defaultValue\` is deprecated and will be removed in Storybook 7.0.
78
79 https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#deprecated-argtype-defaultValue`);
80
81const toExtracted = obj => Object.entries(obj).reduce((acc, [key, value]) => {
82 if (typeof value === 'function') {
83 return acc;
84 } // NOTE: We're serializing argTypes twice, at the top-level and also in parameters.
85 // We currently rely on useParameters in the manager, so strip out the top-level argTypes
86 // instead for performance.
87
88
89 if (['hooks', 'argTypes'].includes(key)) {
90 return acc;
91 }
92
93 if (Array.isArray(value)) {
94 return Object.assign(acc, {
95 [key]: value.slice().sort()
96 });
97 }
98
99 return Object.assign(acc, {
100 [key]: value
101 });
102}, {});
103
104export default class StoryStore {
105 // Keyed on kind name
106 // Keyed on storyId
107 constructor(params) {
108 this._error = void 0;
109 this._channel = void 0;
110 this._configuring = void 0;
111 this._globals = void 0;
112 this._initialGlobals = void 0;
113 this._defaultGlobals = void 0;
114 this._globalMetadata = void 0;
115 this._kinds = void 0;
116 this._stories = void 0;
117 this._argsEnhancers = void 0;
118 this._argTypesEnhancers = void 0;
119 this._selectionSpecifier = void 0;
120 this._selection = void 0;
121
122 this.remove = (id, {
123 allowUnsafe = false
124 } = {}) => {
125 if (!this._configuring && !allowUnsafe) throw new Error('Cannot remove a story when not configuring, see https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#story-store-immutable-outside-of-configuration');
126 const {
127 _stories
128 } = this;
129 const story = _stories[id];
130 delete _stories[id];
131 if (story) story.hooks.clean();
132 };
133
134 this.fromId = id => {
135 try {
136 const data = this._stories[id];
137
138 if (!data || !data.getDecorated) {
139 return null;
140 }
141
142 return this.mergeAdditionalDataToStory(data);
143 } catch (e) {
144 logger.warn('failed to get story:', this._stories);
145 logger.error(e);
146 return null;
147 }
148 };
149
150 this.setError = err => {
151 this._error = err;
152 };
153
154 this.getError = () => this._error;
155
156 this.getSelection = () => this._selection;
157
158 this.getDataForManager = () => {
159 return {
160 v: 2,
161 globalParameters: this._globalMetadata.parameters,
162 globals: this._globals,
163 error: this.getError(),
164 kindParameters: mapValues(this._kinds, metadata => metadata.parameters),
165 stories: this.extract({
166 includeDocsOnly: true,
167 normalizeParameters: true
168 })
169 };
170 };
171
172 this.getStoriesJsonData = () => {
173 const value = this.getDataForManager();
174 const allowed = ['fileName', 'docsOnly', 'framework', '__id', '__isArgsStory'];
175 return {
176 v: 2,
177 globalParameters: pick(value.globalParameters, allowed),
178 kindParameters: mapValues(value.kindParameters, v => pick(v, allowed)),
179 stories: mapValues(value.stories, v => Object.assign({}, pick(v, ['id', 'name', 'kind', 'story']), {
180 parameters: pick(v.parameters, allowed)
181 }))
182 };
183 };
184
185 this.pushToManager = () => {
186 if (this._channel) {
187 // send to the parent frame.
188 this._channel.emit(Events.SET_STORIES, this.getDataForManager());
189 }
190 };
191
192 this.getStoriesForKind = kind => this.raw().filter(story => story.kind === kind);
193
194 // Assume we are configuring until we hear otherwise
195 this._configuring = true;
196 this._globals = {};
197 this._defaultGlobals = {};
198 this._initialGlobals = {};
199 this._globalMetadata = {
200 parameters: {},
201 decorators: [],
202 loaders: []
203 };
204 this._kinds = {};
205 this._stories = {};
206 this._argsEnhancers = [];
207 this._argTypesEnhancers = [ensureArgTypes];
208 this._error = undefined;
209 this._channel = params.channel;
210 this.setupListeners();
211 }
212
213 setupListeners() {
214 // Channel can be null in StoryShots
215 if (!this._channel) return;
216
217 this._channel.on(Events.SET_CURRENT_STORY, ({
218 storyId,
219 viewMode
220 }) => this.setSelection({
221 storyId,
222 viewMode
223 }));
224
225 this._channel.on(Events.UPDATE_STORY_ARGS, ({
226 storyId,
227 updatedArgs
228 }) => this.updateStoryArgs(storyId, updatedArgs));
229
230 this._channel.on(Events.RESET_STORY_ARGS, ({
231 storyId,
232 argNames
233 }) => this.resetStoryArgs(storyId, argNames));
234
235 this._channel.on(Events.UPDATE_GLOBALS, ({
236 globals
237 }) => this.updateGlobals(globals));
238 }
239
240 startConfiguring() {
241 this._configuring = true;
242
243 const safePush = (enhancer, enhancers) => {
244 if (!enhancers.includes(enhancer)) enhancers.push(enhancer);
245 }; // run these at the end
246
247
248 safePush(inferArgTypes, this._argTypesEnhancers);
249 safePush(inferControls, this._argTypesEnhancers);
250 }
251
252 finishConfiguring() {
253 this._configuring = false;
254 const {
255 globals = {},
256 globalTypes = {}
257 } = this._globalMetadata.parameters;
258 const allowedGlobals = new Set([...Object.keys(globals), ...Object.keys(globalTypes)]);
259 const defaultGlobals = Object.entries(globalTypes).reduce((acc, [arg, {
260 defaultValue
261 }]) => {
262 if (defaultValue) acc[arg] = defaultValue;
263 return acc;
264 }, {});
265 this._initialGlobals = Object.assign({}, defaultGlobals, globals); // To deal with HMR & persistence, we consider the previous value of global args, and:
266 // 1. Remove any keys that are not in the new parameter
267 // 2. Preference any keys that were already set
268 // 3. Use any new keys from the new parameter
269
270 this._globals = Object.entries(this._globals || {}).reduce((acc, [key, previousValue]) => {
271 if (allowedGlobals.has(key)) acc[key] = previousValue;
272 return acc;
273 }, Object.assign({}, this._initialGlobals)); // Set the current selection based on the current selection specifier, if selection is not yet set
274
275 const stories = this.sortedStories();
276 let foundStory;
277
278 if (this._selectionSpecifier && !this._selection) {
279 const {
280 storySpecifier,
281 viewMode,
282 args: urlArgs,
283 globals: urlGlobals
284 } = this._selectionSpecifier;
285
286 if (urlGlobals) {
287 const allowedUrlGlobals = Object.entries(urlGlobals).reduce((acc, [key, value]) => {
288 if (allowedGlobals.has(key)) acc[key] = value;
289 return acc;
290 }, {});
291 this._globals = combineParameters(this._globals, allowedUrlGlobals);
292 }
293
294 if (storySpecifier === '*') {
295 // '*' means select the first story. If there is none, we have no selection.
296 [foundStory] = stories;
297 } else if (typeof storySpecifier === 'string') {
298 // Find the story with the exact id that matches the specifier (see #11571)
299 foundStory = Object.values(stories).find(s => s.id === storySpecifier);
300
301 if (!foundStory) {
302 // Fallback to the first story that starts with the specifier
303 foundStory = Object.values(stories).find(s => s.id.startsWith(storySpecifier));
304 }
305 } else {
306 // Try and find a story matching the name/kind, setting no selection if they don't exist.
307 const {
308 name,
309 kind
310 } = storySpecifier;
311 foundStory = this.getRawStory(kind, name);
312 }
313
314 if (foundStory) {
315 if (urlArgs) {
316 const mappedUrlArgs = mapArgsToTypes(urlArgs, foundStory.argTypes);
317 foundStory.args = combineArgs(foundStory.args, mappedUrlArgs);
318 }
319
320 foundStory.args = validateOptions(foundStory.args, foundStory.argTypes);
321 this.setSelection({
322 storyId: foundStory.id,
323 viewMode
324 });
325
326 this._channel.emit(Events.STORY_SPECIFIED, {
327 storyId: foundStory.id,
328 viewMode
329 });
330 }
331 } // If we didn't find a story matching the specifier, we always want to emit CURRENT_STORY_WAS_SET anyway
332 // in order to tell the StoryRenderer to render something (a "missing story" view)
333
334
335 if (!foundStory && this._channel) {
336 this._channel.emit(Events.CURRENT_STORY_WAS_SET, this._selection);
337 }
338
339 this.pushToManager();
340 }
341
342 addGlobalMetadata({
343 parameters = {},
344 decorators = [],
345 loaders = []
346 }) {
347 if (parameters) {
348 const {
349 args,
350 argTypes
351 } = parameters;
352 if (args || argTypes) logger.warn('Found args/argTypes in global parameters.', JSON.stringify({
353 args,
354 argTypes
355 }));
356 }
357
358 const globalParameters = this._globalMetadata.parameters;
359 this._globalMetadata.parameters = combineParameters(globalParameters, parameters);
360
361 function _safeAdd(items, collection, caption) {
362 items.forEach(item => {
363 if (collection.includes(item)) {
364 logger.warn(`You tried to add a duplicate ${caption}, this is not expected`, item);
365 } else {
366 collection.push(item);
367 }
368 });
369 }
370
371 _safeAdd(decorators, this._globalMetadata.decorators, 'decorator');
372
373 _safeAdd(loaders, this._globalMetadata.loaders, 'loader');
374 }
375
376 clearGlobalDecorators() {
377 this._globalMetadata.decorators = [];
378 }
379
380 ensureKind(kind) {
381 if (!this._kinds[kind]) {
382 this._kinds[kind] = {
383 order: Object.keys(this._kinds).length,
384 parameters: {},
385 decorators: [],
386 loaders: []
387 };
388 }
389 }
390
391 addKindMetadata(kind, {
392 parameters = {},
393 decorators = [],
394 loaders = []
395 }) {
396 if (this.shouldBlockAddingKindMetadata(kind)) {
397 return;
398 }
399
400 this.ensureKind(kind);
401
402 if (parameters) {
403 checkGlobals(parameters);
404 checkStorySort(parameters);
405 }
406
407 this._kinds[kind].parameters = combineParameters(this._kinds[kind].parameters, parameters);
408
409 this._kinds[kind].decorators.push(...decorators);
410
411 this._kinds[kind].loaders.push(...loaders);
412 }
413
414 addArgsEnhancer(argsEnhancer) {
415 if (Object.keys(this._stories).length > 0) throw new Error('Cannot add an args enhancer to the store after a story has been added.');
416
417 this._argsEnhancers.push(argsEnhancer);
418 }
419
420 addArgTypesEnhancer(argTypesEnhancer) {
421 if (Object.keys(this._stories).length > 0) throw new Error('Cannot add an argTypes enhancer to the store after a story has been added.');
422
423 this._argTypesEnhancers.push(argTypesEnhancer);
424 } // Combine the global, kind & story parameters of a story
425
426
427 combineStoryParameters(parameters, kind) {
428 return combineParameters(this._globalMetadata.parameters, this._kinds[kind].parameters, parameters);
429 }
430
431 shouldBlockAddingStory(id) {
432 return this.isSingleStoryMode() && id !== extractIdFromStorySpecifier(this._selectionSpecifier.storySpecifier);
433 }
434
435 shouldBlockAddingKindMetadata(kind) {
436 return this.isSingleStoryMode() && sanitize(kind) !== extractSanitizedKindNameFromStorySpecifier(this._selectionSpecifier.storySpecifier);
437 }
438
439 addStory({
440 id,
441 kind,
442 name,
443 storyFn: original,
444 parameters: storyParameters = {},
445 decorators: storyDecorators = [],
446 loaders: storyLoaders = []
447 }, {
448 applyDecorators,
449 allowUnsafe = false
450 }) {
451 if (!this._configuring && !allowUnsafe) throw new Error('Cannot add a story when not configuring, see https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#story-store-immutable-outside-of-configuration');
452
453 if (this.shouldBlockAddingStory(id)) {
454 return;
455 }
456
457 checkGlobals(storyParameters);
458 checkStorySort(storyParameters);
459 const {
460 _stories
461 } = this;
462
463 if (_stories[id]) {
464 logger.warn(dedent`
465 Story with id ${id} already exists in the store!
466
467 Perhaps you added the same story twice, or you have a name collision?
468 Story ids need to be unique -- ensure you aren't using the same names modulo url-sanitization.
469 `);
470 }
471
472 const identification = {
473 id,
474 kind,
475 name,
476 story: name // legacy
477
478 }; // immutable original storyFn
479
480 const getOriginal = () => original;
481
482 this.ensureKind(kind);
483 const kindMetadata = this._kinds[kind];
484 const decorators = [...storyDecorators, ...kindMetadata.decorators, ...this._globalMetadata.decorators];
485 const loaders = [...this._globalMetadata.loaders, ...kindMetadata.loaders, ...storyLoaders];
486
487 const finalStoryFn = context => {
488 const {
489 args = {},
490 argTypes = {},
491 parameters
492 } = context;
493 const {
494 passArgsFirst = true
495 } = parameters;
496 const mapped = Object.assign({}, context, {
497 args: Object.entries(args).reduce((acc, [key, val]) => {
498 const {
499 mapping
500 } = argTypes[key] || {};
501 acc[key] = mapping && val in mapping ? mapping[val] : val;
502 return acc;
503 }, {})
504 });
505 return passArgsFirst ? original(mapped.args, mapped) : original(mapped);
506 }; // lazily decorate the story when it's loaded
507
508
509 const getDecorated = memoize(1)(() => applyDecorators(finalStoryFn, decorators));
510 const hooks = new HooksContext(); // We need the combined parameters now in order to calculate argTypes, but we won't keep them
511
512 const combinedParameters = this.combineStoryParameters(storyParameters, kind); // We are going to make various UI changes in both the manager and the preview
513 // based on whether it's an "args story", i.e. whether the story accepts a first
514 // argument which is an `Args` object. Here we store it as a parameter on every story
515 // for convenience, but we preface it with `__` to denote that it's an internal API
516 // and that users probably shouldn't look at it.
517
518 const {
519 passArgsFirst = true
520 } = combinedParameters;
521
522 const __isArgsStory = passArgsFirst && original.length > 0;
523
524 const {
525 argTypes = {}
526 } = this._argTypesEnhancers.reduce((accumulatedParameters, enhancer) => Object.assign({}, accumulatedParameters, {
527 argTypes: enhancer(Object.assign({}, identification, {
528 storyFn: original,
529 parameters: accumulatedParameters,
530 args: {},
531 argTypes: {},
532 globals: {},
533 originalStoryFn: getOriginal()
534 }))
535 }), Object.assign({
536 __isArgsStory
537 }, combinedParameters));
538
539 const storyParametersWithArgTypes = Object.assign({}, storyParameters, {
540 argTypes,
541 __isArgsStory
542 });
543
544 const storyFn = runtimeContext => {
545 var _this$_selection;
546
547 storyFnWarning();
548 return getDecorated()(Object.assign({}, identification, runtimeContext, {
549 // Calculate "combined" parameters at render time (NOTE: for perf we could just use combinedParameters from above?)
550 parameters: this.combineStoryParameters(storyParametersWithArgTypes, kind),
551 hooks,
552 args: _stories[id].args,
553 argTypes,
554 globals: this._globals,
555 viewMode: (_this$_selection = this._selection) === null || _this$_selection === void 0 ? void 0 : _this$_selection.viewMode,
556 originalStoryFn: getOriginal()
557 }));
558 };
559
560 const unboundStoryFn = context => getDecorated()(context);
561
562 const applyLoaders = async () => {
563 var _this$_selection2;
564
565 const context = Object.assign({}, identification, {
566 // Calculate "combined" parameters at render time (NOTE: for perf we could just use combinedParameters from above?)
567 parameters: this.combineStoryParameters(storyParametersWithArgTypes, kind),
568 hooks,
569 args: _stories[id].args,
570 argTypes,
571 globals: this._globals,
572 viewMode: (_this$_selection2 = this._selection) === null || _this$_selection2 === void 0 ? void 0 : _this$_selection2.viewMode,
573 originalStoryFn: getOriginal()
574 });
575 const loadResults = await Promise.all(loaders.map(loader => loader(context)));
576 const loaded = Object.assign({}, ...loadResults);
577 return Object.assign({}, context, {
578 loaded
579 });
580 }; // Pull out parameters.args.$ || .argTypes.$.defaultValue into initialArgs
581
582
583 const passedArgs = Object.assign({}, this._kinds[kind].parameters.args, storyParameters.args);
584 const defaultArgs = Object.entries(argTypes).reduce((acc, [arg, {
585 defaultValue
586 }]) => {
587 if (typeof defaultValue !== 'undefined') {
588 acc[arg] = defaultValue;
589 }
590
591 return acc;
592 }, {});
593
594 if (Object.keys(defaultArgs).length > 0) {
595 argTypeDefaultValueWarning();
596 }
597
598 const initialArgsBeforeEnhancers = Object.assign({}, defaultArgs, passedArgs);
599
600 const initialArgs = this._argsEnhancers.reduce((accumulatedArgs, enhancer) => Object.assign({}, accumulatedArgs, enhancer(Object.assign({}, identification, {
601 parameters: combinedParameters,
602 args: initialArgsBeforeEnhancers,
603 argTypes,
604 globals: {},
605 originalStoryFn: getOriginal()
606 }))), initialArgsBeforeEnhancers);
607
608 const runPlayFunction = async () => {
609 const {
610 play
611 } = combinedParameters;
612 return play ? play() : undefined;
613 };
614
615 _stories[id] = Object.assign({}, identification, {
616 hooks,
617 getDecorated,
618 getOriginal,
619 applyLoaders,
620 runPlayFunction,
621 storyFn,
622 unboundStoryFn,
623 parameters: storyParametersWithArgTypes,
624 args: initialArgs,
625 argTypes,
626 initialArgs
627 });
628 }
629
630 removeStoryKind(kind, {
631 allowUnsafe = false
632 } = {}) {
633 if (!this._configuring && !allowUnsafe) throw new Error('Cannot remove a kind when not configuring, see https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#story-store-immutable-outside-of-configuration');
634 if (!this._kinds[kind]) return;
635 this._kinds[kind].parameters = {};
636 this._kinds[kind].decorators = [];
637 this.cleanHooksForKind(kind);
638 this._stories = Object.entries(this._stories).reduce((acc, [id, story]) => {
639 if (story.kind !== kind) acc[id] = story;
640 return acc;
641 }, {});
642 }
643
644 updateGlobals(newGlobals) {
645 this._globals = Object.assign({}, this._globals, newGlobals);
646
647 this._channel.emit(Events.GLOBALS_UPDATED, {
648 globals: this._globals,
649 initialGlobals: this._initialGlobals
650 });
651 }
652
653 updateStoryArgs(id, newArgs) {
654 if (!this._stories[id]) throw new Error(`No story for id ${id}`);
655 const {
656 args
657 } = this._stories[id];
658 this._stories[id].args = Object.assign({}, args, newArgs);
659
660 this._channel.emit(Events.STORY_ARGS_UPDATED, {
661 storyId: id,
662 args: this._stories[id].args
663 });
664 }
665
666 resetStoryArgs(id, argNames) {
667 if (!this._stories[id]) throw new Error(`No story for id ${id}`);
668 const {
669 args,
670 initialArgs
671 } = this._stories[id];
672 this._stories[id].args = Object.assign({}, args); // Make a copy to avoid problems
673
674 (argNames || Object.keys(args)).forEach(name => {
675 // We overwrite like this to ensure we can reset to falsey values
676 this._stories[id].args[name] = initialArgs[name];
677 });
678
679 this._channel.emit(Events.STORY_ARGS_UPDATED, {
680 storyId: id,
681 args: this._stories[id].args
682 });
683 }
684
685 raw(options) {
686 return Object.values(this._stories).filter(i => !!i.getDecorated).filter(i => includeStory(i, options)).map(i => this.mergeAdditionalDataToStory(i));
687 }
688
689 sortedStories() {
690 var _this$_globalMetadata, _this$_globalMetadata2;
691
692 // NOTE: when kinds are HMR'ed they get temporarily removed from the `_stories` array
693 // and thus lose order. However `_kinds[x].order` preservers the original load order
694 const kindOrder = mapValues(this._kinds, ({
695 order
696 }) => order);
697 const storySortParameter = (_this$_globalMetadata = this._globalMetadata.parameters) === null || _this$_globalMetadata === void 0 ? void 0 : (_this$_globalMetadata2 = _this$_globalMetadata.options) === null || _this$_globalMetadata2 === void 0 ? void 0 : _this$_globalMetadata2.storySort;
698 const storyEntries = Object.entries(this._stories); // Add the kind parameters and global parameters to each entry
699
700 const stories = storyEntries.map(([id, story]) => [id, story, this._kinds[story.kind].parameters, this._globalMetadata.parameters]);
701
702 if (storySortParameter) {
703 let sortFn;
704
705 if (typeof storySortParameter === 'function') {
706 sortFn = storySortParameter;
707 } else {
708 sortFn = storySort(storySortParameter);
709 }
710
711 stable.inplace(stories, sortFn);
712 } else {
713 stable.inplace(stories, (s1, s2) => kindOrder[s1[1].kind] - kindOrder[s2[1].kind]);
714 }
715
716 return stories.map(([id, s]) => s);
717 }
718
719 extract(options = {}) {
720 const stories = this.sortedStories(); // removes function values from all stories so they are safe to transport over the channel
721
722 return stories.reduce((acc, story) => {
723 if (!includeStory(story, options)) return acc;
724 const extracted = toExtracted(story);
725 if (options.normalizeParameters) return Object.assign(acc, {
726 [story.id]: extracted
727 });
728 const {
729 parameters,
730 kind
731 } = extracted;
732 return Object.assign(acc, {
733 [story.id]: Object.assign(extracted, {
734 parameters: this.combineStoryParameters(parameters, kind)
735 })
736 });
737 }, {});
738 }
739
740 clearError() {
741 this._error = null;
742 }
743
744 setSelectionSpecifier(selectionSpecifier) {
745 this._selectionSpecifier = selectionSpecifier;
746 }
747
748 setSelection(selection) {
749 this._selection = selection;
750
751 if (this._channel) {
752 this._channel.emit(Events.CURRENT_STORY_WAS_SET, this._selection);
753 }
754 }
755
756 isSingleStoryMode() {
757 if (!this._selectionSpecifier) {
758 return false;
759 }
760
761 const {
762 singleStory,
763 storySpecifier
764 } = this._selectionSpecifier;
765 return storySpecifier && storySpecifier !== '*' && singleStory;
766 }
767
768 getStoryKinds() {
769 return Array.from(new Set(this.raw().map(s => s.kind)));
770 }
771
772 getRawStory(kind, name) {
773 return this.getStoriesForKind(kind).find(s => s.name === name);
774 }
775
776 cleanHooks(id) {
777 if (this._stories[id]) {
778 this._stories[id].hooks.clean();
779 }
780 }
781
782 cleanHooksForKind(kind) {
783 this.getStoriesForKind(kind).map(story => this.cleanHooks(story.id));
784 } // This API is a re-implementation of Storybook's original getStorybook() API.
785 // As such it may not behave *exactly* the same, but aims to. Some notes:
786 // - It is *NOT* sorted by the user's sort function, but remains sorted in "insertion order"
787 // - It does not include docs-only stories
788
789
790 getStorybook() {
791 return Object.values(this.raw().reduce((kinds, story) => {
792 if (!includeStory(story)) return kinds;
793 const {
794 kind,
795 name,
796 storyFn,
797 parameters: {
798 fileName
799 }
800 } = story; // eslint-disable-next-line no-param-reassign
801
802 if (!kinds[kind]) kinds[kind] = {
803 kind,
804 fileName,
805 stories: []
806 };
807 kinds[kind].stories.push({
808 name,
809 render: storyFn
810 });
811 return kinds;
812 }, {})).sort((s1, s2) => this._kinds[s1.kind].order - this._kinds[s2.kind].order);
813 }
814
815 mergeAdditionalDataToStory(story) {
816 return Object.assign({}, story, {
817 parameters: this.combineStoryParameters(story.parameters, story.kind),
818 globals: this._globals
819 });
820 }
821
822}
\No newline at end of file