UNPKG

@storybook/core

Version:

Storybook framework-agnostic API

456 lines (440 loc) • 16.8 kB
import { loadAllPresets } from '@storybook/core/common'; export { getPreviewBodyTemplate, getPreviewHeadTemplate } from '@storybook/core/common'; import { CLIOptions, LoadOptions, BuilderOptions, StorybookConfigRaw, IndexInputStats, NormalizedStoriesSpecifier, Path, Tag, IndexEntry, DocsIndexEntry, StoryIndex, Indexer, DocsOptions, StoryIndexEntry, Options } from '@storybook/core/types'; import { EventType } from '@storybook/core/telemetry'; import { Channel } from '@storybook/core/channels'; type BuildStaticStandaloneOptions = CLIOptions & LoadOptions & BuilderOptions & { outputDir: string; }; declare function buildStaticStandalone(options: BuildStaticStandaloneOptions): Promise<void>; declare function buildDevStandalone(options: CLIOptions & LoadOptions & BuilderOptions & { storybookVersion?: string; previewConfigPath?: string; }): Promise<{ port: number; address: string; networkAddress: string; }>; type TelemetryOptions = { cliOptions: CLIOptions; presetOptions?: Parameters<typeof loadAllPresets>[0]; printError?: (err: any) => void; skipPrompt?: boolean; }; type ErrorLevel = 'none' | 'error' | 'full'; declare function getErrorLevel({ cliOptions, presetOptions, skipPrompt, }: TelemetryOptions): Promise<ErrorLevel>; declare function sendTelemetryError(_error: unknown, eventType: EventType, options: TelemetryOptions): Promise<void>; declare function withTelemetry<T>(eventType: EventType, options: TelemetryOptions, run: () => Promise<T>): Promise<T | undefined>; declare function build(options?: any, frameworkOptions?: any): Promise<void | { port: number; address: string; networkAddress: string; }>; declare const mapStaticDir: (staticDir: NonNullable<StorybookConfigRaw["staticDirs"]>[number], configDir: string) => { staticDir: string; staticPath: string; targetDir: string; targetEndpoint: string; }; /** * A function that json from a file */ interface ReadJsonSync { (packageJsonPath: string): any | undefined; } /** * Function that can match a path */ interface MatchPath { (requestedModule: string, readJson?: ReadJsonSync, fileExists?: (name: string) => boolean, extensions?: ReadonlyArray<string>): string | undefined; } declare class IndexingError extends Error { importPaths: string[]; constructor(message: string, importPaths: string[], stack?: string); pathsString(): string; toString(): string; } type IndexStatsSummary = Record<keyof IndexInputStats, number>; type StoryIndexEntryWithExtra = StoryIndexEntry & { extra: { metaId?: string; stats: IndexInputStats; }; }; /** A .mdx file will produce a docs entry */ type DocsCacheEntry = DocsIndexEntry; /** A `_.stories._` file will produce a list of stories and possibly a docs entry */ type StoriesCacheEntry = { entries: (StoryIndexEntryWithExtra | DocsIndexEntry)[]; dependents: Path[]; type: 'stories'; }; type ErrorEntry = { type: 'error'; err: IndexingError; }; type CacheEntry = false | StoriesCacheEntry | DocsCacheEntry | ErrorEntry; type StoryIndexGeneratorOptions = { workingDir: Path; configDir: Path; indexers: Indexer[]; docs: DocsOptions; build?: StorybookConfigRaw['build']; }; /** * The StoryIndexGenerator extracts stories and docs entries for each file matching (one or more) * stories "specifiers", as defined in main.js. * * The output is a set of entries (see above for the types). * * Each file is treated as a stories or a (modern) docs file. * * A stories file is indexed by an indexer (passed in), which produces a list of stories. * * - If the stories have the `parameters.docsOnly` setting, they are disregarded. * - If the stories have `autodocs` enabled, a docs entry is added pointing to the story file. * * A (modern) docs (.mdx) file is indexed, a docs entry is added. * * In the preview, a docs entry with the `autodocs` tag will be rendered as a CSF file that exports * an MDX template on the `docs.page` parameter, whereas other docs entries are rendered as MDX * files directly. * * The entries are "uniq"-ed and sorted. Stories entries are preferred to docs entries and MDX docs * entries are preferred to CSF templates (with warnings). */ declare class StoryIndexGenerator { readonly specifiers: NormalizedStoriesSpecifier[]; readonly options: StoryIndexGeneratorOptions; private specifierToCache; private lastIndex?; private lastStats?; private lastError?; constructor(specifiers: NormalizedStoriesSpecifier[], options: StoryIndexGeneratorOptions); initialize(): Promise<void>; /** Run the updater function over all the empty cache entries */ updateExtracted(updater: (specifier: NormalizedStoriesSpecifier, absolutePath: Path, existingEntry: CacheEntry) => Promise<CacheEntry>, overwrite?: boolean): Promise<void>; isDocsMdx(absolutePath: Path): boolean; ensureExtracted({ projectTags, }: { projectTags?: Tag[]; }): Promise<{ entries: (IndexEntry | ErrorEntry)[]; stats: IndexStatsSummary; }>; findDependencies(absoluteImports: Path[]): StoriesCacheEntry[]; /** * Try to find the component path from a raw import string and return it in the same format as * `importPath`. Respect tsconfig paths if available. * * If no such file exists, assume that the import is from a package and return the raw */ resolveComponentPath(rawComponentPath: Path, absolutePath: Path, matchPath: MatchPath | undefined): string; extractStories(specifier: NormalizedStoriesSpecifier, absolutePath: Path, projectTags?: Tag[]): Promise<StoriesCacheEntry | DocsCacheEntry>; extractDocs(specifier: NormalizedStoriesSpecifier, absolutePath: Path, projectTags?: Tag[]): Promise<false | DocsIndexEntry>; chooseDuplicate(firstEntry: IndexEntry, secondEntry: IndexEntry, projectTags: Tag[]): IndexEntry; sortStories(entries: StoryIndex['entries'], storySortParameter: any): Promise<Record<string, IndexEntry>>; getIndex(): Promise<StoryIndex>; getIndexAndStats(): Promise<{ storyIndex: StoryIndex; stats: IndexStatsSummary; }>; invalidateAll(): void; invalidate(specifier: NormalizedStoriesSpecifier, importPath: Path, removed: boolean): void; getPreviewCode(): Promise<string | undefined>; getProjectTags(previewCode?: string): string[]; storyFileNames(): string[]; } declare function loadStorybook(options: CLIOptions & LoadOptions & BuilderOptions & { storybookVersion?: string; previewConfigPath?: string; }): Promise<Options>; type EnvironmentType = (typeof UniversalStore.Environment)[keyof typeof UniversalStore.Environment]; type StatusType = (typeof UniversalStore.Status)[keyof typeof UniversalStore.Status]; type StateUpdater<TState> = (prevState: TState) => TState; type Actor = { id: string; type: (typeof UniversalStore.ActorType)[keyof typeof UniversalStore.ActorType]; environment: EnvironmentType; }; type EventInfo = { actor: Actor; forwardingActor?: Actor; }; type Listener<TEvent> = (event: TEvent, eventInfo: EventInfo) => void; type BaseEvent = { type: string; payload?: any; }; interface SetStateEvent<TState> extends BaseEvent { type: typeof UniversalStore.InternalEventType.SET_STATE; payload: { state: TState; previousState: TState; }; } interface ExistingStateRequestEvent extends BaseEvent { type: typeof UniversalStore.InternalEventType.EXISTING_STATE_REQUEST; payload: never; } interface ExistingStateResponseEvent<TState> extends BaseEvent { type: typeof UniversalStore.InternalEventType.EXISTING_STATE_RESPONSE; payload: TState; } interface LeaderCreatedEvent extends BaseEvent { type: typeof UniversalStore.InternalEventType.LEADER_CREATED; payload: never; } interface FollowerCreatedEvent extends BaseEvent { type: typeof UniversalStore.InternalEventType.FOLLOWER_CREATED; payload: never; } type InternalEvent<TState> = SetStateEvent<TState> | ExistingStateRequestEvent | ExistingStateResponseEvent<TState> | FollowerCreatedEvent | LeaderCreatedEvent; type Event<TState, TEvent extends BaseEvent> = TEvent | InternalEvent<TState>; type ChannelLike = Pick<Channel, 'on' | 'off' | 'emit'>; type StoreOptions<TState> = { id: string; leader?: boolean; initialState?: TState; debug?: boolean; }; type EnvironmentOverrides = { channel: ChannelLike; environment: EnvironmentType; }; /** * A universal store implementation that synchronizes state across different environments using a * channel-based communication. * * The store follows a leader-follower pattern where: * * - Leader: The main store instance that owns and manages the state * - Follower: Store instances that mirror the leader's state * * Features: * * - State synchronization across environments * - Event-based communication * - Type-safe state and custom events * - Subscription system for state changes and custom events * * @remarks * - The store must be created using the static `create()` method, not the constructor * - Follower stores will automatically sync with their leader's state. If they have initial state, it * will be replaced immediately when it has synced with the leader. * * @example * * ```typescript * interface MyState { * count: number; * } * interface MyCustomEvent { * type: 'INCREMENT'; * payload: number; * } * * // Create a leader store * const leaderStore = UniversalStore.create<MyState, MyCustomEvent>({ * id: 'my-store', * leader: true, * initialState: { count: 0 }, * }); * * // Create a follower store * const followerStore = UniversalStore.create<MyState, MyCustomEvent>({ * id: 'my-store', * leader: false, * }); * ``` * * @template State - The type of state managed by the store * @template CustomEvent - Custom events that can be sent through the store. Must have a `type` * string and optional `payload` * @throws {Error} If constructed directly instead of using `create()` * @throws {Error} If created without setting a channel first * @throws {Error} If a follower is created with initial state * @throws {Error} If a follower cannot find its leader within 1 second */ declare class UniversalStore<State, CustomEvent extends { type: string; payload?: any; } = { type: string; payload?: any; }> { /** * Defines the possible actor types in the store system * * @readonly */ static readonly ActorType: { readonly LEADER: "LEADER"; readonly FOLLOWER: "FOLLOWER"; }; /** * Defines the possible environments the store can run in * * @readonly */ static readonly Environment: { readonly SERVER: "SERVER"; readonly MANAGER: "MANAGER"; readonly PREVIEW: "PREVIEW"; readonly UNKNOWN: "UNKNOWN"; readonly MOCK: "MOCK"; }; /** * Internal event types used for store synchronization * * @readonly */ static readonly InternalEventType: { readonly EXISTING_STATE_REQUEST: "__EXISTING_STATE_REQUEST"; readonly EXISTING_STATE_RESPONSE: "__EXISTING_STATE_RESPONSE"; readonly SET_STATE: "__SET_STATE"; readonly LEADER_CREATED: "__LEADER_CREATED"; readonly FOLLOWER_CREATED: "__FOLLOWER_CREATED"; }; static readonly Status: { readonly UNPREPARED: "UNPREPARED"; readonly SYNCING: "SYNCING"; readonly READY: "READY"; readonly ERROR: "ERROR"; }; protected static isInternalConstructing: boolean; /** * The preparation construct is used to keep track of all store's preparation state the promise is * resolved when the store is prepared with the static __prepare() method which will also change * the state from PENDING to RESOLVED */ private static preparation; private static setupPreparationPromise; /** Enable debug logs for this store */ debugging: boolean; /** The actor object representing the store instance with a unique ID and a type */ get actor(): Actor; /** * The current state of the store, that signals both if the store is prepared by Storybook and * also - in the case of a follower - if the state has been synced with the leader's state. */ get status(): StatusType; /** * A promise that resolves when the store is fully ready. A leader will be ready when the store * has been prepared by Storybook, which is almost instantly. * * A follower will be ready when the state has been synced with the leader's state, within a few * hundred milliseconds. */ untilReady(): Promise<[{ channel: ChannelLike; environment: EnvironmentType; }, void | undefined]>; /** * The syncing construct is used to keep track of if the instance's state has been synced with the * other instances. A leader will immediately have the promise resolved. A follower will initially * be in a PENDING state, and resolve the the leader has sent the existing state, or reject if no * leader has responded before the timeout. */ private syncing?; private channelEventName; private state; private channel?; private environment?; private listeners; private id; private actorId; private actorType; protected constructor(options: StoreOptions<State>, environmentOverrides?: EnvironmentOverrides); /** Creates a new instance of UniversalStore */ static create<State = any, CustomEvent extends { type: string; payload?: any; } = { type: string; payload?: any; }>(options: StoreOptions<State>): UniversalStore<State, CustomEvent>; /** Gets the current state */ getState: () => State; /** * Updates the store's state * * Either a new state or a state updater function can be passed to the method. */ setState(updater: State | StateUpdater<State>): void; /** * Subscribes to store events * * @returns A function to unsubscribe */ subscribe: { (listener: Listener<Event<State, CustomEvent>>): () => void; <EventType extends Event<State, CustomEvent>['type']>(eventType: EventType, listener: Listener<Extract<Event<State, CustomEvent>, { type: EventType; }>>): () => void; }; /** * Subscribes to state changes * * @returns Unsubscribe function */ onStateChange(listener: (state: State, previousState: State, eventInfo: EventInfo) => void): () => void; /** Sends a custom event to the other stores */ send: (event: CustomEvent) => void; private emitToChannel; private prepareThis; private emitToListeners; private handleChannelEvents; private debug; } /** * A mock universal store that can be used when testing code that relies on a universal store. It * functions exactly like a normal universal store, with a few exceptions: * * - It is fully isolated, meaning that it doesn't interact with any channel, and it is always a * leader. * * If the second testUtils argument is provided, all the public methods are spied on, so they can be * asserted. * * When a mock store is re-used across tests (eg. in stories), you manually need to reset the state * after each test. * * @example * * ```ts * import * as testUtils from '@storybook/test'; // in stories * import { vi as testUtils } from 'vitest'; // ... or in Vitest tests * * const initialState = { ... }; * const store = new MockUniversalStore({ initialState }, testUtils); * * export default { * title: 'My story', * beforeEach: () => { * return () => { * store.setState(initialState); * }; * } * } * ``` */ declare class MockUniversalStore<State, CustomEvent extends { type: string; payload?: any; } = { type: string; payload?: any; }> extends UniversalStore<State, CustomEvent> { private testUtils; constructor(options: StoreOptions<State>, testUtils?: any); /** Create a mock universal store. This is just an alias for the constructor */ static create<State = any, CustomEvent extends { type: string; payload?: any; } = { type: string; payload?: any; }>(options: StoreOptions<State>, testUtils?: any): MockUniversalStore<State, CustomEvent>; unsubscribeAll(): void; } export { type BuildStaticStandaloneOptions, StoryIndexGenerator, build, buildDevStandalone, buildStaticStandalone, MockUniversalStore as experimental_MockUniversalStore, UniversalStore as experimental_UniversalStore, loadStorybook as experimental_loadStorybook, getErrorLevel, mapStaticDir, sendTelemetryError, withTelemetry };