storybook
Version:
Storybook: Develop, document, and test UI components in isolation
2,764 lines • 116 kB
JavaScript
import CJS_COMPAT_NODE_URL_mtlvjn99jzq from 'node:url';
import CJS_COMPAT_NODE_PATH_mtlvjn99jzq from 'node:path';
import CJS_COMPAT_NODE_MODULE_mtlvjn99jzq from "node:module";
var __filename = CJS_COMPAT_NODE_URL_mtlvjn99jzq.fileURLToPath(import.meta.url);
var __dirname = CJS_COMPAT_NODE_PATH_mtlvjn99jzq.dirname(__filename);
var require = CJS_COMPAT_NODE_MODULE_mtlvjn99jzq.createRequire(import.meta.url);
// ------------------------------------------------------------
// end of CJS compatibility banner, injected by Storybook's esbuild configuration
// ------------------------------------------------------------
import {
require_build
} from "./chunk-UC4F2QRX.js";
import {
pLimit
} from "./chunk-ZDJEIOKL.js";
import {
isI18nPackage,
isRouterPackage,
isStateManagementPackage,
isStylingPackage
} from "./chunk-YO64L2UI.js";
import {
STORYBOOK_FN_PLACEHOLDER,
generateDummyArgsFromArgTypes,
loadConfig,
printConfig
} from "./chunk-FMSFP5MW.js";
import {
optionalEnvToBoolean,
registerService
} from "./chunk-D6ZWRSJD.js";
import {
array,
description,
lazy,
literal,
number,
object,
optional,
pipe,
record,
string,
undefined_,
variant,
void_
} from "./chunk-KTSPGFK3.js";
import {
UniversalStoreFollowerTimeoutError
} from "./chunk-5WEZSHID.js";
import {
up
} from "./chunk-TVIZSSZ5.js";
import {
resolvePackageDir
} from "./chunk-JGHKZTLJ.js";
import {
jsTsSourceExtensions
} from "./chunk-PWDNJ5SQ.js";
import {
StorybookError
} from "./chunk-3JNEQH4R.js";
import {
require_dist
} from "./chunk-X57LZZTY.js";
import {
require_picocolors
} from "./chunk-Z7YDYOPD.js";
import {
errorToErrorLike,
reverseIndexToStoriesByFile,
toStoryIndexPath
} from "./chunk-VVXOTKLK.js";
import {
extname,
join,
normalize,
relative
} from "./chunk-XQXO4MLK.js";
import {
glob
} from "./chunk-YLFBDO77.js";
import {
__toESM
} from "./chunk-BSXWL7AQ.js";
// src/core-server/utils/server-statics.ts
import { existsSync, statSync } from "node:fs";
import { readFile, stat } from "node:fs/promises";
import { basename, isAbsolute, join as join2, posix, resolve, sep, win32 } from "node:path";
import {
getDirectoryFromWorkingDir,
getProjectRoot,
resolvePathInStorybookCache
} from "storybook/internal/common";
import { CLI_COLORS, logger, once } from "storybook/internal/node-logger";
var import_picocolors = __toESM(require_picocolors(), 1), import_sirv = __toESM(require_build(), 1), import_ts_dedent = __toESM(require_dist(), 1);
var cacheDir = resolvePathInStorybookCache("", "ignored-sub").split("ignored-sub")[0], files = /* @__PURE__ */ new Map(), readFileOnce = async (path) => {
if (files.has(path))
return files.get(path);
{
let [data, stats] = await Promise.all([readFile(path, "utf-8"), stat(path)]), result = { data, mtime: stats.mtimeMs };
return files.set(path, result), result;
}
}, faviconWrapperPath = join2(
resolvePackageDir("storybook"),
"/assets/browser/favicon-wrapper.svg"
), prepareNestedSvg = (svg) => {
let [, openingTag, contents, closingTag] = svg?.match(/(<svg[^>]*>)(.*?)(<\/svg>)/s) ?? [];
if (!openingTag || !contents || !closingTag)
return svg;
let width, height, modifiedTag = openingTag.replace(/width=["']([^"']*)["']/g, (_, value) => (width = parseFloat(value), 'width="32px"')).replace(/height=["']([^"']*)["']/g, (_, value) => (height = parseFloat(value), 'height="32px"'));
return !/viewBox=["'][^"']*["']/.test(modifiedTag) && width && height && (modifiedTag = modifiedTag.replace(/>$/, ` viewBox="0 0 ${width} ${height}">`)), modifiedTag = modifiedTag.replace(/preserveAspectRatio=["'][^"']*["']/g, "").replace(/>$/, ' preserveAspectRatio="xMidYMid meet">'), modifiedTag + contents + closingTag;
};
async function useStatics(app, options) {
let staticDirs = await options.presets.apply("staticDirs") ?? [], faviconPath = await options.presets.apply("favicon"), faviconDir = resolve(faviconPath, ".."), faviconFile = basename(faviconPath);
app.use(`/${faviconFile}`, async (req, res, next) => {
let status = req.query.status;
if (status && faviconFile.endsWith(".svg") && ["active", "critical", "negative", "positive", "warning"].includes(status)) {
let [faviconInfo, faviconWrapperInfo] = await Promise.all([
readFileOnce(join2(faviconDir, faviconFile)),
readFileOnce(faviconWrapperPath)
]).catch((e) => (e instanceof Error && once.warn(`Failed to read favicon: ${e.message}`), [null, null]));
if (faviconInfo && faviconWrapperInfo) {
let svg = faviconWrapperInfo.data.replace('<g id="mask"', `<g mask="url(#${status}-mask)"`).replace('<use id="status"', `<use href="#${status}"`).replace('<use id="icon" />', prepareNestedSvg(faviconInfo.data));
res.setHeader("Content-Type", "image/svg+xml"), res.setHeader("ETag", `"${faviconWrapperInfo.mtime}-${faviconInfo.mtime}"`), res.end(svg);
return;
}
}
return req.url = `/${faviconFile}`, sirvWorkaround(faviconDir)(req, res, next);
}), staticDirs.map((dir) => {
try {
let { staticDir, staticPath, targetEndpoint } = mapStaticDir(dir, options.configDir);
if (!targetEndpoint.startsWith("/sb-") && !staticDir.startsWith(cacheDir)) {
let relativeStaticDir = relative(getProjectRoot(), staticDir);
logger.debug(
`Serving static files from ${CLI_COLORS.info(relativeStaticDir)} at ${CLI_COLORS.info(targetEndpoint)}`
);
}
if (existsSync(staticPath) && statSync(staticPath).isFile()) {
let staticPathDir = resolve(staticPath, ".."), staticPathFile = basename(staticPath);
app.use(targetEndpoint, (req, res, next) => {
req.url = `/${staticPathFile}`, sirvWorkaround(staticPathDir)(req, res, next);
});
} else
app.use(targetEndpoint, sirvWorkaround(staticPath));
} catch (e) {
e instanceof Error && logger.warn(e.message);
}
});
}
var sirvWorkaround = (dir, opts = {}) => (req, res, next) => {
let originalParsedUrl = req._parsedUrl, maybeNext = next ? () => {
req._parsedUrl = originalParsedUrl, next();
} : void 0;
(0, import_sirv.default)(dir, { dev: !0, etag: !0, extensions: [], ...opts })(req, res, maybeNext);
}, parseStaticDir = (arg) => {
let lastColonIndex = arg.lastIndexOf(":"), isWindowsRawDirOnly = win32.isAbsolute(arg) && lastColonIndex === 1, splitIndex = lastColonIndex !== -1 && !isWindowsRawDirOnly ? lastColonIndex : arg.length, [from, to] = [arg.slice(0, splitIndex), arg.slice(splitIndex + 1)], staticDir = isAbsolute(from) ? from : `./${from}`, staticPath = resolve(staticDir);
if (!existsSync(staticPath))
throw new Error(
import_ts_dedent.dedent`
Failed to load static files, no such directory: ${import_picocolors.default.cyan(staticPath)}
Make sure this directory exists.
`
);
let targetDir = (to || (statSync(staticPath).isFile() ? basename(staticPath) : "/")).split(sep).join(posix.sep).replace(/^\/?/, "./"), targetEndpoint = targetDir.substring(1);
return { staticDir, staticPath, targetDir, targetEndpoint };
}, mapStaticDir = (staticDir, configDir) => {
let specifier = typeof staticDir == "string" ? staticDir : `${staticDir.from}:${staticDir.to}`, normalizedDir = isAbsolute(specifier) ? specifier : getDirectoryFromWorkingDir({ configDir, workingDir: process.cwd(), directory: specifier });
return parseStaticDir(normalizedDir);
};
// src/shared/universal-store/index.ts
var import_ts_dedent2 = __toESM(require_dist(), 1);
// src/shared/universal-store/instances.ts
var instances = /* @__PURE__ */ new Map();
// src/shared/universal-store/index.ts
var CHANNEL_EVENT_PREFIX = "UNIVERSAL_STORE:", ProgressState = {
PENDING: "PENDING",
RESOLVED: "RESOLVED",
REJECTED: "REJECTED"
}, UniversalStore = class _UniversalStore {
constructor(options, environmentOverrides) {
/** Enable debug logs for this store */
this.debugging = !1;
// TODO: narrow type of listeners based on event type
this.listeners = /* @__PURE__ */ new Map([["*", /* @__PURE__ */ new Set()]]);
/** Gets the current state */
this.getState = () => (this.debug("getState", { state: this.state }), this.state);
/**
* Subscribes to store events
*
* @returns A function to unsubscribe
*/
this.subscribe = (eventTypeOrListener, maybeListener) => {
let subscribesToAllEvents = typeof eventTypeOrListener == "function", eventType = subscribesToAllEvents ? "*" : eventTypeOrListener, listener = subscribesToAllEvents ? eventTypeOrListener : maybeListener;
if (this.debug("subscribe", { eventType, listener }), !listener)
throw new TypeError(
`Missing first subscribe argument, or second if first is the event type, when subscribing to a UniversalStore with id '${this.id}'`
);
return this.listeners.has(eventType) || this.listeners.set(eventType, /* @__PURE__ */ new Set()), this.listeners.get(eventType).add(listener), () => {
this.debug("unsubscribe", { eventType, listener }), this.listeners.has(eventType) && (this.listeners.get(eventType).delete(listener), this.listeners.get(eventType)?.size === 0 && this.listeners.delete(eventType));
};
};
/** Sends a custom event to the other stores */
this.send = (event) => {
if (this.debug("send", { event }), this.status !== _UniversalStore.Status.READY)
throw new TypeError(
import_ts_dedent2.dedent`Cannot send event before store is ready. You can get the current status with store.status,
or await store.readyPromise to wait for the store to be ready before sending events.
${JSON.stringify(
{
event,
id: this.id,
actor: this.actor,
environment: this.environment
},
null,
2
)}`
);
this.emitToListeners(event, { actor: this.actor }), this.emitToChannel(event, { actor: this.actor });
};
if (this.debugging = options.debug ?? !1, !_UniversalStore.isInternalConstructing)
throw new TypeError(
"UniversalStore is not constructable - use UniversalStore.create() instead"
);
if (_UniversalStore.isInternalConstructing = !1, this.id = options.id, this.actorId = Date.now().toString(36) + Math.random().toString(36).substring(2), this.actorType = options.leader ? _UniversalStore.ActorType.LEADER : _UniversalStore.ActorType.FOLLOWER, this.state = options.initialState, this.channelEventName = `${CHANNEL_EVENT_PREFIX}${this.id}`, this.debug("constructor", {
options,
environmentOverrides,
channelEventName: this.channelEventName
}), this.actor.type === _UniversalStore.ActorType.LEADER)
this.syncing = {
state: ProgressState.RESOLVED,
promise: Promise.resolve()
};
else {
let syncingResolve, syncingReject, syncingPromise = new Promise((resolve2, reject) => {
syncingResolve = () => {
this.syncing.state === ProgressState.PENDING && (this.syncing.state = ProgressState.RESOLVED, resolve2());
}, syncingReject = (reason) => {
this.syncing.state === ProgressState.PENDING && (this.syncing.state = ProgressState.REJECTED, reject(reason));
};
});
this.syncing = {
state: ProgressState.PENDING,
promise: syncingPromise,
resolve: syncingResolve,
reject: syncingReject
};
}
this.getState = this.getState.bind(this), this.setState = this.setState.bind(this), this.subscribe = this.subscribe.bind(this), this.onStateChange = this.onStateChange.bind(this), this.send = this.send.bind(this), this.emitToChannel = this.emitToChannel.bind(this), this.prepareThis = this.prepareThis.bind(this), this.emitToListeners = this.emitToListeners.bind(this), this.handleChannelEvents = this.handleChannelEvents.bind(this), this.debug = this.debug.bind(this), this.channel = environmentOverrides?.channel ?? _UniversalStore.preparation.channel, this.environment = environmentOverrides?.environment ?? _UniversalStore.preparation.environment, this.channel && this.environment ? (environmentOverrides || _UniversalStore.preparation.resolve({
channel: this.channel,
environment: this.environment
}), this.prepareThis({ channel: this.channel, environment: this.environment })) : _UniversalStore.preparation.promise.then(this.prepareThis);
}
static {
/**
* Defines the possible actor types in the store system
*
* @readonly
*/
this.ActorType = {
LEADER: "LEADER",
FOLLOWER: "FOLLOWER"
};
}
static {
/**
* Defines the possible environments the store can run in
*
* @readonly
*/
this.Environment = {
SERVER: "SERVER",
MANAGER: "MANAGER",
PREVIEW: "PREVIEW",
UNKNOWN: "UNKNOWN",
MOCK: "MOCK"
};
}
static {
/**
* Internal event types used for store synchronization
*
* @readonly
*/
this.InternalEventType = {
EXISTING_STATE_REQUEST: "__EXISTING_STATE_REQUEST",
EXISTING_STATE_RESPONSE: "__EXISTING_STATE_RESPONSE",
SET_STATE: "__SET_STATE",
LEADER_CREATED: "__LEADER_CREATED",
FOLLOWER_CREATED: "__FOLLOWER_CREATED"
};
}
static {
this.Status = {
UNPREPARED: "UNPREPARED",
SYNCING: "SYNCING",
READY: "READY",
ERROR: "ERROR"
};
}
static {
// This is used to check if constructor was called from the static factory create()
this.isInternalConstructing = !1;
}
static {
_UniversalStore.setupPreparationPromise();
}
static setupPreparationPromise() {
let resolveRef, rejectRef, promise = new Promise(
(resolve2, reject) => {
resolveRef = (args) => {
resolve2(args);
}, rejectRef = (...args) => {
reject(args);
};
}
);
_UniversalStore.preparation = {
resolve: resolveRef,
reject: rejectRef,
promise
};
}
/** The actor object representing the store instance with a unique ID and a type */
get actor() {
return Object.freeze({
id: this.actorId,
type: this.actorType,
environment: this.environment ?? _UniversalStore.Environment.UNKNOWN
});
}
/**
* 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() {
if (!this.channel || !this.environment)
return _UniversalStore.Status.UNPREPARED;
switch (this.syncing?.state) {
case ProgressState.PENDING:
case void 0:
return _UniversalStore.Status.SYNCING;
case ProgressState.REJECTED:
return _UniversalStore.Status.ERROR;
case ProgressState.RESOLVED:
default:
return _UniversalStore.Status.READY;
}
}
/**
* 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() {
let preparation = this.channel && this.environment ? Promise.resolve() : _UniversalStore.preparation.promise;
return Promise.all([preparation, this.syncing?.promise]);
}
/** Creates a new instance of UniversalStore */
static create(options) {
if (!options || typeof options?.id != "string")
throw new TypeError("id is required and must be a string, when creating a UniversalStore");
options.debug && console.debug(
import_ts_dedent2.dedent`[UniversalStore]
create`,
{ options }
);
let existing = instances.get(options.id);
if (existing)
return console.warn(import_ts_dedent2.dedent`UniversalStore with id "${options.id}" already exists in this environment, re-using existing.
You should reuse the existing instance instead of trying to create a new one.`), existing;
_UniversalStore.isInternalConstructing = !0;
let store = new _UniversalStore(options);
return instances.set(options.id, store), store;
}
/**
* Used by Storybook to set the channel for all instances of UniversalStore in the given
* environment.
*
* @internal
*/
static __prepare(channel, environment) {
_UniversalStore.preparation.channel = channel, _UniversalStore.preparation.environment = environment, _UniversalStore.preparation.resolve({ channel, environment });
}
/**
* Updates the store's state
*
* Either a new state or a state updater function can be passed to the method.
*/
setState(updater) {
let previousState = this.state, newState = typeof updater == "function" ? updater(previousState) : updater;
if (this.debug("setState", { newState, previousState, updater }), this.status !== _UniversalStore.Status.READY)
throw new TypeError(
import_ts_dedent2.dedent`Cannot set state before store is ready. You can get the current status with store.status,
or await store.readyPromise to wait for the store to be ready before sending events.
${JSON.stringify(
{
newState,
id: this.id,
actor: this.actor,
environment: this.environment
},
null,
2
)}`
);
this.state = newState;
let event = {
type: _UniversalStore.InternalEventType.SET_STATE,
payload: {
state: newState,
previousState
}
};
this.emitToChannel(event, { actor: this.actor }), this.emitToListeners(event, { actor: this.actor });
}
/**
* Subscribes to state changes
*
* @returns Unsubscribe function
*/
onStateChange(listener) {
return this.debug("onStateChange", { listener }), this.subscribe(
_UniversalStore.InternalEventType.SET_STATE,
({ payload }, eventInfo) => {
listener(payload.state, payload.previousState, eventInfo);
}
);
}
emitToChannel(event, eventInfo) {
this.debug("emitToChannel", { event, eventInfo, channel: !!this.channel }), this.channel?.emit(this.channelEventName, {
event,
eventInfo
});
}
prepareThis({
channel,
environment
}) {
this.channel = channel, this.environment = environment, this.debug("prepared", { channel: !!channel, environment }), this.channel.on(this.channelEventName, this.handleChannelEvents), this.actor.type === _UniversalStore.ActorType.LEADER ? this.emitToChannel(
{ type: _UniversalStore.InternalEventType.LEADER_CREATED },
{ actor: this.actor }
) : (this.emitToChannel(
{ type: _UniversalStore.InternalEventType.FOLLOWER_CREATED },
{ actor: this.actor }
), this.emitToChannel(
{ type: _UniversalStore.InternalEventType.EXISTING_STATE_REQUEST },
{ actor: this.actor }
), setTimeout(() => {
this.syncing.reject(new UniversalStoreFollowerTimeoutError(this.id));
}, 1e3));
}
emitToListeners(event, eventInfo) {
let eventTypeListeners = this.listeners.get(event.type), everythingListeners = this.listeners.get("*");
this.debug("emitToListeners", {
event,
eventInfo,
eventTypeListeners,
everythingListeners
}), [...eventTypeListeners ?? [], ...everythingListeners ?? []].forEach(
(listener) => listener(event, eventInfo)
);
}
handleChannelEvents(channelEvent) {
let { event, eventInfo } = channelEvent;
if ([eventInfo.actor.id, eventInfo.forwardingActor?.id].includes(this.actor.id)) {
this.debug("handleChannelEvents: Ignoring event from self", { channelEvent });
return;
} else if (this.syncing?.state === ProgressState.PENDING && event.type !== _UniversalStore.InternalEventType.EXISTING_STATE_RESPONSE) {
this.debug("handleChannelEvents: Ignoring event while syncing", { channelEvent });
return;
}
if (this.debug("handleChannelEvents", { channelEvent }), this.actor.type === _UniversalStore.ActorType.LEADER) {
let shouldForwardEvent = !0;
switch (event.type) {
case _UniversalStore.InternalEventType.EXISTING_STATE_REQUEST:
shouldForwardEvent = !1;
let responseEvent = {
type: _UniversalStore.InternalEventType.EXISTING_STATE_RESPONSE,
payload: this.state
};
this.debug("handleChannelEvents: responding to existing state request", {
responseEvent
}), this.emitToChannel(responseEvent, { actor: this.actor }), this.emitToListeners(responseEvent, { actor: this.actor });
break;
case _UniversalStore.InternalEventType.LEADER_CREATED:
shouldForwardEvent = !1, this.syncing.state = ProgressState.REJECTED, this.debug("handleChannelEvents: erroring due to second leader being created", {
event
}), console.error(
import_ts_dedent2.dedent`Detected multiple UniversalStore leaders created with the same id "${this.id}".
Only one leader can exists at a time, your stores are now in an invalid state.
Leaders detected:
this: ${JSON.stringify(this.actor, null, 2)}
other: ${JSON.stringify(eventInfo.actor, null, 2)}`
);
break;
}
shouldForwardEvent && (this.debug("handleChannelEvents: forwarding event", { channelEvent }), this.emitToChannel(event, { actor: eventInfo.actor, forwardingActor: this.actor }));
}
if (this.actor.type === _UniversalStore.ActorType.FOLLOWER)
switch (event.type) {
case _UniversalStore.InternalEventType.EXISTING_STATE_RESPONSE:
if (this.debug("handleChannelEvents: Setting state from leader's existing state response", {
event
}), this.syncing?.state !== ProgressState.PENDING)
break;
this.syncing.resolve?.();
let setStateEvent = {
type: _UniversalStore.InternalEventType.SET_STATE,
payload: {
state: event.payload,
previousState: this.state
}
};
this.state = event.payload, this.emitToListeners(setStateEvent, eventInfo);
break;
}
event.type === _UniversalStore.InternalEventType.SET_STATE && (this.debug("handleChannelEvents: Setting state", { event }), this.state = event.payload.state), this.emitToListeners(event, { actor: eventInfo.actor });
}
debug(message, data) {
this.debugging && console.debug(
import_ts_dedent2.dedent`[UniversalStore::${this.id}::${this.environment ?? _UniversalStore.Environment.UNKNOWN}]
${message}`,
JSON.stringify(
{
data,
actor: this.actor,
state: this.state,
status: this.status
},
null,
2
)
);
}
/**
* Used to reset the static fields of the UniversalStore class when cleaning up tests
*
* @internal
*/
static __reset() {
_UniversalStore.preparation.reject(new Error("reset")), _UniversalStore.setupPreparationPromise(), _UniversalStore.isInternalConstructing = !1;
}
};
// src/shared/open-service/service-definition.ts
var defineService = (def) => def;
// src/shared/open-service/services/module-graph/server.ts
import { STORY_INDEX_INVALIDATED } from "storybook/internal/core-events";
// src/shared/open-service/services/module-graph-index/definition.ts
var storyIndexPathSchema = pipe(
string(),
description("A story-index-style relative path such as `./src/Button.stories.tsx`.")
), storyDependencyDepthSchema = pipe(
number(),
description(
"Breadth-first-search depth: the shortest number of import edges between the source file and this story file."
)
), storiesByFileSchema = record(
storyIndexPathSchema,
record(storyIndexPathSchema, storyDependencyDepthSchema)
), moduleGraphIndexServiceDef = defineService({
id: "core/module-graph-index",
internal: !0,
description: "Reverse index from source files to story files. Paired with `core/module-graph` (revisions/status); updated only when the index structure moves, not on bump-only patches.",
initialState: {
workingDir: process.cwd(),
storiesByFile: {}
},
queries: {
_storiesForFiles: {
internal: !0,
description: "Internal lookup used by `core/module-graph.storiesForFiles`. Prefer the hot service query from consumers.",
input: object({
files: pipe(
array(
pipe(
string(),
description(
"Input source file path. Accepts absolute paths, story-index-style relative paths with `./`, or relative paths without `./`."
)
)
),
description("Source files to look up. Output arrays match this input order.")
)
}),
output: array(
array(
object({
storyFile: pipe(
storyIndexPathSchema,
description(
"Affected story file, returned in the same `./`-prefixed relative import-path format used by the story index."
)
),
depth: storyDependencyDepthSchema
})
)
),
handler: (input, ctx) => {
let { workingDir } = ctx.self.state;
return input.files.map((file) => {
let entries = ctx.self.state.storiesByFile[toStoryIndexPath(file, workingDir)];
return entries ? Object.entries(entries).map(([storyFile, depth]) => ({
storyFile,
depth
})) : [];
});
}
}
},
commands: {
_applyIndex: {
internal: !0,
description: "Replaces the reverse index. Called by `core/module-graph` snapshot/update commands when the index moved.",
input: object({
storiesByFile: pipe(
storiesByFileSchema,
description(
"Complete relative reverse index keyed by story-index-style source file paths. Values map affected story-index-style story file paths to breadth-first-search depths."
)
)
}),
output: void_(),
handler: async (input, ctx) => {
ctx.self.setState((state) => {
state.storiesByFile = input.storiesByFile;
});
}
}
}
});
// src/shared/open-service/services/module-graph-index/server.ts
function registerModuleGraphIndexService(workingDir = process.cwd()) {
return registerService({
...moduleGraphIndexServiceDef,
initialState: {
...moduleGraphIndexServiceDef.initialState,
workingDir
}
});
}
// src/shared/open-service/services/module-graph/definition.ts
var errorLikeSchema = object({
message: pipe(string(), description("Human-readable error message.")),
name: optional(pipe(string(), description("Error class/name, when available."))),
stack: optional(pipe(string(), description("Stack trace, when available."))),
cause: optional(lazy(() => errorLikeSchema))
}), moduleGraphStatusSchema = variant("value", [
object({
value: literal("booting")
}),
object({
value: literal("ready")
}),
object({
value: literal("error"),
error: pipe(
errorLikeSchema,
description("Serializable error describing why the module graph failed unexpectedly.")
)
}),
object({
value: literal("unavailable"),
reason: pipe(
string(),
description(
"Human-readable reason why the current builder/runtime cannot provide module graph functionality."
)
),
error: optional(
pipe(
errorLikeSchema,
description("Optional serializable error reported by the builder adapter.")
)
)
})
]), storyIndexPathSchema2 = pipe(
string(),
description("A story-index-style relative path such as `./src/Button.stories.tsx`.")
), storyDependencyDepthSchema2 = pipe(
number(),
description(
"Breadth-first-search depth: the shortest number of import edges between the source file and this story file."
)
), storiesByFileSchema2 = record(
storyIndexPathSchema2,
record(storyIndexPathSchema2, storyDependencyDepthSchema2)
), noInputSchema = undefined_(), moduleGraphServiceDef = defineService({
id: "core/module-graph",
description: "Story module dependency graph: status and revision counters for reactive updates. The reverse index lives in `core/module-graph-index`.",
initialState: {
workingDir: process.cwd(),
status: { value: "booting" },
graphRevision: 0,
storyChangeRevisions: {},
latestChangedStoryFiles: []
},
queries: {
storiesForFiles: {
description: "Returns, for each input file (same order), story-index-relative story files that depend on it and their breadth-first-search depth: the shortest number of import edges between the input file and the story file.",
input: object({
files: pipe(
array(
pipe(
string(),
description(
"Input source file path. Accepts absolute paths, story-index-style relative paths with `./`, or relative paths without `./`."
)
)
),
description("Source files to look up. Output arrays match this input order.")
)
}),
output: array(
array(
object({
storyFile: pipe(
storyIndexPathSchema2,
description(
"Affected story file, returned in the same `./`-prefixed relative import-path format used by the story index."
)
),
depth: storyDependencyDepthSchema2
})
)
),
handler: (input, ctx) => ctx.getService("core/module-graph-index").queries._storiesForFiles.get(input)
},
status: {
description: "Current module graph lifecycle status. `booting` means the graph is still expected to become ready; `ready` means query state is populated; `error` means an unexpected graph failure; `unavailable` means the current builder/runtime cannot provide module graph functionality.",
input: noInputSchema,
output: moduleGraphStatusSchema,
load: async (_input, ctx) => {
await ctx.self.commands._waitForSettledEngine(void 0);
},
handler: (_input, ctx) => ctx.self.state.status
},
graphRevision: {
description: "Monotonic revision counter for module graph changes, advanced only by in-graph file changes and story-index reconciliation (out-of-graph file changes never advance it). Omit the input to watch the entire graph. Provide `storyFiles` to scope the watch to specific stories: returns the highest revision at which any of those story subgraphs last changed (0 if none have changed yet, or for unknown stories).",
input: optional(
object({
storyFiles: array(
pipe(
string(),
description(
"Story file to scope the watch to. Accepts absolute paths, story-index-style relative paths with `./`, or relative paths without `./`. Pass an empty array to watch nothing (returns 0)."
)
)
)
})
),
output: number(),
handler: (input, ctx) => {
if (!input)
return ctx.self.state.graphRevision;
if (input.storyFiles.length === 0)
return 0;
let max = 0, { workingDir } = ctx.self.state;
for (let file of input.storyFiles) {
let revision = ctx.self.state.storyChangeRevisions[toStoryIndexPath(file, workingDir)] ?? 0;
revision > max && (max = revision);
}
return max;
}
},
latestStoryChanges: {
description: "Latest story files whose module graph changed, paired with the graph revision that produced the change set.",
input: noInputSchema,
output: object({
revision: pipe(
number(),
description("Graph revision number for this latest story change set.")
),
storyFiles: pipe(
array(storyIndexPathSchema2),
description(
"Story-index-relative story files touched by the latest module graph change set."
)
)
}),
handler: (_input, ctx) => ({
revision: ctx.self.state.graphRevision,
storyFiles: ctx.self.state.latestChangedStoryFiles
})
},
/** @deprecated Use {@link status} instead. */
getStatus: {
description: "Deprecated alias for `status`. Use `status` instead.",
input: noInputSchema,
output: moduleGraphStatusSchema,
handler: (input, ctx) => ctx.self.queries.status.get(input),
load: async (input, ctx) => {
await ctx.self.queries.status.loaded(input);
}
},
/** @deprecated Use {@link graphRevision} instead. */
getGraphRevision: {
description: "Deprecated alias for `graphRevision`. Use `graphRevision` instead.",
input: optional(
object({
storyFiles: array(
pipe(
string(),
description(
"Story file to scope the watch to. Accepts absolute paths, story-index-style relative paths with `./`, or relative paths without `./`. Pass an empty array to watch nothing (returns 0)."
)
)
)
})
),
output: number(),
handler: (input, ctx) => ctx.self.queries.graphRevision.get(input),
load: async (input, ctx) => {
await ctx.self.queries.graphRevision.loaded(input);
}
}
},
commands: {
_applyGraphSnapshot: {
internal: !0,
description: "Replaces the reverse index after the initial graph build. Called by the graph engine, not by external consumers.",
input: object({
storiesByFile: pipe(
storiesByFileSchema2,
description(
"Complete relative reverse index keyed by story-index-style source file paths. Values map affected story-index-style story file paths to breadth-first-search depths."
)
)
}),
output: void_(),
handler: async (input, ctx) => {
await ctx.getService("core/module-graph-index").commands._applyIndex({
storiesByFile: input.storiesByFile
}), ctx.self.setState((state) => {
state.status = { value: "ready" }, state.storyChangeRevisions = {};
for (let stories of Object.values(input.storiesByFile))
for (let storyFile of Object.keys(stories))
state.storyChangeRevisions[storyFile] = 0;
state.latestChangedStoryFiles = [];
});
}
},
_applyGraphUpdate: {
internal: !0,
description: "Optionally replaces the reverse index after an incremental patch and bumps versions for affected story files. Called by the graph engine, not by external consumers.",
input: object({
storiesByFile: optional(
pipe(
storiesByFileSchema2,
description(
"Complete relative reverse index keyed by story-index-style source file paths. Values map affected story-index-style story file paths to breadth-first-search depths. Omitted when the patch left the reverse index unchanged."
)
)
),
bumpedStoryFiles: pipe(
array(storyIndexPathSchema2),
description(
"Story files whose graph changed, using story-index-style relative paths. Each listed file has its version incremented."
)
)
}),
output: void_(),
handler: async (input, ctx) => {
input.storiesByFile !== void 0 && await ctx.getService("core/module-graph-index").commands._applyIndex({
storiesByFile: input.storiesByFile
}), input.bumpedStoryFiles.length !== 0 && ctx.self.setState((state) => {
state.graphRevision += 1, state.latestChangedStoryFiles = input.bumpedStoryFiles;
for (let storyFile of input.bumpedStoryFiles)
state.storyChangeRevisions[storyFile] = state.graphRevision;
});
}
},
_setStatus: {
internal: !0,
description: "Sets the module graph lifecycle status after engine startup, failure, or adapter availability changes.",
input: moduleGraphStatusSchema,
output: void_(),
handler: async (input, ctx) => {
ctx.self.setState((state) => {
state.status = input;
});
}
},
_waitForSettledEngine: {
internal: !0,
description: "Waits for the module graph engine to finish its current build or patch cycle. Handler is supplied at server registration.",
input: noInputSchema,
output: void_()
}
}
});
// src/shared/open-service/services/module-graph/engine/module-graph-engine.ts
import { writeFile } from "node:fs/promises";
import { getProjectRoot as getProjectRoot2 } from "storybook/internal/common";
import { logger as logger4 } from "storybook/internal/node-logger";
// src/shared/open-service/services/module-graph/errors.ts
var ModuleGraphFailureError = class extends Error {
constructor(message, options) {
super(message, options), this.name = "ModuleGraphFailureError";
}
};
// src/shared/open-service/services/module-graph/story-files.ts
var cache = /* @__PURE__ */ new WeakMap();
function getStoryIdsByAbsolutePath(storyIndex, workingDir) {
let cached = cache.get(storyIndex);
if (cached && cached.workingDir === workingDir)
return cached.storyIdsByFile;
let storyIdsByFile = /* @__PURE__ */ new Map();
return Object.values(storyIndex.entries).forEach((entry) => {
if (entry.type === "story" && !entry.importPath.startsWith("virtual:")) {
let filePath = normalize(join(workingDir, entry.importPath)), storyIds = storyIdsByFile.get(filePath) ?? /* @__PURE__ */ new Set();
storyIds.add(entry.id), storyIdsByFile.set(filePath, storyIds);
}
}), cache.set(storyIndex, { workingDir, storyIdsByFile }), storyIdsByFile;
}
// src/shared/open-service/services/module-graph/engine/dependency-graph/dependency-graph-builder.ts
import { cpus } from "node:os";
import { logger as defaultLogger } from "storybook/internal/node-logger";
// src/shared/open-service/services/module-graph/engine/dependency-graph/parse-resolve-cache.ts
import { readFile as readFile2 } from "node:fs/promises";
import { parseBarrelInfo } from "storybook/internal/oxc-parser";
// src/shared/open-service/services/module-graph/engine/dependency-graph/scope.ts
var NODE_MODULES_SEGMENT = "/node_modules/";
function isInsideAnyWorkspace(absolute, workspaceRoots) {
if (absolute.includes(NODE_MODULES_SEGMENT))
return !1;
for (let root of workspaceRoots)
if (absolute === root || absolute.startsWith(root.endsWith("/") ? root : `${root}/`))
return !0;
return !1;
}
function isInScope(absolute, projectRoot, workspaceRoots) {
let projectPrefix = projectRoot.endsWith("/") ? projectRoot : `${projectRoot}/`;
return (absolute === projectRoot || absolute.startsWith(projectPrefix)) && !absolute.includes(NODE_MODULES_SEGMENT) ? !0 : isInsideAnyWorkspace(absolute, workspaceRoots);
}
// src/shared/open-service/services/module-graph/engine/dependency-graph/parse-resolve-cache.ts
var BARREL_FOLLOW_MAX_DEPTH = 10, ParseResolveCache = class {
constructor(opts) {
this.parseCache = /* @__PURE__ */ new Map();
this.resolveCache = /* @__PURE__ */ new Map();
this.barrelInfoCache = /* @__PURE__ */ new Map();
this.registry = opts.registry, this.resolver = opts.resolver, this.workspaceRoots = new Set(Array.from(opts.workspaceRoots, (r) => normalize(r))), this.projectRoot = normalize(opts.projectRoot), this.logger = opts.logger, this.debugTrace = opts.debug ? [] : null;
}
/** Returns accumulated barrel resolution events, or null when debug mode is off. */
getBarrelTrace() {
return this.debugTrace;
}
/**
* Parses the file once and caches the resulting edge list. Returns `[]` for unreadable
* files, parse failures, or files whose extension has no registered parser — callers
* cannot distinguish between "no edges" and "we couldn't look", which is by design:
* either way the file contributes nothing to the dependency graph.
*/
parseOnce(filePath) {
let existing = this.parseCache.get(filePath);
if (existing)
return existing;
let promise = (async () => {
let source;
try {
source = await readFile2(filePath, "utf8");
} catch (error) {
return this.logger.debug(
`Change detection: could not read ${filePath}: ${error instanceof Error ? error.message : String(error)}`
), [];
}
try {
return await this.registry.parse(filePath, source) ?? [];
} catch (error) {
return this.logger.debug(
`Change detection: failed to parse ${filePath}: ${error instanceof Error ? error.message : String(error)}`
), [];
}
})();
return this.parseCache.set(filePath, promise), promise;
}
/** Resolves every in-scope edge declared by `filePath` and returns the dep Set. */
resolveOnce(filePath) {
let existing = this.resolveCache.get(filePath);
if (existing)
return existing;
let promise = (async () => {
let edges = await this.parseOnce(filePath), deps = /* @__PURE__ */ new Set();
for (let edge of edges) {
let resolved = await this.resolver.resolve(filePath, edge.specifier);
if (resolved === null) {
this.logger.debug(`Could not resolve ${edge.specifier} from ${filePath}`);
continue;
}
let normalised = normalize(resolved);
if (isInScope(normalised, this.projectRoot, this.workspaceRoots)) {
if (edge.importedNames !== null && edge.importedNames.size > 0) {
let { sources, barrels, needBarrel } = await this.followBarrel(
normalised,
edge.importedNames
);
this.debugTrace?.push({
from: filePath,
specifier: edge.specifier,
barrel: normalised,
names: Array.from(edge.importedNames),
resolved: Array.from(sources),
needBarrel
});
for (let src of sources)
deps.add(src);
for (let barrel of barrels)
deps.add(barrel);
}
deps.add(normalised);
}
}
return deps;
})();
return this.resolveCache.set(filePath, promise), promise;
}
/**
* For each name in `requestedNames`, walks the barrel chain starting at `barrelPath`
* until it reaches the actual source file that defines the symbol. Handles multi-level
* chains where intermediate barrels use `export * from '...'` by recursing through them.
*
* Returns `needBarrel = true` for any name that could not be fully resolved so the
* caller falls back to including the barrel itself.
*/
async followBarrel(barrelPath, requestedNames) {
let barrels = /* @__PURE__ */ new Set(), results = await Promise.all(
Array.from(requestedNames, (name) => this.followName(barrelPath, name, /* @__PURE__ */ new Set(), 0, barrels))
), sources = /* @__PURE__ */ new Set(), needBarrel = !1;
for (let source of results)
source !== null ? sources.add(source) : needBarrel = !0;
return { sources, barrels, needBarrel };
}
/**
* Recursively follows a single exported name through barrel re-exports.
*
* 1. Checks named re-exports in `barrelPath`; if found, resolves the specifier and
* recurses with the inner name in case the target is itself a barrel.
* 2. Falls through to wildcard re-exports (`export * from '...'`) and searches each
* transitively until the name is found or all paths are exhausted.
*
* Returns the normalised absolute path of the first non-barrel source found, or `null`
* when the chain is unresolvable (triggering the conservative `needBarrel` fallback).
* Cycle detection via `visited`; depth limit of 10 hops prevents infinite recursion.
*/
async followName(barrelPath, name, visited, depth, barrels) {
if (depth > BARREL_FOLLOW_MAX_DEPTH)
return this.logger.debug(
`Change detection: barrel chain depth limit reached at ${barrelPath} (looking for "${name}")`
), null;
if (visited.has(barrelPath))
return null;
visited.add(barrelPath), barrels.add(barrelPath);
let info = await this.barrelInfoOnce(barrelPath), entry = info.named.get(name);
if (entry) {
let sourceResolved = await this.resolver.resolve(barrelPath, entry.specifier);
if (sourceResolved !== null) {
let sourceNorm = normalize(sourceResolved);
if (isInScope(sourceNorm, this.projectRoot, this.workspaceRoots))
return await this.followName(
sourceNorm,
entry.importedName,
new Set(visited),
depth + 1,
barrels
) ?? sourceNorm;
}
return null;
}
for (let wildcardSpec of info.wildcards) {
let wildcardResolved = await this.resolver.resolve(barrelPath, wildcardSpec);
if (wildcardResolved === null)
continue;
let wildcardNorm = normalize(wildcardResolved);
if (!isInScope(wildcardNorm, this.projectRoot, this.workspaceRoots))
continue;
let result = await this.followName(
wildcardNorm,
name,
new Set(visited),
depth + 1,
barrels
);
if (result !== null)
return result;
}
return null;
}
/**
* Lazily parses and caches the barrel info (named re-exports + wildcard specifiers)
* for `filePath`. Returns empty info for files that cannot be read or have no exports.
*/
barrelInfoOnce(filePath) {
let existing = this.barrelInfoCache.get(filePath);
if (existing)
return existing;
let promise = (async () => {
let source;
try {
source = await readFile2(filePath, "utf8");
} catch {
return { named: /* @__PURE__ */ new Map(), wildcards: [] };
}
try {
return await parseBarrelInfo(filePath, source);
} catch {
return { named: /* @__PURE__ */ new Map(), wildcards: [] };
}
})();
return this.barrelInfoCache.set(filePath, promise), promise;
}
/** Drops all cached entries for `filePath`. Call on every `change`/`unlink` event. */
invalidate(filePath) {
this.parseCache.delete(filePath), this.resolveCache.delete(filePath), this.barrelInfoCache.delete(filePath);
}
/** Test-only: full reset. */
clear() {
this.parseCache.clear(), this.resolveCache.clear(), this.barrelInfoCache.clear();
}
};
// src/shared/open-service/services/module-graph/engine/dependency-graph/reverse-index.ts
var ReverseIndexImpl = class {
constructor() {
this.index = /* @__PURE__ */ new Map();
/** Forward mapping from story file -> Set of dep files it reaches. */
this.forwardIndex = /* @__PURE__ */ new Map();
// Bumped only when a mutator actually changes index contents. Callers compare before/after
// to learn whether the reverse index moved, without reverse-engineering patch control flow.
this._revision = 0;
}
get revision() {
return this._revision;
}
/** Records (or updates with min) the depth for (dep, story). */
record(dep, story, depth) {
let inner = this.index.get(dep);
inner || (inner = /* @__PURE__ */ new Map(), this.index.set(dep, inner));
let previous = inner.get(story);
if (previous === void 0 || depth < previous) {
inner.set(story, depth);
let deps = this.forwardIndex.get(story);
deps || (deps = /* @__PURE__ */ new Set(), this.forwardIndex.set(story, deps)), deps.add(dep), this._revision += 1;
}
}
/** Removes a story from every inner map; prunes outer entries that become empty. */
removeStory(story) {
let deps = this.forwardIndex.get(story);
if (deps) {
for (let dep of deps) {
let inner = this.index.get(dep);
inner && (inner.delete(story), inner.size === 0 && this.index.delete(dep));
}
this.forwardIndex.delete(story), this._revision += 1;
}
}
/** Removes a single (dep, story) pair without affecting other stories' depths to that dep. */
removeEdge(dep, story) {
let inner = this.index.get(dep);
if (inner && inner.delete(story)) {
inner.size === 0 && this.index.delete(dep);
let deps = this.forwardIndex.get(story);
deps && (deps.delete(dep), deps.size === 0 && this.forwardIndex.delete(story)), this._revision += 1;
}
}
/** Returns the per-story depth map for dep. EMPTY map (not undefined) if dep unknown. */
lookup(dep) {
return this.index.get(dep) ?? /* @__PURE__ */ new Map();
}
/** Internal state inspection — for tests. */
asMap() {
return this.index;
}
};
// src/shared/open-service/services/module-graph/engine/dependency-graph/walk-from-story.ts
async function walkFromStory({
storyRoot,
registry,
cache: cache3,
reverseIndex,
recordEdges
}) {
reverseIndex.record(storyRoot, storyRoot, 0);
let visited = /* @__PURE__ */ new Map();
visited.set(storyRoot, 0);
let queue = [{ file: storyRoot, depth: 0 }], head = 0;
for (; head < queue.length; ) {
let { file, depth } = queue[head++];
if (registry.parserFor(file) === void 0)
continue;
let resolvedDeps = await cache3.resolveOnce(file);
recordEdges(file, resolvedDeps);
let nextDepth = depth + 1;
for (let normalised of resolvedDeps) {
if (nextDepth > 50)
continue;
let previousDepth = visited.get(normalised);
previousDepth !== void 0 && previousDepth <= nextDepth || (visited.set(normalised, nextDepth), reverseIndex.record(normalised, storyRoot, nextDepth), queue.push({ file: normalised, depth: nextDepth }));
}
}
}
// src/shared/open-service/services/module-graph/engine/dependency-graph/dependency-graph-builder.ts
var DependencyGraphBuilder = class {
constructor(opts) {
this.registry = opts.registry, this.logger = opts.logger ?? defaultLogger, this.cache = opts.cache ?? new ParseResolveCache({
registry: opts.registry,
resolver: opts.resolver,
workspaceRoots: opts.workspaceRoots,
projectRoot: opts.projectRoot,
logger: this.logger
});
}
async build(storyFiles) {
let startedAt = Date.now(), reverseIndex = new ReverseIndexImpl(), graph = /* @__PURE__ */ new Map(), limit = pLimit(cpus().length * 2), stories = Array.from(storyFiles, (s) => normalize(s));
await Promise.all(
stories.map(
(story) => limit(
() => walkFromStory({
storyRoot: story,
registry: this.registry,
cache: this.cache,
reverseIndex,
recordEdges: (file, deps) => graph.set(file, deps)
})
)
)
);
let elapsed = Date.now() - startedAt;
return this.logger.debug(
`Change detection graph built: ${stories.length} stories, ${reverseIndex.asMap().size} deps tracked, ${elapsed}ms`
), { reverseIndex, graph };
}
};
// src/shared/open-service/services/module-graph/engine/dependency-graph/incremental-patcher.ts
import { logger as defaultLogger2 } from "storybook/internal/node-logger";
function setsEqual(a, b) {
if (a.size !== b.size)
return !1;
for (let item of a)
if (!b.has(item))
return !1;
return !0;
}
var IncrementalPatcher = class {
constructor(opts) {
this.reverseIndex = opts.reverseIndex, this.graph = opts.graph, this.registry = opts.registry, this.logger = opts.logger ?? defaultLogger2, this.isStoryFile = opts.isStoryFile, this.cache = opts.cache ?? new ParseResolveCache({
registry: opts.registry,
resolver: opts.resolver,
workspaceRoots: opts.workspaceRoots,
projectRoot: opts.projectRoot,
logger: this.logger
});
}
/**
* Applies one file-system event to the graph and reverse index.
*
* Most events in a dev session leave the reverse index untouched: a comment-only edit keeps the
* same dependency set, and a write to a file the graph has never seen matches nothing at all.
* Callers detect movement via {@link ReverseIndexImpl.revision} before/after this call.
*/
async patch(event) {
let path = normalize(event.path);
if (this.cache.invalidate(path), event.kind === "add") {
this.isStoryFile(path) && await this.walkStory(path);
return;
}
if (event.kind === "unlink") {
let dependentsSet = new Set(this.reverseIndex.lookup(path).keys());
this.graph.delete(path), this.reverseIndex.removeStory(path);
let storiesToWalk2 = [];
for (let story of dependentsSet)
story === path || !this.isStoryFile(story) || (this.reverseIndex.removeStory(story), storiesToWalk2.push(story));
await Promise.all(storiesToWalk2.map((story) => this.walkStory(story)));
return;
}
let affectedStories = new Set(this.reverseIndex.lookup(path).keys());
this.isStoryFile(path) && affectedStories.add(path);
let oldDeps = this.graph.get(path);
if (oldDeps !== void 0) {
let newDeps = await this.cache.resolveOnce(path);
if (setsEqual(oldDeps, newDeps))
return;
}
let storiesToWalk = [];
for (let story of affectedStories)
this.isStoryFile(story) && (this.reverseIndex.removeStory(story), storiesToWalk.push(story));
await Promise.all(storiesToWalk.map((story) => this.walkStory(story)));
}
walkStory(storyRoot) {
return this.cache.invalidate(storyRoot), walkFromStory({
storyRoot,
registry: this.registry,
cache: this.cache,
reverseIndex: this.reverseIndex,
recordEdges: (file, deps) => {
this.graph.set(file, deps);
}
});
}
};
// src/shared/open-service/services/module-graph/engine/dependency-graph/resolver-factory.ts
import { ResolverFactory as OxcResolverFactory } from "oxc-resolver";
import { logger as logger2 } from "storybook/internal/node-logger";
var DEFAULT_EXTENSIONS = [".tsx", ".ts", ".d.ts", ".jsx", ".js", ".mjs", ".cjs", ".json"], DEFAULT_CONDITIONS = ["storybook", "import", "module", "default"], AliasNormalizer = class {
constructor() {
this.warnedRegexAliases = /* @__PURE__ */ new Set();
}
normalize(alias) {
if (!alias)
return;
let out = {}, skippedRegex = [];
if (Array.isArray(alias))
for (let entry of alias)
if (typeof entry.find == "string") {
let find = entry.find.replace(/\/$/, ""), replacement = entry.replacement.replace(/\/$/, "");
out[find] = [replacement];
} else
skippedRegex.push(String(entry.find));
else
for (let [find, replacement] of Object.entries(alias))
out[find.replace(/\/$/, "")] = [replacement.replace(/\/$/, "")];
if (skippedRegex.length > 0) {
let newPatterns = skippedRegex.filter((p) => !this.warnedRegexAliases.has(p));
if (newPatterns.length > 0) {
for (let p of newPatterns)
this.warnedRegexAliases.add(p);
logger2.debug(
`Change detection: ignored ${skippedRegex.length} regex alias(es); related modules tracked as opaque-leaf.`
), logger2.debug(
`ChangeDetectionResolverFactory: skipped regex aliases [${skippedRegex.join(", ")}]`
);
} else
for (let pattern of skippedRegex)
logger2.debug(`ChangeDetectionResolverFactory: skipping regex alias '${pattern}'`);
}
return Object.keys(out).length > 0 ? out : void 0;
}
}, ChangeDetectionResolverFactory = class {
constructor(config) {
this.aliasNormalizer = new AliasNormalizer();
let alias = this.aliasNormalizer.normalize(config.alias), conditionNames = config.conditions ?? DEFAULT_CONDITIONS;
this.factory = new OxcResolverFactory({
tsconfig: "auto",
alias,
conditionNames,
extensions: DEFAULT_EXTENSIONS
}), this.projectRootEntry = join(config.projectRoot, "__sb_resolver_root__.ts");
}
/**
* Resolves `specifier` from the file at `from` (must be an absolute path).
*
* Two-pass strategy:
* 1. Resolve from `from` — handles per-package tsconfig paths and local node_modules.
* 2. On failure, retry from the project root — picks up root-level tsconfig `paths`
* (e.g. workspace package aliases) that intermediate per-package tsconfigs may
* not inherit, as well as root-level node_modules symlinks.
*
* Returns the absolute resolved path, or `null` if both passes fail.
* Never throws — internal errors are converted to `null` and a debug-level log
* line is emitted.
*/
async resolve(from, specifier) {
try {
let result = await this.factory.resolveFileAsync(from, specifier);
if (result.path)
return result.path;
if (from !== this.projectRootEntry) {
let rootResult = await this.factory.resolveFileAsync(this.projectRootEntry, specifier);
return rootResult.path ? rootResult.path : ((result.error ?? rootResult.error) && logger2.debug(
`ChangeDetectionResolverFactory: '${specifier}' from '${from}' unresolved (${result.error ?? rootResult.error})`
), null);
}
return result.error && logger2.debug(
`ChangeDetectionResolverFactory: '${specifier}' from '${from}' unresolved (${result.error})`
), null;
} catch (error) {
return logger2.debug(
`ChangeDetectionResolverFactory: error resolving '${specifier}' from '${from}': ${String(error)}`
), null;
}
}
};
// src/shared/open-service/services/module-graph/engine/parser-registry/builtins.ts
import { parseWithOxc } from "storybook/internal/oxc-parser";
// src/shared/open-service/services/module-graph/engine/parser-registry/mdx-parse.ts
var MDX_IMPORT_REGEX = /(?:import\s+(?:[\s\S]*?\s+from\s+)?|export\s+[\s\S]*?\s+from\s+)['"]([^'"]+)['"]/g;
function stripCodeRegions(source) {
let stripped = source.replace(/```[\s\S]*?```/gs, (match) => " ".repeat(match.length));
return stripped = stripped.replace(/`[^`]*`/g, (match) => " ".repeat(match.length)), stripped;
}
function mdxParse(source) {
let stripped = stripCodeRegions(source), edges = [], seen = /* @__PURE__ */ new Set();
MDX_IMPORT_REGEX.lastIndex = 0;
let match = MDX_IMPORT_REGEX.exec(stripped);
for (; match !== null; ) {
let specifier = match[1], key = `static:${specifier}`;
seen.has(key) || (seen.add(key), edges.push({ specifier, kind: "static", importedNames: null })), match = MDX_IMPORT_REGEX.exec(stripped);
}
return edges;
}
// src/shared/open-service/services/module-graph/engine/parser-registry/builtins.ts
var oxcImportParser = {
extensions: [...jsTsSourceExtensions],
async parse({ filePath, source }) {
return parseWithOxc(filePath, source);
}
}, mdxImportParser = {
extensions: [".mdx"],
async parse({ source }) {
return mdxParse(source);
}
}, builtinImportParsers = [oxcImportParser, mdxImportParser];
// src/shared/open-service/services/module-graph/engine/parser-registry/parser-registry.ts
import { logger as logger3 } from "storybook/internal/node-logger";
import { parseWithOxc as parseWithOxc2 } from "storybook/internal/oxc-parser";
var ParserRegistry = class {
constructor(opts) {
this.byExtension = /* @__PURE__ */ new Map();
this.context = { parseScriptWithOxc: this.parseScriptWithOxc.bind(this) };
for (let p of opts.defaultParsers)
this.register(p);
for (let p of opts.pluginParsers)
this.register(p);
}
register(plugin) {
for (let ext of plugin.extensions) {
let lower = ext.toLowerCase();
this.byExtension.has(lower) && logger3.debug(`ParserRegistry: ${lower} parser overridden`), this.byExtension.set(lower, plugin.parse);
}
}
parserFor(filePath) {
return this.byExtension.get(extname(filePath).toLowerCase());
}
/**
* Returns `null` when no parser claims the extension — callers interpret this as
* "opaque leaf, do not walk into".
*/
async parse(filePath, source) {
let fn = this.parserFor(filePath);
return fn ? fn({ filePath, source }, this.context) : null;
}
async parseScriptWithOxc(source, virtualFilePath) {
return parseWithOxc2(virtualFilePath, source);
}
};
// src/shared/open-service/services/module-graph/engine/module-graph-engine.ts
var ModuleGraphEngine = class {
constructor(options) {
this.options = options;
this.storyFiles = /* @__PURE__ */ new Set();
this.refreshInFlight = !1;
/**
* Resolves once the in-flight story-index reconciliation has enqueued its add/unlink patches.
* {@link whenSettled} awaits this before snapshotting {@link patchQueue}, so a barrier taken
* while a reconciliation is still in `getIndex()` does not miss its patches (which would let a
* later {@link lookup} observe a pre-reconciliation graph).
*/
this.refreshSettled = Promise.resolve();
/**
* Serialises file-change patches so two events touching the same dep set never interleave
* across `await` points inside `IncrementalPatcher.patch`. The chain ignores rejections
* (each call's failure is logged in {@link handleFileChange}).
*/
this.patchQueue = Promise.resolve();
this.workingDir = options.workingDir ?? process.cwd();
}
start(adapter) {
this.adapter = adapter, this.startInternal().catch((error) => {
let failure = error instanceof Error ? error : new ModuleGraphFailureError(String(error));
logger4.error(`Module graph failed to start: ${failure.message}`), this.options.onError?.(failure);
});
}
mirrorSnapshot() {
this.reverseIndex && this.options.onSnapshot?.(
reverseIndexToStoriesByFile(this.reverseIndex.asMap(), this.workingDir)
);
}
collectBumpedStoryFiles(changedFile) {
if (!this.reverseIndex)
return /* @__PURE__ */ new Set();
let normalized = normalize(changedFile), bumpedStoryFiles = /* @__PURE__ */ new Set();
for (let [storyFile] of this.reverseIndex.lookup(normalized))
bumpedStoryFiles.add(storyFile);
return this.storyFiles.has(normalized) && bumpedStoryFiles.add(normalized), bumpedStoryFiles;
}
async mirrorUpdate(changedFile, prePatchBumped, indexChanged) {
if (!this.reverseIndex)
return;
let bumpedStoryFiles = /* @__PURE__ */ new Set([
...prePatchBumped,
...this.collectBumpedStoryFiles(changedFile)
]);
indexChanged && await this.options.onIndex?.(
reverseIndexToStoriesByFile(this.reverseIndex.asMap(), this.workingDir)
), bumpedStoryFiles.size > 0 && await this.options.onBump?.(
Array.from(bumpedStoryFiles, (storyFile) => toStoryIndexPath(storyFile, this.workingDir))
);
}
/**
* Returns the per-story breadth-first-search depth map for `dep`. Depth is the shortest number of
* import edges from the changed file to each affected story. Empty map if `dep` is unknown or
* unbuilt.
*/
lookup(dep) {
return this.reverseIndex?.lookup(dep) ?? /* @__PURE__ */ new Map();
}
/** True once the initial build has produced a reverse index. */
hasGraph() {
return this.reverseIndex !== void 0;
}
/**
* Read barrier. First awaits any in-flight story-index reconciliation so its add/unlink patches
* are enqueued, then snapshots the current tail of {@link patchQueue} (rather than re-reading the
* live field, so a continuous stream of file events cannot livelock the awaiter) and awaits it.
* When it resolves, every patch enqueued as of this call — including that reconciliation — has
* fully settled.
*
* This is a point-in-time barrier, not a freeze: file events arriving after the snapshot enqueue
* patches this call does not await, so a {@link lookup} taken after any further `await` may
* observe a newer (still non-mid-patch) graph. For a read pinned to this barrier, call
* {@link lookup} immediately after this resolves with no intervening `await`.
*/
async whenSettled() {
await this.refreshSettled.catch(() => {
}), await this.patchQueue.catch(() => {
});
}
/**
* Builds parser registry, resolver, dependency graph, and patcher; subscribes to file-change
* events queued behind {@link patchQueue}; then mirrors the initial snapshot to the service.
*/
async startInternal() {
let adapter = this.adapter;
if (!adapter)
return;
adapter.onStartupFailure?.((event) => {
this.options.onUnavailable?.(event.reason, event.error);
});
let resolveConfig = await adapter.getResolveConfig(), projectRoot = normalize(resolveConfig.projectRoot ?? this.workingDir), pluginParsers = this.options.presets ? await this.options.presets.apply("experimental_importParsers", []) : [], registry = new ParserRegistry({
defaultParsers: builtinImportParsers,
pluginParsers
}), resolver = new ChangeDetectionResolverFactory(resolveConfig), workspaceRoots = /* @__PURE__ */ new Set([normalize(getProjectRoot2())]), storyIndex = await this.options.getIndex(), storyIdsByFile = getStoryIdsByAbsolutePath(storyIndex, this.workingDir);
this.storyFiles = new Set(storyIdsByFile.keys());
let debugEnv = process.env.STORYBOOK_CHANGE_DETECTION_DEBUG, cache3 = new ParseResolveCache({
registry,
resolver,
workspaceRoots,
projectRoot,
logger: logger4,
debug: !!debugEnv
});
this.dependencyGraphBuilder = new DependencyGraphBuilder({
registry,
resolver,
workspaceRoots,
projectRoot,
cache: cache3
});
let eventBuffer = [], unsubscribeBuffer = adapter.onFileChange((event) => {
eventBuffer.push(event);
}), { reverseIndex, graph } = await this.dependencyGraphBuilder.build(this.storyFiles);
this.reverseIndex = reverseIndex, this.dumpDebugSnapshot(reverseIndex, graph, projectRoot, workspaceRoots, cache3), this.incrementalPatcher = new IncrementalPatcher({
reverseIndex,
graph,
registry,
resolver,
workspaceRoots,
projectRoot,
cache: cache3,
isStoryFile: (path) => this.storyFiles.has(normalize(path))
}), unsubscribeBuffer();
for (let event of eventBuffer)
this.patchQueue = this.patchQueue.then(() => this.handleFileChange(event)).catch(() => {
});
adapter.onFileChange((event) => {
this.patchQueue = this.patchQueue.then(() => this.handleFileChange(event)).catch(() => {
});
}), this.mirrorSnapshot();
}
onStoryIndexInvalidated() {
!this.refreshInFlight && this.incrementalPatcher && (this.refreshInFlight = !0, this.refreshSettled = this.refreshStoryFiles().catch(() => {
}).finally(() => {
this.refreshInFlight = !1;
}));
}
/**
* Re-reads the story index and reconciles {@link storyFiles} with stories that have appeared or
* disappeared since startup. For each story that newly entered the index, the patcher is asked
* to walk it (so its forward edges are recorded). For each story that left the index, the
* patcher is asked to unlink it (so its reverse-index entries are pruned). Replays are queued
* behind {@link patchQueue} to keep the serialised-patch invariant intact.
*
* Single-flight is enforced by the sole caller, {@link onStoryIndexInvalidated}, which also
* exposes this run via {@link refreshSettled} so {@link whenSettled} can wait for the add/unlink
* patches to be enqueued.
*/
async refreshStoryFiles() {
let storyIndex = await this.options.getIndex(), storyIdsByFile = getStoryIdsByAbsolutePath(storyIndex, this.workingDir), next = new Set(storyIdsByFile.keys()), previous = this.storyFiles, added = [];
for (let path of next)
previous.has(path) || added.push(path);
let removed = [];
for (let path of previous)
next.has(path) || removed.push(path);
if (!(added.length === 0 && removed.length === 0)) {
this.storyFiles = next;
for (let path of added)
this.patchQueue = this.patchQueue.then(() => this.handleFileChange({ kind: "add", path })).catch(() => {
});
for (let path of removed)
this.patchQueue = this.patchQueue.then(() => this.handleFileChange({ kind: "unlink", path })).catch(() => {
});
}
}
async dumpDebugSnapshot(reverseIndex, graph, projectRoot, workspaceRoots, cache3) {
let debugEnv = process.env.STORYBOOK_CHANGE_DETECTION_DEBUG;
if (!debugEnv)
return;
let outPath = debugEnv === "1" || debugEnv === "true" ? join(projectRoot, "storybook-graph-debug.json") : debugEnv, graphObj = {};
for (let [story, deps] of graph)
graphObj[story] = Array.from(deps).sort();
let reverseObj = {};
for (let [dep, stories] of reverseIndex.asMap())
reverseObj[dep] = Array.from(stories.entries()).map(([story, depth]) => ({ story, depth })).sort((a, b) => a.depth - b.depth || a.story.localeCompare(b.story));
let snapshot = {
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
projectRoot,
workspaceRoots: Array.from(workspaceRoots).sort(),
// `graph` is keyed by every walked node (story roots + their transitive deps),
// and `reverseIndex` records each story root at depth 0 alongside real deps —
// so `graph.size` / `reverseIndex.asMap().size` over-report story and dep totals.
// Report `storyFiles` from the authoritative source-of-truth set, plus the raw
// node/entry counts under unambiguous names for diagnostics.
storyFiles: this.storyFiles.size,
graphNodes: graph.size,
reverseIndexEntries: reverseIndex.asMap().size,
graph: graphObj,
reverseIndex: reverseObj,
// Each entry records one named-import barrel lookup: which names were requested,
// which source files they resolved to, and whether the barrel itself was also
// included (needBarrel: true means at least one name fell back to the barrel).
barrelResolutions: cache3.getBarrelTrace() ?? []
};
try {
await writeFile(outPath, JSON.stringify(snapshot, null, 2), "utf8"), logger4.debug(`Change detection: graph debug snapshot written to ${outPath}`);
} catch (error) {
logger4.warn(
`Change detection: failed to write debug snapshot to ${outPath}: ${error instanceof Error ? error.message : String(error)}`
);
}
}
async handleFileChange(event) {
if (!this.incrementalPatcher || !this.reverseIndex)
return;
let prePatchBumped = this.collectBumpedStoryFiles(event.path), revisionBefore = this.reverseIndex.revision;
try {
await this.incrementalPatcher.patch(event);
} catch (error) {
logger4.warn(
`Change detection: failed to apply ${event.kind} for ${event.path}: ${error instanceof Error ? error.message : String(error)}`
);
}
await this.mirrorUpdate(
event.path,
prePatchBumped,
this.reverseIndex.revision !== revisionBefore
);
}
};
// src/shared/open-service/services/module-graph/server.ts
var resolveAdapter, changeDetectionAdapterPromise = new Promise(
(resolve2) => {
resolveAdapter = resolve2;
}
);
function resolveChangeDetectionAdapter(adapter) {
resolveAdapter(adapter);
}
function registerModuleGraphService(options) {
let workingDir = options.workingDir ?? process.cwd(), engine, indexRuntime = registerModuleGraphIndexService(workingDir), runtime = registerService(
{
...moduleGraphServiceDef,
initialState: {
...moduleGraphServiceDef.initialState,
workingDir
}
},
{
commands: {
_waitForSettledEngine: {
handler: async () => {
await engine.whenSettled();
}
}
}
}
);
return engine = new ModuleGraphEngine({
getIndex: options.getIndex,
workingDir,
presets: options.presets,
onSnapshot: (storiesByFile) => {
runtime.commands._applyGraphSnapshot({ storiesByFile });
},
onIndex: (storiesByFile) => indexRuntime.commands._applyIndex({ storiesByFile }),
onBump: (bumpedStoryFiles) => runtime.commands._applyGraphUpdate({ bumpedStoryFiles }),
onError: (error) => {
runtime.commands._setStatus({ value: "error", error: errorToErrorLike(error) });
},
onUnavailable: (reason, error) => {
runtime.commands._setStatus({
value: "unavailable",
reason,
...error ? { error: errorToErrorLike(error) } : {}
});
}
}), options.channel.on(STORY_INDEX_INVALIDATED, () => {
engine.onStoryIndexInvalidated();
}), changeDetectionAdapterPromise.then((adapter) => {
if (!adapter) {
runtime.commands._setStatus({
value: "unavailable",
reason: "builder does not support change detection"
});
return;
}
engine.start(adapter);
}), runtime;
}
// src/core-server/presets/wsToken.ts
import { randomUUID } from "crypto";
var getWsToken = () => (globalThis.STORYBOOK_WEBSOCKET_TOKEN || (globalThis.STORYBOOK_WEBSOCKET_TOKEN = randomUUID()), globalThis.STORYBOOK_WEBSOCKET_TOKEN);
// src/core-server/withTelemetry.ts
import {
HandledError,
cache as cache2,
loadMainConfig,
isCI,
loadAllPresets
} from "storybook/internal/common";
import { logger as logger5, prompt } from "storybook/internal/node-logger";
import {
ErrorCollector,
getPrecedingUpgrade,
isTelemetryStateResolved,
oneWayHash,
onPayloadError,
setTelemetryEnabled,
telemetry
} from "storybook/internal/telemetry";
var promptCrashReports = async () => {
if (isCI() || !process.stdout.isTTY)
return;
let enableCrashReports = await prompt.confirm({
message: "Would you like to send anonymous crash reports to improve Storybook and fix bugs faster?",
initialValue: !0
});
return await cache2.set("enableCrashReports", enableCrashReports), enableCrashReports;
};
async function getErrorLevel({
cliOptions,
presetOptions,
skipPrompt,
eventType
}) {
if (cliOptions.disableTelemetry)
return "none";
if (!presetOptions && eventType !== "init")
return "error";
if (presetOptions) {
let core = await (await loadAllPresets(presetOptions)).apply("core");
if (core?.enableCrashReports !== void 0)
return core.enableCrashReports ? "full" : "error";
if (core?.disableTelemetry)
return "none";
}
let valueFromCache = await cache2.get("enableCrashReports") ?? await cache2.get("enableCrashreports");
if (valueFromCache !== void 0)
return valueFromCache ? "full" : "error";
if (skipPrompt)
return "error";
let valueFromPrompt = await promptCrashReports();
return valueFromPrompt !== void 0 ? valueFromPrompt ? "full" : "error" : "full";
}
async function sendTelemetryError(_error, eventType, options, blocking = !0, parent) {
try {
let errorLevel = "error";
try {
errorLevel = await getErrorLevel({
...options,
eventType,
skipPrompt: options.skipPrompt || eventType === "init" && !blocking
});
} catch {
}
if (errorLevel !== "none") {
let precedingUpgrade = await getPrecedingUpgrade(), error = _error, errorHash;
"message" in error ? errorHash = error.message ? oneWayHash(error.message) : "EMPTY_MESSAGE" : errorHash = "NO_MESSAGE";
let { code, name, category } = error;
if (await telemetry(
"error",
{
code,
name,
category,
eventType,
blocking,
precedingUpgrade,
error: errorLevel === "full" ? error : void 0,
errorHash,
// if we ever end up sending a non-error instance, we'd like to know
isErrorInstance: error instanceof Error,
// Include parent error information if this is a sub-error
...parent ? { parent: parent.fullErrorCode } : {}
},
{
immediate: !0,
configDir: options.cliOptions.configDir || options.presetOptions?.configDir,
enableCrashReports: errorLevel === "full",
force: !0
}
), error && "subErrors" in error && error.subErrors.length > 0)
for (let subError of error.subErrors)
await sendTelemetryError(subError, eventType, options, blocking, error);
}
} catch {
}
}
async function resolveTelemetryState(options) {
if (options.cliOptions.disableTelemetry !== void 0)
return await setTelemetryEnabled(!options.cliOptions.disableTelemetry);
let mainConfig, configDir = options.cliOptions.configDir ?? options.presetOptions?.configDir ?? ".storybook";
try {
mainConfig = await loadMainConfig({ configDir });
} catch {
}
if (mainConfig)
return await setTelemetryEnabled(!mainConfig?.core?.disableTelemetry);
await setTelemetryEnabled(options.fallbackTelemetryState ?? !1);
}
function isInterruptionError(error) {
if (!error || typeof error != "object")
return !1;
let signal = "signal" in error ? error.signal : void 0, code = "code" in error ? error.code : void 0, name = "name" in error ? error.name : void 0, message = "message" in error && typeof error.message == "string" ? error.message : void 0, cause = "cause" in error ? error.cause : void 0;
return signal === "SIGINT" || code === "ABORT_ERR" || code === "ERR_CANCELED" || name === "AbortError" || message?.includes("Command was killed with SIGINT") || message?.includes("The operation was aborted") || isInterruptionError(cause);
}
var CANCELLATION_TRACKED_EVENTS = ["init", "ai-command"];
async function withTelemetry(eventType, options, run) {
isTelemetryStateResolved() || await resolveTelemetryState(options);
let canceled = !1;
async function cancelTelemetry() {
canceled = !0, await telemetry("canceled", { eventType }, { stripMetadata: !0, immediate: !0 }), process.exit(0);
}
let trackCancellation = CANCELLATION_TRACKED_EVENTS.includes(eventType);
trackCancellation && process.on("SIGINT", cancelTelemetry), onPayloadError(async (error, evtType) => {
await sendTelemetryError(error, evtType, options);
}), telemetry("boot", { eventType }, { stripMetadata: !0 });
try {
return await run();
} catch (error) {
if (canceled)
return;
if (trackCancellation && isInterruptionError(error)) {
await cancelTelemetry();
return;
}
if (!(error instanceof HandledError || error instanceof StorybookError && error.isHandledError)) {
let { printError = logger5.error } = options;
printError(error);
}
throw await sendTelemetryError(error, eventType, options), error;
} finally {
let errors = ErrorCollector.getErrors();
for (let error of errors)
await sendTelemetryError(error, eventType, options, !1);
process.off("SIGINT", cancelTelemetry), onPayloadError(void 0);
}
}
// src/core-server/utils/generate-story.ts
import { existsSync as existsSync3 } from "node:fs";
import { writeFile as writeFile2 } from "node:fs/promises";
import { relative as relative3 } from "node:path";
import { getProjectRoot as getProjectRoot4, getStoryId } from "storybook/internal/common";
// src/core-server/utils/get-new-story-file.ts
import { existsSync as existsSync2 } from "node:fs";
import { readFile as readFile3 } from "node:fs/promises";
import { basename as basename2, dirname as dirname2, extname as extname2, join as join3, relative as relative2 } from "node:path";
import { types as t, traverse } from "storybook/internal/babel";
import {
extractFrameworkPackageName,
findConfigFile,
formatFileContent,
getFrameworkName,
getProjectRoot as getProjectRoot3
} from "storybook/internal/common";
import { isCsfFactoryPreview } from "storybook/internal/csf-tools";
import { logger as logger6 } from "storybook/internal/node-logger";
// src/core-server/utils/new-story-templates/csf-factory-template.ts
var import_ts_dedent3 = __toESM(require_dist(), 1);
// src/core-server/utils/get-component-variable-name.ts
var getComponentVariableName = async (name) => (await import("./camelcase-5BRLVUOI.js")).default(name.replace(/^[^a-zA-Z_$]*/, ""), { pascalCase: !0 }).replace(/[^a-zA-Z_$]+/, "");
// src/core-server/utils/new-story-templates/csf-factory-template.ts
async function getCsfFactoryTemplateForNewStoryFile(data) {
let importName = data.componentIsDefaultExport ? await getComponentVariableName(data.basenameWithoutExtension) : data.componentExportName, importStatement = data.componentIsDefaultExport ? `import ${importName} from './${data.basenameWithoutExtension}';` : `import { ${importName} } from './${data.basenameWithoutExtension}';`, previewImport = data.previewImportPath ? `import preview from '${data.previewImportPath}';` : "import preview from '#.storybook/preview';", argsString = data.args && Object.keys(data.args).length > 0 ? `{ args: ${JSON.stringify(data.args, null, 2)} }` : "{}";
return import_ts_dedent3.dedent`
${previewImport}
${importStatement}
const meta = preview.meta({
component: ${importName},
});
export const ${data.exportedStoryName} = meta.story(${argsString});
`;
}
// src/core-server/utils/new-story-templates/javascript.ts
var import_ts_dedent4 = __toESM(require_dist(), 1);
async function getJavaScriptTemplateForNewStoryFile(data) {
let importName = data.componentIsDefaultExport ? await getComponentVariableName(data.basenameWithoutExtension) : data.componentExportName, importStatement = data.componentIsDefaultExport ? `import ${importName} from './${data.basenameWithoutExtension}';` : `import { ${importName} } from './${data.basenameWithoutExtension}';`, hasArgs = !!(data.args && Object.keys(data.args).length > 0), argsString = hasArgs ? `args: ${JSON.stringify(data.args, null, 2)},` : "", storyExport = hasArgs ? import_ts_dedent4.dedent`
export const ${data.exportedStoryName} = {
${argsString}
};
` : `export const ${data.exportedStoryName} = {};`;
return import_ts_dedent4.dedent`
${importStatement}
const meta = {
component: ${importName},
};
export default meta;
${storyExport}
`;
}
// src/core-server/utils/new-story-templates/typescript.ts
var import_ts_dedent5 = __toESM(require_dist(), 1);
async function getTypeScriptTemplateForNewStoryFile(data) {
let importName = data.componentIsDefaultExport ? await getComponentVariableName(data.basenameWithoutExtension) : data.componentExportName, importStatement = data.componentIsDefaultExport ? `import ${importName} from './${data.basenameWithoutExtension}'` : `import { ${importName} } from './${data.basenameWithoutExtension}'`, hasArgs = !!(data.args && Object.keys(data.args).length > 0), argsString = hasArgs ? `args: ${JSON.stringify(data.args, null, 2)},` : "", storyExport = hasArgs ? import_ts_dedent5.dedent`
export const ${data.exportedStoryName}: Story = {
${argsString}
};
` : `export const ${data.exportedStoryName}: Story = {};`;
return import_ts_dedent5.dedent`
import type { Meta, StoryObj } from '${data.frameworkPackage}';
${importStatement};
const meta = {
component: ${importName},
} satisfies Meta<typeof ${importName}>;
export default meta;
type Story = StoryObj<typeof meta>;
${storyExport}
`;
}
// src/core-server/utils/safeString.ts
function escapeForTemplate(str) {
return str.replace(/\\/g, "\\\\").replace(/(['"$`])/g, "\\$&").replace(/[\n\r]/g, "\\$&");
}
// src/core-server/utils/get-new-story-file.ts
async function getNewStoryFile({
componentFilePath,
componentExportName,
componentIsDefaultExport,
componentExportCount
}, options) {
let frameworkPackageName = await getFrameworkName(options), sanitizedFrameworkPackageName = extractFrameworkPackageName(frameworkPackageName), base = basename2(componentFilePath), extension = extname2(componentFilePath), basenameWithoutExtension = escapeForTemplate(base.replace(extension, "")), dir = dirname2(componentFilePath), { storyFileName, isTypescript, storyFileExtension } = getStoryMetadata(componentFilePath), storyFileNameWithExtension = `${storyFileName}.${storyFileExtension}`, alternativeStoryFileNameWithExtension = `${basenameWithoutExtension}.${componentExportName}.stories.${storyFileExtension}`, exportedStoryName = "Default", useCsfFactory = !1, previewConfigPath;
try {
let previewConfig = findConfigFile("preview", options.configDir);
if (previewConfig) {
let previewContent = await readFile3(previewConfig, "utf-8");
useCsfFactory = isCsfFactoryPreview(loadConfig(previewContent)), previewConfigPath = previewConfig;
}
} catch {
}
let args;
try {
let argTypes = await options.presets.apply("internal_getArgTypesData", null, {
...options,
componentFilePath,
componentExportName
});
if (logger6.debug(`Extracted argTypes for ${componentExportName}: ${JSON.stringify(argTypes)}`), argTypes) {
let { required } = generateDummyArgsFromArgTypes(argTypes);
Object.keys(required).length > 0 && (args = required, logger6.debug(
`Generated dummy data using ArgTypes for ${componentExportName}: ${JSON.stringify(args)}`
));
}
} catch (error) {
logger6.debug(`Could not generate dummy data for ${componentExportName}: ${error}`);
}
let storyFileContent = "";
if (useCsfFactory) {
let previewImportPath;
if (previewConfigPath && !await checkForImportsMap(options.configDir)) {
let storyFilePath2 = join3(getProjectRoot3(), dir), pathWithoutExt = relative2(storyFilePath2, previewConfigPath).replace(/\.(ts|js|mts|cts|tsx|jsx)$/, "");
previewImportPath = escapeForTemplate(
pathWithoutExt.startsWith(".") ? pathWithoutExt : `./${pathWithoutExt}`
);
}
storyFileContent = await getCsfFactoryTemplateForNewStoryFile({
basenameWithoutExtension,
componentExportName,
componentIsDefaultExport,
exportedStoryName,
previewImportPath,
args
});
} else
storyFileContent = isTypescript && frameworkPackageName ? await getTypeScriptTemplateForNewStoryFile({
basenameWithoutExtension,
componentExportName,
componentIsDefaultExport,
frameworkPackage: sanitizedFrameworkPackageName,
exportedStoryName,
args
}) : await getJavaScriptTemplateForNewStoryFile({
basenameWithoutExtension,
componentExportName,
componentIsDefaultExport,
exportedStoryName,
args
});
storyFileContent = replaceArgsPlaceholders(storyFileContent);
let storyFilePath = doesStoryFileExist(join3(getProjectRoot3(), dir), storyFileName) && componentExportCount > 1 ? join3(getProjectRoot3(), dir, alternativeStoryFileNameWithExtension) : join3(getProjectRoot3(), dir, storyFileNameWithExtension), formattedStoryFileContent = await formatFileContent(storyFilePath, storyFileContent);
return {
storyFilePath,
exportedStoryName,
storyFileContent: formattedStoryFileContent,
dirname: dir
};
}
var getStoryMetadata = (componentFilePath) => {
let isTypescript = /\.(ts|tsx|mts|cts)$/.test(componentFilePath), base = basename2(componentFilePath), extension = extname2(componentFilePath), basenameWithoutExtension = base.replace(extension, ""), storyFileExtension = isTypescript ? "tsx" : "jsx";
return {
storyFileName: `${basenameWithoutExtension}.stories`,
storyFileExtension,
isTypescript
};
}, doesStoryFileExist = (parentFolder, storyFileName) => existsSync2(join3(parentFolder, `${storyFileName}.ts`)) || existsSync2(join3(parentFolder, `${storyFileName}.tsx`)) || existsSync2(join3(parentFolder, `${storyFileName}.js`)) || existsSync2(join3(parentFolder, `${storyFileName}.jsx`));
async function checkForImportsMap(configDir) {
try {
for (let directory of up(configDir, { last: getProjectRoot3() })) {
let packageJsonPath = join3(directory, "package.json");
if (existsSync2(packageJsonPath)) {
let packageJsonContent = await readFile3(packageJsonPath, "utf-8");
if (JSON.parse(packageJsonContent).imports)
return !0;
}
}
return !1;
} catch {
return !1;
}
}
function replaceArgsPlaceholders(storyFileContent) {
if (!storyFileContent.includes(STORYBOOK_FN_PLACEHOLDER))
return storyFileContent;
let storyFile = loadConfig(storyFileContent).parse(), needsFnImport = !1;
return traverse(storyFile._ast, {
StringLiteral(path) {
path.node.value === STORYBOOK_FN_PLACEHOLDER && (needsFnImport = !0, path.replaceWith(t.callExpression(t.identifier("fn"), [])));
}
}), needsFnImport && storyFile.setImport(["fn"], "storybook/test"), printConfig(storyFile).code;
}
// src/core-server/utils/generate-story.ts
async function generateStoryFile(payload, options, generateOptions = {}) {
let { checkFileExists = !0 } = generateOptions;
try {
let { storyFilePath, exportedStoryName, storyFileContent } = await getNewStoryFile(
payload,
options
), relativeStoryFilePath = relative3(getProjectRoot4(), storyFilePath), { storyId, kind } = await getStoryId({ storyFilePath, exportedStoryName }, options);
return checkFileExists && existsSync3(storyFilePath) ? {
success: !1,
kind,
storyFilePath: relativeStoryFilePath,
error: `A story file already exists at ${relativeStoryFilePath}`,
errorType: "STORY_FILE_EXISTS"
} : (await writeFile2(storyFilePath, storyFileContent, "utf-8"), {
success: !0,
storyId,
kind,
storyFilePath: relativeStoryFilePath,
exportedStoryName
});
} catch (e) {
return {
success: !1,
error: e?.message || "Unknown error occurred",
errorType: "UNKNOWN"
};
}
}
// src/shared/test-provider-store/index.ts
var UNIVERSAL_TEST_PROVIDER_STORE_OPTIONS = {
id: "storybook/test-provider",
leader: !0,
initialState: {}
};
function createTestProviderStore({
universalTestProviderStore: universalTestProviderStore2,
useUniversalStore
}) {
let baseStore = {
settingsChanged: () => {
universalTestProviderStore2.untilReady().then(() => {
universalTestProviderStore2.send({ type: "settings-changed" });
});
},
onRunAll: (listener) => universalTestProviderStore2.subscribe("run-all", listener),
onClearAll: (listener) => universalTestProviderStore2.subscribe("clear-all", listener)
}, fullTestProviderStore2 = {
...baseStore,
getFullState: universalTestProviderStore2.getState,
setFullState: universalTestProviderStore2.setState,
onSettingsChanged: (listener) => universalTestProviderStore2.subscribe("settings-changed", listener),
runAll: async () => {
await universalTestProviderStore2.untilReady(), universalTestProviderStore2.send({ type: "run-all" });
},
clearAll: async () => {
await universalTestProviderStore2.untilReady(), universalTestProviderStore2.send({ type: "clear-all" });
}
}, getTestProviderStoreById2 = (testProviderId) => {
let getStateForTestProvider = () => universalTestProviderStore2.getState()[testProviderId] ?? "test-provider-state:pending", setStateForTestProvider = (state) => {
universalTestProviderStore2.untilReady().then(() => {
universalTestProviderStore2.setState((currentState) => ({
...currentState,
[testProviderId]: state
}));
});
};
return {
...baseStore,
testProviderId,
getState: getStateForTestProvider,
setState: setStateForTestProvider,
runWithState: async (callback) => {
setStateForTestProvider("test-provider-state:running");
try {
await callback(), setStateForTestProvider("test-provider-state:succeeded");
} catch {
setStateForTestProvider("test-provider-state:crashed");
}
}
};
};
return useUniversalStore ? {
getTestProviderStoreById: getTestProviderStoreById2,
fullTestProviderStore: fullTestProviderStore2,
universalTestProviderStore: universalTestProviderStore2,
useTestProviderStore: (selector) => useUniversalStore(universalTestProviderStore2, selector)[0]
} : {
getTestProviderStoreById: getTestProviderStoreById2,
fullTestProviderStore: fullTestProviderStore2,
universalTestProviderStore: universalTestProviderStore2
};
}
// src/core-server/stores/test-provider.ts
var testProviderStore = createTestProviderStore({
universalTestProviderStore: UniversalStore.create({
...UNIVERSAL_TEST_PROVIDER_STORE_OPTIONS,
/*
This is a temporary workaround, to ensure that the store is not created in the
vitest sub-process in addon-vitest, even though it imports from core-server
If it was created in the sub-process, it would try to connect to the leader in the dev server
before it was ready.
This will be fixed when we do the planned UniversalStore v0.2.
*/
leader: !optionalEnvToBoolean(process.env.VITEST_CHILD_PROCESS)
})
}), { fullTestProviderStore, getTestProviderStoreById, universalTestProviderStore } = testProviderStore;
// src/core-server/utils/ghost-stories/get-candidates.ts
import { readFile as readFile4 } from "node:fs/promises";
import { babelParse, traverse as traverse2 } from "storybook/internal/babel";
// src/core-server/utils/ghost-stories/component-analyzer.ts
var COMPLEXITY_CONFIG = {
/** Weight applied to non-empty lines */
locWeight: 1,
/** Imports can be cheap, so they get a lower weight */
importWeight: 0.5,
/**
* Defines what raw complexity value should map to the upper bound of a "simple" file For instance
* 30 LOC + 4 imports = 32. This would result in a score of 0.3
*/
simpleBaseline: 32,
simpleScore: 0.3
}, getComponentComplexity = (fileContent) => {
let lines = fileContent.split(`
`), nonEmptyLines = lines.filter((line) => line.trim() !== "").length, importCount = lines.filter((line) => line.trim().startsWith("import")).length, normalizedScore = (nonEmptyLines * COMPLEXITY_CONFIG.locWeight + importCount * COMPLEXITY_CONFIG.importWeight) / (COMPLEXITY_CONFIG.simpleBaseline / COMPLEXITY_CONFIG.simpleScore);
return Math.min(normalizedScore, 1);
};
// src/core-server/utils/ghost-stories/get-candidates.ts
function isValidCandidate(source) {
let ast = babelParse(source), hasJSX = !1, hasExport = !1;
return traverse2(ast, {
JSXElement(path) {
hasJSX = !0, hasExport && path.stop();
},
JSXFragment(path) {
hasJSX = !0, hasExport && path.stop();
},
ExportNamedDeclaration(path) {
hasExport = !0, hasJSX && path.stop();
},
ExportDefaultDeclaration(path) {
hasExport = !0, hasJSX && path.stop();
},
ExportAllDeclaration(path) {
hasExport = !0, hasJSX && path.stop();
}
}), hasJSX && hasExport;
}
async function getCandidatesForStorybook(files2, sampleCount) {
let simpleCandidates = [], analyzedCandidates = [];
for (let file of files2) {
let source;
try {
if (source = await readFile4(file, "utf-8"), !isValidCandidate(source))
continue;
} catch {
continue;
}
let complexity = getComponentComplexity(source);
if (analyzedCandidates.push({ file, complexity }), complexity < 0.3 && (simpleCandidates.push({ file, complexity }), simpleCandidates.length >= sampleCount))
break;
}
let selectedCandidates = [];
simpleCandidates.length >= sampleCount ? selectedCandidates = simpleCandidates.sort((a, b) => a.complexity - b.complexity).slice(0, sampleCount) : selectedCandidates = analyzedCandidates.sort((a, b) => a.complexity - b.complexity).slice(0, sampleCount);
let avgComplexity = selectedCandidates.length > 0 ? Number(
(selectedCandidates.reduce((acc, curr) => acc + curr.complexity, 0) / selectedCandidates.length).toFixed(2)
) : 0;
return {
candidates: selectedCandidates.map(({ file }) => file),
analyzedCount: analyzedCandidates.length,
avgComplexity
};
}
async function getComponentCandidates({
sampleSize = 20,
globPattern = "**/*.{tsx,jsx}",
cwd = process.cwd()
} = {}) {
let globMatchCount = 0;
try {
let files2 = [];
if (files2 = await glob(globPattern, {
cwd,
absolute: !0,
ignore: [
"**/node_modules/**",
"**/.git/**",
"**/dist/**",
"**/__mocks__/**",
"**/build/**",
"**/storybook-static/**",
"**/*.test.*",
"**/*.d.*",
"**/*.config.*",
"**/*.spec.*",
"**/*.stories.*",
// skip example story files that come from the CLI
"**/stories/{Button,Header,Page}.*",
"**/stories/{button,header,page}.*"
]
}), globMatchCount = files2.length, globMatchCount === 0)
return {
candidates: [],
globMatchCount
};
let { analyzedCount, avgComplexity, candidates } = await getCandidatesForStorybook(
files2,
sampleSize
);
return {
analyzedCount,
avgComplexity,
candidates,
globMatchCount
};
} catch {
return {
candidates: [],
error: "Failed to find candidates",
globMatchCount
};
}
}
// src/shared/utils/categorize-render-errors.ts
var ERROR_CATEGORIES = {
MISSING_PROVIDER: "MISSING_PROVIDER",
MISSING_STATE_PROVIDER: "MISSING_STATE_PROVIDER",
MISSING_ROUTER_PROVIDER: "MISSING_ROUTER_PROVIDER",
MISSING_THEME_PROVIDER: "MISSING_THEME_PROVIDER",
MISSING_TRANSLATION_PROVIDER: "MISSING_TRANSLATION_PROVIDER",
MISSING_PORTAL_ROOT: "MISSING_PORTAL_ROOT",
HOOK_USAGE_ERROR: "HOOK_USAGE_ERROR",
MODULE_IMPORT_ERROR: "MODULE_IMPORT_ERROR",
COMPONENT_RENDER_ERROR: "COMPONENT_RENDER_ERROR",
SERVER_COMPONENTS_ERROR: "SERVER_COMPONENTS_ERROR",
UNKNOWN_ERROR: "UNKNOWN_ERROR",
// Vite related errors
DYNAMIC_MODULE_IMPORT_ERROR: "DYNAMIC_MODULE_IMPORT_ERROR",
// Vitest test run related errors
TEST_FILE_IMPORT_ERROR: "TEST_FILE_IMPORT_ERROR"
};
function buildErrorContext(message, stack) {
let normalizedMessage = message.toLowerCase(), normalizedStack = (stack ?? "").toLowerCase(), stackDeps = /* @__PURE__ */ new Set(), stackLines = normalizedStack.split(`
`).filter(Boolean);
for (let line of stackLines) {
let depMatch = line.match(/\/deps\/([^:]+)\.js/);
depMatch && stackDeps.add(depMatch[1]);
}
return {
message,
stack,
normalizedMessage,
normalizedStack,
stackDeps
};
}
var CATEGORIZATION_RULES = [
{
category: ERROR_CATEGORIES.MODULE_IMPORT_ERROR,
priority: 100,
match: (ctx) => ctx.normalizedMessage.includes("cannot find module") || ctx.normalizedMessage.includes("module not found") || ctx.normalizedMessage.includes("cannot resolve module")
},
{
category: ERROR_CATEGORIES.TEST_FILE_IMPORT_ERROR,
priority: 95,
match: (ctx) => ctx.normalizedMessage.includes("failed to import test file")
},
{
category: ERROR_CATEGORIES.DYNAMIC_MODULE_IMPORT_ERROR,
priority: 95,
match: (ctx) => ctx.normalizedMessage.includes("failed to fetch dynamically imported module")
},
{
category: ERROR_CATEGORIES.HOOK_USAGE_ERROR,
priority: 90,
match: (ctx) => ctx.normalizedMessage.includes("invalid hook call") || ctx.normalizedMessage.includes("rendered more hooks") || ctx.normalizedMessage.includes("hooks can only be called") || ctx.normalizedMessage.includes("too many re-renders") || ctx.normalizedMessage.includes("maximum update depth exceeded") || ctx.normalizedMessage.includes("hook") && ctx.normalizedMessage.includes("function component")
},
{
category: ERROR_CATEGORIES.MISSING_STATE_PROVIDER,
priority: 85,
match: (ctx) => Array.from(ctx.stackDeps).some(isStateManagementPackage) && (ctx.normalizedMessage.includes("context") || ctx.normalizedMessage.includes("undefined") || ctx.normalizedMessage.includes("null"))
},
{
category: ERROR_CATEGORIES.MISSING_ROUTER_PROVIDER,
priority: 85,
match: (ctx) => Array.from(ctx.stackDeps).some(isRouterPackage) || ctx.normalizedMessage.includes("usenavigate") || ctx.normalizedMessage.includes("router")
},
{
category: ERROR_CATEGORIES.SERVER_COMPONENTS_ERROR,
priority: 85,
match: (ctx) => ctx.normalizedMessage.includes("server components") || ctx.normalizedMessage.includes("use client") || ctx.normalizedMessage.includes("async/await") && ctx.normalizedMessage.includes("not supported")
},
{
category: ERROR_CATEGORIES.MISSING_THEME_PROVIDER,
priority: 80,
match: (ctx) => Array.from(ctx.stackDeps).some(isStylingPackage) && (ctx.normalizedMessage.includes("theme") || ctx.normalizedMessage.includes("undefined")) || ctx.normalizedMessage.includes("usetheme") || ctx.normalizedMessage.includes("theme") && ctx.normalizedMessage.includes("provider")
},
{
category: ERROR_CATEGORIES.MISSING_TRANSLATION_PROVIDER,
priority: 80,
match: (ctx) => Array.from(ctx.stackDeps).some(isI18nPackage) || ctx.normalizedMessage.includes("i18n") || ctx.normalizedMessage.includes("translation") || ctx.normalizedMessage.includes("locale")
},
{
category: ERROR_CATEGORIES.MISSING_PORTAL_ROOT,
priority: 70,
match: (ctx) => ctx.normalizedMessage.includes("target container is not a dom element") || ctx.normalizedMessage.includes("portal") && (ctx.normalizedMessage.includes("container") || ctx.normalizedMessage.includes("root"))
},
{
category: ERROR_CATEGORIES.MISSING_PROVIDER,
priority: 60,
match: (ctx) => ctx.normalizedMessage.includes("use") && ctx.normalizedMessage.includes("provider") || ctx.normalizedMessage.includes("<provider>") || ctx.normalizedMessage.includes("no provider") || ctx.normalizedMessage.includes("without a provider") || (ctx.normalizedMessage.includes("could not find") || ctx.normalizedMessage.includes("missing") || ctx.normalizedMessage.includes("not found")) && ctx.normalizedMessage.includes("context") || ctx.normalizedMessage.includes("context") && (ctx.normalizedMessage.includes("null") || ctx.normalizedMessage.includes("undefined"))
},
{
category: ERROR_CATEGORIES.COMPONENT_RENDER_ERROR,
priority: 10,
match: (ctx) => ctx.normalizedMessage.includes("cannot read") || ctx.normalizedMessage.includes("is not a function") || ctx.normalizedMessage.includes("is not an object") || ctx.normalizedMessage.includes("is not defined") || ctx.normalizedMessage.includes("element type is invalid") || ctx.normalizedMessage.includes("objects are not valid as a react child") || ctx.normalizedMessage.includes("maximum call stack") || ctx.normalizedMessage.includes("render")
}
], RULES = CATEGORIZATION_RULES.sort((a, b) => b.priority - a.priority);
function categorizeError(message, stack) {
let ctx = buildErrorContext(message, stack), rule = RULES.find((r) => r.match(ctx));
if (!rule)
return { category: ERROR_CATEGORIES.UNKNOWN_ERROR, matchedDependencies: [] };
let matchedDependencies = getMatchedDependencies(rule.category, ctx);
return { category: rule.category, matchedDependencies };
}
function getMatchedDependencies(category, ctx) {
switch (category) {
case ERROR_CATEGORIES.MISSING_STATE_PROVIDER:
return Array.from(ctx.stackDeps).filter(isStateManagementPackage);
case ERROR_CATEGORIES.MISSING_ROUTER_PROVIDER:
return Array.from(ctx.stackDeps).filter(isRouterPackage);
case ERROR_CATEGORIES.MISSING_THEME_PROVIDER:
return Array.from(ctx.stackDeps).filter(isStylingPackage);
case ERROR_CATEGORIES.MISSING_TRANSLATION_PROVIDER:
return Array.from(ctx.stackDeps).filter(isI18nPackage);
default:
return [];
}
}
// src/shared/utils/analyze-test-results.ts
function extractCategorizedErrors(testResults) {
let failed = testResults.filter((r) => r.status === "FAIL" && r.error), map = /* @__PURE__ */ new Map(), uniqueErrorMessages = /* @__PURE__ */ new Set();
for (let r of failed) {
let { category, matchedDependencies } = categorizeError(r.error, r.stack);
map.has(category) || map.set(category, { count: 0, uniqueErrors: /* @__PURE__ */ new Set(), matchedDependencies: /* @__PURE__ */ new Set() });
let data = map.get(category);
data.count++, matchedDependencies.forEach((dep) => data.matchedDependencies.add(dep)), uniqueErrorMessages.add(r.error), data.uniqueErrors.add(r.error);
}
let categorizedErrors = Array.from(map.entries()).reduce(
(acc, [category, data]) => (acc[category] = {
uniqueCount: data.uniqueErrors.size,
count: data.count,
matchedDependencies: Array.from(data.matchedDependencies).sort()
}, acc),
{}
);
return {
totalErrors: failed.length,
uniqueErrorCount: uniqueErrorMessages.size,
categorizedErrors
};
}
var CSS_CHECK_STORY_ID_SUFFIX = "--css-check";
function summarizeResults(results) {
let total = results.length, passed = results.filter((r) => r.status === "PASS").length, passedButEmptyRender = results.filter((r) => r.status === "PASS" && r.emptyRender).length, successRate = total > 0 ? parseFloat((passed / total).toFixed(2)) : 0, successRateWithoutEmptyRender = total > 0 ? parseFloat(((passed - passedButEmptyRender) / total).toFixed(2)) : 0, errorClassification = extractCategorizedErrors(results), cssCheckMatch = results.find(
(r) => r.storyId.toLowerCase().endsWith(CSS_CHECK_STORY_ID_SUFFIX)
), cssCheck = cssCheckMatch?.status === "PASS" ? "pass" : cssCheckMatch?.status === "FAIL" ? "fail" : "not-run";
return {
total,
passed,
passedButEmptyRender,
successRate,
successRateWithoutEmptyRender,
uniqueErrorCount: errorClassification.uniqueErrorCount,
categorizedErrors: errorClassification.categorizedErrors,
cssCheck
};
}
function analyzeTestResults(results, cumulativeResults) {
let run = summarizeResults(results), analysis = {
total: run.total,
passed: run.passed,
passedButEmptyRender: run.passedButEmptyRender,
successRate: run.successRate,
successRateWithoutEmptyRender: run.successRateWithoutEmptyRender,
uniqueErrorCount: run.uniqueErrorCount,
categorizedErrors: run.categorizedErrors,
cssCheck: run.cssCheck
};
if (cumulativeResults) {
let cumulative = summarizeResults(cumulativeResults);
analysis.cumulativeTotal = cumulative.total, analysis.cumulativePassed = cumulative.passed, analysis.cumulativePassedButEmptyRender = cumulative.passedButEmptyRender, analysis.cumulativeSuccessRate = cumulative.successRate, analysis.cumulativeSuccessRateWithoutEmptyRender = cumulative.successRateWithoutEmptyRender, analysis.cumulativeUniqueErrorCount = cumulative.uniqueErrorCount, analysis.cumulativeCategorizedErrors = cumulative.categorizedErrors, analysis.cumulativeCssCheck = cumulative.cssCheck;
}
return analysis;
}
// src/shared/utils/to-story-test-result.ts
var DEBUG_BANNER_RE = /^\n(?:\x1B\[\d+m)?Click to debug\b[^\n]*\n\n/;
function extractErrorMessage(message, stack) {
return (message ?? "").replace(DEBUG_BANNER_RE, "").split(`
`)[0] || stack?.split(`
`)[0] || "unknown error";
}
function detectEmptyRender(reports) {
return reports?.some(
(report) => report.type === "render-analysis" && report.result?.emptyRender === !0
) ?? !1;
}
function normalizeStatus(statusRaw) {
return statusRaw === "passed" ? "PASS" : statusRaw === "failed" ? "FAIL" : "PENDING";
}
function toStoryTestResult(input) {
if (!input.storyId)
return null;
let status = normalizeStatus(input.statusRaw), emptyRender = status === "PASS" && detectEmptyRender(input.reports), error, stack;
if (input.errors && input.errors.length > 0) {
let firstError = input.errors[0];
error = extractErrorMessage(firstError.message, firstError.stack), stack = firstError.stack ?? firstError.message;
}
return {
storyId: input.storyId,
status,
error,
stack,
emptyRender: emptyRender || void 0
};
}
// src/core-server/utils/ghost-stories/run-story-tests.ts
import { existsSync as existsSync4 } from "node:fs";
import { mkdir, readFile as readFile5 } from "node:fs/promises";
import { executeCommand, resolvePathInStorybookCache as resolvePathInStorybookCache2 } from "storybook/internal/common";
// src/core-server/utils/ghost-stories/parse-vitest-report.ts
function parseVitestResults(report) {
let storyTestResults = [];
for (let testSuite of report.testResults)
for (let assertion of testSuite.assertionResults) {
let result = toStoryTestResult({
storyId: assertion.meta?.storyId ?? assertion.fullName,
statusRaw: assertion.status,
reports: assertion.meta?.reports,
errors: assertion.failureMessages?.map((message) => ({ stack: message }))
});
result && storyTestResults.push(result);
}
return {
summary: analyzeTestResults(storyTestResults)
};
}
// src/core-server/utils/ghost-stories/run-story-tests.ts
async function runStoryTests(componentFilePaths, options) {
let cwd = options?.cwd;
try {
let cacheDir2 = resolvePathInStorybookCache2("story-tests");
await mkdir(cacheDir2, { recursive: !0 });
let timestamp = Date.now(), outputFile = join(cacheDir2, `test-results-${timestamp}.json`), startTime = Date.now(), testFailureMessage;
try {
await executeCommand({
command: "npx",
args: [
"vitest",
"run",
"--reporter=json",
"--testTimeout=1000",
`--outputFile=${outputFile}`,
...componentFilePaths
],
cwd,
stdio: "pipe",
env: {
STORYBOOK_INTERNAL_TEST_RUN: "1",
...options?.ghostRun ? { STORYBOOK_COMPONENT_PATHS: componentFilePaths.join(";") } : {}
}
});
} catch (error) {
let errorMessage = (error.stderr || String(error) || "").toLowerCase();
errorMessage.includes("browsertype.launch") ? testFailureMessage = "Playwright is not installed" : errorMessage.includes("startup error") ? testFailureMessage = "Startup Error" : errorMessage.includes("no tests found") ? testFailureMessage = "No tests found" : errorMessage.includes("test timeout") ? testFailureMessage = "Test timeout" : errorMessage.includes("react-native-web") ? testFailureMessage = "React Native Web error" : errorMessage.includes("unhandled rejection") && (testFailureMessage = "Unhandled Rejection");
}
let duration = Date.now() - startTime;
if (testFailureMessage)
return {
duration,
runError: testFailureMessage
};
if (!existsSync4(outputFile))
return {
duration,
runError: "JSON report not found"
};
let vitestReport;
try {
let resultsJson = await readFile5(outputFile, "utf8");
vitestReport = JSON.parse(resultsJson);
} catch {
return {
duration,
runError: "Failed to read or parse JSON report"
};
}
return !vitestReport.testResults || vitestReport.testResults.length === 0 ? {
duration,
runError: "No tests found"
} : { ...parseVitestResults(vitestReport), duration };
} catch {
return {
runError: "Uncaught error running story tests",
duration: 0
};
}
}
// ../../node_modules/es-toolkit/dist/function/debounce.mjs
function debounce(func, debounceMs, { signal, edges } = {}) {
let pendingThis, pendingArgs = null, leading = edges != null && edges.includes("leading"), trailing = edges == null || edges.includes("trailing"), invoke = () => {
pendingArgs !== null && (func.apply(pendingThis, pendingArgs), pendingThis = void 0, pendingArgs = null);
}, onTimerEnd = () => {
trailing && invoke(), cancel();
}, timeoutId = null, schedule = () => {
timeoutId != null && clearTimeout(timeoutId), timeoutId = setTimeout(() => {
timeoutId = null, onTimerEnd();
}, debounceMs);
}, cancelTimer = () => {
timeoutId !== null && (clearTimeout(timeoutId), timeoutId = null);
}, cancel = () => {
cancelTimer(), pendingThis = void 0, pendingArgs = null;
}, flush = () => {
invoke();
}, debounced = function(...args) {
if (signal?.aborted)
return;
pendingThis = this, pendingArgs = args;
let isFirstCall = timeoutId == null;
schedule(), leading && isFirstCall && invoke();
};
return debounced.schedule = schedule, debounced.cancel = cancel, debounced.flush = flush, signal?.addEventListener("abort", cancel, { once: !0 }), debounced;
}
// ../../node_modules/es-toolkit/dist/function/throttle.mjs
function throttle(func, throttleMs, { signal, edges = ["leading", "trailing"] } = {}) {
let pendingAt = null, debounced = debounce(function(...args) {
pendingAt = Date.now(), func.apply(this, args);
}, throttleMs, { signal, edges }), throttled = function(...args) {
if (pendingAt == null && (pendingAt = Date.now()), Date.now() - pendingAt >= throttleMs) {
pendingAt = Date.now(), func.apply(this, args), debounced.cancel(), debounced.schedule();
return;
}
debounced.apply(this, args);
};
return throttled.cancel = debounced.cancel, throttled.flush = debounced.flush, throttled;
}
// ../../node_modules/es-toolkit/dist/function/partial.mjs
function partial(func, ...partialArgs) {
return partialImpl(func, placeholderSymbol, ...partialArgs);
}
function partialImpl(func, placeholder, ...partialArgs) {
let partialed = function(...providedArgs) {
let providedArgsIndex = 0, substitutedArgs = partialArgs.slice().map((arg) => arg === placeholder ? providedArgs[providedArgsIndex++] : arg), remainingArgs = providedArgs.slice(providedArgsIndex);
return func.apply(this, substitutedArgs.concat(remainingArgs));
};
return func.prototype && (partialed.prototype = Object.create(func.prototype)), partialed;
}
var placeholderSymbol = /* @__PURE__ */ Symbol("partial.placeholder");
partial.placeholder = placeholderSymbol;
// ../../node_modules/es-toolkit/dist/function/partialRight.mjs
function partialRight(func, ...partialArgs) {
return partialRightImpl(func, placeholderSymbol2, ...partialArgs);
}
function partialRightImpl(func, placeholder, ...partialArgs) {
let partialedRight = function(...providedArgs) {
let placeholderLength = partialArgs.filter((arg) => arg === placeholder).length, rangeLength = Math.max(providedArgs.length - placeholderLength, 0), remainingArgs = providedArgs.slice(0, rangeLength), providedArgsIndex = rangeLength, substitutedArgs = partialArgs.slice().map((arg) => arg === placeholder ? providedArgs[providedArgsIndex++] : arg);
return func.apply(this, remainingArgs.concat(substitutedArgs));
};
return func.prototype && (partialedRight.prototype = Object.create(func.prototype)), partialedRight;
}
var placeholderSymbol2 = /* @__PURE__ */ Symbol("partialRight.placeholder");
partialRight.placeholder = placeholderSymbol2;
// ../../node_modules/es-toolkit/dist/function/retry.mjs
var DEFAULT_RETRIES = Number.POSITIVE_INFINITY;
// src/shared/open-service/services/docgen/paths.ts
var DOCGEN_SERVICE_ID = "core/docgen";
function docgenQueryStaticPath(id) {
return `${id}.json`;
}
function docgenStaticStorePath(id) {
return `${DOCGEN_SERVICE_ID}/${docgenQueryStaticPath(id)}`;
}
function docgenPayloadJsonPointer(id) {
return `/components/${id}`;
}
function docgenManifestRef(id) {
return `../services/${docgenStaticStorePath(id)}#${docgenPayloadJsonPointer(id)}`;
}
// src/shared/open-service/services/story-docs/paths.ts
var STORY_DOCS_SERVICE_ID = "core/story-docs";
function storyDocsQueryStaticPath(id) {
return `${id}.json`;
}
function storyDocsStaticStorePath(id) {
return `${STORY_DOCS_SERVICE_ID}/${storyDocsQueryStaticPath(id)}`;
}
function storyDocsPayloadJsonPointer(id) {
return `/components/${id}`;
}
function storyDocsManifestRef(id) {
return `../services/${storyDocsStaticStorePath(id)}#${storyDocsPayloadJsonPointer(id)}`;
}
export {
useStatics,
parseStaticDir,
mapStaticDir,
debounce,
throttle,
docgenQueryStaticPath,
docgenStaticStorePath,
docgenManifestRef,
storyDocsQueryStaticPath,
storyDocsStaticStorePath,
storyDocsManifestRef,
UniversalStore,
defineService,
getStoryIdsByAbsolutePath,
resolveChangeDetectionAdapter,
registerModuleGraphService,
getWsToken,
getErrorLevel,
sendTelemetryError,
withTelemetry,
getStoryMetadata,
doesStoryFileExist,
generateStoryFile,
fullTestProviderStore,
getTestProviderStoreById,
universalTestProviderStore,
getComponentCandidates,
analyzeTestResults,
toStoryTestResult,
runStoryTests
};