diff --git a/code/lib/manager-api/src/index.tsx b/code/lib/manager-api/src/index.tsx index 3b0a21bdcb94..aa805b83d790 100644 --- a/code/lib/manager-api/src/index.tsx +++ b/code/lib/manager-api/src/index.tsx @@ -75,7 +75,7 @@ export { default as merge } from './lib/merge'; export type { Options as StoreOptions, Listener as ChannelListener }; export { ActiveTabs }; -export const ManagerContext = createContext({ api: undefined, state: getInitialState({}) }); +export const ManagerContext = createContext({ api: {} as API, state: getInitialState({}) }); export type State = layout.SubState & stories.SubState & @@ -200,7 +200,10 @@ class ManagerProvider extends Component { ); // Create our initial state by combining the initial state of all modules, then overlaying any saved state - const state = getInitialState(this.state, ...this.modules.map((m) => m.state)); + const state = getInitialState( + this.state, + this.modules.map((m) => m.state as State) + ); // Get our API by combining the APIs exported by each module const api: API = Object.assign(this.api, { navigate }, ...this.modules.map((m) => m.api)); @@ -209,7 +212,9 @@ class ManagerProvider extends Component { this.api = api; } - static getDerivedStateFromProps(props: ManagerProviderProps, state: State): State { + static getDerivedStateFromProps(props: ManagerProviderProps, state: State): State | null { + if (!props.storyId) throw new Error('StoryId cannot be undefined'); + if (state.path !== props.path) { return { ...state, @@ -241,7 +246,7 @@ class ManagerProvider extends Component { // a chance to do things that call other modules' APIs. this.modules.forEach((module) => { if ('init' in module) { - module.init(); + module.init?.(); } }); }; @@ -283,7 +288,7 @@ interface ManagerConsumerProps

