storybook
Version:
Storybook: Develop, document, and test UI components in isolation
1,375 lines (1,352 loc) • 443 kB
TypeScript
import { loadAllPresets } from 'storybook/internal/common';
export { getPreviewBodyTemplate, getPreviewHeadTemplate } from 'storybook/internal/common';
import * as storybook_internal_types from 'storybook/internal/types';
import { CLIOptions, LoadOptions, BuilderOptions, StorybookConfigRaw, ModuleResolveConfig, NormalizedProjectAnnotations, ProjectAnnotations, ComposedStoryFn, IndexInputStats, NormalizedStoriesSpecifier, Path as Path$1, Indexer, DocsOptions, StoryIndexEntry, DocsIndexEntry, IndexEntry, StoryIndex, Options, StatusStoreByTypeId as StatusStoreByTypeId$1 } from 'storybook/internal/types';
export { ModuleResolveConfig } from 'storybook/internal/types';
import { EventType } from 'storybook/internal/telemetry';
import { CreateNewStoryRequestPayload } from 'storybook/internal/core-events';
import { StoryId, StrictArgTypes as StrictArgTypes$1, SBType as SBType$1 } from 'storybook/internal/csf';
import { Channel } from 'storybook/internal/channels';
import { ImportEdge } from 'storybook/internal/oxc-parser';
export { ImportEdge } from 'storybook/internal/oxc-parser';
import { watch } from 'node:fs';
import { readFile, stat } from 'node:fs/promises';
type BuildStaticStandaloneOptions = CLIOptions & LoadOptions & BuilderOptions & {
outputDir: string;
};
declare function buildStaticStandalone(options: BuildStaticStandaloneOptions): Promise<void>;
/**
* Resolves the initialPath for the browser open URL.
* CLI-provided initialPath always wins. If not set and not running in an agent context,
* checks the project cache for an `onboarding-pending` entry written by `storybook init`.
* If found, returns '/onboarding' and removes the cache entry so it only triggers once.
* The cache entry is only written by init when onboarding is known to be supported,
* so no further addon check is needed here.
*/
declare function resolveOnboardingInitialPath(cliInitialPath: string | undefined): Promise<string | undefined>;
declare function buildDevStandalone(options: CLIOptions & LoadOptions & BuilderOptions & {
storybookVersion?: string;
previewConfigPath?: string;
}): Promise<{
port: number;
address: string;
networkAddress: string;
}>;
type BuildIndexOptions = CLIOptions & LoadOptions & BuilderOptions;
declare const buildIndex: (options: BuildIndexOptions) => Promise<storybook_internal_types.StoryIndex>;
declare const buildIndexStandalone: (options: BuildIndexOptions & {
outputFile: string;
}) => Promise<void>;
declare abstract class StorybookError extends Error {
private _name;
/** Category of the error. Used to classify the type of error, e.g., 'PREVIEW_API'. */
readonly category: string;
/** Code representing the error. Used to uniquely identify the error, e.g., 1. */
readonly code: number;
/**
* Data associated with the error. Used to provide additional information in the error message or
* to be passed to telemetry.
*/
readonly data: {};
/**
* Specifies the documentation for the error.
*
* - If `true`, links to a documentation page on the Storybook website (make sure it exists before
* enabling) – This is not implemented yet.
* - If a string, uses the provided URL for documentation (external or FAQ links).
* - If `false` (default), no documentation link is added.
*/
readonly documentation: boolean | string | string[];
/** Flag used to easily determine if the error originates from Storybook. */
readonly fromStorybook: true;
/**
* Flag used to determine if the error is handled by us and should therefore not be shown to the
* user.
*/
isHandledError: boolean;
get fullErrorCode(): `SB_${string}_${string}`;
/** Overrides the default `Error.name` property in the format: SB_<CATEGORY>_<CODE>. */
get name(): string;
set name(name: string);
/**
* A collection of sub errors which relate to a parent error.
*
* Sub-errors are used to represent multiple related errors that occurred together. When a
* StorybookError with sub-errors is sent to telemetry, both the parent error and each sub-error
* are sent as separate telemetry events. This allows for better error tracking and debugging.
*
* @example
*
* ```ts
* const error1 = new SomeError();
* const error2 = new AnotherError();
* const parentError = new ParentError({
* // ... other props
* subErrors: [error1, error2],
* });
* ```
*/
subErrors: StorybookError[];
constructor(props: {
category: string;
code: number;
message: string;
cause?: unknown;
documentation?: boolean | string | string[];
isHandledError?: boolean;
name: string;
/**
* Optional array of sub-errors that are related to this error. When this error is sent to
* telemetry, each sub-error will be sent as a separate event.
*/
subErrors?: StorybookError[];
});
/** Generates the error message along with additional documentation link (if applicable). */
static getFullMessage({ documentation, code, category, message, }: ConstructorParameters<typeof StorybookError>[0]): string;
}
type TelemetryOptions = {
cliOptions: CLIOptions;
presetOptions?: Parameters<typeof loadAllPresets>[0];
printError?: (err: any) => void;
skipPrompt?: boolean;
eventType?: EventType;
fallbackTelemetryState?: boolean;
};
type ErrorLevel = 'none' | 'error' | 'full';
declare function getErrorLevel({ cliOptions, presetOptions, skipPrompt, eventType, }: TelemetryOptions): Promise<ErrorLevel>;
declare function sendTelemetryError(_error: unknown, eventType: EventType, options: TelemetryOptions, blocking?: boolean, parent?: StorybookError): 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;
}
/**
* Actions represent the type of change to a location value.
*/
declare enum Action {
/**
* A POP indicates a change to an arbitrary index in the history stack, such
* as a back or forward navigation. It does not describe the direction of the
* navigation, only that the current index changed.
*
* Note: This is the default action for newly created history objects.
*/
Pop = "POP",
/**
* A PUSH indicates a new entry being added to the history stack, such as when
* a link is clicked and a new page loads. When this happens, all subsequent
* entries in the stack are lost.
*/
Push = "PUSH",
/**
* A REPLACE indicates the entry at the current index in the history stack
* being replaced by a new one.
*/
Replace = "REPLACE"
}
/**
* The pathname, search, and hash values of a URL.
*/
interface Path {
/**
* A URL pathname, beginning with a /.
*/
pathname: string;
/**
* A URL search string, beginning with a ?.
*/
search: string;
/**
* A URL fragment identifier, beginning with a #.
*/
hash: string;
}
/**
* An entry in a history stack. A location contains information about the
* URL path, as well as possibly some arbitrary state and a key.
*/
interface Location extends Path {
/**
* A value of arbitrary data associated with this location.
*/
state: any;
/**
* A unique string associated with this location. May be used to safely store
* and retrieve data in some other storage API, like `localStorage`.
*
* Note: This value is always "default" on the initial location.
*/
key: string;
}
/**
* Map of routeId -> data returned from a loader/action/error
*/
interface RouteData {
[routeId: string]: any;
}
declare enum ResultType {
data = "data",
deferred = "deferred",
redirect = "redirect",
error = "error"
}
/**
* Successful result from a loader or action
*/
interface SuccessResult {
type: ResultType.data;
data: any;
statusCode?: number;
headers?: Headers;
}
/**
* Successful defer() result from a loader or action
*/
interface DeferredResult {
type: ResultType.deferred;
deferredData: DeferredData;
statusCode?: number;
headers?: Headers;
}
/**
* Redirect result from a loader or action
*/
interface RedirectResult {
type: ResultType.redirect;
status: number;
location: string;
revalidate: boolean;
reloadDocument?: boolean;
}
/**
* Unsuccessful result from a loader or action
*/
interface ErrorResult {
type: ResultType.error;
error: any;
headers?: Headers;
}
/**
* Result from a loader or action - potentially successful or unsuccessful
*/
type DataResult = SuccessResult | DeferredResult | RedirectResult | ErrorResult;
type LowerCaseFormMethod = "get" | "post" | "put" | "patch" | "delete";
type UpperCaseFormMethod = Uppercase<LowerCaseFormMethod>;
/**
* Active navigation/fetcher form methods are exposed in lowercase on the
* RouterState
*/
type FormMethod = LowerCaseFormMethod;
/**
* In v7, active navigation/fetcher form methods are exposed in uppercase on the
* RouterState. This is to align with the normalization done via fetch().
*/
type V7_FormMethod = UpperCaseFormMethod;
type FormEncType = "application/x-www-form-urlencoded" | "multipart/form-data" | "application/json" | "text/plain";
type JsonObject = {
[Key in string]: JsonValue;
} & {
[Key in string]?: JsonValue | undefined;
};
type JsonArray = JsonValue[] | readonly JsonValue[];
type JsonPrimitive = string | number | boolean | null;
type JsonValue = JsonPrimitive | JsonObject | JsonArray;
/**
* @private
* Internal interface to pass around for action submissions, not intended for
* external consumption
*/
type Submission = {
formMethod: FormMethod | V7_FormMethod;
formAction: string;
formEncType: FormEncType;
formData: FormData;
json: undefined;
text: undefined;
} | {
formMethod: FormMethod | V7_FormMethod;
formAction: string;
formEncType: FormEncType;
formData: undefined;
json: JsonValue;
text: undefined;
} | {
formMethod: FormMethod | V7_FormMethod;
formAction: string;
formEncType: FormEncType;
formData: undefined;
json: undefined;
text: string;
};
/**
* @private
* Arguments passed to route loader/action functions. Same for now but we keep
* this as a private implementation detail in case they diverge in the future.
*/
interface DataFunctionArgs {
request: Request;
params: Params;
context?: any;
}
/**
* Arguments passed to loader functions
*/
interface LoaderFunctionArgs extends DataFunctionArgs {
}
/**
* Arguments passed to action functions
*/
interface ActionFunctionArgs extends DataFunctionArgs {
}
/**
* Loaders and actions can return anything except `undefined` (`null` is a
* valid return value if there is no data to return). Responses are preferred
* and will ease any future migration to Remix
*/
type DataFunctionValue = Response | NonNullable<unknown> | null;
/**
* Route loader function signature
*/
interface LoaderFunction {
(args: LoaderFunctionArgs): Promise<DataFunctionValue> | DataFunctionValue;
}
/**
* Route action function signature
*/
interface ActionFunction {
(args: ActionFunctionArgs): Promise<DataFunctionValue> | DataFunctionValue;
}
/**
* Route shouldRevalidate function signature. This runs after any submission
* (navigation or fetcher), so we flatten the navigation/fetcher submission
* onto the arguments. It shouldn't matter whether it came from a navigation
* or a fetcher, what really matters is the URLs and the formData since loaders
* have to re-run based on the data models that were potentially mutated.
*/
interface ShouldRevalidateFunction {
(args: {
currentUrl: URL;
currentParams: AgnosticDataRouteMatch["params"];
nextUrl: URL;
nextParams: AgnosticDataRouteMatch["params"];
formMethod?: Submission["formMethod"];
formAction?: Submission["formAction"];
formEncType?: Submission["formEncType"];
text?: Submission["text"];
formData?: Submission["formData"];
json?: Submission["json"];
actionResult?: DataResult;
defaultShouldRevalidate: boolean;
}): boolean;
}
/**
* Keys we cannot change from within a lazy() function. We spread all other keys
* onto the route. Either they're meaningful to the router, or they'll get
* ignored.
*/
type ImmutableRouteKey = "lazy" | "caseSensitive" | "path" | "id" | "index" | "children";
type RequireOne<T, Key = keyof T> = Exclude<{
[K in keyof T]: K extends Key ? Omit<T, K> & Required<Pick<T, K>> : never;
}[keyof T], undefined>;
/**
* lazy() function to load a route definition, which can add non-matching
* related properties to a route
*/
interface LazyRouteFunction<R extends AgnosticRouteObject> {
(): Promise<RequireOne<Omit<R, ImmutableRouteKey>>>;
}
/**
* Base RouteObject with common props shared by all types of routes
*/
type AgnosticBaseRouteObject = {
caseSensitive?: boolean;
path?: string;
id?: string;
loader?: LoaderFunction;
action?: ActionFunction;
hasErrorBoundary?: boolean;
shouldRevalidate?: ShouldRevalidateFunction;
handle?: any;
lazy?: LazyRouteFunction<AgnosticBaseRouteObject>;
};
/**
* Index routes must not have children
*/
type AgnosticIndexRouteObject = AgnosticBaseRouteObject & {
children?: undefined;
index: true;
};
/**
* Non-index routes may have children, but cannot have index
*/
type AgnosticNonIndexRouteObject = AgnosticBaseRouteObject & {
children?: AgnosticRouteObject[];
index?: false;
};
/**
* A route object represents a logical route, with (optionally) its child
* routes organized in a tree-like structure.
*/
type AgnosticRouteObject = AgnosticIndexRouteObject | AgnosticNonIndexRouteObject;
type AgnosticDataIndexRouteObject = AgnosticIndexRouteObject & {
id: string;
};
type AgnosticDataNonIndexRouteObject = AgnosticNonIndexRouteObject & {
children?: AgnosticDataRouteObject[];
id: string;
};
/**
* A data route object, which is just a RouteObject with a required unique ID
*/
type AgnosticDataRouteObject = AgnosticDataIndexRouteObject | AgnosticDataNonIndexRouteObject;
/**
* The parameters that were parsed from the URL path.
*/
type Params<Key extends string = string> = {
readonly [key in Key]: string | undefined;
};
/**
* A RouteMatch contains info about how a route matched a URL.
*/
interface AgnosticRouteMatch<ParamKey extends string = string, RouteObjectType extends AgnosticRouteObject = AgnosticRouteObject> {
/**
* The names and values of dynamic parameters in the URL.
*/
params: Params<ParamKey>;
/**
* The portion of the URL pathname that was matched.
*/
pathname: string;
/**
* The portion of the URL pathname that was matched before child routes.
*/
pathnameBase: string;
/**
* The route object that was used to match.
*/
route: RouteObjectType;
}
interface AgnosticDataRouteMatch extends AgnosticRouteMatch<string, AgnosticDataRouteObject> {
}
declare class DeferredData {
private pendingKeysSet;
private controller;
private abortPromise;
private unlistenAbortSignal;
private subscribers;
data: Record<string, unknown>;
init?: ResponseInit;
deferredKeys: string[];
constructor(data: Record<string, unknown>, responseInit?: ResponseInit);
private trackPromise;
private onSettle;
private emit;
subscribe(fn: (aborted: boolean, settledKey?: string) => void): () => boolean;
cancel(): void;
resolveData(signal: AbortSignal): Promise<boolean>;
get done(): boolean;
get unwrappedData(): {};
get pendingKeys(): string[];
}
/**
* State maintained internally by the router. During a navigation, all states
* reflect the the "old" location unless otherwise noted.
*/
interface RouterState {
/**
* The action of the most recent navigation
*/
historyAction: Action;
/**
* The current location reflected by the router
*/
location: Location;
/**
* The current set of route matches
*/
matches: AgnosticDataRouteMatch[];
/**
* Tracks whether we've completed our initial data load
*/
initialized: boolean;
/**
* Current scroll position we should start at for a new view
* - number -> scroll position to restore to
* - false -> do not restore scroll at all (used during submissions)
* - null -> don't have a saved position, scroll to hash or top of page
*/
restoreScrollPosition: number | false | null;
/**
* Indicate whether this navigation should skip resetting the scroll position
* if we are unable to restore the scroll position
*/
preventScrollReset: boolean;
/**
* Tracks the state of the current navigation
*/
navigation: Navigation;
/**
* Tracks any in-progress revalidations
*/
revalidation: RevalidationState;
/**
* Data from the loaders for the current matches
*/
loaderData: RouteData;
/**
* Data from the action for the current matches
*/
actionData: RouteData | null;
/**
* Errors caught from loaders for the current matches
*/
errors: RouteData | null;
/**
* Map of current fetchers
*/
fetchers: Map<string, Fetcher>;
/**
* Map of current blockers
*/
blockers: Map<string, Blocker>;
}
/**
* Data that can be passed into hydrate a Router from SSR
*/
type HydrationState = Partial<Pick<RouterState, "loaderData" | "actionData" | "errors">>;
/**
* Potential states for state.navigation
*/
type NavigationStates = {
Idle: {
state: "idle";
location: undefined;
formMethod: undefined;
formAction: undefined;
formEncType: undefined;
formData: undefined;
json: undefined;
text: undefined;
};
Loading: {
state: "loading";
location: Location;
formMethod: Submission["formMethod"] | undefined;
formAction: Submission["formAction"] | undefined;
formEncType: Submission["formEncType"] | undefined;
formData: Submission["formData"] | undefined;
json: Submission["json"] | undefined;
text: Submission["text"] | undefined;
};
Submitting: {
state: "submitting";
location: Location;
formMethod: Submission["formMethod"];
formAction: Submission["formAction"];
formEncType: Submission["formEncType"];
formData: Submission["formData"];
json: Submission["json"];
text: Submission["text"];
};
};
type Navigation = NavigationStates[keyof NavigationStates];
type RevalidationState = "idle" | "loading";
/**
* Potential states for fetchers
*/
type FetcherStates<TData = any> = {
Idle: {
state: "idle";
formMethod: undefined;
formAction: undefined;
formEncType: undefined;
text: undefined;
formData: undefined;
json: undefined;
data: TData | undefined;
" _hasFetcherDoneAnything "?: boolean;
};
Loading: {
state: "loading";
formMethod: Submission["formMethod"] | undefined;
formAction: Submission["formAction"] | undefined;
formEncType: Submission["formEncType"] | undefined;
text: Submission["text"] | undefined;
formData: Submission["formData"] | undefined;
json: Submission["json"] | undefined;
data: TData | undefined;
" _hasFetcherDoneAnything "?: boolean;
};
Submitting: {
state: "submitting";
formMethod: Submission["formMethod"];
formAction: Submission["formAction"];
formEncType: Submission["formEncType"];
text: Submission["text"];
formData: Submission["formData"];
json: Submission["json"];
data: TData | undefined;
" _hasFetcherDoneAnything "?: boolean;
};
};
type Fetcher<TData = any> = FetcherStates<TData>[keyof FetcherStates<TData>];
interface BlockerBlocked {
state: "blocked";
reset(): void;
proceed(): void;
location: Location;
}
interface BlockerUnblocked {
state: "unblocked";
reset: undefined;
proceed: undefined;
location: undefined;
}
interface BlockerProceeding {
state: "proceeding";
reset: undefined;
proceed: undefined;
location: Location;
}
type Blocker = BlockerUnblocked | BlockerBlocked | BlockerProceeding;
/**
* NOTE: If you refactor this to split up the modules into separate files,
* you'll need to update the rollup config for react-router-dom-v5-compat.
*/
declare global {
var __staticRouterHydrationData: HydrationState | undefined;
}
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<[void | {
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 when 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;
}
type StatusValue = 'status-value:pending' | 'status-value:success' | 'status-value:new' | 'status-value:modified' | 'status-value:affected' | 'status-value:reviewing' | 'status-value:warning' | 'status-value:error' | 'status-value:unknown';
type StatusTypeId = string;
type StatusByTypeId = Record<StatusTypeId, Status>;
type StatusesByStoryIdAndTypeId = Record<StoryId, StatusByTypeId>;
interface Status {
value: StatusValue;
typeId: StatusTypeId;
storyId: StoryId;
title: string;
description: string;
data?: any;
sidebarContextMenu?: boolean;
}
declare const StatusStoreEventType: {
readonly SELECT: "select";
};
type StatusStoreEvent = {
type: typeof StatusStoreEventType.SELECT;
payload: Status[];
};
type StatusStore = {
getAll: () => StatusesByStoryIdAndTypeId;
set: (statuses: Status[]) => void;
onAllStatusChange: (listener: (statuses: StatusesByStoryIdAndTypeId, previousStatuses: StatusesByStoryIdAndTypeId) => void) => () => void;
onSelect: (listener: (selectedStatuses: Status[]) => void) => () => void;
unset: (storyIds?: StoryId[]) => void;
};
type StatusStoreByTypeId = StatusStore & {
typeId: StatusTypeId;
};
type TestProviderState = 'test-provider-state:pending' | 'test-provider-state:running' | 'test-provider-state:succeeded' | 'test-provider-state:crashed';
type TestProviderId = string;
type TestProviderStateByProviderId = Record<TestProviderId, TestProviderState>;
type TestProviderStoreEventType = 'run-all' | 'clear-all' | 'settings-changed';
type TestProviderStoreEvent = BaseEvent & {
type: TestProviderStoreEventType;
};
type BaseTestProviderStore = {
/**
* Notifies all listeners that settings have changed for test providers. The Storybook UI will
* highlight the test providers to tell the user that settings has changed.
*/
settingsChanged: () => void;
/**
* Subscribe to clicks on the "Run All" button, that is supposed to trigger all test providers to
* run. Your test provider should do the "main thing" when this happens, similar to when the user
* triggers your test provider specifically.
*
* @example
*
* ```typescript
* // Subscribe to run-all events
* const unsubscribe = myTestProviderStore.onRunAll(() => {
* await runAllMyTests();
* });
* ```
*/
onRunAll: (listener: () => void) => () => void;
/**
* Subscribe to clicks on the "Clear All" button, that is supposed to clear all state from test
* providers. Storybook already clears all statuses, but if your test provider has more
* non-status-based state, you can use this to clear that here.
*
* @remarks
* The purpose of this is _not_ to clear your test provider's settings, only the test results.
* @example
*
* ```typescript
* // Subscribe to clear-all events
* const unsubscribe = myTestProviderStore.onClearAll(() => {
* clearMyTestResults();
* });
*
* // Later, when no longer needed
* unsubscribe();
* ```
*/
onClearAll: (listener: () => void) => () => void;
};
/**
* Represents a store for a specific test provider, identified by its unique ID. This store provides
* methods to manage the state of an individual test provider, including getting and setting its
* state, running operations with automatic state management, and accessing its unique identifier.
*
* Each test provider has its own instance of this store, allowing for independent state management
* across different test providers in the application.
*
* @example
*
* ```typescript
* // Get a store for a specific test provider
* const grammarStore = getTestProviderStoreById('addon-grammar');
*
* // Check the current state
* if (grammarStore.getState() === 'test-provider-state:pending') {
* console.log('Grammar tests are ready to run');
* }
*
* // Run tests with automatic state management
* grammarStore.runWithState(async () => {
* await runGrammarTests();
* });
* ```
*
* @see {@link TestProviderState} for possible state values
* @see {@link BaseTestProviderStore} for methods inherited from the base store
*/
type TestProviderStoreById = BaseTestProviderStore & {
/**
* Gets the current state of this specific test provider
*
* The state represents the current execution status of the test provider, which can be one of the
* following:
*
* - 'test-provider-state:pending': Tests have not been run yet
* - 'test-provider-state:running': Tests are currently running
* - 'test-provider-state:succeeded': Tests completed successfully
* - 'test-provider-state:crashed': Running tests failed or encountered an error
*
* Storybook UI will use this state to determine what to show in the UI.
*
* @remarks
* The 'test-provider-state:crashed' is meant to signify that the test run as a whole failed to
* execute for some reason. It should _not_ be set just because a number of tests failed, use
* statuses and the status store for that. See {@link TestStatusStore} for managing individual test
* statuses.
* @example
*
* ```typescript
* // Get the current state of a specific test provider
* const state = testProviderStore.getState();
*
* // Conditionally render UI based on the state
* const TestStatus = () => {
* const state = testProviderStore.getState();
*
* if (state === 'test-provider-state:running') {
* return <Spinner />;
* } else if (state === 'test-provider-state:succeeded') {
* return <SuccessIcon />;
* } else if (state === 'test-provider-state:crashed') {
* return <ErrorIcon />;
* }
*
* return <PendingIcon />;
* };
* ```
*/
getState: () => TestProviderState;
/**
* Sets the state of this specific test provider
*
* This method allows you to manually update the execution state of the test provider. It's
* typically used when you need to reflect the current status of test execution in the UI or when
* you want to programmatically control the test provider's state.
*
* Common use cases include:
*
* - Setting to 'running' when tests start
* - Setting to 'succeeded' when tests complete successfully
* - Setting to 'crashed' when tests fail or encounter errors
* - Setting to 'pending' to reset the state
*
* The state represents the current execution status of the test provider, which can be one of the
* following:
*
* - 'test-provider-state:pending': Tests have not been run yet
* - 'test-provider-state:running': Tests are currently running
* - 'test-provider-state:succeeded': Tests completed successfully
* - 'test-provider-state:crashed': Running tests failed or encountered an error
*
* Storybook UI will use this state to determine what to show in the UI.
*
* @remarks
* The 'test-provider-state:crashed' is meant to signify that the test run as a whole failed to
* execute for some reason. It should _not_ be set just because a number of tests failed, use
* statuses and the status store for that. See {@link TestStatusStore} for managing individual test
* statuses.
*
* For most use cases, consider using {@link runWithState} instead, which provides automatic state
* management and error handling during test execution.
* @example
*
* ```typescript
* // Update the state when tests start running
* const startTests = async () => {
* testProviderStore.setState('test-provider-state:running');
* ... run tests ...
* };
* ```
*/
setState: (state: TestProviderState) => void;
/**
* Runs a callback and automatically updates the test provider's state with running, succeeded or
* crashed, depending on the end result.
*
* - Immediately changes the state to 'running'
* - If the callback returns/resolves, change the state to 'succeeded'.
* - If the callback throws an error/rejects, change the state to 'crashed'.
*
* This approach helps prevent state inconsistencies that might occur if exceptions are thrown
* during test execution.
*
* @example
*
* ```typescript
* // Run tests with automatic state management
* const runTests = () => {
* testProviderStore.runWithState(async () => {
* // The state is automatically set to 'running' before this callback
*
* // Run tests here...
* const results = await executeTests();
* });
* };
* ```
*/
runWithState: (callback: () => void | Promise<void>) => Promise<void>;
/** The unique identifier for this test provider */
testProviderId: TestProviderId;
};
/** Arguments handed to an {@link ImportParser} when the registry dispatches a file to it. */
interface ParseFileArgs {
filePath: string;
source: string;
}
/**
* Services passed to every parser. SFC parsers (vue, svelte) extract a `<script>` block
* and then delegate the actual import-edge extraction to the built-in oxc wrapper via
* {@link ImportParserContext.parseScriptWithOxc}.
*/
interface ImportParserContext {
/** Core's oxc-parser wrapper. SFC plugins call this after extracting <script> content. */
parseScriptWithOxc(source: string, virtualFilePath: string): Promise<ImportEdge[]>;
}
/**
* A parser plugin that claims one or more file extensions and knows how to extract import
* edges from that file type. May be registered with a {@link ParserRegistry} directly or
* surfaced through the `experimental_importParsers` preset key.
*
* Extensions are compared with `path.extname(filePath).toLowerCase()` lookup — compound
* extensions such as `.svelte.ts` are NOT supported here (only the last segment matches).
*/
interface ImportParser {
/**
* Lowercase, leading dot. Compound extensions like `.svelte.ts` are NOT supported here —
* `path.extname` returns the last segment only, so `.svelte.ts` matches `.ts`.
*/
extensions: readonly string[];
parse(args: ParseFileArgs, ctx: ImportParserContext): Promise<ImportEdge[]>;
}
/** JSON-serializable reverse index shape stored in open-service state. */
type StoriesByFileRecord = Record<string, Record<string, number>>;
type ErrorLike = {
message: string;
name?: string;
stack?: string;
cause?: ErrorLike;
};
type ModuleGraphStatus = {
value: 'booting';
} | {
value: 'ready';
} | {
value: 'error';
error: ErrorLike;
} | {
value: 'unavailable';
reason: string;
error?: ErrorLike;
};
type ModuleGraphServiceState = {
/** Project root used to normalize absolute file paths in query inputs. */
workingDir: string;
status: ModuleGraphStatus;
graphRevision: number;
storiesByFile: StoriesByFileRecord;
/**
* Per-story revision stamps keyed by story-index-style relative path. Each entry holds the
* {@link graphRevision} at which that story's subgraph last changed. Seeded to `0` for every
* story at snapshot time so scoped `graphRevision` reads observe existing keys.
*/
storyChangeRevisions: Record<string, number>;
latestChangedStoryFiles: string[];
};
/** The Standard Typed interface. This is a base type extended by other specs. */
interface StandardTypedV1<Input = unknown, Output = Input> {
/** The Standard properties. */
readonly "~standard": StandardTypedV1.Props<Input, Output>;
}
declare namespace StandardTypedV1 {
/** The Standard Typed properties interface. */
interface Props<Input = unknown, Output = Input> {
/** The version number of the standard. */
readonly version: 1;
/** The vendor name of the schema library. */
readonly vendor: string;
/** Inferred types associated with the schema. */
readonly types?: Types<Input, Output> | undefined;
}
/** The Standard Typed types interface. */
interface Types<Input = unknown, Output = Input> {
/** The input type of the schema. */
readonly input: Input;
/** The output type of the schema. */
readonly output: Output;
}
/** Infers the input type of a Standard Typed. */
type InferInput<Schema extends StandardTypedV1> = NonNullable<Schema["~standard"]["types"]>["input"];
/** Infers the output type of a Standard Typed. */
type InferOutput<Schema extends StandardTypedV1> = NonNullable<Schema["~standard"]["types"]>["output"];
}
/** The Standard Schema interface. */
interface StandardSchemaV1<Input = unknown, Output = Input> {
/** The Standard Schema properties. */
readonly "~standard": StandardSchemaV1.Props<Input, Output>;
}
declare namespace StandardSchemaV1 {
/** The Standard Schema properties interface. */
interface Props<Input = unknown, Output = Input> extends StandardTypedV1.Props<Input, Output> {
/** Validates unknown input values. */
readonly validate: (value: unknown, options?: StandardSchemaV1.Options | undefined) => Result<Output> | Promise<Result<Output>>;
}
/** The result interface of the validate function. */
type Result<Output> = SuccessResult<Output> | FailureResult;
/** The result interface if validation succeeds. */
interface SuccessResult<Output> {
/** The typed output value. */
readonly value: Output;
/** A falsy value for `issues` indicates success. */
readonly issues?: undefined;
}
interface Options {
/** Explicit support for additional vendor-specific parameters, if needed. */
readonly libraryOptions?: Record<string, unknown> | undefined;
}
/** The result interface if validation fails. */
interface FailureResult {
/** The issues of failed validation. */
readonly issues: ReadonlyArray<Issue>;
}
/** The issue interface of the failure output. */
interface Issue {
/** The error message of the issue. */
readonly message: string;
/** The path of the issue, if any. */
readonly path?: ReadonlyArray<PropertyKey | PathSegment> | undefined;
}
/** The path segment interface of the issue. */
interface PathSegment {
/** The key representing a path segment. */
readonly key: PropertyKey;
}
/** The Standard types interface. */
interface Types<Input = unknown, Output = Input> extends StandardTypedV1.Types<Input, Output> {
}
/** Infers the input type of a Standard. */
type InferInput<Schema extends StandardTypedV1> = StandardTypedV1.InferInput<Schema>;
/** Infers the output type of a Standard. */
type InferOutput<Schema extends StandardTypedV1> = StandardTypedV1.InferOutput<Schema>;
}
/** Generic Standard Schema constraint used across open-service definitions. */
type AnySchema = StandardSchemaV1<unknown, unknown>;
/** Stable alias for service identifiers across definition, runtime, and registration APIs. */
type ServiceId = string;
/**
* Constrains a service's state to a plain object — the only shape the architecture supports.
*
* This is not an arbitrary restriction; two layers require it:
*
* 1. State is wrapped in a `deepSignal` proxy for fine-grained per-field reactivity, and `deepSignal`
* throws ("this object can't be observed") on primitives, `null`, and `undefined` — there are no
* fields to track on a scalar.
* 2. Cross-peer sync (`applyStatePatch` in `service-sync.ts`) merges state by walking object keys;
* it has no notion of replacing a whole scalar, so the wire protocol only carries keyed objects.
*
* Arrays are technically observable by `deepSignal` but are still rejected here: `applyStatePatch`
* replaces arrays wholesale rather than merging by key, so a *top-level* array state would silently
* fail to sync between peers. Wrap collections in a field instead (`{ items: [...] }`).
*
* Authoring helpers pair this with an `extends object` bound (which rejects primitives, `null`, and
* `undefined` while still accepting both `interface` and `type` declarations). The naked `TState` in
* the intersection keeps it transparent to inference; only an array collapses to the branded error.
*/
type ServiceState<TState> = TState & (TState extends readonly unknown[] ? {
__openServiceStateError: 'Service state must be a plain object, not an array.';
} : unknown);
/** Public schema shape exposed when describing a schema-backed service contract. */
type SchemaDescriptor = AnySchema;
/** Raw caller-facing value type accepted by a schema-backed operation. */
type InferSchemaInput<TSchema extends AnySchema> = StandardSchemaV1.InferInput<TSchema>;
/** Parsed value type produced by a schema after validation. */
type InferSchemaOutput<TSchema extends AnySchema> = StandardSchemaV1.InferOutput<TSchema>;
/**
* Named schema maps are the core inference surface for inline open-service authoring.
*
* `defineService()` infers one input-schema map and one output-schema map per operation family
* (queries and commands). Keeping those maps separate gives TypeScript a place to correlate the
* `input` and `output` properties of each inline object before it contextually types sibling
* callbacks like `handler`, `load`, `staticPath`, and `staticInputs`.
*/
type OperationInputSchemas = Record<string, AnySchema>;
/**
* Output-schema maps must stay key-aligned with their input-schema map.
*
* The authoring helper uses this alias instead of a plain `Record<string, AnySchema>` so each
* operation key retains its own input/output schema pair during inference.
*/
type MatchingOutputSchemas<TInputSchemas extends OperationInputSchemas> = {
[TKey in keyof TInputSchemas]: AnySchema;
};
/**
* Internal utility used to keep handler maps assignable without collapsing everything to `unknown`.
*/
type BivariantCallback<TArgs extends unknown[], TResult> = {
bivarianceHack(...args: TArgs): TResult;
}['bivarianceHack'];
/** Runtime shape shared by all command collections after they are built. */
type Command = Record<string, (input: unknown) => Promise<unknown>>;
/**
* Runtime command map derived directly from the inferred command schema maps.
*
* Queries only need command-call typing, not the full command definition objects, so this helper
* keeps query contexts readable while still preserving exact input/output types per command.
*/
type CommandFunctions<TCommandInputSchemas extends OperationInputSchemas, TCommandOutputSchemas extends MatchingOu