{ const defaultFilter = (c: Combo) => c; -function ManagerConsumer

({ +function ManagerConsumer

({ // @ts-expect-error (Converted from ts-ignore) filter = defaultFilter, children, @@ -299,7 +304,9 @@ function ManagerConsumer

({ const data = filterer.current(c); const l = useMemo(() => { - return [...Object.entries(data).reduce((acc, keyval) => acc.concat(keyval), [])]; + return [ + ...Object.entries(data as object).reduce((acc, keyval) => acc.concat(keyval), []), + ]; }, [c.state]); return useMemo(() => { @@ -375,14 +382,14 @@ export const useChannel = (eventMap: API_EventMap, deps: any[] = []) => { export function useStoryPrepared(storyId?: StoryId) { const api = useStorybookApi(); - return api.isPrepared(storyId); + return storyId ? api.isPrepared(storyId) : undefined; } -export function useParameter(parameterKey: string, defaultValue?: S) { +export function useParameter(parameterKey: string, defaultValue: S) { const api = useStorybookApi(); const result = api.getCurrentParameter(parameterKey); - return orDefault(result, defaultValue); + return orDefault(result, defaultValue as S); } // cache for taking care of HMR @@ -390,7 +397,7 @@ globalThis.STORYBOOK_ADDON_STATE = {}; const { STORYBOOK_ADDON_STATE } = globalThis; // shared state -export function useSharedState(stateId: string, defaultState?: S) { +export function useSharedState(stateId: string, defaultState?: S | API_StateMerger) { const api = useStorybookApi(); const existingState = api.getAddonState(stateId) || STORYBOOK_ADDON_STATE[stateId]; const state = orDefault( @@ -472,7 +479,7 @@ export function useArgs(): [Args, (newArgs: Args) => void, (argNames?: string[]) const { getCurrentStoryData, updateStoryArgs, resetStoryArgs } = useStorybookApi(); const data = getCurrentStoryData(); - const args = data?.type === 'story' ? data.args : {}; + const args = data?.type === 'story' ? data.args ?? {} : {}; const updateArgs = useCallback( (newArgs: Args) => updateStoryArgs(data as API_StoryEntry, newArgs), [data, updateStoryArgs] @@ -494,7 +501,7 @@ export function useGlobalTypes(): ArgTypes { return useStorybookApi().getGlobalTypes(); } -function useCurrentStory(): API_StoryEntry | API_DocsEntry { +function useCurrentStory(): API_StoryEntry | API_DocsEntry | undefined { const { getCurrentStoryData } = useStorybookApi(); return getCurrentStoryData(); diff --git a/code/lib/manager-api/src/lib/addons.ts b/code/lib/manager-api/src/lib/addons.ts index 1145ab1826f5..91b065e5ce57 100644 --- a/code/lib/manager-api/src/lib/addons.ts +++ b/code/lib/manager-api/src/lib/addons.ts @@ -61,7 +61,11 @@ export class AddonStore { getChannel = (): Channel => { // this.channel should get overwritten by setChannel. If it wasn't called (e.g. in non-browser environment), set a mock instead. if (!this.channel) { - this.setChannel(mockChannel()); + const nextChannel = mockChannel(); + + this.setChannel(nextChannel); + + return nextChannel; } return this.channel; diff --git a/code/lib/manager-api/src/lib/events.ts b/code/lib/manager-api/src/lib/events.ts index 34038585f3ba..f41740ad4751 100644 --- a/code/lib/manager-api/src/lib/events.ts +++ b/code/lib/manager-api/src/lib/events.ts @@ -5,7 +5,7 @@ import type { API_ComposedRef } from '@storybook/types'; import { getSourceType } from '../modules/refs'; import type { API } from '../index'; -interface Meta { +export interface Meta { ref?: API_ComposedRef; source?: string; sourceType?: 'local' | 'external'; diff --git a/code/lib/manager-api/src/lib/shortcut.ts b/code/lib/manager-api/src/lib/shortcut.ts index dab77ec28543..47919b9a6914 100644 --- a/code/lib/manager-api/src/lib/shortcut.ts +++ b/code/lib/manager-api/src/lib/shortcut.ts @@ -92,7 +92,11 @@ export const eventMatchesShortcut = ( e: KeyboardEventLike, shortcut: API_KeyCollection ): boolean => { - return shortcutMatchesShortcut(eventToShortcut(e), shortcut); + const inputShortcut = eventToShortcut(e); + + if (inputShortcut === null) return false; + + return shortcutMatchesShortcut(inputShortcut, shortcut); }; export const keyToSymbol = (key: string): string => { diff --git a/code/lib/manager-api/src/lib/store-setup.ts b/code/lib/manager-api/src/lib/store-setup.ts index 3ee481a35013..bc96fca36f67 100644 --- a/code/lib/manager-api/src/lib/store-setup.ts +++ b/code/lib/manager-api/src/lib/store-setup.ts @@ -1,14 +1,21 @@ /* eslint-disable no-underscore-dangle */ /* eslint-disable func-names */ +import type { DeveloperTools } from 'store2'; import { parse, stringify } from 'telejson'; // setting up the store, overriding set and get to use telejson -export default (_: any) => { - _.fn('set', function (key: string, data: object) { - return _.set(this._area, this._in(key), stringify(data, { maxDepth: 50 })); - }); - _.fn('get', function (key: string, alt: string) { - const value = _.get(this._area, this._in(key)); - return value !== null ? parse(value) : alt || value; - }); +export default (_: DeveloperTools) => { + _.fn( + 'set', + function (this: { _area: Storage; _in: (key: string) => string }, key: string, data: object) { + return _.set(this._area, this._in(key), stringify(data, { maxDepth: 50 })); + } + ); + _.fn( + 'get', + function (this: { _area: Storage; _in: (key: string) => string }, key: string, alt: string) { + const value = _.get(this._area, this._in(key)); + return value !== null ? parse(value) : alt || value; + } + ); }; diff --git a/code/lib/manager-api/src/lib/stories.ts b/code/lib/manager-api/src/lib/stories.ts index b48cbd51b877..977c83faf9bb 100644 --- a/code/lib/manager-api/src/lib/stories.ts +++ b/code/lib/manager-api/src/lib/stories.ts @@ -21,6 +21,7 @@ import type { API_HashEntry, SetStoriesPayload, StoryIndexV2, + API_PreparedIndexEntry, } from '@storybook/types'; // eslint-disable-next-line import/no-cycle import { type API, combineParameters, type State } from '../index'; @@ -121,7 +122,7 @@ export const transformStoryIndexV3toV4 = (index: StoryIndexV3): API_PreparedStor type, ...(type === 'docs' && { tags: ['stories-mdx'], storiesImports: [] }), ...entry, - }; + } as API_PreparedIndexEntry; // @ts-expect-error (we're removing something that should not be there) delete acc[entry.id].story; @@ -185,7 +186,7 @@ export const transformStoryIndexToStoriesHash = ( // Now create a "path" or sub id for each name const paths = names.reduce((list, name, idx) => { const parent = idx > 0 && list[idx - 1]; - const id = sanitize(parent ? `${parent}-${name}` : name); + const id = sanitize(parent ? `${parent}-${name}` : `${name}`); if (parent === id) { throw new Error( @@ -206,21 +207,24 @@ export const transformStoryIndexToStoriesHash = ( const childId = paths[idx + 1] || item.id; if (root.length && idx === 0) { - acc[id] = merge((acc[id] || {}) as API_RootEntry, { - type: 'root', - id, - name: names[idx], - depth: idx, - renderLabel, - startCollapsed: collapsedRoots.includes(id), - // Note that this will later get appended to the previous list of children (see below) - children: [childId], - - // deprecated fields - isRoot: true, - isComponent: false, - isLeaf: false, - }); + acc[id] = merge( + (acc[id] || {}) as API_RootEntry, + { + type: 'root', + id, + name: names[idx], + depth: idx, + renderLabel, + startCollapsed: collapsedRoots.includes(id), + // Note that this will later get appended to the previous list of children (see below) + children: [childId], + + // deprecated fields + isRoot: true, + isComponent: false, + isLeaf: false, + } as API_RootEntry + ); // Usually the last path/name pair will be displayed as a component, // *unless* there are other stories that are more deeply nested under it // @@ -230,43 +234,48 @@ export const transformStoryIndexToStoriesHash = ( // // In this example the entry for 'atoms-button' would *not* be a component. } else if ((!acc[id] || acc[id].type === 'component') && idx === paths.length - 1) { - acc[id] = merge((acc[id] || {}) as API_ComponentEntry, { - type: 'component', - id, - name: names[idx], - parent: paths[idx - 1], - depth: idx, - renderLabel, - ...(childId && { - children: [childId], - }), - // deprecated fields - isRoot: false, - isComponent: true, - isLeaf: false, - }); + acc[id] = merge( + (acc[id] || {}) as API_ComponentEntry, + { + type: 'component', + id, + name: names[idx], + parent: paths[idx - 1], + depth: idx, + renderLabel, + ...(childId && { + children: [childId], + }), + // deprecated fields + isRoot: false, + isComponent: true, + isLeaf: false, + } as API_ComponentEntry + ); } else { - acc[id] = merge((acc[id] || {}) as API_GroupEntry, { - type: 'group', - id, - name: names[idx], - parent: paths[idx - 1], - depth: idx, - renderLabel, - ...(childId && { - children: [childId], - }), - // deprecated fields - isRoot: false, - isComponent: false, - isLeaf: false, - }); + acc[id] = merge( + (acc[id] || {}) as API_GroupEntry, + { + type: 'group', + id, + name: names[idx], + parent: paths[idx - 1], + depth: idx, + renderLabel, + ...(childId && { + children: [childId], + }), + // deprecated fields + isRoot: false, + isComponent: false, + isLeaf: false, + } as API_GroupEntry + ); } }); // Finally add an entry for the docs/story itself acc[item.id] = { - type: 'story', ...item, depth: paths.length, parent: paths[paths.length - 1], diff --git a/code/lib/manager-api/src/modules/addons.ts b/code/lib/manager-api/src/modules/addons.ts index 4f618a23b7ff..71d1e0ac0e05 100644 --- a/code/lib/manager-api/src/modules/addons.ts +++ b/code/lib/manager-api/src/modules/addons.ts @@ -11,7 +11,7 @@ import type { ModuleFn } from '../lib/types'; import type { Options } from '../store'; export interface SubState { - selectedPanel: string; + selectedPanel: string | undefined; addons: Record; } @@ -51,7 +51,7 @@ export interface SubAPI { * Returns the id of the currently selected panel. * @returns {string} - The ID of the currently selected panel. */ - getSelectedPanel: () => string; + getSelectedPanel: () => string | undefined; /** * Sets the currently selected panel via it's ID. * @param {string} panelName - The ID of the panel to select. @@ -69,7 +69,7 @@ export interface SubAPI { */ setAddonState( addonId: string, - newStateOrMerger: S | API_StateMerger, + newStateOrMerger?: S | API_StateMerger, options?: Options ): Promise; /** @@ -85,6 +85,10 @@ export interface SubAPI { export function ensurePanel(panels: API_Panels, selectedPanel?: string, currentPanel?: string) { const keys = Object.keys(panels); + if (typeof selectedPanel === 'undefined') { + return currentPanel; + } + if (keys.indexOf(selectedPanel) >= 0) { return selectedPanel; } @@ -130,7 +134,7 @@ export const init: ModuleFn = ({ provider, store, fullAPI }) = }, setAddonState( addonId: string, - newStateOrMerger: S | API_StateMerger, + newStateOrMerger?: S | API_StateMerger, options?: Options ): Promise { let nextState; diff --git a/code/lib/manager-api/src/modules/channel.ts b/code/lib/manager-api/src/modules/channel.ts index c83c342c5253..0ab1dc910087 100644 --- a/code/lib/manager-api/src/modules/channel.ts +++ b/code/lib/manager-api/src/modules/channel.ts @@ -56,12 +56,12 @@ export const init: ModuleFn = ({ provider }) => { const api: SubAPI = { getChannel: () => provider.channel, on: (type, handler) => { - provider.channel.on(type, handler); + provider.channel?.on(type, handler); - return () => provider.channel.off(type, handler); + return () => provider.channel?.off(type, handler); }, - off: (type, handler) => provider.channel.off(type, handler), - once: (type, handler) => provider.channel.once(type, handler), + off: (type, handler) => provider.channel?.off(type, handler), + once: (type, handler) => provider.channel?.once(type, handler), emit: (type, data, ...args) => { if ( data?.options?.target && @@ -73,7 +73,7 @@ export const init: ModuleFn = ({ provider }) => { ? `storybook-ref-${data.options.target}` : 'storybook-preview-iframe'; } - provider.channel.emit(type, data, ...args); + provider.channel?.emit(type, data, ...args); }, collapseAll: () => { api.emit(STORIES_COLLAPSE_ALL, {}); diff --git a/code/lib/manager-api/src/modules/globals.ts b/code/lib/manager-api/src/modules/globals.ts index 393deb58c4a2..c59f280cc681 100644 --- a/code/lib/manager-api/src/modules/globals.ts +++ b/code/lib/manager-api/src/modules/globals.ts @@ -5,6 +5,7 @@ import type { SetGlobalsPayload, Globals, GlobalTypes } from '@storybook/types'; import type { ModuleFn } from '../lib/types'; +import type { Meta } from '../lib/events'; // eslint-disable-next-line import/no-cycle import { getEventMetadata } from '../lib/events'; @@ -35,14 +36,14 @@ export interface SubAPI { export const init: ModuleFn = ({ store, fullAPI, provider }) => { const api: SubAPI = { getGlobals() { - return store.getState().globals; + return store.getState().globals as Globals; }, getGlobalTypes() { - return store.getState().globalTypes; + return store.getState().globalTypes as GlobalTypes; }, updateGlobals(newGlobals) { // Only emit the message to the local ref - provider.channel.emit(UPDATE_GLOBALS, { + provider.channel?.emit(UPDATE_GLOBALS, { globals: newGlobals, options: { target: 'storybook-preview-iframe', @@ -62,32 +63,39 @@ export const init: ModuleFn = ({ store, fullAPI, provider }) = } }; - provider.channel.on( + provider.channel?.on( GLOBALS_UPDATED, - function handleGlobalsUpdated({ globals }: { globals: Globals }) { - const { ref } = getEventMetadata(this, fullAPI); + function handleGlobalsUpdated(this: Meta, { globals }: { globals: Globals }) { + const metadata = getEventMetadata(this, fullAPI); - if (!ref) { - updateGlobals(globals); - } else { - logger.warn( - 'received a GLOBALS_UPDATED from a non-local ref. This is not currently supported.' - ); + if (metadata) { + const { ref } = metadata; + if (!ref) { + updateGlobals(globals); + } else { + logger.warn( + 'received a GLOBALS_UPDATED from a non-local ref. This is not currently supported.' + ); + } } } ); // Emitted by the preview on initialization - provider.channel.on( + provider.channel?.on( SET_GLOBALS, - function handleSetStories({ globals, globalTypes }: SetGlobalsPayload) { - const { ref } = getEventMetadata(this, fullAPI); + function handleSetStories(this: Meta, { globals, globalTypes }: SetGlobalsPayload) { + const metadata = getEventMetadata(this, fullAPI); + const currentGlobals = store.getState()?.globals; - if (!ref) { - store.setState({ globals, globalTypes }); - } else if (Object.keys(globals).length > 0) { - logger.warn('received globals from a non-local ref. This is not currently supported.'); + if (metadata) { + const { ref } = metadata; + if (!ref) { + store.setState({ globals, globalTypes }); + } else if (Object.keys(globals).length > 0) { + logger.warn('received globals from a non-local ref. This is not currently supported.'); + } } if ( diff --git a/code/lib/manager-api/src/modules/layout.ts b/code/lib/manager-api/src/modules/layout.ts index f37e51e19942..678f08d2f545 100644 --- a/code/lib/manager-api/src/modules/layout.ts +++ b/code/lib/manager-api/src/modules/layout.ts @@ -285,7 +285,7 @@ export const init: ModuleFn = ({ store, provider, singleStory, fullAPI }) => { state: merge(api.getInitialOptions(), persisted), init: () => { api.setOptions(merge(api.getInitialOptions(), persisted)); - provider.channel.on(SET_CONFIG, () => { + provider.channel?.on(SET_CONFIG, () => { api.setOptions(merge(api.getInitialOptions(), persisted)); }); }, diff --git a/code/lib/manager-api/src/modules/refs.ts b/code/lib/manager-api/src/modules/refs.ts index 02884665cf5a..eb4a6101d59e 100644 --- a/code/lib/manager-api/src/modules/refs.ts +++ b/code/lib/manager-api/src/modules/refs.ts @@ -32,7 +32,7 @@ export interface SubAPI { * @param {string} source - The source/URL of the composed ref. * @returns {API_ComposedRef} - The composed ref object. */ - findRef: (source: string) => API_ComposedRef; + findRef: (source: string | null) => API_ComposedRef | undefined; /** * Sets a composed ref by its ID and data. * @param {string} id - The ID of the composed ref. @@ -71,7 +71,11 @@ export interface SubAPI { changeRefState: (id: string, previewInitialized: boolean) => void; } -export const getSourceType = (source: string, refId?: string) => { +export const getSourceType = (source?: string, refId?: string) => { + if (typeof source === 'undefined') { + return [null, null]; + } + const { origin: localOrigin, pathname: localPathname } = location; const { origin: sourceOrigin, pathname: sourcePathname } = new URL(source); @@ -115,7 +119,7 @@ async function handleRequest( return json as API_SetRefData; } catch (err) { - return { indexError: err }; + return { indexError: err as Error }; } } @@ -160,6 +164,8 @@ export const init: ModuleFn = ( ) => { const api: SubAPI = { findRef: (source) => { + if (!source) return undefined; + const refs = api.getRefs(); return Object.values(refs).find(({ url }) => url.match(source)); @@ -184,6 +190,8 @@ export const init: ModuleFn = ( const { id, url, version, type } = ref; const isPublic = type === 'server-checked'; + if (!url || !id) return; + // ref's type starts as either 'unknown' or 'server-checked' // "server-checked" happens when we were able to verify the storybook is accessible from node (without cookies) // "unknown" happens if the request was declined of failed (this can happen because the storybook doesn't exists or authentication is required) @@ -279,7 +287,7 @@ export const init: ModuleFn = ( const { storyMapper = defaultStoryMapper } = provider.getConfig(); const ref = api.getRefs()[id]; - let index: API_IndexHash; + let index: API_IndexHash | undefined; if (setStoriesData) { index = transformSetStoriesStoryDataToStoriesHash( map(setStoriesData, ref, { storyMapper }), diff --git a/code/lib/manager-api/src/modules/settings.ts b/code/lib/manager-api/src/modules/settings.ts index 4c850f9aca1a..3e2000cdd452 100644 --- a/code/lib/manager-api/src/modules/settings.ts +++ b/code/lib/manager-api/src/modules/settings.ts @@ -79,7 +79,7 @@ export const init: ModuleFn = ({ store, navigate, fullAPI }) = }; return { - state: { settings: { lastTrackedStoryId: null } }, + state: { settings: { lastTrackedStoryId: '' } }, api, }; }; diff --git a/code/lib/manager-api/src/modules/shortcuts.ts b/code/lib/manager-api/src/modules/shortcuts.ts index 6d1dc4814c43..36b5fb81ecce 100644 --- a/code/lib/manager-api/src/modules/shortcuts.ts +++ b/code/lib/manager-api/src/modules/shortcuts.ts @@ -14,7 +14,7 @@ export const isMacLike = () => navigator && navigator.platform ? !!navigator.platform.match(/(Mac|iPhone|iPod|iPad)/i) : false; export const controlOrMetaKey = () => (isMacLike() ? 'meta' : 'control'); -export function keys(o: O) { +export function keys(o: O) { return Object.keys(o) as (keyof O)[]; } @@ -215,9 +215,11 @@ export const init: ModuleFn = ({ store, fullAPI, provider }) => { const shortcut = eventToShortcut(event); const shortcuts = api.getShortcutKeys(); const actions = keys(shortcuts); - const matchedFeature = actions.find((feature: API_Action) => - shortcutMatchesShortcut(shortcut, shortcuts[feature]) - ); + const matchedFeature = + !!shortcut && + actions.find((feature: API_Action) => + shortcutMatchesShortcut(shortcut, shortcuts[feature]) + ); if (matchedFeature) { api.handleShortcutFeature(matchedFeature, event); } @@ -276,7 +278,7 @@ export const init: ModuleFn = ({ store, fullAPI, provider }) => { if (element) { try { // should be like a channel message and all that, but yolo for now - element.contentWindow.focus(); + element.contentWindow?.focus(); } catch (e) { // } @@ -403,7 +405,7 @@ export const init: ModuleFn = ({ store, fullAPI, provider }) => { }); // Also listen to keydown events sent over the channel - provider.channel.on(PREVIEW_KEYDOWN, (data: { event: KeyboardEventLike }) => { + provider.channel?.on(PREVIEW_KEYDOWN, (data: { event: KeyboardEventLike }) => { api.handleKeydownEvent(data.event); }); }; diff --git a/code/lib/manager-api/src/modules/stories.ts b/code/lib/manager-api/src/modules/stories.ts index aff85aabcea3..ba5227501106 100644 --- a/code/lib/manager-api/src/modules/stories.ts +++ b/code/lib/manager-api/src/modules/stories.ts @@ -22,6 +22,7 @@ import type { API_StatusState, API_StatusUpdate, API_FilterFunction, + API_StatusObject, } from '@storybook/types'; import { PRELOAD_ENTRIES, @@ -44,6 +45,7 @@ import { } from '@storybook/core-events'; import { logger } from '@storybook/client-logger'; +import type { Meta } from '../lib/events'; // eslint-disable-next-line import/no-cycle import { getEventMetadata } from '../lib/events'; @@ -78,6 +80,8 @@ export interface SubState extends API_LoadedRefData { filters: Record; } +type APIStatusUpdateOrNull = Record; + export interface SubAPI { /** * The `storyId` method is a reference to the `toId` function from `@storybook/csf`, which is used to generate a unique ID for a story. @@ -93,7 +97,7 @@ export interface SubAPI { * @param {string} [refsId] - The ID of the refs to use for resolving the story. * @returns {API_HashEntry} - The hash entry corresponding to the given story ID. */ - resolveStory: (storyId: StoryId, refsId?: string) => API_HashEntry; + resolveStory: (storyId: StoryId, refsId?: string) => API_HashEntry | undefined | null; /** * Selects the first story to display in the Storybook UI. * @@ -120,7 +124,7 @@ export interface SubAPI { * * @returns {API_LeafEntry} The current story's data. */ - getCurrentStoryData: () => API_LeafEntry; + getCurrentStoryData: () => API_LeafEntry | undefined; /** * Sets the prepared story index to the given value. * @@ -150,7 +154,7 @@ export interface SubAPI { * @param {string} [refId] - The ID of the ref to retrieve data for. If not provided, retrieves data for the default ref. * @returns {API_LeafEntry} The data for the given story ID and optional ref ID. */ - getData: (storyId: StoryId, refId?: string) => API_LeafEntry; + getData: (storyId: StoryId, refId?: string) => API_LeafEntry | undefined; /** * Returns a boolean indicating whether the given story ID and optional ref ID have been prepared. * @@ -167,7 +171,7 @@ export interface SubAPI { * @returns {API_StoryEntry['parameters'] | any} The parameters for the given story ID and optional ref ID. */ getParameters: ( - storyId: StoryId | { storyId: StoryId; refId: string }, + storyId: StoryId | { storyId: StoryId; refId?: string }, parameterName?: ParameterName ) => API_StoryEntry['parameters'] | any; /** @@ -221,10 +225,10 @@ export interface SubAPI { */ findSiblingStoryId( storyId: StoryId, - index: API_IndexHash, + index: API_IndexHash | undefined, direction: Direction, toSiblingGroup: boolean // when true, skip over leafs within the same group - ): StoryId; + ): StoryId | undefined; /** * Fetches the story index from the server. * @@ -265,7 +269,7 @@ export interface SubAPI { */ experimental_updateStatus: ( addonId: string, - update: API_StatusUpdate | ((state: API_StatusState) => API_StatusUpdate) + update: APIStatusUpdateOrNull | ((state: API_StatusState) => API_StatusUpdate) ) => Promise; /** * Updates the filtering of the index. @@ -279,7 +283,9 @@ export interface SubAPI { const removedOptions = ['enableShortcuts', 'theme', 'showRoots']; -function removeRemovedOptions = Record>(options?: T): T { +function removeRemovedOptions = Record>( + options?: T +): T | undefined { if (!options || typeof options === 'string') { return options; } @@ -314,7 +320,7 @@ export const init: ModuleFn = ({ }, isPrepared: (storyId, refId) => { const data = api.getData(storyId, refId); - return data.type === 'story' ? data.prepared : true; + return data?.type === 'story' ? data.prepared : true; }, resolveStory: (storyId, refId) => { const { refs, index } = store.getState(); @@ -322,7 +328,7 @@ export const init: ModuleFn = ({ return null; } if (refId) { - return refs[refId].index ? refs[refId].index[storyId] : undefined; + return refs[refId].index ? refs[refId].index?.[storyId] : undefined; } return index ? index[storyId] : undefined; }, @@ -338,6 +344,8 @@ export const init: ModuleFn = ({ : storyIdOrCombo; const data = api.getData(storyId, refId); + if (!data) return null; + if (['story', 'docs'].includes(data?.type)) { const { parameters } = data; @@ -389,7 +397,9 @@ export const init: ModuleFn = ({ }, selectFirstStory: () => { const { index } = store.getState(); - const firstStory = Object.keys(index).find((id) => index[id].type === 'story'); + const firstStory = index + ? Object.keys(index).find((id) => index[id].type === 'story') + : undefined; if (firstStory) { api.selectStory(firstStory); @@ -408,14 +418,18 @@ export const init: ModuleFn = ({ if (!name) { // Find the entry (group, component or story) that is referred to - const entry = titleOrId ? hash[titleOrId] || hash[sanitize(titleOrId)] : hash[kindSlug]; + const entry = titleOrId + ? hash?.[titleOrId] || hash?.[sanitize(titleOrId)] + : hash?.[kindSlug]; if (!entry) throw new Error(`Unknown id or title: '${titleOrId}'`); // We want to navigate to the first ancestor entry that is a leaf - const leafEntry = api.findLeafEntry(hash, entry.id); - const fullId = leafEntry.refId ? `${leafEntry.refId}_${leafEntry.id}` : leafEntry.id; - navigate(`/${leafEntry.type}/${fullId}`); + if (hash) { + const leafEntry = api.findLeafEntry(hash, entry.id); + const fullId = leafEntry.refId ? `${leafEntry.refId}_${leafEntry.id}` : leafEntry.id; + navigate(`/${leafEntry.type}/${fullId}`); + } } else if (!titleOrId) { // This is a slugified version of the kind, but that's OK, our toId function is idempotent const id = toId(kindSlug, name); @@ -423,13 +437,13 @@ export const init: ModuleFn = ({ api.selectStory(id, undefined, options); } else { const id = ref ? `${ref}_${toId(titleOrId, name)}` : toId(titleOrId, name); - if (hash[id]) { + if (hash?.[id]) { api.selectStory(id, undefined, options); } else { // Support legacy API with component permalinks, where kind is `x/y` but permalink is 'z' - const entry = hash[sanitize(titleOrId)]; + const entry = hash?.[sanitize(titleOrId)]; if (entry?.type === 'component') { - const foundId = entry.children.find((childId) => hash[childId].name === name); + const foundId = entry.children.find((childId) => hash?.[childId].name === name); if (foundId) { api.selectStory(foundId, undefined, options); } @@ -450,6 +464,7 @@ export const init: ModuleFn = ({ return api.findLeafEntry(index, storyId)?.id; }, findSiblingStoryId(storyId, index, direction, toSiblingGroup) { + if (!index) return; if (toSiblingGroup) { const lookupList = getComponentLookupList(index); const position = lookupList.findIndex((i) => i.includes(storyId)); @@ -484,15 +499,15 @@ export const init: ModuleFn = ({ }, updateStoryArgs: (story, updatedArgs) => { const { id: storyId, refId } = story; - provider.channel.emit(UPDATE_STORY_ARGS, { + provider.channel?.emit(UPDATE_STORY_ARGS, { storyId, updatedArgs, options: { target: refId }, }); }, - resetStoryArgs: (story, argNames?: [string]) => { + resetStoryArgs: (story, argNames?: string[]) => { const { id: storyId, refId } = story; - provider.channel.emit(RESET_STORY_ARGS, { + provider.channel?.emit(RESET_STORY_ARGS, { storyId, argNames, options: { target: refId }, @@ -513,7 +528,7 @@ export const init: ModuleFn = ({ await api.setIndex(storyIndex); } catch (err) { - await store.setState({ indexError: err }); + await store.setState({ indexError: err as Error }); } }, // The story index we receive on SET_INDEX is "prepared" in that it has parameters @@ -540,17 +555,21 @@ export const init: ModuleFn = ({ ): Promise => { if (!ref) { const { index } = store.getState(); - index[storyId] = { - ...index[storyId], - ...update, - } as API_StoryEntry; + if (index) { + index[storyId] = { + ...index[storyId], + ...update, + } as API_StoryEntry; + } await store.setState({ index }); } else { const { id: refId, index } = ref; - index[storyId] = { - ...index[storyId], - ...update, - } as API_StoryEntry; + if (index) { + index[storyId] = { + ...index[storyId], + ...update, + } as API_StoryEntry; + } await fullAPI.updateRef(refId, { index }); } }, @@ -561,17 +580,21 @@ export const init: ModuleFn = ({ ): Promise => { if (!ref) { const { index } = store.getState(); - index[docsId] = { - ...index[docsId], - ...update, - } as API_DocsEntry; + if (index) { + index[docsId] = { + ...index[docsId], + ...update, + } as API_DocsEntry; + } await store.setState({ index }); } else { const { id: refId, index } = ref; - index[docsId] = { - ...index[docsId], - ...update, - } as API_DocsEntry; + if (index) { + index[docsId] = { + ...index[docsId], + ...update, + } as API_DocsEntry; + } await fullAPI.updateRef(refId, { index }); } }, @@ -608,30 +631,37 @@ export const init: ModuleFn = ({ }); await store.setState({ status: newStatus }, { persistence: 'session' }); - await api.setIndex(index); + if (index) { + await api.setIndex(index); + } }, experimental_setFilter: async (id, filterFunction) => { const { internal_index: index } = store.getState(); await store.setState({ filters: { ...store.getState().filters, [id]: filterFunction } }); - await api.setIndex(index); + if (index) { + await api.setIndex(index); + } }, }; // On initial load, the local iframe will select the first story (or other "selection specifier") // and emit STORY_SPECIFIED with the id. We need to ensure we respond to this change. - provider.channel.on( + provider.channel?.on( STORY_SPECIFIED, - function handler({ - storyId, - viewMode, - }: { - storyId: string; - viewMode: API_ViewMode; - [k: string]: any; - }) { - const { sourceType } = getEventMetadata(this, fullAPI); - - if (sourceType === 'local') { + function handler( + this: Meta, + { + storyId, + viewMode, + }: { + storyId: string; + viewMode: API_ViewMode; + [k: string]: any; + } + ) { + const metadata = getEventMetadata(this, fullAPI); + + if (metadata?.sourceType === 'local') { const state = store.getState(); const isCanvasRoute = state.path === '/' || state.viewMode === 'story' || state.viewMode === 'docs'; @@ -647,7 +677,7 @@ export const init: ModuleFn = ({ if (isCanvasRoute) { if (stateHasSelection && stateSelectionDifferent) { // The manager state is correct, the preview state is lagging behind - provider.channel.emit(SET_CURRENT_STORY, { + provider.channel?.emit(SET_CURRENT_STORY, { storyId: state.storyId, viewMode: state.viewMode, }); @@ -664,15 +694,15 @@ export const init: ModuleFn = ({ // Until the ref has a selection, it will not render anything (e.g. while waiting for // the preview.js file or the index to load). Once it has a selection, it will render its own // preparing spinner. - provider.channel.on(CURRENT_STORY_WAS_SET, function handler() { - const { ref } = getEventMetadata(this, fullAPI); - api.setPreviewInitialized(ref); + provider.channel?.on(CURRENT_STORY_WAS_SET, function handler(this: Meta) { + const metadata = getEventMetadata(this, fullAPI); + api.setPreviewInitialized(metadata?.ref); }); - provider.channel.on(STORY_CHANGED, function handler() { - const { sourceType } = getEventMetadata(this, fullAPI); + provider.channel?.on(STORY_CHANGED, function handler(this: Meta) { + const metadata = getEventMetadata(this, fullAPI); - if (sourceType === 'local') { + if (metadata?.sourceType === 'local') { const options = api.getCurrentParameter('options'); if (options) { @@ -681,112 +711,124 @@ export const init: ModuleFn = ({ } }); - provider.channel.on(STORY_PREPARED, function handler({ id, ...update }: StoryPreparedPayload) { - const { ref, sourceType } = getEventMetadata(this, fullAPI); - api.updateStory(id, { ...update, prepared: true }, ref); + provider.channel?.on( + STORY_PREPARED, + function handler(this: Meta, { id, ...update }: StoryPreparedPayload) { + const metadata = getEventMetadata(this, fullAPI); + api.updateStory(id, { ...update, prepared: true }, metadata?.ref); + + if (!metadata?.ref) { + if (!store.getState().hasCalledSetOptions) { + const { options } = update.parameters; + fullAPI.setOptions(removeRemovedOptions(options)); + store.setState({ hasCalledSetOptions: true }); + } + } - if (!ref) { - if (!store.getState().hasCalledSetOptions) { - const { options } = update.parameters; - fullAPI.setOptions(removeRemovedOptions(options)); - store.setState({ hasCalledSetOptions: true }); + if (metadata?.sourceType === 'local') { + const { storyId, index, refId } = store.getState(); + + // create a list of related stories to be preloaded + const toBePreloaded = Array.from( + new Set([ + api.findSiblingStoryId(storyId, index, 1, true), + api.findSiblingStoryId(storyId, index, -1, true), + ]) + ).filter(Boolean); + + provider.channel?.emit(PRELOAD_ENTRIES, { + ids: toBePreloaded, + options: { target: refId }, + }); } } + ); - if (sourceType === 'local') { - const { storyId, index, refId } = store.getState(); - - // create a list of related stories to be preloaded - const toBePreloaded = Array.from( - new Set([ - api.findSiblingStoryId(storyId, index, 1, true), - api.findSiblingStoryId(storyId, index, -1, true), - ]) - ).filter(Boolean); - - provider.channel.emit(PRELOAD_ENTRIES, { - ids: toBePreloaded, - options: { target: refId }, - }); + provider.channel?.on( + DOCS_PREPARED, + function handler(this: Meta, { id, ...update }: DocsPreparedPayload) { + const metadata = getEventMetadata(this, fullAPI); + api.updateStory(id, { ...update, prepared: true }, metadata?.ref); } - }); - - provider.channel.on(DOCS_PREPARED, function handler({ id, ...update }: DocsPreparedPayload) { - const { ref } = getEventMetadata(this, fullAPI); - api.updateStory(id, { ...update, prepared: true }, ref); - }); + ); - provider.channel.on(SET_INDEX, function handler(index: API_PreparedStoryIndex) { - const { ref } = getEventMetadata(this, fullAPI); + provider.channel?.on(SET_INDEX, function handler(this: Meta, index: API_PreparedStoryIndex) { + const metadata = getEventMetadata(this, fullAPI); - if (!ref) { + if (!metadata?.ref) { api.setIndex(index); - const options = api.getCurrentParameter('options'); + const options = api.getCurrentParameter>('options'); fullAPI.setOptions(removeRemovedOptions(options)); } else { - fullAPI.setRef(ref.id, { ...ref, storyIndex: index }, true); + fullAPI.setRef(metadata?.ref.id, { ...metadata?.ref, storyIndex: index }, true); } }); // For composition back-compatibilty - provider.channel.on(SET_STORIES, function handler(data: SetStoriesPayload) { - const { ref } = getEventMetadata(this, fullAPI); + provider.channel?.on(SET_STORIES, function handler(this: Meta, data: SetStoriesPayload) { + const metadata = getEventMetadata(this, fullAPI); const setStoriesData = data.v ? denormalizeStoryParameters(data) : data.stories; - if (!ref) { + if (!metadata?.ref) { throw new Error('Cannot call SET_STORIES for local frame'); } else { - fullAPI.setRef(ref.id, { ...ref, setStoriesData }, true); + fullAPI.setRef(metadata?.ref.id, { ...metadata?.ref, setStoriesData }, true); } }); - provider.channel.on( + provider.channel?.on( SELECT_STORY, - function handler({ - kind, - title = kind, - story, - name = story, - storyId, - ...rest - }: { - kind?: StoryKind; - title?: ComponentTitle; - story?: StoryName; - name?: StoryName; - storyId: string; - viewMode: API_ViewMode; - }) { - const { ref } = getEventMetadata(this, fullAPI); + function handler( + this: Meta, + { + kind, + title = kind, + story, + name = story, + storyId, + ...rest + }: { + kind?: StoryKind; + title?: ComponentTitle; + story?: StoryName; + name?: StoryName; + storyId: string; + viewMode: API_ViewMode; + } + ) { + const metadata = getEventMetadata(this, fullAPI); - if (!ref) { + if (!metadata?.ref) { fullAPI.selectStory(storyId || title, name, rest); } else { - fullAPI.selectStory(storyId || title, name, { ...rest, ref: ref.id }); + fullAPI.selectStory(storyId || title, name, { ...rest, ref: metadata?.ref.id }); } } ); - provider.channel.on( + provider.channel?.on( STORY_ARGS_UPDATED, - function handleStoryArgsUpdated({ storyId, args }: { storyId: StoryId; args: Args }) { - const { ref } = getEventMetadata(this, fullAPI); - api.updateStory(storyId, { args }, ref); + function handleStoryArgsUpdated( + this: Meta, + { storyId, args }: { storyId: StoryId; args: Args } + ) { + const metadata = getEventMetadata(this, fullAPI); + api.updateStory(storyId, { args }, metadata?.ref); } ); // When there's a preview error, we don't show it in the manager, but simply - provider.channel.on(CONFIG_ERROR, function handleConfigError(err) { - const { ref } = getEventMetadata(this, fullAPI); - api.setPreviewInitialized(ref); + provider.channel?.on(CONFIG_ERROR, function handleConfigError(this: Meta, err) { + const metadata = getEventMetadata(this, fullAPI); + api.setPreviewInitialized(metadata?.ref); }); - provider.channel.on(STORY_MISSING, function handleConfigError(err) { - const { ref } = getEventMetadata(this, fullAPI); - api.setPreviewInitialized(ref); + provider.channel?.on(STORY_MISSING, function handleConfigError(this: Meta, err) { + const metadata = getEventMetadata(this, fullAPI); + api.setPreviewInitialized(metadata?.ref); }); - provider.channel.on(SET_CONFIG, () => { + provider.channel?.on(SET_CONFIG, () => { const config = provider.getConfig(); if (config?.sidebar?.filters) { store.setState({ @@ -803,7 +845,7 @@ export const init: ModuleFn = ({ return { api, state: { - storyId: initialStoryId, + storyId: initialStoryId as string, viewMode: initialViewMode, hasCalledSetOptions: false, previewInitialized: false, @@ -812,7 +854,7 @@ export const init: ModuleFn = ({ }, init: async () => { if (FEATURES?.storyStoreV7) { - provider.channel.on(STORY_INDEX_INVALIDATED, () => api.fetchIndex()); + provider.channel?.on(STORY_INDEX_INVALIDATED, () => api.fetchIndex()); await api.fetchIndex(); } }, diff --git a/code/lib/manager-api/src/modules/url.ts b/code/lib/manager-api/src/modules/url.ts index c6cfc5abbd80..ba733e7ceca7 100644 --- a/code/lib/manager-api/src/modules/url.ts +++ b/code/lib/manager-api/src/modules/url.ts @@ -71,7 +71,7 @@ const initialUrlSupport = ({ }; export interface QueryParams { - [key: string]: string | null; + [key: string]: string | undefined; } /** @@ -90,7 +90,7 @@ export interface SubAPI { * @param {string} key - The key of the query parameter to get. * @returns {string | undefined} The value of the query parameter, or undefined if it does not exist. */ - getQueryParam: (key: string) => string | undefined; + getQueryParam: (key: string) => string | undefined | null; /** * Returns an object containing the current state of the URL. * @returns {{ @@ -155,7 +155,7 @@ export const init: ModuleFn = (moduleArgs) => { }; if (!deepEqual(customQueryParams, update)) { store.setState({ customQueryParams: update }); - provider.channel.emit(UPDATE_QUERY_PARAMS, update); + provider.channel?.emit(UPDATE_QUERY_PARAMS, update); } }, navigateUrl(url, options) { @@ -173,16 +173,16 @@ export const init: ModuleFn = (moduleArgs) => { const currentStory = fullAPI.getCurrentStoryData(); if (currentStory?.type !== 'story') return; - const { args, initialArgs } = currentStory; + const { args = {}, initialArgs } = currentStory; const argsString = buildArgsParam(initialArgs, args); navigateTo(path, { ...queryParams, args: argsString }, { replace: true }); api.setQueryParams({ args: argsString }); }; - provider.channel.on(SET_CURRENT_STORY, () => updateArgsParam()); + provider.channel?.on(SET_CURRENT_STORY, () => updateArgsParam()); let handleOrId: any; - provider.channel.on(STORY_ARGS_UPDATED, () => { + provider.channel?.on(STORY_ARGS_UPDATED, () => { if ('requestIdleCallback' in globalWindow) { if (handleOrId) globalWindow.cancelIdleCallback(handleOrId); handleOrId = globalWindow.requestIdleCallback(updateArgsParam, { timeout: 1000 }); @@ -192,14 +192,14 @@ export const init: ModuleFn = (moduleArgs) => { } }); - provider.channel.on(GLOBALS_UPDATED, ({ globals, initialGlobals }) => { + provider.channel?.on(GLOBALS_UPDATED, ({ globals, initialGlobals }) => { const { path, queryParams } = api.getUrlState(); const globalsString = buildArgsParam(initialGlobals, globals); navigateTo(path, { ...queryParams, globals: globalsString }, { replace: true }); api.setQueryParams({ globals: globalsString }); }); - provider.channel.on(NAVIGATE_URL, (url: string, options: NavigateOptions) => { + provider.channel?.on(NAVIGATE_URL, (url: string, options: NavigateOptions) => { api.navigateUrl(url, options); }); diff --git a/code/lib/manager-api/src/modules/versions.ts b/code/lib/manager-api/src/modules/versions.ts index 2d90a14fcd69..574c104d26f9 100644 --- a/code/lib/manager-api/src/modules/versions.ts +++ b/code/lib/manager-api/src/modules/versions.ts @@ -29,13 +29,13 @@ export interface SubAPI { * * @returns {API_Version} The current version of the Storybook Manager. */ - getCurrentVersion: () => API_Version; + getCurrentVersion: () => API_Version | undefined; /** * Returns the latest version of the Storybook Manager. * * @returns {API_Version} The latest version of the Storybook Manager. */ - getLatestVersion: () => API_Version; + getLatestVersion: () => API_Version | undefined; /** * Checks if an update is available for the Storybook Manager. * @@ -81,7 +81,7 @@ export const init: ModuleFn = ({ store }) => { if (!latest.version) { return true; } - if (!current.version) { + if (!current?.version) { return true; } @@ -96,7 +96,7 @@ export const init: ModuleFn = ({ store }) => { const diff = semver.diff(actualCurrent, latest.version); return ( - semver.gt(latest.version, actualCurrent) && diff !== 'patch' && !diff.includes('pre') + semver.gt(latest.version, actualCurrent) && diff !== 'patch' && !diff?.includes('pre') ); } return false; @@ -107,10 +107,8 @@ export const init: ModuleFn = ({ store }) => { const initModule = async () => { const { versions = {} } = store.getState(); - const { latest, next } = getVersionCheckData(); - await store.setState({ - versions: { ...versions, latest, next }, + versions: { ...versions, ...getVersionCheckData() }, }); }; diff --git a/code/lib/manager-api/src/modules/whatsnew.ts b/code/lib/manager-api/src/modules/whatsnew.ts index 6ee90558bc7c..a92433061626 100644 --- a/code/lib/manager-api/src/modules/whatsnew.ts +++ b/code/lib/manager-api/src/modules/whatsnew.ts @@ -47,7 +47,7 @@ export const init: ModuleFn = ({ fullAPI, store, provider }) => { ...state.whatsNewData, disableWhatsNewNotifications: !state.whatsNewData.disableWhatsNewNotifications, }); - provider.channel.emit(TOGGLE_WHATS_NEW_NOTIFICATIONS, { + provider.channel?.emit(TOGGLE_WHATS_NEW_NOTIFICATIONS, { disableWhatsNewNotifications: state.whatsNewData.disableWhatsNewNotifications, }); } @@ -55,17 +55,17 @@ export const init: ModuleFn = ({ fullAPI, store, provider }) => { }; function getLatestWhatsNewPost(): Promise { - provider.channel.emit(REQUEST_WHATS_NEW_DATA); + provider.channel?.emit(REQUEST_WHATS_NEW_DATA); return new Promise((resolve) => - provider.channel.once(RESULT_WHATS_NEW_DATA, ({ data }: { data: WhatsNewData }) => + provider.channel?.once(RESULT_WHATS_NEW_DATA, ({ data }: { data: WhatsNewData }) => resolve(data) ) ); } function setWhatsNewCache(cache: WhatsNewCache): void { - provider.channel.emit(SET_WHATS_NEW_CACHE, cache); + provider.channel?.emit(SET_WHATS_NEW_CACHE, cache); } const initModule = async () => { diff --git a/code/lib/manager-api/src/tests/refs.test.ts b/code/lib/manager-api/src/tests/refs.test.ts index f0556d560ba3..9150d7d30afa 100644 --- a/code/lib/manager-api/src/tests/refs.test.ts +++ b/code/lib/manager-api/src/tests/refs.test.ts @@ -93,16 +93,16 @@ const setupResponses = ({ throw new Error('Wrong request type'); } - if (l.includes('index') && o.credentials === 'include' && indexPrivate) { + if (l.includes('index') && o?.credentials === 'include' && indexPrivate) { return respond(indexPrivate); } - if (l.includes('index') && o.credentials === 'omit' && indexPublic) { + if (l.includes('index') && o?.credentials === 'omit' && indexPublic) { return respond(indexPublic); } - if (l.includes('stories') && o.credentials === 'include' && storiesPrivate) { + if (l.includes('stories') && o?.credentials === 'include' && storiesPrivate) { return respond(storiesPrivate); } - if (l.includes('stories') && o.credentials === 'omit' && storiesPublic) { + if (l.includes('stories') && o?.credentials === 'omit' && storiesPublic) { return respond(storiesPublic); } if (l.includes('iframe') && iframe) { diff --git a/code/lib/manager-api/src/tests/stories.test.ts b/code/lib/manager-api/src/tests/stories.test.ts index a93cd1df9a99..5816b64fcbbc 100644 --- a/code/lib/manager-api/src/tests/stories.test.ts +++ b/code/lib/manager-api/src/tests/stories.test.ts @@ -101,7 +101,13 @@ describe('stories API', () => { const { store } = moduleArgs; api.setIndex({ v: 4, entries: mockEntries }); const { index } = store.getState(); + + if (!index) { + throw new Error('🚨 Make sure index in story is defined.'); + } + // We need exact key ordering, even if in theory JS doesn't guarantee it + console.log(index); expect(Object.keys(index)).toEqual([ 'component-a', 'component-a--docs', @@ -110,7 +116,7 @@ describe('stories API', () => { 'component-b', 'component-b--story-3', ]); - expect(index['component-a']).toMatchObject({ + expect(index?.['component-a']).toMatchObject({ type: 'component', id: 'component-a', children: ['component-a--docs', 'component-a--story-1', 'component-a--story-2'], @@ -153,6 +159,9 @@ describe('stories API', () => { }, }); const { index } = store.getState(); + if (!index) { + throw new Error('🚨 Make sure index in story is defined.'); + } // We need exact key ordering, even if in theory JS doesn't guarantee it expect(Object.keys(index)).toEqual([ 'design-system', @@ -191,6 +200,9 @@ describe('stories API', () => { }, }); const { index } = store.getState(); + if (!index) { + throw new Error('🚨 Make sure index in story is defined.'); + } // We need exact key ordering, even if in theory JS doesn't guarantee it expect(Object.keys(index)).toEqual([ 'component-a', @@ -227,6 +239,9 @@ describe('stories API', () => { }, }); const { index } = store.getState(); + if (!index) { + throw new Error('🚨 Make sure index in story is defined.'); + } // We need exact key ordering, even if in theory JS doens't guarantee it expect(Object.keys(index)).toEqual(['a', 'a-b', 'a-b--1']); expect(index.a).toMatchObject({ @@ -266,6 +281,9 @@ describe('stories API', () => { }, }); const { index } = store.getState(); + if (!index) { + throw new Error('🚨 Make sure index in story is defined.'); + } // We need exact key ordering, even if in theory JS doens't guarantee it expect(Object.keys(index)).toEqual(['a', 'a--1']); expect(index.a).toMatchObject({ @@ -297,6 +315,9 @@ describe('stories API', () => { }, }); const { index } = store.getState(); + if (!index) { + throw new Error('🚨 Make sure index in story is defined.'); + } // We need exact key ordering, even if in theory JS doens't guarantee it expect(Object.keys(index)).toEqual(['a', 'a--1', 'a--2', 'b', 'b--1']); expect(index.a).toMatchObject({ @@ -330,6 +351,9 @@ describe('stories API', () => { }, }); const { index } = store.getState(); + if (!index) { + throw new Error('🚨 Make sure index in story is defined.'); + } expect(index['prepared--story']).toMatchObject({ type: 'story', id: 'prepared--story', @@ -354,7 +378,7 @@ describe('stories API', () => { }); // Let the promise/await chain resolve await new Promise((r) => setTimeout(r, 0)); - expect(store.getState().index['component-a--story-1'] as API_StoryEntry).toMatchObject({ + expect(store.getState().index?.['component-a--story-1'] as API_StoryEntry).toMatchObject({ prepared: true, parameters: { a: 'b' }, args: { c: 'd' }, @@ -362,7 +386,7 @@ describe('stories API', () => { api.setIndex({ v: 4, entries: mockEntries }); // Let the promise/await chain resolve await new Promise((r) => setTimeout(r, 0)); - expect(store.getState().index['component-a--story-1'] as API_StoryEntry).toMatchObject({ + expect(store.getState().index?.['component-a--story-1'] as API_StoryEntry).toMatchObject({ prepared: true, parameters: { a: 'b' }, args: { c: 'd' }, @@ -377,6 +401,9 @@ describe('stories API', () => { api.setIndex({ v: 4, entries: docsEntries }); const { index } = store.getState(); + if (!index) { + throw new Error('🚨 Make sure index in story is defined.'); + } // We need exact key ordering, even if in theory JS doesn't guarantee it expect(Object.keys(index)).toEqual([ 'component-a', @@ -402,6 +429,9 @@ describe('stories API', () => { const { store } = moduleArgs; api.setIndex({ v: 4, entries: docsEntries }); const { index } = store.getState(); + if (!index) { + throw new Error('🚨 Make sure index in story is defined.'); + } expect(Object.keys(index)).toEqual(['component-b', 'component-b--docs']); }); }); @@ -452,7 +482,7 @@ describe('stories API', () => { const { init } = initStories(moduleArgs as unknown as ModuleArgs); const { store } = moduleArgs; - await init(); + await init?.(); const { indexError } = store.getState(); expect(indexError).toBeDefined(); @@ -481,7 +511,7 @@ describe('stories API', () => { const { init } = initStories(moduleArgs as unknown as ModuleArgs); const { store, provider } = moduleArgs; - await init(); + await init?.(); expect(fetch).toHaveBeenCalledTimes(1); provider.channel.emit(STORY_INDEX_INVALIDATED); @@ -491,6 +521,9 @@ describe('stories API', () => { await wait(16); const { index } = store.getState(); + if (!index) { + throw new Error('🚨 Make sure index in story is defined.'); + } expect(Object.keys(index)).toEqual(['component-a', 'component-a--story-1']); }); it('clears 500 errors when invalidated', async () => { @@ -504,7 +537,7 @@ describe('stories API', () => { const { init } = initStories(moduleArgs as unknown as ModuleArgs); const { store, provider } = moduleArgs; - await init(); + await init?.(); const { indexError } = store.getState(); expect(indexError).toBeDefined(); @@ -536,6 +569,9 @@ describe('stories API', () => { const { index, indexError: newIndexError } = store.getState(); expect(newIndexError).not.toBeDefined(); + if (!index) { + throw new Error('🚨 Make sure index in story is defined.'); + } expect(Object.keys(index)).toEqual(['component-a', 'component-a--story-1']); }); }); @@ -616,10 +652,16 @@ describe('stories API', () => { api.setIndex({ v: 4, entries: preparedEntries }); const { index } = store.getState(); + if (!index) { + throw new Error('🚨 Make sure index in story is defined.'); + } expect((index['a--1'] as API_StoryEntry).args).toEqual({ a: 'b' }); expect((index['b--1'] as API_StoryEntry).args).toEqual({ x: 'y' }); provider.channel.emit(STORY_ARGS_UPDATED, { storyId: 'a--1', args: { foo: 'bar' } }); const { index: changedIndex } = store.getState(); + if (!changedIndex) { + throw new Error('🚨 Make sure index in story is defined.'); + } expect((changedIndex['a--1'] as API_StoryEntry).args).toEqual({ foo: 'bar' }); expect((changedIndex['b--1'] as API_StoryEntry).args).toEqual({ x: 'y' }); }); @@ -663,6 +705,9 @@ describe('stories API', () => { }); const { index } = store.getState(); + if (!index) { + throw new Error('🚨 Make sure index in story is defined.'); + } expect((index['a--1'] as API_StoryEntry).args).toEqual({ a: 'b' }); expect((index['b--1'] as API_StoryEntry).args).toEqual({ x: 'y' }); }); @@ -705,6 +750,9 @@ describe('stories API', () => { }); const { index } = store.getState(); + if (!index) { + throw new Error('🚨 Make sure index in story is defined.'); + } expect((index['a--1'] as API_StoryEntry).args).toEqual({ a: 'b' }); expect((index['b--1'] as API_StoryEntry).args).toEqual({ x: 'y' }); }); @@ -777,7 +825,6 @@ describe('stories API', () => { expect(navigate).not.toHaveBeenCalled(); }); it('does nothing if you have not selected a story', () => { - // @ts-expect-error (storyId type is maybe wrong?) const initialState = { path: '/story', storyId: undefined, viewMode: 'story' }; const moduleArgs = createMockModuleArgs({ initialState }); const { api } = initStories(moduleArgs as unknown as ModuleArgs); @@ -1016,6 +1063,9 @@ describe('stories API', () => { args: { c: 'd' }, }); const { index } = store.getState(); + if (!index) { + throw new Error('🚨 Make sure index in story is defined.'); + } expect(index['component-a--story-1']).toMatchObject({ type: 'story', id: 'component-a--story-1', @@ -1064,6 +1114,9 @@ describe('stories API', () => { parameters: { a: 'b' }, }); const { index } = store.getState(); + if (!index) { + throw new Error('🚨 Make sure index in story is defined.'); + } expect(index['component-a--docs']).toMatchObject({ type: 'docs', id: 'component-a--docs', @@ -1460,7 +1513,7 @@ describe('stories API', () => { 'myCustomFilter', (item) => item.status !== undefined && - Object.values(item.status).some((v) => v.status === 'pending') + Object.values(item.status).some((v) => v?.status === 'pending') ); // empty, because there are no stories with status diff --git a/code/lib/manager-api/tsconfig.json b/code/lib/manager-api/tsconfig.json index a6f65038a17b..1dc5a72190bd 100644 --- a/code/lib/manager-api/tsconfig.json +++ b/code/lib/manager-api/tsconfig.json @@ -2,7 +2,7 @@ "extends": "../../tsconfig.json", "compilerOptions": { "skipLibCheck": true, - "strict": false + "strict": true }, "include": ["src/**/*"] }