storybook
Version:
Storybook: Develop, document, and test UI components in isolation
6,770 lines • 279 kB
JavaScript
import CJS_COMPAT_NODE_URL_qjinduibsyq from 'node:url';
import CJS_COMPAT_NODE_PATH_qjinduibsyq from 'node:path';
import CJS_COMPAT_NODE_MODULE_qjinduibsyq from "node:module";
var __filename = CJS_COMPAT_NODE_URL_qjinduibsyq.fileURLToPath(import.meta.url);
var __dirname = CJS_COMPAT_NODE_PATH_qjinduibsyq.dirname(__filename);
var require = CJS_COMPAT_NODE_MODULE_qjinduibsyq.createRequire(import.meta.url);
// ------------------------------------------------------------
// end of CJS compatibility banner, injected by Storybook's esbuild configuration
// ------------------------------------------------------------
import {
UniversalStore,
instances
} from "./chunk-HFXQZX54.js";
import {
CHANGE_DETECTION_STATUS_TYPE_ID,
UNIVERSAL_STATUS_STORE_OPTIONS,
createStatusStore
} from "./chunk-5DGTSUE6.js";
import {
require_build
} from "./chunk-G4ZCAUZF.js";
import {
pLimit
} from "./chunk-LBGJYL6K.js";
import {
isI18nPackage,
isRouterPackage,
isStateManagementPackage,
isStylingPackage
} from "./chunk-XTZ2XLTY.js";
import {
STORYBOOK_FN_PLACEHOLDER,
generateDummyArgsFromArgTypes,
loadConfig,
printConfig
} from "./chunk-ISBS76F6.js";
import {
reportToolsetTelemetry
} from "./chunk-EEWXQXZP.js";
import {
getToolName,
toMcpToolName
} from "./chunk-52XR72NR.js";
import {
execa,
getComponentIdFromEntry,
optionalEnvToBoolean,
selectComponentEntriesByComponentId
} from "./chunk-5XWVTFUD.js";
import {
up
} from "./chunk-7BA6K3X5.js";
import {
invariant
} from "./chunk-NQDYICFM.js";
import {
Tag
} from "./chunk-PG6QDVJT.js";
import {
getService as getService2,
registerService
} from "./chunk-UAW7LJY2.js";
import {
any,
array,
boolean,
custom,
description,
getRegisteredServices,
getService,
integer,
isEqual,
isPlainObject,
lazy,
literal,
looseObject,
minLength,
minValue,
nullable,
number,
object,
optional,
pipe,
record,
string,
undefined_,
union,
variant,
void_
} from "./chunk-5EYYAT2W.js";
import {
importModule,
resolvePackageDir
} from "./chunk-5G6TGQDE.js";
import {
jsTsSourceExtensions
} from "./chunk-7LZUPCQB.js";
import {
OpenServiceDocgenMissingComponentError,
OpenServiceMissingOriginError,
OpenServiceMissingServiceError,
OpenServiceModuleGraphUnavailableError,
OpenServiceUnknownStoryIdsError,
describeUnknownStoryIds
} from "./chunk-DW727PJG.js";
import {
require_dist
} from "./chunk-FE5AV6EP.js";
import {
require_picocolors
} from "./chunk-VGN5LGMI.js";
import {
errorToErrorLike,
reverseIndexToStoriesByFile,
toStoryIndexPath
} from "./chunk-HVTJ6ETP.js";
import {
basename,
dirname,
extname,
join,
normalize,
relative,
resolve
} from "./chunk-PPVCVHOS.js";
import {
glob
} from "./chunk-S76TV45M.js";
import {
__commonJS,
__toESM
} from "./chunk-4US4PNS3.js";
// ../../node_modules/map-or-similar/src/similar.js
var require_similar = __commonJS({
"../../node_modules/map-or-similar/src/similar.js"(exports, module) {
function Similar() {
return this.list = [], this.lastItem = void 0, this.size = 0, this;
}
Similar.prototype.get = function(key) {
var index;
if (this.lastItem && this.isEqual(this.lastItem.key, key))
return this.lastItem.val;
if (index = this.indexOf(key), index >= 0)
return this.lastItem = this.list[index], this.list[index].val;
};
Similar.prototype.set = function(key, val) {
var index;
return this.lastItem && this.isEqual(this.lastItem.key, key) ? (this.lastItem.val = val, this) : (index = this.indexOf(key), index >= 0 ? (this.lastItem = this.list[index], this.list[index].val = val, this) : (this.lastItem = { key, val }, this.list.push(this.lastItem), this.size++, this));
};
Similar.prototype.delete = function(key) {
var index;
if (this.lastItem && this.isEqual(this.lastItem.key, key) && (this.lastItem = void 0), index = this.indexOf(key), index >= 0)
return this.size--, this.list.splice(index, 1)[0];
};
Similar.prototype.has = function(key) {
var index;
return this.lastItem && this.isEqual(this.lastItem.key, key) ? !0 : (index = this.indexOf(key), index >= 0 ? (this.lastItem = this.list[index], !0) : !1);
};
Similar.prototype.forEach = function(callback, thisArg) {
var i;
for (i = 0; i < this.size; i++)
callback.call(thisArg || this, this.list[i].val, this.list[i].key, this);
};
Similar.prototype.indexOf = function(key) {
var i;
for (i = 0; i < this.size; i++)
if (this.isEqual(this.list[i].key, key))
return i;
return -1;
};
Similar.prototype.isEqual = function(val1, val2) {
return val1 === val2 || val1 !== val1 && val2 !== val2;
};
module.exports = Similar;
}
});
// ../../node_modules/map-or-similar/src/map-or-similar.js
var require_map_or_similar = __commonJS({
"../../node_modules/map-or-similar/src/map-or-similar.js"(exports, module) {
module.exports = function(forceSimilar) {
if (typeof Map != "function" || forceSimilar) {
var Similar = require_similar();
return new Similar();
} else
return /* @__PURE__ */ new Map();
};
}
});
// ../../node_modules/memoizerific/src/memoizerific.js
var require_memoizerific = __commonJS({
"../../node_modules/memoizerific/src/memoizerific.js"(exports, module) {
var MapOrSimilar = require_map_or_similar();
module.exports = function(limit) {
var cache4 = new MapOrSimilar(process.env.FORCE_SIMILAR_INSTEAD_OF_MAP === "true"), lru = [];
return function(fn) {
var memoizerific = function() {
var currentCache = cache4, newMap, fnResult, argsLengthMinusOne = arguments.length - 1, lruPath = Array(argsLengthMinusOne + 1), isMemoized = !0, i;
if ((memoizerific.numArgs || memoizerific.numArgs === 0) && memoizerific.numArgs !== argsLengthMinusOne + 1)
throw new Error("Memoizerific functions should always be called with the same number of arguments");
for (i = 0; i < argsLengthMinusOne; i++) {
if (lruPath[i] = {
cacheItem: currentCache,
arg: arguments[i]
}, currentCache.has(arguments[i])) {
currentCache = currentCache.get(arguments[i]);
continue;
}
isMemoized = !1, newMap = new MapOrSimilar(process.env.FORCE_SIMILAR_INSTEAD_OF_MAP === "true"), currentCache.set(arguments[i], newMap), currentCache = newMap;
}
return isMemoized && (currentCache.has(arguments[argsLengthMinusOne]) ? fnResult = currentCache.get(arguments[argsLengthMinusOne]) : isMemoized = !1), isMemoized || (fnResult = fn.apply(null, arguments), currentCache.set(arguments[argsLengthMinusOne], fnResult)), limit > 0 && (lruPath[argsLengthMinusOne] = {
cacheItem: currentCache,
arg: arguments[argsLengthMinusOne]
}, isMemoized ? moveToMostRecentLru(lru, lruPath) : lru.push(lruPath), lru.length > limit && removeCachedResult(lru.shift())), memoizerific.wasMemoized = isMemoized, memoizerific.numArgs = argsLengthMinusOne + 1, fnResult;
};
return memoizerific.limit = limit, memoizerific.wasMemoized = !1, memoizerific.cache = cache4, memoizerific.lru = lru, memoizerific;
};
};
function moveToMostRecentLru(lru, lruPath) {
var lruLen = lru.length, lruPathLen = lruPath.length, isMatch, i, ii;
for (i = 0; i < lruLen; i++) {
for (isMatch = !0, ii = 0; ii < lruPathLen; ii++)
if (!isEqual2(lru[i][ii].arg, lruPath[ii].arg)) {
isMatch = !1;
break;
}
if (isMatch)
break;
}
lru.push(lru.splice(i, 1)[0]);
}
function removeCachedResult(removedLru) {
var removedLruLen = removedLru.length, currentLru = removedLru[removedLruLen - 1], tmp, i;
for (currentLru.cacheItem.delete(currentLru.arg), i = removedLruLen - 2; i >= 0 && (currentLru = removedLru[i], tmp = currentLru.cacheItem.get(currentLru.arg), !tmp || !tmp.size); i--)
currentLru.cacheItem.delete(currentLru.arg);
}
function isEqual2(val1, val2) {
return val1 === val2 || val1 !== val1 && val2 !== val2;
}
}
});
// src/core-server/utils/server-statics.ts
import { existsSync, statSync } from "node:fs";
import { readFile, stat } from "node:fs/promises";
import { basename as basename2, isAbsolute, join as join2, posix, resolve as resolve2, 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 (path3) => {
if (files.has(path3))
return files.get(path3);
{
let [data, stats] = await Promise.all([readFile(path3, "utf-8"), stat(path3)]), result = { data, mtime: stats.mtimeMs };
return files.set(path3, 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 = resolve2(faviconPath, ".."), faviconFile = basename2(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 = resolve2(staticPath, ".."), staticPathFile = basename2(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 = resolve2(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() ? basename2(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/core-server/utils/get-builders.ts
import { MissingBuilderError } from "storybook/internal/server-errors";
async function getManagerBuilder() {
return await import("./builder-manager-TBY7BTS6.js");
}
async function getPreviewBuilder(resolvedPreviewBuilder) {
return await importModule(resolvedPreviewBuilder);
}
async function getBuilders({ presets }) {
let { builder } = await presets.apply("core", {});
if (!builder)
throw new MissingBuilderError();
let resolvedPreviewBuilder = typeof builder == "string" ? builder : builder.name;
return Promise.all([getPreviewBuilder(resolvedPreviewBuilder), getManagerBuilder()]);
}
// src/core-server/utils/manifests/mdx-manifest.ts
var MDX_SERVICE_ID = "addon-docs/mdx";
function mdxQueryStaticPath(id) {
return `${id}.json`;
}
function mdxStaticStorePath(id) {
return `${MDX_SERVICE_ID}/${mdxQueryStaticPath(id)}`;
}
function mdxPayloadJsonPointer(id) {
return `/components/${id}`;
}
function mdxDocJsonPointer(componentId, docId) {
return `${mdxPayloadJsonPointer(componentId)}/docs/${docId}`;
}
function mdxManifestRef(componentId, docId) {
return `../services/${mdxStaticStorePath(componentId)}#${mdxDocJsonPointer(componentId, docId)}`;
}
// src/core-server/utils/manifests/manifests.ts
import { mkdir, writeFile } from "node:fs/promises";
import { selectComponentEntriesByComponentId as selectComponentEntriesByComponentId2 } from "storybook/internal/common";
import { logger as logger2 } from "storybook/internal/node-logger";
// src/core-server/utils/manifests/components-ref-manifest.ts
import { readFile as readFile2 } from "node:fs/promises";
// 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)}`;
}
// src/core-server/utils/manifests/components-ref-manifest.ts
var COMPONENTS_REF_MANIFEST_VERSION = 1;
function readDocgenSnapshotPayload(document, id) {
let payload = document?.components?.[id];
return payload !== null && typeof payload == "object" ? payload : void 0;
}
function readStoryDocsSnapshotPayload(document, id) {
let payload = document?.components?.[id];
return payload !== null && typeof payload == "object" ? payload : void 0;
}
function mergeManifestPayloads(docgen, storyDocs) {
return {
...docgen,
stories: storyDocs?.stories ?? {},
...storyDocs?.import ? { import: storyDocs.import } : {}
};
}
async function loadDocgenPayloadsFromDisk(outputDir, componentIds) {
let entries = await Promise.all(
componentIds.map(async (id) => {
let snapshotPath = join(outputDir, "services", ...docgenStaticStorePath(id).split("/"));
try {
let document = JSON.parse(await readFile2(snapshotPath, "utf8")), payload = readDocgenSnapshotPayload(document, id);
return payload ? [id, payload] : null;
} catch {
return null;
}
})
);
return Object.fromEntries(entries.filter((entry) => entry !== null));
}
async function loadStoryDocsPayloadsFromDisk(outputDir, componentIds) {
let entries = await Promise.all(
componentIds.map(async (id) => {
let snapshotPath = join(outputDir, "services", ...storyDocsStaticStorePath(id).split("/"));
try {
let document = JSON.parse(await readFile2(snapshotPath, "utf8")), payload = readStoryDocsSnapshotPayload(document, id);
return payload ? [id, payload] : null;
} catch {
return null;
}
})
);
return Object.fromEntries(entries.filter((entry) => entry !== null));
}
function toComponentManifestIndexEntries(componentIds, docgenPayloads, storyDocsPayloads = {}, docsByComponentId = {}) {
let entries = {};
for (let id of componentIds) {
let payload = docgenPayloads[id], storyDocs = storyDocsPayloads[id];
entries[id] = {
id: payload?.id ?? id,
name: payload?.name ?? id,
...payload?.description !== void 0 ? { description: payload.description } : {},
...payload?.summary !== void 0 ? { summary: payload.summary } : {},
...payload ? { docgen: { $ref: docgenManifestRef(id) } } : {},
// Stories are indexed whether or not a component was extracted, so the ref does not hang off
// the docgen payload: dropping it hid every story of a componentless component.
...storyDocs ? { stories: { $ref: storyDocsManifestRef(id) } } : {},
...docsByComponentId[id] ? { docs: docsByComponentId[id] } : {}
};
}
return entries;
}
function buildComponentsRefManifest(components, meta) {
return {
v: COMPONENTS_REF_MANIFEST_VERSION,
components,
...meta ? { meta } : {}
};
}
// src/core-server/utils/manifests/mdx-ref-resolution.ts
import { readFile as readFile3 } from "node:fs/promises";
var fullTransform = (_entry, resolved) => resolved, shallowSummaryTransform = (entry, resolved) => resolved.summary !== void 0 ? { ...entry, summary: resolved.summary } : entry;
function hasMdxRef(entry) {
return typeof entry?.mdx?.$ref == "string";
}
function createDiskLoader(outputDir) {
let cache4 = /* @__PURE__ */ new Map();
return (relativePath) => {
let pending = cache4.get(relativePath);
return pending || (pending = readFile3(join(outputDir, "manifests", relativePath), "utf8").then(
(raw) => JSON.parse(raw)
), cache4.set(relativePath, pending)), pending;
};
}
function createServiceLoader(mdxPayloads) {
let document = { components: mdxPayloads };
return () => document;
}
function getJsonPointerValue(document, pointer) {
return pointer === "" || pointer === "/" ? document : pointer.split("/").slice(1).map((part) => part.replace(/~1/g, "/").replace(/~0/g, "~")).reduce((value, key) => value?.[key], document);
}
async function resolveRef(ref, load) {
let [relativePath, pointer = ""] = ref.$ref.split("#");
invariant(relativePath, `Invalid MDX manifest ref "${ref.$ref}"`);
let document = await load(relativePath), value = getJsonPointerValue(document, pointer);
return invariant(
value && typeof value == "object",
`MDX manifest ref "${ref.$ref}" did not resolve to an object`
), value;
}
async function resolveDocsRecord(docs = {}, load, transform) {
let entries = await Promise.all(
Object.entries(docs).map(
async ([id, entry]) => hasMdxRef(entry) ? [id, transform(entry, await resolveRef(entry.mdx, load))] : [id, entry]
)
);
return Object.fromEntries(entries);
}
function getAttachedDocsByComponent(manifest) {
let components = manifest?.components ?? {};
return Object.fromEntries(
Object.entries(components).flatMap(
([id, component]) => component.docs ? [[id, component.docs]] : []
)
);
}
function hasMdxRefs(manifests, docsManifest) {
let hasUnattachedRefs = Object.values(docsManifest?.docs ?? {}).some(
(entry) => hasMdxRef(entry)
), hasAttachedRefs = Object.values(getAttachedDocsByComponent(manifests.components)).some(
(docs) => Object.values(docs).some((entry) => hasMdxRef(entry))
);
return hasUnattachedRefs || hasAttachedRefs;
}
function createDocsOnlyDocgenPayload(id) {
return { id, name: id, path: "", jsDocTags: {}, stories: [] };
}
async function resolveComponentDocs(components, manifests, load, transform) {
let docsByComponentId = getAttachedDocsByComponent(manifests.components), allIds = /* @__PURE__ */ new Set([...Object.keys(components), ...Object.keys(docsByComponentId)]), entries = await Promise.all(
[...allIds].map(async (id) => {
let docs = docsByComponentId[id], component = components[id] ?? {
...createDocsOnlyDocgenPayload(id),
stories: {}
};
return docs ? [id, { ...component, docs: await resolveDocsRecord(docs, load, transform) }] : [id, component];
})
);
return Object.fromEntries(entries);
}
async function resolveDocsManifestRefs(docsManifest, load, transform) {
if (docsManifest)
return {
...docsManifest,
docs: await resolveDocsRecord(
docsManifest.docs,
load,
transform
)
};
}
async function injectAttachedDocsSummaries(attachedByComponent, load) {
let entries = await Promise.all(
Object.entries(attachedByComponent).map(
async ([id, docs]) => [id, await resolveDocsRecord(docs, load, shallowSummaryTransform)]
)
);
return Object.fromEntries(entries);
}
async function loadMdxPayloadsFromServiceIfNeeded(manifests, docsManifest) {
return hasMdxRefs(manifests, docsManifest) ? getService2(MDX_SERVICE_ID, { internal: !0 }).queries.mdxForAllComponents.loaded() : {};
}
// src/core-server/utils/manifests/render-components-manifest.ts
import path from "node:path";
import { groupBy } from "storybook/internal/common";
function storyEntries(stories) {
return Array.isArray(stories) ? stories : Object.values(stories ?? {});
}
function renderComponentsManifest(manifest, docsManifest) {
let entries = Object.entries(manifest?.components ?? {}).sort(
(a, b) => (a[1].name || a[0]).localeCompare(b[1].name || b[0])
), docsEntries = Object.entries(docsManifest?.docs ?? {}).sort(
(a, b) => (a[1].name || a[0]).localeCompare(b[1].name || b[0])
), analyses = entries.map(([, c]) => analyzeComponent(c)), docsAnalyses = docsEntries.map(([, d]) => analyzeDoc(d)), attachedDocs = analyses.reduce((sum, a) => sum + a.totalDocs, 0), attachedDocsWithError = analyses.reduce((sum, a) => sum + a.docsErrors, 0), unattachedDocsWithError = docsAnalyses.filter((a) => a.hasError).length, totals = {
components: entries.length,
componentsWithApiError: analyses.filter((a) => a.hasApiError).length,
infos: analyses.filter((a) => a.hasWarns).length,
stories: analyses.reduce((sum, a) => sum + a.totalStories, 0),
storyErrors: analyses.reduce((sum, a) => sum + a.storyErrors, 0),
storyWarnings: analyses.reduce((sum, a) => sum + a.storyWarnings, 0),
componentsWithoutApi: analyses.filter((a) => a.api.kind === "none").length,
docs: docsEntries.length + attachedDocs,
docsWithError: unattachedDocsWithError + attachedDocsWithError
}, activeEngine = manifest?.meta?.docgen ?? "react-docgen", durationMs = manifest?.meta?.durationMs ?? 0, allPill = '<a class="filter-pill all" data-k="all" href="#filter-all">All</a>', compErrorsPill = totals.componentsWithApiError > 0 ? `<a class="filter-pill err" data-k="errors" href="#filter-errors">${totals.componentsWithApiError}/${totals.components} API ${plural(totals.componentsWithApiError, "error")}</a>` : totals.components > 0 ? `<span class="filter-pill ok" aria-disabled="true">${totals.components} components ok</span>` : "", compInfosPill = totals.infos > 0 ? `<a class="filter-pill info" data-k="infos" href="#filter-infos">${totals.infos}/${totals.components} ${plural(totals.infos, "info", "infos")}</a>` : "", storiesPill = totals.storyErrors > 0 ? `<a class="filter-pill err" data-k="story-errors" href="#filter-story-errors">${totals.storyErrors}/${totals.stories} story errors</a>` : totals.stories > 0 ? `<span class="filter-pill ok" aria-disabled="true">${totals.stories} ${plural(totals.stories, "story", "stories")} ok</span>` : "", snippetWarningsPill = totals.storyWarnings > 0 ? `<a class="filter-pill info" data-k="story-warnings" href="#filter-story-warnings">${totals.storyWarnings}/${totals.stories} incomplete snippets</a>` : "", noApiPill = totals.componentsWithoutApi > 0 ? `<a class="filter-pill" data-k="no-api" href="#filter-no-api">${totals.componentsWithoutApi}/${totals.components} without API description</a>` : "", docsPill = totals.docs > 0 ? totals.docsWithError > 0 ? `<a class="filter-pill info" data-k="docs" href="#filter-docs">${totals.docsWithError}/${totals.docs} doc ${plural(totals.docsWithError, "error")}</a>` : `<a class="filter-pill ok" data-k="docs" href="#filter-docs">${totals.docs} ${plural(totals.docs, "doc")} ok</a>` : "", grid = entries.map(([key, c], idx) => renderComponentCard(key, c, `${idx}`, key)).join(""), docsGrid = docsEntries.map(([key, d], idx) => renderDocCard(key, d, `doc-${idx}`)).join(""), errorGroups = Object.entries(
groupBy(
entries.map(([, it]) => it).filter((it) => it.error),
(manifest2) => manifest2.error?.name ?? "Error"
)
).sort(([, a], [, b]) => b.length - a.length), errorGroupsHTML = errorGroups.map(([error, grouped]) => {
let id = error.toLowerCase().replace(/[^a-z0-9]+/g, "-"), headerText = `${esc(error)}`, cards = grouped.map((manifest2, id2) => renderComponentCard(manifest2.id, manifest2, `error-${id2}`)).join("");
return `
<section class="group">
<input id="${id}-toggle" class="group-tg" type="checkbox" hidden />
<label for="${id}-toggle" class="group-header">
<span class="caret">\u25B8</span>
<span class="group-title">${headerText}</span>
<span class="group-count">${grouped.length}</span>
</label>
<div class="group-cards">${cards}</div>
</section>
`;
}).join("");
return `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Components Manifest</title>
<style>
:root {
--bg: #0b0c10;
--panel: #121318;
--muted: #9aa0a6;
--fg: #e8eaed;
--ok: #22c55e;
--info: #1e88e5;
--err: #c62828;
--ok-bg: #0c1a13;
--info-bg: #0c1624;
--err-bg: #1a0e0e;
--chip: #1f2330;
--border: #2b2f3a;
--link: #8ab4f8;
--active-ring: 1px; /* 1px active ring for pills and toggles */
}
* {
box-sizing: border-box;
}
html,
body {
margin: 0;
background: var(--bg);
color: var(--fg);
font: 14px/1.5 system-ui,
-apple-system,
Segoe UI,
Roboto,
Ubuntu,
Cantarell,
'Helvetica Neue',
Arial,
'Noto Sans';
}
.wrap {
max-width: 1100px;
margin: 0 auto;
padding: 16px 20px;
}
header {
position: sticky;
top: 0;
backdrop-filter: blur(6px);
background: color-mix(in srgb, var(--bg) 84%, transparent);
border-bottom: 1px solid var(--border);
z-index: 10;
}
h1 {
font-size: 20px;
margin: 0 0 6px;
}
.summary {
display: flex;
gap: 12px;
flex-wrap: wrap;
align-items: center;
}
/* Top filter pills */
.filter-pill {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
border: 1px solid var(--border);
border-radius: 999px;
background: var(--panel);
text-decoration: none;
cursor: pointer;
user-select: none;
color: var(--fg);
}
.filter-pill.ok {
color: #b9f6ca;
border-color: color-mix(in srgb, var(--ok) 55%, var(--border));
background: color-mix(in srgb, var(--ok) 18%, #000);
}
.filter-pill.info {
color: #b3d9ff;
border-color: color-mix(in srgb, var(--info) 55%, var(--border));
background: var(--info-bg);
}
.filter-pill.err {
color: #ff9aa0;
border-color: color-mix(in srgb, var(--err) 55%, var(--border));
background: var(--err-bg);
}
.filter-pill.all {
color: #d7dbe0;
border-color: var(--border);
background: var(--panel);
}
.filter-pill[aria-disabled='true'] {
cursor: default;
text-decoration: none;
}
.filter-pill:focus,
.filter-pill:active {
outline: none;
box-shadow: none;
}
/* Selected top pill ring via :target */
#filter-all:target ~ header .filter-pill[data-k='all'],
#filter-errors:target ~ header .filter-pill[data-k='errors'],
#filter-infos:target ~ header .filter-pill[data-k='infos'],
#filter-story-errors:target ~ header .filter-pill[data-k='story-errors'],
#filter-story-warnings:target ~ header .filter-pill[data-k='story-warnings'],
#filter-no-api:target ~ header .filter-pill[data-k='no-api'],
#filter-doc-errors:target ~ header .filter-pill[data-k='docs'],
#filter-docs:target ~ header .filter-pill[data-k='docs'] {
box-shadow: 0 0 0 var(--active-ring) currentColor;
border-color: currentColor;
}
/* Hidden targets for filtering */
#filter-all,
#filter-errors,
#filter-infos,
#filter-story-errors,
#filter-story-warnings,
#filter-no-api,
#filter-doc-errors,
#filter-docs {
display: none;
}
main {
padding: 36px 0 40px;
}
.grid {
display: grid;
grid-template-columns: 1fr;
gap: 18px;
}
/* one card per row */
.card {
border: 1px solid var(--border);
background: var(--panel);
border-radius: 14px;
padding: 14px;
display: flex;
flex-direction: column;
gap: 10px;
/* Keep a deep-linked card clear of the sticky header when scrolled into view */
scroll-margin-top: 110px;
}
/* Highlight the card deep-linked via components.html#<manifest-id> */
.card:target {
box-shadow: 0 0 0 2px var(--info);
border-color: var(--info);
}
.head {
display: flex;
flex-direction: column;
gap: 8px;
}
.title {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.title h2 {
font-size: 16px;
margin: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.meta {
font-size: 12px;
color: var(--muted);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.kv {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.chip {
font-size: 12px;
padding: 4px 8px;
border-radius: 999px;
background: var(--chip);
border: 1px solid var(--border);
}
.hint {
color: var(--muted);
font-size: 12px;
}
.badges {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
/* Per-card badges: labels become toggles when clickable */
.badge {
font-size: 12px;
padding: 3px 8px;
border-radius: 999px;
border: 1px solid var(--border);
background: var(--chip);
color: #d7dbe0;
}
.badge.ok {
color: #b9f6ca;
border-color: color-mix(in srgb, var(--ok) 55%, var(--border));
}
.badge.info {
color: #b3d9ff;
border-color: color-mix(in srgb, var(--info) 55%, var(--border));
}
.badge.err {
color: #ff9aa0;
border-color: color-mix(in srgb, var(--err) 55%, var(--border));
}
.snippet-warning {
margin-top: 8px;
padding: 8px 10px;
border-radius: 8px;
border: 1px solid color-mix(in srgb, var(--info) 45%, var(--border));
background: var(--info-bg);
color: #b3d9ff;
font-size: 12px;
}
.as-toggle {
cursor: pointer;
}
/* 1px ring on active toggle */
.tg-err:checked + label.as-toggle,
.tg-info:checked + label.as-toggle,
.tg-stories:checked + label.as-toggle,
.tg-docs:checked + label.as-toggle,
.tg-subcomponents:checked + label.as-toggle,
.tg-content:checked + label.as-toggle,
.tg-props:checked + label.as-toggle {
box-shadow: 0 0 0 var(--active-ring) currentColor;
border-color: currentColor;
}
/* Panels: hidden by default, shown when respective toggle checked */
.panels {
display: grid;
gap: 10px;
}
.panel {
display: none;
}
.tg-err:checked ~ .panels .panel-err {
display: grid;
}
.tg-info:checked ~ .panels .panel-info {
display: grid;
gap: 8px;
}
.tg-stories:checked ~ .panels .panel-stories {
display: grid;
gap: 8px;
}
.tg-docs:checked ~ .panels .panel-docs {
display: grid;
gap: 8px;
}
.tg-subcomponents:checked ~ .panels .panel-subcomponents {
display: grid;
gap: 8px;
}
.tg-content:checked ~ .panels .panel-content {
display: grid;
gap: 8px;
}
.tg-props:checked ~ .panels .panel-props {
display: grid;
}
/* Colored notes for API error + info */
.note {
padding: 12px;
border: 1px solid var(--border);
border-radius: 10px;
}
.note.err {
border-color: color-mix(in srgb, var(--err) 55%, var(--border));
background: var(--err-bg);
color: #ffd1d4;
}
.note.info {
border-color: color-mix(in srgb, var(--info) 55%, var(--border));
background: var(--info-bg);
color: #d6e8ff;
}
.note.ok {
border-color: color-mix(in srgb, var(--ok) 55%, var(--border));
background: var(--ok-bg);
color: var(--fg);
}
.note-title {
font-weight: 600;
margin-bottom: 6px;
}
.note-body {
white-space: normal;
}
/* Story error cards */
.ex {
padding: 10px;
border: 1px solid var(--border);
border-radius: 10px;
background: #0f131b;
}
.ex.err {
border-color: color-mix(in srgb, var(--err) 55%, var(--border));
}
.row {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
}
.dot-ok {
background: var(--ok);
}
.dot-err {
background: var(--err);
}
.ex-name {
font-weight: 600;
}
/* Error groups (visible in errors filter) */
.error-groups {
display: none;
margin-bottom: 16px;
}
.group {
border: 1px solid var(--border);
background: var(--panel);
border-radius: 14px;
overflow: hidden;
}
.group + .group {
margin-top: 12px;
}
.group-header {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 14px;
cursor: pointer;
border-bottom: 1px solid var(--border);
}
.group-header:hover {
background: #141722;
}
.group-title {
font-weight: 600;
flex: 1;
}
.group-count {
font-size: 12px;
color: var(--muted);
}
.group-cards {
display: none;
padding: 12px;
}
.group .card {
margin: 12px 0;
}
.group .card:first-child {
margin-top: 0;
}
.group .card:last-child {
margin-bottom: 0;
}
/* caret rotation */
.group-tg:checked + label .caret {
transform: rotate(90deg);
}
.caret {
transition: transform 0.15s ease;
}
/* toggle body */
.group-tg:checked ~ .group-cards {
display: block;
}
/* CSS-only filtering of cards via top pills */
#filter-errors:target ~ main .card:not(.has-error):not(.has-story-error) {
display: none;
}
#filter-infos:target ~ main .card:not(.has-info) {
display: none;
}
#filter-story-errors:target ~ main .card:not(.has-story-error) {
display: none;
}
#filter-story-warnings:target ~ main .card:not(.has-story-warning) {
display: none;
}
#filter-no-api:target ~ main .card:not(.missing-api) {
display: none;
}
#filter-doc-errors:target ~ main .card:not(.has-doc-error) {
display: none;
}
#filter-docs:target ~ main .card:has(> .tg-docs),
#filter-docs:target ~ main .card.is-doc {
display: block;
}
#filter-docs:target ~ main .card:not(:has(> .tg-docs)):not(.is-doc) {
display: none;
}
#filter-all:target ~ main .card {
display: block;
}
/* In errors view, hide standalone component-error cards in the regular grid (they will appear in groups) */
#filter-errors:target ~ main .grid .card.has-error {
display: none;
}
/* Show grouped section only in errors view */
#filter-errors:target ~ main .error-groups {
display: block;
}
/* Section titles */
.section-title {
font-size: 16px;
font-weight: 600;
color: var(--muted);
margin: 24px 0 12px;
padding-bottom: 8px;
border-bottom: 1px solid var(--border);
}
.section-title:first-child {
margin-top: 0;
}
/* When a toggle is checked, show the corresponding panel */
.card > .tg-err:checked ~ .panels .panel-err {
display: grid;
}
.card > .tg-info:checked ~ .panels .panel-info {
display: grid;
}
.card > .tg-stories:checked ~ .panels .panel-stories {
display: grid;
}
.card > .tg-docs:checked ~ .panels .panel-docs {
display: grid;
}
.card > .tg-subcomponents:checked ~ .panels .panel-subcomponents {
display: grid;
}
.card > .tg-content:checked ~ .panels .panel-content {
display: grid;
}
/* Add vertical spacing around panels only when any panel is visible */
.card > .tg-err:checked ~ .panels,
.card > .tg-info:checked ~ .panels,
.card > .tg-stories:checked ~ .panels,
.card > .tg-docs:checked ~ .panels,
.card > .tg-subcomponents:checked ~ .panels,
.card > .tg-content:checked ~ .panels,
.card > .tg-props:checked ~ .panels {
margin: 10px 0;
}
/* Optional: a subtle 1px ring on the active badge, using :has() if available */
@supports selector(.card:has(.tg-err:checked)) {
.card:has(.tg-err:checked) label[for$='-err'],
.card:has(.tg-info:checked) label[for$='-info'],
.card:has(.tg-stories:checked) label[for$='-stories'],
.card:has(.tg-docs:checked) label[for$='-docs'],
.card:has(.tg-subcomponents:checked) label[for$='-subcomponents'],
.card:has(.tg-content:checked) label[for$='-content'],
.card:has(.tg-props:checked) label[for$='-props'] {
box-shadow: 0 0 0 1px currentColor;
border-color: currentColor;
}
}
/* Wrap long lines in code blocks at ~120 characters */
pre, code {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}
pre {
white-space: pre-wrap;
overflow-wrap: anywhere;
word-break: break-word;
overflow-x: auto; /* fallback for extremely long tokens */
margin: 8px 0 0;
}
pre > code {
display: block;
white-space: inherit;
overflow-wrap: inherit;
word-break: inherit;
inline-size: min(100%, 120ch);
}
/* MDX content container for docs */
.mdx-content {
background: #0f131b;
border: 1px solid var(--border);
border-radius: 10px;
padding: 12px;
max-height: 400px;
overflow-y: auto;
margin-top: 8px;
}
</style>
</head>
<body>
<!-- Hidden targets for the top-level filters -->
<span id="filter-all"></span>
<span id="filter-errors"></span>
<span id="filter-infos"></span>
<span id="filter-story-errors"></span>
<span id="filter-story-warnings"></span>
<span id="filter-no-api"></span>
<span id="filter-doc-errors"></span>
<span id="filter-docs"></span>
<header>
<div class="wrap">
<h1>Manifest Debugger</h1>
<div class="summary">${allPill}${compErrorsPill}${compInfosPill}${noApiPill}${storiesPill}${snippetWarningsPill}${docsPill}</div>
</div>
</header>
<main>
<div class="wrap">
${activeEngine === "react-docgen" ? `<div class="note info" style="margin-bottom: 16px;">
<strong>Tip:</strong> You are using <code>react-docgen</code> (the default). Generation took <strong>${(durationMs / 1e3).toFixed(1)}s</strong>. For higher quality prop types, consider switching to <code>react-docgen-typescript</code> in your <code>main.ts</code>:
<pre><code>typescript: {
reactDocgen: 'react-docgen-typescript',
}</code></pre>
Note: <code>react-docgen-typescript</code> can be slower. If performance is acceptable for your project, it generally produces better results.
<a href="https://storybook.js.org/docs/api/main-config/main-config-typescript#reactdocgen" target="_blank">Learn more</a>
</div>` : activeEngine === "react-docgen-typescript" && durationMs > 7500 ? `<div class="note err" style="margin-bottom: 16px;">
<strong>Performance warning:</strong> <code>react-docgen-typescript</code> took <strong>${(durationMs / 1e3).toFixed(1)}s</strong> to generate the manifest. This delay applies every time the manifest is used by an agent. Consider switching to the faster <code>react-docgen</code> in your <code>main.ts</code>:
<pre><code>typescript: {
reactDocgen: 'react-docgen',
}</code></pre>
<a href="https://storybook.js.org/docs/api/main-config/main-config-typescript#reactdocgen" target="_blank">Learn more</a>
</div>` : `<div class="note ok" style="margin-bottom: 16px;">
Using <code>${activeEngine}</code>. Generation took <strong>${(durationMs / 1e3).toFixed(1)}s</strong>.
</div>`}
${grid ? `<h2 class="section-title">Components</h2>
<div class="grid" role="list">
${grid}
</div>` : ""}
${errorGroups.length ? `<div class="error-groups" role="region" aria-label="API error groups">${errorGroupsHTML}</div>` : ""}
${docsGrid ? `<h2 class="section-title">Unattached Docs</h2>
<div class="grid" role="list">
${docsGrid}
</div>` : ""}
${!grid && !docsGrid ? '<div class="card"><div class="head"><div class="hint">No components or docs.</div></div></div>' : ""}
</div>
</main>
</body>
</html> `;
}
var esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c] ?? c), plural = (n, one, many = `${one}s`) => n === 1 ? one : many;
function analyzeComponent(c) {
let api = resolveComponentApi(c), hasApiError = api.kind === "error", warns = [];
c.description?.trim() || warns.push("No description found. Write a jsdoc comment such as /** Component description */."), c.import?.trim() || warns.push(
`Specify an @import jsdoc tag on your component or your stories meta such as @import import { ${c.name} } from 'my-design-system';`
);
let allStories = storyEntries(c.stories), totalStories = allStories.length, storyErrors = allStories.filter((e) => !!e?.error).length, storyWarnings = allStories.filter((e) => !!e?.warning).length, storyOk = totalStories - storyErrors, docsEntries = c.docs ? Object.values(c.docs) : [], totalDocs = docsEntries.length, docsErrors = docsEntries.filter((d) => !!d?.error).length, docsOk = totalDocs - docsErrors, hasAnyError = hasApiError || storyErrors > 0 || docsErrors > 0;
return {
api,
hasApiError,
hasAnyError,
hasWarns: warns.length > 0,
warns,
totalStories,
storyErrors,
storyWarnings,
storyOk,
totalDocs,
docsErrors,
docsOk
};
}
function analyzeDoc(d) {
return {
hasError: !!d.error
};
}
function note(title, bodyHTML, kind) {
return `
<div class="note ${kind}">
<div class="note-title">${esc(title)}</div>
<div class="note-body">${bodyHTML}</div>
</div>`;
}
function renderDocCard(key, d, id) {
let a = analyzeDoc(d), statusDot = a.hasError ? "dot-err" : "dot-ok", slug = `${id}-${(d.id || key).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "")}`, errorBadge = a.hasError ? `<label for="${slug}-err" class="badge err as-toggle">error</label>` : "", contentBadge = d.content ? `<label for="${slug}-content" class="badge ok as-toggle">view content</label>` : "";
return `
<article
class="card is-doc ${a.hasError ? "has-doc-error" : "no-doc-error"}"
role="listitem"
aria-label="${esc(d.name || key)}">
<div class="head">
<div class="title">
<h2><span class="status-dot ${statusDot}"></span> ${esc(d.title || d.name || key)}</h2>
<div class="badges">
${errorBadge}
${contentBadge}
</div>
</div>
<div class="meta" title="${esc(d.path ?? d.id)}">${d.path ? `${esc(d.id)} \xB7 ${esc(d.path)}` : esc(d.id)}</div>
${d.summary ? `<div>${esc(d.summary)}</div>` : ""}
</div>
<!-- Hidden toggles must be siblings BEFORE .panels -->
${a.hasError ? `<input id="${slug}-err" class="tg tg-err" type="checkbox" hidden />` : ""}
${d.content ? `<input id="${slug}-content" class="tg tg-content" type="checkbox" hidden />` : ""}
<div class="panels">
${a.hasError ? `
<div class="panel panel-err">
<div class="note err">
<div class="note-title">${esc(d.error?.name || "Error")}</div>
<div class="note-body"><pre><code>${esc(d.error?.message || "Unknown error")}</code></pre></div>
</div>
</div>` : ""}
${d.content ? `
<div class="panel panel-content">
<div class="mdx-content">
<pre><code>${esc(d.content)}</code></pre>
</div>
</div>` : ""}
</div>
</article>`;
}
function renderComponentCard(key, c, id, anchorId) {
let a = analyzeComponent(c), statusDot = a.hasAnyError ? "dot-err" : "dot-ok", allStories = storyEntries(c.stories), errorStories = allStories.filter((ex) => !!ex?.error), okStories = allStories.filter((ex) => !ex?.error), subcomponentEntries = Object.entries(c.subcomponents ?? {}), allDocs = c.docs ? Object.values(c.docs) : [], errorDocs = allDocs.filter((d) => !!d?.error), okDocs = allDocs.filter((d) => !d?.error), slug = `c-${id}-${(c.id || key).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "")}`, { api } = a, primaryBadge = api.kind === "error" ? `<label for="${slug}-err" class="badge err as-toggle">API error</label>` : api.kind === "markdown" ? `<label for="${slug}-props" class="badge ok as-toggle">API description</label>` : api.kind === "props" ? `<label for="${slug}-props" class="badge ok as-toggle">${api.entries.length} ${plural(api.entries.length, "prop type")}</label>` : '<span class="badge">no API description</span>', infosBadge = a.hasWarns ? `<label for="${slug}-info" class="badge info as-toggle">${a.warns.length} ${plural(a.warns.length, "info", "infos")}</label>` : "", storiesBadge = a.totalStories > 0 ? `<label for="${slug}-stories" class="badge ${a.storyErrors > 0 ? "err" : "ok"} as-toggle">${a.storyErrors > 0 ? `${a.storyErrors}/${a.totalStories} story errors` : `${a.totalStories} ${plural(a.totalStories, "story", "stories")}`}${a.storyWarnings > 0 ? ` \xB7 ${a.storyWarnings} incomplete` : ""}</label>` : "", docsBadge = a.totalDocs > 0 ? `<label for="${slug}-docs" class="badge ${a.docsErrors > 0 ? "err" : "ok"} as-toggle">${a.docsErrors > 0 ? `${a.docsErrors}/${a.totalDocs} doc errors` : `${a.totalDocs} ${plural(a.totalDocs, "doc")}`}</label>` : "", subcomponentsBadge = subcomponentEntries.length > 0 ? `<label for="${slug}-subcomponents" class="badge ok as-toggle">${subcomponentEntries.length} ${plural(subcomponentEntries.length, "subcomponent")}</label>` : "", apiPanel = api.kind === "markdown" ? `
<div class="panel panel-props">
<div class="note ok">
<div class="row">
<span class="ex-name">API description</span>
${c.renderer ? `<span class="badge ok">${esc(c.renderer)}</span>` : ""}
</div>
<pre><code>${esc(api.markdown)}</code></pre>
</div>
</div>` : api.kind === "props" ? `
<div class="panel panel-props">
<div class="note ok">
<div class="row">
<span class="ex-name">Prop types <small>(${api.engine})</small></span>
<span class="badge ok">${api.entries.length} ${plural(api.entries.length, "prop type")}</span>
</div>
<pre><code>Component: ${api.filePath ? esc(path.relative(process.cwd(), api.filePath)) : ""}${api.exportName ? "::" + esc(api.exportName) : ""}</code></pre>
<pre><code>Props:</code></pre>
<pre><code>${esc(renderPropsCode(api.entries))}</code></pre>
</div>
</div>` : "", tags = c.jsDocTags && typeof c.jsDocTags == "object" ? Object.entries(c.jsDocTags).flatMap(
([k, v]) => (Array.isArray(v) ? v : [v]).map(
(val) => `<span class="chip">${esc(k)}: ${esc(val)}</span>`
)
).join("") : "";
return `
<article
${anchorId ? `id="${esc(anchorId)}"` : ""}
class="card
${a.hasApiError ? "has-error" : "no-error"}
${a.hasWarns ? "has-info" : "no-info"}
${a.storyErrors ? "has-story-error" : "no-story-error"}
${a.storyWarnings ? "has-story-warning" : "no-story-warning"}
${api.kind === "none" ? "missing-api" : ""}
${a.docsErrors ? "has-doc-error" : "no-doc-error"}"
role="listitem"
aria-label="${esc(c.name || key)}">
<div class="head">
<div class="title">
<h2><span class="status-dot ${statusDot}"></span> ${esc(c.name || key)}</h2>
<div class="badges">
${primaryBadge}
${infosBadge}
${storiesBadge}
${docsBadge}
${subcomponentsBadge}
</div>
</div>
<div class="meta" title="${esc(c.path)}">${esc(c.id)} \xB7 ${esc(c.path)}</div>
${c.summary ? `<div>${esc(c.summary)}</div>` : ""}
${c.description ? `<div class="hint">${esc(c.description)}</div>` : ""}
${tags ? `<div class="kv">${tags}</div>` : ""}
</div>
<!-- \u2B07\uFE0F Hidden toggles must be siblings BEFORE .panels -->
${api.kind === "error" ? `<input id="${slug}-err" class="tg tg-err" type="checkbox" hidden />` : ""}
${a.hasWarns ? `<input id="${slug}-info" class="tg tg-info" type="checkbox" hidden />` : ""}
${a.totalStories > 0 ? `<input id="${slug}-stories" class="tg tg-stories" type="checkbox" hidden />` : ""}
${a.totalDocs > 0 ? `<input id="${slug}-docs" class="tg tg-docs" type="checkbox" hidden />` : ""}
${subcomponentEntries.length > 0 ? `<input id="${slug}-subcomponents" class="tg tg-subcomponents" type="checkbox" hidden />` : ""}
${apiPanel ? `<input id="${slug}-props" class="tg tg-props" type="checkbox" hidden />` : ""}
<div class="panels">
${api.kind === "error" ? `
<div class="panel panel-err">
${note("API error", `<pre><code>${esc(api.message)}</code></pre>`, "err")}
</div>` : ""}
${a.hasWarns ? `
<div class="panel panel-info">
${a.warns.map((w) => note("Info", esc(w), "info")).join("")}
</div>` : ""}
${apiPanel}
${a.totalStories > 0 ? `
<div class="panel panel-stories">
${errorStories.map(
(ex) => `
<div class="note err">
<div class="row">
<span class="ex-name">${esc(ex.name)}</span>
<span class="badge err">story error</span>
${ex?.warning ? '<span class="badge info">incomplete example</span>' : ""}
</div>
${ex?.summary ? `<div class="hint">Summary: ${esc(ex.summary)}</div>` : ""}
${ex?.description ? `<div class="hint">${esc(ex.description)}</div>` : ""}
${ex?.snippet ? `<pre><code>${esc(ex.snippet)}</code></pre>` : ""}
${ex?.warning ? `<div class="snippet-warning">${esc(ex.warning)}</div>` : ""}
${ex?.error?.message ? `<pre><code>${esc(ex.error.message)}</code></pre>` : ""}
</div>`
).join("")}
${c.import ? `<div class="note ok">
<div class="row">
<span class="ex-name">Imports</span>
</div>
<pre><code>${c.import}</code></pre>
</div>` : ""}
${okStories.map(
(ex) => `
<div class="note ok">
<div class="row">
<span class="ex-name">${esc(ex.name)}</span>
<span class="badge ${ex?.warning ? "info" : "ok"}">${ex?.warning ? "incomplete example" : "story ok"}</span>
</div>
${ex?.summary ? `<div>${esc(ex.summary)}</div>` : ""}
${ex?.description ? `<div class="hint">${esc(ex.description)}</div>` : ""}
${ex?.snippet ? `<pre><code>${esc(ex.snippet)}</code></pre>` : ""}
${ex?.warning ? `<div class="snippet-warning">${esc(ex.warning)}</div>` : ""}
</div>`
).join("")}
</div>` : ""}
${a.totalDocs > 0 ? `
<div class="panel panel-docs">
${errorDocs.map(
(doc) => `
<div class="note err">
<div class="row">
<span class="ex-name">${esc(doc.name)}</span>
<span class="badge err">doc error</span>
</div>
${doc.path ? `<div class="hint">${esc(doc.path)}</div>` : ""}
${doc?.summary ? `<div>${esc(doc.summary)}</div>` : ""}
${doc?.error?.message ? `<pre><code>${esc(doc.error.message)}</code></pre>` : ""}
</div>`
).join("")}
${okDocs.map(
(doc) => `
<div class="note ok">
<div class="row">
<span class="ex-name">${esc(doc.name)}</span>
<span class="badge ok">doc ok</span>
</div>
${doc.path ? `<div class="hint">${esc(doc.path)}</div>` : ""}
${doc?.summary ? `<div>${esc(doc.summary)}</div>` : ""}
${doc?.content ? `<div class="mdx-content"><pre><code>${esc(doc.content)}</code></pre></div>` : ""}
</div>`
).join("")}
</div>` : ""}
${subcomponentEntries.length > 0 ? `
<div class="panel panel-subcomponents">
${subcomponentEntries.map(
([subcomponentName, subcomponent]) => renderSubcomponentNote(subcomponentName, subcomponent)
).join("")}
</div>` : ""}
</div>
</article>`;
}
var docgenRenderData = (component) => component.reactDocgen ? {
parsed: parseReactDocgen(component.reactDocgen),
engine: "react-docgen",
filePath: component.reactDocgen.definedInFile,
exportName: component.reactDocgen.exportName
} : component.reactDocgenTypescript ? {
parsed: parseReactDocgenTypescript(component.reactDocgenTypescript),
engine: "react-docgen-typescript",
filePath: component.reactDocgenTypescript.filePath,
exportName: component.reactDocgenTypescript.exportName
} : component.reactComponentMeta ? {
parsed: parseReactComponentMeta(component.reactComponentMeta),
engine: "react-component-meta",
filePath: component.reactComponentMeta.filePath,
exportName: component.reactComponentMeta.exportName
} : {};
function resolveComponentApi(component) {
if (component.error)
return { kind: "error", message: component.error.message || "Unknown error" };
let markdown = component.apiDescription?.trim();
if (markdown)
return { kind: "markdown", markdown };
let { parsed, ...source } = docgenRenderData(component), entries = Object.entries(parsed?.props ?? {}).sort(([a], [b]) => a.localeCompare(b));
return entries.length > 0 ? { kind: "props", entries, ...source } : { kind: "none" };
}
function renderPropsCode(entries) {
return entries.map(([propName, info]) => {
let description2 = (info?.description ?? "").trim(), type = (info?.type ?? "any").trim(), optional2 = info?.required ? "" : "?", defaultValue = (info?.defaultValue ?? "").trim(), fallback = defaultValue ? ` = ${defaultValue}` : "", doc = ["/**", ...description2.split(`
`).map((line) => ` * ${line}`), " */"].join(`
`) + `
`;
return `${description2 ? doc : ""}${propName}${optional2}: ${type}${fallback}`;
}).join(`
`);
}
function renderSubcomponentNote(subcomponentName, subcomponent) {
let api = resolveComponentApi(subcomponent), tags = subcomponent.jsDocTags && typeof subcomponent.jsDocTags == "object" ? Object.entries(subcomponent.jsDocTags).flatMap(
([key, value]) => (Array.isArray(value) ? value : [value]).map(
(tagValue) => `<span class="chip">${esc(key)}: ${esc(tagValue)}</span>`
)
).join("") : "", badgeLabel = api.kind === "error" ? "API error" : api.kind === "markdown" ? "API description" : api.kind === "props" ? `${api.entries.length} ${plural(api.entries.length, "prop type")}` : "no API description";
return `
<div class="note ${api.kind === "error" ? "err" : "ok"}">
<div class="row">
<span class="ex-name">${esc(subcomponentName)}</span>
<span class="badge ${api.kind === "error" ? "err" : api.kind === "none" ? "" : "ok"}">
${badgeLabel}
</span>
</div>
<div class="hint">${esc(subcomponent.path)}</div>
${subcomponent.summary ? `<div>${esc(subcomponent.summary)}</div>` : ""}
${subcomponent.description ? `<div class="hint">${esc(subcomponent.description)}</div>` : ""}
${tags ? `<div class="kv">${tags}</div>` : ""}
${subcomponent.import ? `<pre><code>${esc(subcomponent.import)}</code></pre>` : ""}
${api.kind === "error" ? `<pre><code>${esc(api.message)}</code></pre>` : ""}
${api.kind === "markdown" ? `<pre><code>${esc(api.markdown)}</code></pre>` : ""}
${api.kind === "props" ? `
<pre><code>Component: ${api.filePath ? esc(path.relative(process.cwd(), api.filePath)) : ""}${api.exportName ? "::" + esc(api.exportName) : ""}${api.engine ? ` (${esc(api.engine)})` : ""}</code></pre>
<pre><code>${esc(renderPropsCode(api.entries))}</code></pre>` : ""}
</div>
`;
}
var parseReactDocgenTypescript = (reactDocgenTypescript) => {
let props = reactDocgenTypescript.props ?? {};
return {
props: Object.fromEntries(
Object.entries(props).map(([propName, prop]) => [
propName,
{
description: prop.description,
// RDT uses prop.type.name as a flat string (e.g. "() => void", "{ id: string }")
// For enums, prefer prop.type.raw which has the full union
type: prop.type?.raw ?? prop.type?.name,
defaultValue: prop.defaultValue?.value,
required: prop.required
}
])
)
};
}, parseReactComponentMeta = (reactComponentMeta) => {
let props = reactComponentMeta.props ?? {};
return {
props: Object.fromEntries(
Object.entries(props).map(([propName, prop]) => [
propName,
{
description: prop.description,
type: prop.type?.raw ?? prop.type?.name,
defaultValue: prop.defaultValue?.value,
required: prop.required
}
])
)
};
}, parseReactDocgen = (reactDocgen) => {
let props = reactDocgen.props ?? {};
return {
props: Object.fromEntries(
Object.entries(props).map(([propName, prop]) => [
propName,
{
description: prop.description,
type: serializeTsType(prop.tsType ?? prop.type),
defaultValue: prop.defaultValue?.value,
required: prop.required
}
])
)
};
};
function serializeTsType(tsType) {
if (tsType) {
if (tsType.raw && tsType.raw.trim().length > 0)
return tsType.raw;
if (tsType.name) {
if (tsType.elements) {
if (tsType.name === "union")
return tsType.elements.map((el) => serializeTsType(el) ?? "unknown").join(" | ");
if (tsType.name === "intersection")
return tsType.elements.map((el) => serializeTsType(el) ?? "unknown").join(" & ");
if (tsType.name === "Array") {
let el = tsType.elements[0];
return `${serializeTsType(el) ?? "unknown"}[]`;
}
if (tsType.name === "tuple")
return `[${tsType.elements.map((el) => serializeTsType(el) ?? "unknown").join(", ")}]`;
}
if (tsType.value && tsType.name === "literal")
return tsType.value;
if (tsType.signature && tsType.name === "signature") {
if (tsType.type === "function") {
let args = (tsType.signature.arguments ?? []).map((a) => {
let argType = serializeTsType(a.type) ?? "any";
return `${a.name}: ${argType}`;
}), ret = serializeTsType(tsType.signature.return) ?? "void";
return `(${args.join(", ")}) => ${ret}`;
}
return tsType.type === "object" ? `{ ${(tsType.signature.properties ?? []).map((p) => {
let req = !!p.value?.required, propType = serializeTsType(p.value) ?? "any";
return `${p.key}${req ? "" : "?"}: ${propType}`;
}).join("; ")} }` : "unknown";
}
if (tsType.elements) {
let inner = tsType.elements.map((el) => serializeTsType(el) ?? "unknown");
if (inner.length > 0)
return `${tsType.name}<${inner.join(", ")}>`;
}
return tsType.name;
}
}
}
// src/core-server/utils/manifests/manifests.ts
function buildComponentsManifest(components, meta) {
return {
v: 1,
components,
meta
};
}
function mergeServicePayloads(docgenPayloads, storyDocsPayloads, componentIds) {
return Object.fromEntries(
componentIds.flatMap((id) => {
let docgen = docgenPayloads[id], storyDocs = storyDocsPayloads[id];
return docgen ? [[id, mergeManifestPayloads(docgen, storyDocs)]] : storyDocs ? [[id, mergeManifestPayloads(createDocsOnlyDocgenPayload(id), storyDocs)]] : [];
})
);
}
function isDocgenServerManifestMode(features) {
return features.experimentalDocgenServer === !0 && features.componentsManifest === !0;
}
function isDocsManifest(manifest) {
return typeof manifest == "object" && manifest !== null && "docs" in manifest;
}
async function getManifestEntries(presets) {
let generator = await presets.apply("storyIndexGenerator");
invariant(generator, "storyIndexGenerator must be configured");
let index = await generator.getIndex();
return Object.values(index.entries).filter(
(entry) => entry.tags?.includes(Tag.MANIFEST) ?? !1
);
}
function getManifestComponentIds(manifestEntries) {
return Array.from(selectComponentEntriesByComponentId2(manifestEntries).keys());
}
async function getManifests(presets, manifestEntries, { watch: watch2 } = {}) {
return await presets.apply("experimental_manifests", void 0, {
manifestEntries,
watch: watch2
}) ?? {};
}
async function loadManifests(presets) {
return getManifests(presets, await getManifestEntries(presets), { watch: !0 });
}
function resolveDocgenMeta(manifests, durationMs) {
let presetMeta = manifests.components?.meta;
return invariant(
presetMeta?.docgen,
"experimental_manifests must supply components.meta.docgen when experimentalDocgenServer is enabled"
), { docgen: presetMeta.docgen, durationMs };
}
function withDocsOnlyComponents(components, manifestComponentIds, docsByComponentId) {
let next = { ...components };
for (let id of manifestComponentIds)
!next[id] && docsByComponentId[id] && (next[id] = mergeManifestPayloads(createDocsOnlyDocgenPayload(id)));
return next;
}
async function renderComponentsHtmlFromService(manifests, manifestComponentIds, docsManifest) {
let docgenService = getService2("core/docgen", { internal: !0 }), storyDocsService = getService2("core/story-docs", { internal: !0 }), startTime = performance.now(), [allDocgenPayloads, allStoryDocsPayloads, mdxPayloads] = await Promise.all([
docgenService.queries.docgenForAllComponents.loaded(),
storyDocsService.queries.storyDocsForAllComponents.loaded(),
loadMdxPayloadsFromServiceIfNeeded(manifests, docsManifest)
]), durationMs = Math.round(performance.now() - startTime), docsByComponentId = getAttachedDocsByComponent(manifests.components), components = withDocsOnlyComponents(
mergeServicePayloads(allDocgenPayloads, allStoryDocsPayloads, manifestComponentIds),
manifestComponentIds,
docsByComponentId
), load = createServiceLoader(mdxPayloads), [componentsWithDocs, resolvedDocsManifest] = await Promise.all([
resolveComponentDocs(components, manifests, load, fullTransform),
resolveDocsManifestRefs(docsManifest, load, fullTransform)
]);
return renderComponentsManifest(
buildComponentsManifest(componentsWithDocs, resolveDocgenMeta(manifests, durationMs)),
resolvedDocsManifest
);
}
async function writeManifestJsonFiles(outputDir, manifests, { skipComponents = !1 } = {}) {
await Promise.all(
Object.entries(manifests).filter(([name]) => !skipComponents || name !== "components").map(
([name, content]) => writeFile(join(outputDir, "manifests", `${name}.json`), JSON.stringify(content, null, 2))
)
);
}
async function writeDocgenServerManifests(outputDir, manifests, manifestComponentIds, docsManifest) {
let hasOtherManifests = Object.keys(manifests).some((name) => name !== "components"), shouldWriteHtml = manifestComponentIds.length > 0 || !!docsManifest;
if (!hasOtherManifests && !shouldWriteHtml)
return;
let manifestsDir = join(outputDir, "manifests");
await mkdir(manifestsDir, { recursive: !0 });
let startTime = performance.now(), [docgenPayloads, storyDocsPayloads] = await Promise.all([
loadDocgenPayloadsFromDisk(outputDir, manifestComponentIds),
loadStoryDocsPayloadsFromDisk(outputDir, manifestComponentIds)
]), durationMs = Math.round(performance.now() - startTime), docsByComponentId = getAttachedDocsByComponent(manifests.components), mergedComponents = withDocsOnlyComponents(
mergeServicePayloads(docgenPayloads, storyDocsPayloads, manifestComponentIds),
manifestComponentIds,
docsByComponentId
), load = createDiskLoader(outputDir), [attachedDocsWithSummaries, docsManifestWithSummaries] = await Promise.all([
injectAttachedDocsSummaries(docsByComponentId, load),
resolveDocsManifestRefs(docsManifest, load, shallowSummaryTransform)
]);
if (manifestComponentIds.length > 0 && await writeFile(
join(manifestsDir, "components.json"),
JSON.stringify(
buildComponentsRefManifest(
toComponentManifestIndexEntries(
manifestComponentIds,
docgenPayloads,
storyDocsPayloads,
attachedDocsWithSummaries
),
manifests.components?.meta
),
null,
2
)
), await writeManifestJsonFiles(
outputDir,
docsManifestWithSummaries ? { ...manifests, docs: docsManifestWithSummaries } : manifests,
{ skipComponents: !0 }
), shouldWriteHtml) {
let [componentsWithDocs, resolvedDocsManifest] = await Promise.all([
resolveComponentDocs(mergedComponents, manifests, load, fullTransform),
resolveDocsManifestRefs(docsManifest, load, fullTransform)
]);
await writeFile(
join(manifestsDir, "components.html"),
renderComponentsManifest(
buildComponentsManifest(componentsWithDocs, resolveDocgenMeta(manifests, durationMs)),
resolvedDocsManifest
)
);
}
}
async function writeLegacyManifests(outputDir, manifests, docsManifest) {
if (Object.keys(manifests).length === 0)
return;
let manifestsDir = join(outputDir, "manifests");
await mkdir(manifestsDir, { recursive: !0 }), await writeManifestJsonFiles(outputDir, manifests), ("components" in manifests || "docs" in manifests) && await writeFile(
join(manifestsDir, "components.html"),
renderComponentsManifest(manifests.components, docsManifest)
);
}
async function writeManifests(outputDir, presets) {
try {
let features = await presets.apply("features"), manifestEntries = await getManifestEntries(presets), manifests = await getManifests(presets, manifestEntries), docsManifest = isDocsManifest(manifests.docs) ? manifests.docs : void 0;
if (isDocgenServerManifestMode(features)) {
await writeDocgenServerManifests(
outputDir,
manifests,
getManifestComponentIds(manifestEntries),
docsManifest
);
return;
}
await writeLegacyManifests(outputDir, manifests, docsManifest);
} catch (e) {
logger2.error("Failed to generate manifests"), logger2.error(e instanceof Error ? e : String(e));
}
}
function registerManifests({ app, presets }) {
let useDocgenServerPromise, isDocgenServerEnabled = () => (useDocgenServerPromise ??= presets.apply("features").then((features) => isDocgenServerManifestMode(features ?? {})), useDocgenServerPromise);
app.get("/manifests/:name.json", async (req, res) => {
try {
if (await isDocgenServerEnabled() && (req.params.name === "components" || req.params.name === "docs")) {
res.statusCode = 404, res.end(
`Manifest "${req.params.name}" is not available in dev when experimentalDocgenServer is enabled`
);
return;
}
let manifestEntries = await getManifestEntries(presets), manifest = (await getManifests(presets, manifestEntries, { watch: !0 }))[req.params.name];
manifest ? (res.setHeader("Content-Type", "application/json"), res.end(JSON.stringify(manifest))) : (res.statusCode = 404, res.end(`Manifest "${req.params.name}" not found`));
} catch (e) {
logger2.error(e instanceof Error ? e : String(e)), res.statusCode = 500, res.end(e instanceof Error ? e.toString() : String(e));
}
}), app.get("/manifests/components.html", async (req, res) => {
try {
let manifestEntries = await getManifestEntries(presets), manifests = await getManifests(presets, manifestEntries, { watch: !0 }), docsManifest = isDocsManifest(manifests.docs) ? manifests.docs : void 0;
if (await isDocgenServerEnabled()) {
res.setHeader("Content-Type", "text/html; charset=utf-8"), res.end(
await renderComponentsHtmlFromService(
manifests,
getManifestComponentIds(manifestEntries),
docsManifest
)
);
return;
}
let componentsManifest = manifests.components;
if (!componentsManifest && !docsManifest) {
res.statusCode = 404, res.setHeader("Content-Type", "text/html; charset=utf-8"), res.end("<pre>No components or docs manifest configured.</pre>");
return;
}
res.setHeader("Content-Type", "text/html; charset=utf-8"), res.end(renderComponentsManifest(componentsManifest, docsManifest));
} catch (e) {
res.statusCode = 500, res.setHeader("Content-Type", "text/html; charset=utf-8"), res.end(`<pre>${e instanceof Error ? e.stack : String(e)}</pre>`);
}
});
}
// 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/schemas.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)
), storiesForFilesInputSchema = 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.")
)
}), storiesForFilesOutputSchema = 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
})
)
);
// src/shared/open-service/services/module-graph-index/definition.ts
var 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: {
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: storiesForFilesInputSchema,
output: storiesForFilesOutputSchema,
load: async (_input, ctx) => {
await ctx.getService("core/module-graph", {
internal: !0
}).commands._waitForSettledEngine(void 0);
},
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 when the engine mirrors a new index (snapshot or index-moving patch).",
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.")
)
)
})
]), noInputSchema = undefined_(), changeDetectionReadinessSchema = variant("status", [
object({
status: literal("pending")
}),
object({
status: literal("ready")
}),
object({
status: literal("unavailable"),
reason: pipe(
string(),
description("Why change detection cannot publish statuses, such as disabled or no git.")
),
error: optional(
object({
message: pipe(
string(),
description("Optional diagnostic from the provider that marked scanning unavailable.")
)
})
)
}),
object({
status: literal("error"),
error: object({
message: pipe(string(), description("Human-readable scan failure message."))
})
})
]), moduleGraphServiceDef = defineService({
id: "core/module-graph",
internal: !0,
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,
fileActivityRevision: 0,
storyChangeRevisions: {},
latestChangedStoryFiles: [],
changeDetectionReadiness: { status: "pending" }
},
queries: {
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
},
changeDetectionReadiness: {
description: "Change-detection scan readiness. Distinct from `status`: the graph can be ready while change detection is disabled or its initial scan has failed.",
input: noInputSchema,
output: changeDetectionReadinessSchema,
load: async (_input, ctx) => {
await ctx.self.commands._waitForChangeDetectionReadiness(void 0);
},
handler: (_input, ctx) => ctx.self.state.changeDetectionReadiness
},
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;
}
},
fileActivityRevision: {
description: "Monotonic counter advanced on every processed file-change event, including out-of-graph paths that do not advance `graphRevision`. Change detection watches this to rescan git after working-tree edits.",
input: noInputSchema,
output: number(),
handler: (_input, ctx) => ctx.self.state.fileActivityRevision
},
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(storyIndexPathSchema),
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(
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) => {
await ctx.getService("core/module-graph-index", { internal: !0 }).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: "Advances file activity for every processed file event. When `bumpedStoryFiles` is non-empty, also bumps graph revision and records those stories. Called by the graph engine after any index apply for the same patch; does not write the reverse index.",
input: object({
bumpedStoryFiles: pipe(
array(storyIndexPathSchema),
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) => {
ctx.self.setState((state) => {
if (state.fileActivityRevision += 1, input.bumpedStoryFiles.length !== 0) {
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: "Starts the engine if needed and waits until its current build or patch cycle has finished. Handler is supplied at server registration.",
input: noInputSchema,
output: void_()
},
_waitForChangeDetectionReadiness: {
internal: !0,
description: "Waits until change-detection scan readiness is published on the process that owns the scanner.",
input: noInputSchema,
output: changeDetectionReadinessSchema
}
}
});
// src/shared/open-service/services/module-graph/engine/module-graph-engine.ts
import { writeFile as writeFile2 } from "node:fs/promises";
import { getProjectRoot as getProjectRoot2 } from "storybook/internal/common";
import { logger as logger5 } 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 readFile4 } 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 readFile4(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 readFile4(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: cache4,
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 cache4.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 path3 = normalize(event.path);
if (this.cache.invalidate(path3), event.kind === "add") {
this.isStoryFile(path3) && await this.walkStory(path3);
return;
}
if (event.kind === "unlink") {
let dependentsSet = new Set(this.reverseIndex.lookup(path3).keys());
this.graph.delete(path3), this.reverseIndex.removeStory(path3);
let storiesToWalk2 = [];
for (let story of dependentsSet)
story === path3 || !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(path3).keys());
this.isStoryFile(path3) && affectedStories.add(path3);
let oldDeps = this.graph.get(path3);
if (oldDeps !== void 0) {
let newDeps = await this.cache.resolveOnce(path3);
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 { defaultResolveConditionNames } from "storybook/internal/common";
import { logger as logger3 } from "storybook/internal/node-logger";
var DEFAULT_EXTENSIONS = [".tsx", ".ts", ".d.ts", ".jsx", ".js", ".mjs", ".cjs", ".json"], 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);
logger3.debug(
`Change detection: ignored ${skippedRegex.length} regex alias(es); related modules tracked as opaque-leaf.`
), logger3.debug(
`ChangeDetectionResolverFactory: skipped regex aliases [${skippedRegex.join(", ")}]`
);
} else
for (let pattern of skippedRegex)
logger3.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 ?? defaultResolveConditionNames;
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) && logger3.debug(
`ChangeDetectionResolverFactory: '${specifier}' from '${from}' unresolved (${result.error ?? rootResult.error})`
), null);
}
return result.error && logger3.debug(
`ChangeDetectionResolverFactory: '${specifier}' from '${from}' unresolved (${result.error})`
), null;
} catch (error) {
return logger3.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 logger4 } 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) && logger4.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 {@link startInternal} finishes (success or handled failure). The initial build
* does not go through {@link patchQueue}, so {@link whenSettled} awaits this first.
*/
this.startSettled = Promise.resolve();
/**
* 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.startSettled = this.startInternal().catch((error) => {
let failure = error instanceof Error ? error : new ModuleGraphFailureError(String(error));
logger5.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)
), 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.startSettled, 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, cache4 = new ParseResolveCache({
registry,
resolver,
workspaceRoots,
projectRoot,
logger: logger5,
debug: !!debugEnv
});
this.dependencyGraphBuilder = new DependencyGraphBuilder({
registry,
resolver,
workspaceRoots,
projectRoot,
cache: cache4
});
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, cache4), this.incrementalPatcher = new IncrementalPatcher({
reverseIndex,
graph,
registry,
resolver,
workspaceRoots,
projectRoot,
cache: cache4,
isStoryFile: (path3) => this.storyFiles.has(normalize(path3))
}), 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 path3 of next)
previous.has(path3) || added.push(path3);
let removed = [];
for (let path3 of previous)
next.has(path3) || removed.push(path3);
if (!(added.length === 0 && removed.length === 0)) {
this.storyFiles = next;
for (let path3 of added)
this.patchQueue = this.patchQueue.then(() => this.handleFileChange({ kind: "add", path: path3 })).catch(() => {
});
for (let path3 of removed)
this.patchQueue = this.patchQueue.then(() => this.handleFileChange({ kind: "unlink", path: path3 })).catch(() => {
});
}
}
async dumpDebugSnapshot(reverseIndex, graph, projectRoot, workspaceRoots, cache4) {
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: cache4.getBarrelTrace() ?? []
};
try {
await writeFile2(outPath, JSON.stringify(snapshot, null, 2), "utf8"), logger5.debug(`Change detection: graph debug snapshot written to ${outPath}`);
} catch (error) {
logger5.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) {
logger5.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
function createAdapterDeferred() {
let resolve3;
return {
promise: new Promise((fulfill) => {
resolve3 = fulfill;
}),
resolve: resolve3
};
}
var adapterDeferred = createAdapterDeferred(), adapterResolved = !1;
function resolveChangeDetectionAdapter(adapter) {
adapterResolved || (adapterResolved = !0, adapterDeferred.resolve(adapter));
}
function registerModuleGraphService(options) {
let workingDir = options.workingDir ?? process.cwd(), adapterPromise = adapterDeferred.promise, engine, engineStarted = !1, obtainAdapter, indexRuntime = registerModuleGraphIndexService(workingDir), runtime = registerService(
{
...moduleGraphServiceDef,
initialState: {
...moduleGraphServiceDef.initialState,
workingDir
}
},
{
commands: {
_waitForSettledEngine: {
handler: async () => {
await ensureAdapter(), applyAdapter(await adapterPromise), await engine.whenSettled();
}
},
_waitForChangeDetectionReadiness: {
handler: async (_input, ctx) => {
let readiness = options.getChangeDetectionReadiness ? await options.getChangeDetectionReadiness() : { status: "ready" }, serialized;
switch (readiness.status) {
case "ready":
serialized = { status: "ready" };
break;
case "unavailable":
serialized = {
status: "unavailable",
reason: readiness.reason,
...readiness.error ? { error: { message: readiness.error.message } } : {}
};
break;
case "error":
serialized = {
status: "error",
error: { message: readiness.error.message }
};
break;
default:
throw readiness;
}
return ctx.self.setState((state) => {
state.changeDetectionReadiness = serialized;
}), serialized;
}
}
}
}
);
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) } : {}
});
}
});
function applyAdapter(adapter) {
if (!engineStarted) {
if (engineStarted = !0, !adapter) {
runtime.commands._setStatus({
value: "unavailable",
reason: "builder does not support change detection"
});
return;
}
engine.start(adapter);
}
}
async function ensureAdapter() {
if (adapterResolved)
return;
let getAdapter = options.getAdapter;
if (!getAdapter) {
await adapterPromise;
return;
}
obtainAdapter ??= Promise.resolve().then(() => getAdapter()).then(
(adapter) => {
resolveChangeDetectionAdapter(adapter);
},
() => {
resolveChangeDetectionAdapter(void 0);
}
), await obtainAdapter;
}
return options.channel.on(STORY_INDEX_INVALIDATED, () => {
engine.onStoryIndexInvalidated();
}), adapterPromise.then(applyAdapter), runtime;
}
// src/core-server/change-detection/errors.ts
var ChangeDetectionUnavailableError = class extends Error {
constructor(message, options) {
super(message, options), this.name = "ChangeDetectionUnavailableError";
}
}, ChangeDetectionFailureError = class extends Error {
constructor(message, options) {
super(message, options), this.name = "ChangeDetectionFailureError";
}
};
// src/core-server/change-detection/GitDiffProvider.ts
import { watch } from "node:fs";
import { readFile as readFile5, stat as stat2 } from "node:fs/promises";
import { logger as logger6 } from "storybook/internal/node-logger";
function parseChangedFiles(stdout) {
return new Set(
stdout.split(`
`).map((line) => line.trim()).filter(Boolean)
);
}
var GitDiffProvider = class {
constructor(cwd = process.cwd(), fileSystem = { watch, readFile: readFile5, stat: stat2 }) {
this.cwd = cwd;
this.fileSystem = fileSystem;
this.gitStateCallback = () => {
};
this.watchingInitialized = !1;
this.watchingStopped = !1;
}
async getRepoRoot() {
if (this.repoRoot)
return this.repoRoot;
try {
let { stdout } = await execa("git", ["rev-parse", "--show-toplevel"], {
cwd: this.cwd,
stdio: "pipe",
// Prevent git from acquiring .git/index.lock for stat-cache refreshes
// during read-only operations. Without this, parallel scans can race
// an interactive `git commit` and break the user's commit with
// "Unable to create '.git/index.lock': File exists".
env: { GIT_OPTIONAL_LOCKS: "0" }
});
return this.repoRoot = stdout.trim(), this.repoRoot;
} catch (error) {
throw this.toGitError(error, "git rev-parse --show-toplevel");
}
}
async getChangedFiles() {
let repoRoot = await this.getRepoRoot(), runGitCommand = (args) => this.runGitCommand(repoRoot, args), [staged, unstaged, added, intentToAdd, untracked] = await Promise.all([
runGitCommand(["diff", "--name-only", "--diff-filter=ad", "--cached"]),
runGitCommand(["diff", "--name-only", "--diff-filter=ad"]),
runGitCommand(["diff", "--name-only", "--diff-filter=A", "--cached"]),
runGitCommand(["diff", "--name-only", "--diff-filter=A"]),
runGitCommand(["ls-files", "--others", "--exclude-standard"])
]);
return {
changed: /* @__PURE__ */ new Set([
...parseChangedFiles(staged.stdout),
...parseChangedFiles(unstaged.stdout)
]),
new: /* @__PURE__ */ new Set([
...parseChangedFiles(added.stdout),
...parseChangedFiles(intentToAdd.stdout),
...parseChangedFiles(untracked.stdout)
])
};
}
async getHeadCommit() {
let repoRoot = await this.getRepoRoot(), { stdout } = await this.runGitCommand(repoRoot, ["rev-parse", "HEAD"]);
return stdout.trim();
}
async isWorkingTreeClean() {
let repoRoot = await this.getRepoRoot(), { stdout } = await this.runGitCommand(repoRoot, ["status", "--porcelain"]);
return stdout.trim().length === 0;
}
onGitStateChange(callback) {
this.gitStateCallback = callback, !this.watchingInitialized && !this.watchingStopped && (this.watchingInitialized = !0, this.initializeWatching());
}
async initializeWatching() {
try {
let gitDir = await this.getGitDir();
this.headWatcher = this.attachWatcher({
filePath: gitDir,
currentWatcher: this.headWatcher,
onChange: () => {
this.gitStateCallback(), this.reconfigureBranchWatcher(gitDir);
}
}), this.packedRefsWatcher = this.attachWatcher({
filePath: gitDir,
currentWatcher: this.packedRefsWatcher,
onChange: () => {
this.gitStateCallback();
}
}), await this.configureBranchWatcher(gitDir);
} catch {
}
}
attachWatcher(options) {
let { filePath, currentWatcher, onChange } = options;
if (!this.watchingStopped)
try {
let watcher = this.fileSystem.watch(filePath, { persistent: !1 }, () => {
this.watchingStopped || onChange();
});
return currentWatcher?.close(), watcher.on("error", () => {
watcher.close(), this.headWatcher === watcher && (this.headWatcher = void 0), this.packedRefsWatcher === watcher && (this.packedRefsWatcher = void 0), this.branchWatcher === watcher && (this.branchWatcher = void 0), this.stopWatching(), logger6.warn(
`Change detection git watcher failed for ${filePath}. Git state updates may stop until restart.`
);
}), watcher;
} catch (error) {
if (this.isEnoentError(error))
return;
throw error;
}
}
async configureBranchWatcher(gitDir) {
let branchRef = await this.readHeadRef(gitDir);
this.branchWatcher?.close(), this.branchWatcher = void 0;
let watchBranch = (filePath) => (this.branchWatcher = this.attachWatcher({
filePath,
currentWatcher: this.branchWatcher,
onChange: () => {
this.gitStateCallback(), this.reconfigureBranchWatcher(gitDir);
}
}), this.branchWatcher);
branchRef?.startsWith("refs/heads/") && (watchBranch(dirname(join(gitDir, branchRef))) || watchBranch(join(gitDir, "refs", "heads")));
}
reconfigureBranchWatcher(gitDir) {
this.configureBranchWatcher(gitDir).catch(() => {
logger6.warn("Change detection failed to reconfigure git branch watcher."), this.stopWatching();
});
}
dispose() {
this.stopWatching();
}
stopWatching() {
this.watchingStopped || (this.watchingStopped = !0, this.headWatcher?.close(), this.headWatcher = void 0, this.packedRefsWatcher?.close(), this.packedRefsWatcher = void 0, this.branchWatcher?.close(), this.branchWatcher = void 0);
}
async getGitDir() {
let repoRoot = await this.getRepoRoot(), gitPath = join(repoRoot, ".git");
try {
let gitStat = await this.fileSystem.stat(gitPath);
if (gitStat.isDirectory())
return gitPath;
if (gitStat.isFile()) {
let gitPointer = await this.fileSystem.readFile(gitPath, "utf8"), match = /^gitdir:\s+(.+)$/m.exec(gitPointer.trim());
if (match)
return resolve(repoRoot, match[1]);
}
} catch (error) {
if (!this.isEnoentError(error))
throw error;
}
return gitPath;
}
async readHeadRef(gitDir) {
try {
let headContents = await this.fileSystem.readFile(join(gitDir, "HEAD"), "utf8");
return /^ref:\s+(.+)$/m.exec(headContents.trim())?.[1];
} catch (error) {
if (!this.isEnoentError(error))
throw error;
}
}
async runGitCommand(repoRoot, args) {
try {
return await execa("git", args, {
cwd: repoRoot,
stdio: "pipe",
// GIT_OPTIONAL_LOCKS=0 disables the stat-cache refresh that
// `git status` and `git diff` would otherwise write into
// .git/index.lock. The diff/status output stays correct; we only
// skip the optional cache update. This prevents change detection
// scans from racing an interactive `git commit` running in the
// user's shell.
env: { GIT_OPTIONAL_LOCKS: "0" }
});
} catch (error) {
throw this.toGitError(error, `git ${args.join(" ")}`);
}
}
isEnoentError(error) {
return !!(error && typeof error == "object" && "code" in error && error.code === "ENOENT");
}
toGitError(error, command) {
let execaError = error, stderr = [execaError.stderr, execaError.shortMessage, execaError.message].filter(Boolean).join(`
`);
return execaError.code === "ENOENT" ? new ChangeDetectionUnavailableError("git is not available", { cause: error }) : stderr.includes("not a git repository") ? new ChangeDetectionUnavailableError("not a git repository", {
cause: error
}) : new ChangeDetectionFailureError(`${command} failed${stderr ? `: ${stderr}` : ""}`, {
cause: error
});
}
};
// src/core-server/change-detection/readiness.ts
function createDeferred() {
let resolve3;
return {
promise: new Promise((fulfill) => {
resolve3 = fulfill;
}),
resolve: resolve3
};
}
var readinessDeferred = createDeferred(), readinessState, host, hostStarted;
function setChangeDetectionHost(next) {
host = next, hostStarted = void 0;
}
function getChangeDetectionReadiness() {
return host && !hostStarted && (hostStarted = Promise.resolve().then(() => host?.()).catch((error) => {
setChangeDetectionReadiness({
status: "error",
error: error instanceof Error ? error : new Error(String(error))
});
})), (hostStarted ?? Promise.resolve()).then(
() => readinessState ? Promise.resolve(readinessState) : readinessDeferred.promise
);
}
function setChangeDetectionReadiness(readiness) {
readinessState || (readinessState = readiness, readinessDeferred.resolve(readiness));
}
function resetChangeDetectionReadiness() {
readinessDeferred = createDeferred(), readinessState = void 0;
}
// src/core-server/stores/server-store-leadership.ts
function isServerStoreLeader() {
return !optionalEnvToBoolean(process.env.VITEST_CHILD_PROCESS) && !optionalEnvToBoolean(process.env.STORYBOOK_ATTACHED_TOOLS) && UniversalStore.preparedEnvironment !== UniversalStore.Environment.UNKNOWN;
}
function getOrRecreateStore(id, cache4, create) {
let leader = isServerStoreLeader();
return cache4.store !== void 0 && cache4.leader === leader || (cache4.store !== void 0 && instances.delete(id), cache4.leader = leader, cache4.store = create(leader)), cache4.store;
}
// src/core-server/stores/status.ts
function createServerStatusStore(leader) {
return createStatusStore({
universalStatusStore: UniversalStore.create({
...UNIVERSAL_STATUS_STORE_OPTIONS,
leader
}),
environment: "server"
});
}
var cache2 = {};
function getStatusStoreBundle() {
return getOrRecreateStore(UNIVERSAL_STATUS_STORE_OPTIONS.id, cache2, createServerStatusStore);
}
var getStatusStoreByTypeId = (typeId) => getStatusStoreBundle().getStatusStoreByTypeId(typeId), fullStatusStore = new Proxy(
{},
{
get(_target, prop) {
let store = getStatusStoreBundle().fullStatusStore, value = Reflect.get(store, prop, store);
return typeof value == "function" ? value.bind(store) : value;
}
}
), universalStatusStore = new Proxy(
{},
{
get(_target, prop) {
let store = getStatusStoreBundle().universalStatusStore, value = Reflect.get(store, prop, store);
return typeof value == "function" ? value.bind(store) : value;
}
}
);
// 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/utils/generate-story.ts
import { existsSync as existsSync3 } from "node:fs";
import { writeFile as writeFile3 } 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 readFile6 } from "node:fs/promises";
import { basename as basename3, dirname as dirname3, 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 logger7 } from "storybook/internal/node-logger";
// src/core-server/utils/new-story-templates/csf-factory-template.ts
var import_ts_dedent2 = __toESM(require_dist(), 1);
// src/core-server/utils/get-component-variable-name.ts
var getComponentVariableName = async (name) => (await import("./camelcase-GPBUWO7L.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_dedent2.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_dedent3 = __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_dedent3.dedent`
export const ${data.exportedStoryName} = {
${argsString}
};
` : `export const ${data.exportedStoryName} = {};`;
return import_ts_dedent3.dedent`
${importStatement}
const meta = {
component: ${importName},
};
export default meta;
${storyExport}
`;
}
// src/core-server/utils/new-story-templates/typescript.ts
var import_ts_dedent4 = __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_dedent4.dedent`
export const ${data.exportedStoryName}: Story = {
${argsString}
};
` : `export const ${data.exportedStoryName}: Story = {};`;
return import_ts_dedent4.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 = basename3(componentFilePath), extension = extname2(componentFilePath), basenameWithoutExtension = escapeForTemplate(base.replace(extension, "")), dir = dirname3(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 readFile6(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 (logger7.debug(`Extracted argTypes for ${componentExportName}: ${JSON.stringify(argTypes)}`), argTypes) {
let { required } = generateDummyArgsFromArgTypes(argTypes);
Object.keys(required).length > 0 && (args = required, logger7.debug(
`Generated dummy data using ArgTypes for ${componentExportName}: ${JSON.stringify(args)}`
));
}
} catch (error) {
logger7.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 = basename3(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 readFile6(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(path3) {
path3.node.value === STORYBOOK_FN_PLACEHOLDER && (needsFnImport = !0, path3.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 writeFile3(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/open-service/toolsets/stories/find-story-ids.ts
import path2 from "node:path";
import { normalizeStoryPath } from "storybook/internal/common";
import { storyNameFromExport } from "storybook/internal/csf/csf-utils";
function isStoryIdInput(input) {
return "storyId" in input;
}
function toPosixPath(value) {
return value.startsWith("\\\\?\\") ? value : value.replace(/\\/g, "/");
}
function normalizeImportPath(importPath) {
let normalized = path2.posix.normalize(toPosixPath(importPath));
return toPosixPath(normalizeStoryPath(normalized));
}
function findStoryIds(index, stories) {
let entriesList = Object.values(index.entries), result = [];
for (let storyInput of stories) {
if (isStoryIdInput(storyInput)) {
let foundEntry2 = index.entries[storyInput.storyId];
foundEntry2 ? result.push({
entry: foundEntry2,
input: storyInput
}) : result.push({
input: storyInput,
errorMessage: `No story found for story ID "${storyInput.storyId}"`
});
continue;
}
let { exportName, explicitStoryName, absoluteStoryPath } = storyInput, normalizedCwd = toPosixPath(process.cwd()), normalizedAbsolutePath = toPosixPath(absoluteStoryPath), relativePath = normalizeImportPath(
path2.posix.relative(normalizedCwd, normalizedAbsolutePath)
), foundEntry = entriesList.find(
(entry) => normalizeImportPath(entry.importPath) === relativePath && [explicitStoryName, storyNameFromExport(exportName)].includes(entry.name)
);
if (foundEntry)
result.push({
entry: foundEntry,
input: storyInput
});
else {
let errorMessage = `No story found for export name "${exportName}" with absolute file path "${absoluteStoryPath}"`;
explicitStoryName || (errorMessage += " (did you forget to pass the explicit story name?)"), result.push({
input: storyInput,
errorMessage
});
}
}
return result;
}
// src/shared/open-service/toolsets/stories/story-input.ts
var storyInputProps = {
props: pipe(
optional(record(string(), any())),
description(`Optional custom props to pass to the story for rendering. Use this when you don't want to render the default story,
but you want to customize some args or other props.
You can look up the component's documentation using the get-storybook-story-instructions tool to see what props are available.`)
),
globals: pipe(
optional(record(string(), any())),
description(`Optional Storybook globals to set for the story preview. Globals are used for things like theme, locale, viewport, and other cross-cutting concerns.
Common globals include 'theme' (e.g., 'dark', 'light'), 'locale' (e.g., 'en', 'fr'), and 'backgrounds' (e.g., { value: '#000' }).`)
)
}, storyInputSchema = union([
object({
exportName: pipe(
string(),
description(
`The export name of the story from the story file.
Use this path-based shape only when you're already editing a .stories.* file and know the export names in that file.
If you do not already have story file context, prefer the storyId shape instead of searching files.`
)
),
explicitStoryName: pipe(
optional(string()),
description(
`If the story has an explicit name set via the "name" property, that is different from the export name, provide it here.
Otherwise don't set this.`
)
),
absoluteStoryPath: pipe(
string(),
description(
"Absolute path to the story file. Use together with exportName only when story file context is already available."
)
),
...storyInputProps
}),
object({
storyId: pipe(
string(),
description(
`The full Storybook story ID (for example "button--primary").
Prefer this shape whenever you are not already working in a specific story file.
Use IDs discovered from ${toMcpToolName("docs.list")} (withStoryIds=true) or ${toMcpToolName("docs.show")}.`
)
),
...storyInputProps
})
]), storyInputArraySchema = array(storyInputSchema);
// src/shared/open-service/toolsets/docs/access.ts
function emptyManifests() {
return { componentManifest: { v: 1, components: {} } };
}
function toShallowManifests(components, docs) {
return {
componentManifest: { v: 1, components },
...Object.keys(docs).length > 0 ? { docsManifest: { v: 1, docs } } : {}
};
}
// src/shared/open-service/toolsets/docs/manifest-formatter/markdown.ts
var import_ts_dedent5 = __toESM(require_dist(), 1);
// src/shared/open-service/toolsets/docs/sources.ts
function getSourceMcpEndpoint(source) {
let base = new URL(source.url);
return base.pathname = `${base.pathname.replace(/\/$/, "")}/`, new URL("mcp", base).toString();
}
function formatRequiresOwnMcpNotice(source, endpoint, options = {}) {
let parts = [];
return (options.includeHeader ?? !0) && (parts.push(`# ${source.title}`), parts.push(`id: ${source.id}`), parts.push("")), parts.push(
"This composed Storybook is private and cannot be read through the local Storybook MCP proxy."
), parts.push(""), parts.push("Use this source's own MCP endpoint instead:"), parts.push(endpoint), parts.join(`
`);
}
var RequiresOwnMcpError = class extends Error {
constructor(source) {
let endpoint = getSourceMcpEndpoint(source);
super(`Composed Storybook "${source.title}" requires its own MCP endpoint: ${endpoint}`), this.name = "RequiresOwnMcpError", this.source = source, this.endpoint = endpoint;
}
}, ManifestGetError = class extends Error {
constructor(message, url, cause) {
super(message), this.name = "ManifestGetError", this.url = url ?? "No source URL provided", this.cause = cause;
}
};
// src/shared/open-service/toolsets/docs/manifest-formatter/extract-docs-summary.ts
function extractDocsSummary(content) {
let result = content;
result = result.replace(/^\s*import\s+(?:[\s\S]*?from\s+)?['"][^'"]+['"];?\s*$/gm, "");
let prevResult = "";
for (; prevResult !== result; )
prevResult = result, result = result.replace(/\{[^{}]*\}/g, "");
for (result = result.replace(/<[^>]+\/>/g, ""), prevResult = ""; prevResult !== result; )
prevResult = result, result = result.replace(/<(\w+)[^>]*>([\s\S]*?)<\/\1>/g, "$2");
if (result = result.replace(/<[^>]+>/g, ""), result = result.replace(/\s+/g, " ").trim(), !!result)
return result.length > 90 ? `${result.slice(0, 90)}...` : result;
}
// src/shared/open-service/toolsets/docs/manifest-formatter/parse-react-docgen.ts
function serializeTsType2(tsType) {
if (tsType) {
if ("raw" in tsType && typeof tsType.raw == "string" && tsType.raw.trim().length > 0)
return tsType.raw;
if (tsType.name) {
if ("elements" in tsType) {
let serializeElements = () => (tsType.elements ?? []).map((el) => serializeTsType2(el) ?? "unknown");
switch (tsType.name) {
case "union":
return serializeElements().join(" | ");
case "intersection":
return serializeElements().join(" & ");
case "Array":
return `${serializeTsType2((tsType.elements ?? [])[0]) ?? "unknown"}[]`;
case "tuple":
return `[${serializeElements().join(", ")}]`;
}
}
if ("value" in tsType && tsType.name === "literal")
return tsType.value;
if ("signature" in tsType && tsType.name === "signature") {
if (tsType.type === "function") {
let args = (tsType.signature?.arguments ?? []).map((a) => {
let argType = serializeTsType2(a.type) ?? "any";
return `${a.name}: ${argType}`;
}), ret = serializeTsType2(tsType.signature?.return) ?? "void";
return `(${args.join(", ")}) => ${ret}`;
}
return tsType.type === "object" ? `{ ${(tsType.signature?.properties ?? []).map((p) => {
let req = !!p.value?.required, propType = serializeTsType2(p.value) ?? "any";
return `${p.key}${req ? "" : "?"}: ${propType}`;
}).join("; ")} }` : "unknown";
}
if ("elements" in tsType) {
let inner = (tsType.elements ?? []).map((el) => serializeTsType2(el) ?? "unknown");
if (inner.length > 0) return `${tsType.name}<${inner.join(", ")}>`;
}
return tsType.name;
}
}
}
var parseReactDocgen2 = (reactDocgen) => {
let props = reactDocgen?.props ?? {};
return {
props: Object.fromEntries(
Object.entries(props).map(([propName, prop]) => [
propName,
{
description: prop.description,
type: serializeTsType2(prop.tsType ?? prop.type),
defaultValue: prop.defaultValue?.value,
required: prop.required
}
])
)
};
}, tagValues = (value) => Array.isArray(value) ? value.flatMap(tagValues) : value == null ? [""] : [typeof value == "object" ? JSON.stringify(value) : String(value)], parseComponentDocLike = (componentDoc) => {
let props = componentDoc.props ?? {}, tags = Object.fromEntries(
Object.entries(componentDoc.tags ?? {}).map(([tagName, value]) => [tagName, tagValues(value)])
), parsedDocgen = {
props: Object.fromEntries(
Object.entries(props).map(([propName, prop]) => [
propName,
{
description: prop.description || void 0,
// RDT uses prop.type.name as a flat string (e.g. "() => void", "{ id: string }")
// For enums, prefer prop.type.raw which has the full union
type: prop.type?.raw ?? prop.type?.name,
defaultValue: prop.defaultValue?.value,
required: prop.required
}
])
)
};
return Object.keys(tags).length > 0 && (parsedDocgen.tags = tags), parsedDocgen;
}, parseReactDocgenTypescript2 = (reactDocgenTypescript) => parseComponentDocLike(reactDocgenTypescript), parseReactComponentMeta2 = (reactComponentMeta) => parseComponentDocLike(reactComponentMeta);
// src/shared/open-service/toolsets/docs/manifest-formatter/markdown.ts
var MAX_STORIES_TO_SHOW = 3, TOP_JSDOC_TAG_NAMES = /* @__PURE__ */ new Set(["deprecated"]), EXAMPLE_JSDOC_TAG_NAME = "example", HIDDEN_JSDOC_TAG_NAMES = /* @__PURE__ */ new Set([
// Asks for the component to be hidden, so echoing it back is the opposite of the intent.
"ignore",
// Consumed by `extractComponentDescription` as the description, which is rendered on its own.
"desc",
"description",
// Storybook's own description override, consumed alongside `desc`.
"describe",
// Consumed into the `summary` field, which the component listing already prints.
"summary",
// Consumed into the import statement printed above every story snippet.
"import"
]);
function formatComponentLine(component) {
let summary = component.summary ?? (component.description ? component.description.length > 90 ? `${component.description.slice(0, 90)}...` : component.description : void 0);
return summary ? `- ${component.name} (${component.id}): ${summary}` : `- ${component.name} (${component.id})`;
}
function formatDocLine(doc) {
let title = "title" in doc ? doc.title : void 0, content = "content" in doc ? doc.content : void 0, summary = doc.summary ?? extractDocsSummary(content ?? "");
return `- ${title ?? doc.name} (${doc.id})${summary ? `: ${summary}` : ""}`;
}
function formatStorySubLine(story) {
return ` - ${story.name}` + (story.id ? ` (${story.id})` : "");
}
function extractSummary(item, maxLength = 90) {
if (item.summary)
return item.summary;
if (item.description)
return item.description.length > maxLength ? `${item.description.slice(0, maxLength)}...` : item.description;
}
function getParsedDocgen(componentManifest) {
if (componentManifest.reactDocgen)
return parseReactDocgen2(componentManifest.reactDocgen);
if (componentManifest.reactDocgenTypescript)
return parseReactDocgenTypescript2(componentManifest.reactDocgenTypescript);
if (componentManifest.reactComponentMeta)
return parseReactComponentMeta2(componentManifest.reactComponentMeta);
}
function getNonEmptyJsDocTags(jsDocTags) {
return jsDocTags && Object.keys(jsDocTags).length > 0 ? jsDocTags : void 0;
}
function getComponentTags(componentManifest, parsedDocgen) {
return getNonEmptyJsDocTags(componentManifest.jsDocTags) ?? parsedDocgen?.tags;
}
function formatTagName(tagName) {
return tagName.charAt(0).toUpperCase() + tagName.slice(1);
}
function formatJsDocTagBlockquote(tagName, values) {
let described = values.filter((value) => value.trim());
return (described.length > 0 ? described : [""]).flatMap((value) => {
let [firstLine, ...remainingLines] = value.trim().split(`
`), label = formatTagName(tagName);
return [
firstLine ? `> **${label}:** ${firstLine}` : `> **${label}**`,
...remainingLines.map((line) => line ? `> ${line}` : ">")
];
});
}
function isTopJsDocTag(tagName) {
return TOP_JSDOC_TAG_NAMES.has(tagName);
}
function isGenericJsDocTag(tagName) {
return !TOP_JSDOC_TAG_NAMES.has(tagName) && tagName !== EXAMPLE_JSDOC_TAG_NAME && !HIDDEN_JSDOC_TAG_NAMES.has(tagName);
}
function formatJsDocTags(jsDocTags, includeTag) {
let parts = [];
for (let [tagName, values] of Object.entries(jsDocTags ?? {}))
includeTag(tagName) && parts.push(...formatJsDocTagBlockquote(tagName, values));
return parts.length > 0 && parts.push(""), parts;
}
function formatExampleJsDocTags(jsDocTags) {
let examples = jsDocTags?.[EXAMPLE_JSDOC_TAG_NAME];
return !examples || examples.length === 0 ? [] : examples.flatMap((example) => ["**Example:**", "```", example, "```", ""]);
}
function formatStoryContent(story, importStatement) {
let parts = [];
return story.description && (parts.push(story.description), parts.push("")), parts.push("```"), importStatement && (parts.push(importStatement), parts.push("")), parts.push(story.snippet ?? ""), parts.push("```"), parts;
}
function formatPropsSection(parsedDocgen, options = {}) {
let propEntries = parsedDocgen ? Object.entries(parsedDocgen.props) : [];
if (propEntries.length === 0)
return [];
let title = options.title ?? "## Props", typeName = options.typeName ?? "Props", parts = [];
parts.push(title), parts.push(""), parts.push("```"), parts.push(`export type ${typeName} = {`);
for (let [propName, propInfo] of propEntries) {
let type = propInfo.type ?? "any", isRequired = propInfo.required ?? !0, hasDefault = propInfo.defaultValue !== void 0;
propInfo.description !== void 0 && (parts.push(" /**"), parts.push(` ${propInfo.description}`), parts.push(" */"));
let propLine = ` ${propName}`;
isRequired || (propLine += "?"), propLine += `: ${type}`, hasDefault && (propLine += ` = ${propInfo.defaultValue}`), propLine += ";", parts.push(propLine);
}
return parts.push("}"), parts.push("```"), parts.push(""), parts;
}
var FENCE_LINE = /^\s*(?:```|~~~)/, HEADING_LINE = /^(#{1,4}) /;
function demoteHeadings(markdown) {
let inFence = !1;
return markdown.split(`
`).map((line) => FENCE_LINE.test(line) ? (inFence = !inFence, line) : inFence ? line : line.replace(HEADING_LINE, "##$1 ")).join(`
`);
}
function formatSubcomponentsSection(subcomponents) {
if (!subcomponents || Object.keys(subcomponents).length === 0)
return [];
let parts = [];
parts.push("## Subcomponents"), parts.push("");
for (let [key, subcomponent] of Object.entries(subcomponents)) {
let parsedDocgen = getParsedDocgen(subcomponent), jsDocTags = getComponentTags(subcomponent, parsedDocgen);
if (parts.push(`### ${subcomponent.name || key}`), parts.push(""), parts.push(...formatJsDocTags(jsDocTags, isTopJsDocTag)), subcomponent.summary && (parts.push(subcomponent.summary), parts.push("")), subcomponent.description && (parts.push(subcomponent.description), parts.push("")), parts.push(...formatJsDocTags(jsDocTags, isGenericJsDocTag)), parts.push(...formatExampleJsDocTags(jsDocTags)), subcomponent.import && (parts.push("```"), parts.push(subcomponent.import), parts.push("```"), parts.push("")), subcomponent.error) {
parts.push(`Error: ${subcomponent.error.name}`), parts.push(""), parts.push("```"), parts.push(subcomponent.error.message), parts.push("```"), parts.push("");
continue;
}
if (subcomponent.apiDescription) {
parts.push(demoteHeadings(subcomponent.apiDescription)), parts.push("");
continue;
}
let typeName = `${(subcomponent.name || key).replace(/\W+/g, "")}Props`;
parts.push(...formatPropsSection(parsedDocgen, { title: "#### Props", typeName }));
}
return parts;
}
function formatComponentManifest(componentManifest) {
let parts = [], parsedDocgen = getParsedDocgen(componentManifest), jsDocTags = getComponentTags(componentManifest, parsedDocgen);
parts.push(`# ${componentManifest.name}`), parts.push(""), parts.push(`ID: ${componentManifest.id}`), parts.push(""), parts.push(...formatJsDocTags(jsDocTags, isTopJsDocTag)), componentManifest.description && (parts.push(componentManifest.description), parts.push("")), parts.push(...formatJsDocTags(jsDocTags, isGenericJsDocTag)), parts.push(...formatExampleJsDocTags(jsDocTags)), parts.push(...formatSubcomponentsSection(componentManifest.subcomponents));
let { apiDescription } = componentManifest;
apiDescription && (parts.push(apiDescription), parts.push(""));
let stories = Array.isArray(componentManifest.stories) ? componentManifest.stories : [];
if (stories.length > 0) {
parts.push("## Stories"), parts.push("");
let storiesWithSnippets = stories.filter((s) => s.snippet), storiesToShow = !!apiDescription || parsedDocgen && Object.keys(parsedDocgen.props).length > 0 ? storiesWithSnippets.slice(0, MAX_STORIES_TO_SHOW) : storiesWithSnippets, shown = new Set(storiesToShow), remainingStories = stories.filter((story) => !shown.has(story));
for (let story of storiesToShow)
parts.push(`### ${story.name}`), parts.push(""), story.id && (parts.push(`Story ID: ${story.id}`), parts.push("")), parts.push(...formatStoryContent(story, componentManifest.import)), parts.push("");
if (remainingStories.length > 0) {
storiesToShow.length > 0 && (parts.push("### Other Stories"), parts.push(""));
for (let story of remainingStories) {
let summary = extractSummary(story), summaryPart = summary ? `: ${summary}` : "", storyLabel = story.id ? `${story.name} (${story.id})` : story.name;
parts.push(`- ${storyLabel}${summaryPart}`);
}
parts.push("");
}
}
if (apiDescription || parts.push(...formatPropsSection(parsedDocgen)), componentManifest.docs && Object.keys(componentManifest.docs).length > 0) {
let docsWithContent = Object.values(componentManifest.docs).filter(
(doc) => (doc.content ?? "").trim().length > 0
);
if (docsWithContent.length > 0) {
parts.push("## Docs"), parts.push("");
for (let doc of docsWithContent)
parts.push(`### ${doc.name}`), parts.push(""), parts.push(doc.content ?? ""), parts.push("");
}
}
return parts.join(`
`).trim();
}
function formatDocsManifest(doc) {
return import_ts_dedent5.dedent`# ${doc.title ?? doc.name}
${doc.content ?? ""}`;
}
function formatManifestsToLists(manifests, options = {}) {
let parts = [];
parts.push("# Components"), parts.push("");
for (let component of Object.values(manifests.componentManifest.components))
if (parts.push(formatComponentLine(component)), options.withStoryIds && Array.isArray(component.stories))
for (let story of component.stories)
parts.push(formatStorySubLine(story));
if (parts.push(""), !manifests.docsManifest)
return parts.join(`
`).trim();
parts.push("# Docs"), parts.push("");
for (let doc of Object.values(manifests.docsManifest.docs))
parts.push(formatDocLine(doc));
return parts.join(`
`).trim();
}
function formatMultiSourceManifestsToLists(sources, options = {}) {
let parts = [];
for (let { source, manifests, error, notice } of sources) {
if (parts.push(`# ${source.title}`), parts.push(`id: ${source.id}`), parts.push(""), error) {
parts.push(`error: ${error}`), parts.push("");
continue;
}
if (notice) {
parts.push(formatRequiresOwnMcpNotice(source, notice.endpoint, { includeHeader: !1 })), parts.push("");
continue;
}
let components = Object.values(manifests?.componentManifest.components ?? {});
if (components.length > 0) {
parts.push("## Components"), parts.push("");
for (let component of components)
if (parts.push(formatComponentLine(component)), options.withStoryIds && Array.isArray(component.stories))
for (let story of component.stories)
parts.push(formatStorySubLine(story));
parts.push("");
}
let docs = Object.values(manifests?.docsManifest?.docs ?? {});
if (docs.length > 0) {
parts.push("## Docs"), parts.push("");
for (let doc of docs)
parts.push(formatDocLine(doc));
parts.push("");
}
}
return parts.join(`
`).trim();
}
function formatStoryDocumentation(componentManifest, storyName) {
let story = Array.isArray(componentManifest.stories) ? componentManifest.stories.find((s) => s.name === storyName) : void 0;
if (!story)
return "";
let parts = [];
return parts.push(`# ${componentManifest.name} - ${story.name}`), parts.push(""), story.snippet ? (parts.push(...formatStoryContent(story, componentManifest.import)), parts.join(`
`).trim()) : (story.id && (parts.push(`Story ID: ${story.id}`), parts.push("")), story.description && (parts.push(story.description), parts.push("")), parts.push("No code snippet was extracted for this story."), parts.join(`
`).trim());
}
// src/shared/open-service/toolsets/docs/manifest-formatter/adapt-core-manifest.ts
var ARG_TYPES_KEY = "argTypes";
function adaptCoreStories(stories) {
if (stories)
return Array.isArray(stories) ? stories : Object.values(stories);
}
function adaptCoreDoc(doc) {
return { ...doc };
}
function adaptCoreComponent(core) {
let { stories, docs, [ARG_TYPES_KEY]: _argTypes, ...rest2 } = core, component = { ...rest2 }, adaptedStories = adaptCoreStories(stories);
return adaptedStories && (component.stories = adaptedStories), docs && (component.docs = Object.fromEntries(
Object.entries(docs).map(([id, doc]) => [id, adaptCoreDoc(doc)])
)), component;
}
// src/shared/open-service/toolsets/docs/manifest-formatter/manifest-types.ts
var JSDocTag = record(string(), array(string())), ManifestErrorSchema = object({
name: string(),
message: string()
}), BaseManifest = object({
name: string(),
description: optional(string()),
jsDocTags: optional(JSDocTag),
error: optional(ManifestErrorSchema)
}), StorySchema = object({
...BaseManifest.entries,
id: optional(string()),
snippet: optional(string()),
summary: optional(string())
}), JsonRef = object({
$ref: string()
}), DocV0 = object({
id: string(),
name: string(),
title: optional(string()),
path: optional(string()),
content: optional(string()),
summary: optional(string()),
error: optional(ManifestErrorSchema)
}), BaseInlineComponentProperties = object({
...BaseManifest.entries,
path: optional(string()),
summary: optional(string()),
import: optional(string()),
// Framework-authored Markdown, rendered in place of the props section derived from `react*`.
apiDescription: optional(string()),
renderer: optional(string()),
// Mirrors the docgen-engine payloads, which the parsers narrow structurally at runtime.
reactDocgen: optional(any()),
reactDocgenTypescript: optional(any()),
reactComponentMeta: optional(any())
}), SubcomponentManifest = object({
...BaseInlineComponentProperties.entries
}), ComponentManifestV0 = object({
...BaseInlineComponentProperties.entries,
id: string(),
stories: optional(array(StorySchema)),
subcomponents: optional(record(string(), SubcomponentManifest)),
docs: optional(record(string(), DocV0))
}), ComponentManifestMapV0 = object({
v: literal(0),
components: record(string(), ComponentManifestV0)
}), DocsManifestMapV0 = object({
v: literal(0),
docs: record(string(), DocV0)
}), DocV1 = object({
id: string(),
name: string(),
summary: optional(string()),
mdx: optional(JsonRef),
error: optional(ManifestErrorSchema)
}), ComponentManifestV1 = object({
id: string(),
name: string(),
description: optional(string()),
summary: optional(string()),
error: optional(ManifestErrorSchema),
docgen: optional(JsonRef),
stories: optional(union([JsonRef, array(StorySchema)])),
docs: optional(record(string(), DocV1))
}), ComponentManifestMapV1 = object({
v: literal(1),
components: record(string(), ComponentManifestV1)
}), DocsManifestMapV1 = object({
v: literal(1),
docs: record(string(), DocV1)
}), ComponentManifestMap = variant("v", [
ComponentManifestMapV0,
ComponentManifestMapV1
]), DocsManifestMap = variant("v", [DocsManifestMapV0, DocsManifestMapV1]);
// src/shared/open-service/toolsets/docs/access-provider.ts
var jsonObject = (schema) => pipe(
// Checked on the raw input: `looseObject` copies an array's entries into a plain object, so by
// the time it has run there is nothing left to recognise.
custom((input) => !Array.isArray(input), "Expected a JSON object"),
schema
), DocgenRefPayload = jsonObject(looseObject({})), StoryDocsRefPayload = nullable(
jsonObject(
looseObject({
// A record keyed by story id, or an already-resolved array — `adaptCoreStories` accepts both.
stories: optional(
union([record(string(), looseObject({})), array(looseObject({}))])
),
import: optional(string())
})
)
), MdxRefPayload = jsonObject(
looseObject({ id: optional(string()), name: optional(string()) })
);
// src/shared/open-service/toolsets/docs/multi-source.ts
async function listSources(sources, options) {
let listings = await Promise.all(
sources.map(async ({ source, access }) => {
try {
return { source, manifests: await access.list(options) };
} catch (error) {
return error instanceof RequiresOwnMcpError ? { source, notice: { kind: "requires-own-mcp", endpoint: error.endpoint } } : { source, error: error instanceof Error ? error.message : String(error) };
}
})
);
if (listings.filter((listing) => !listing.error).length === 0)
throw new ManifestGetError(
`Failed to fetch manifests from any source. Errors:
${listings.map((listing) => `- ${listing.source.title}: ${listing.error}`).join(`
`)}`
);
return listings;
}
// src/shared/open-service/toolsets/estimate-tokens.ts
function isWhitespace(code) {
return code === 32 || code === 9 || code === 10 || code === 13;
}
function isAlphanumeric(code) {
return code >= 48 && code <= 57 || code >= 65 && code <= 90 || code >= 97 && code <= 122 || code === 95;
}
function estimateTokens(text) {
if (!text) return 0;
let tokenCount = 0, i = 0, len = text.length;
for (; i < len; ) {
let code = text.charCodeAt(i);
if (isWhitespace(code))
for (tokenCount++, i++; i < len && isWhitespace(text.charCodeAt(i)); )
i++;
else if (isAlphanumeric(code))
for (tokenCount++, i++; i < len && isAlphanumeric(text.charCodeAt(i)); )
i++;
else
tokenCount++, i++;
}
return tokenCount;
}
// src/shared/open-service/toolsets/docs/definition.ts
var DOCS_TOOLSET_ID = "docs", DOCS_METHOD_NAMES = {
list: "list",
show: "show",
showStory: "showStory"
}, DOCS_METHOD_REFS = {
list: `${DOCS_TOOLSET_ID}.${DOCS_METHOD_NAMES.list}`,
show: `${DOCS_TOOLSET_ID}.${DOCS_METHOD_NAMES.show}`,
showStory: `${DOCS_TOOLSET_ID}.${DOCS_METHOD_NAMES.showStory}`
};
function selectReportedManifests({
manifests,
sources
}) {
return manifests ?? sources?.find((listing) => listing.manifests)?.manifests;
}
function resolveShow({ entry, sourceError }) {
return sourceError !== void 0 ? { kind: "source-error", message: sourceError } : entry === void 0 ? { kind: "entry-missing" } : { kind: "found", entry };
}
function isShowStorySelector({ storyId, componentId, storyName }) {
return storyId !== void 0 || componentId !== void 0 && storyName !== void 0;
}
function componentIdOfStoryId(storyId) {
let separator = storyId.indexOf("--");
return separator === -1 ? storyId : storyId.slice(0, separator);
}
function resolveShowStory(data) {
let { entry, storyId, storyName, sourceError } = data;
if (!isShowStorySelector(data))
return { kind: "input-invalid" };
if (sourceError !== void 0)
return { kind: "source-error", message: sourceError };
if (entry === void 0 || entry.kind !== "component")
return { kind: "component-missing" };
let { component } = entry, story = storyId !== void 0 ? component.stories?.find((candidate) => candidate.id === storyId) : component.stories?.find((candidate) => candidate.name === storyName);
return story ? { kind: "found", component, storyName: story.name } : { kind: "story-missing", component };
}
function isDocsShowError(output) {
return resolveShow(output).kind !== "found";
}
function isDocsShowStoryError(output) {
return resolveShowStory(output).kind !== "found";
}
function describeList(ctx) {
return `List all available UI components and documentation entries from the Storybook, returning the IDs the other documentation tools take as input. Call this first for any UI task \u2014 before writing a new component, check what the design system already provides and build on it instead of hand-rolling a duplicate; before answering any question about props, API, or usage, discover the relevant IDs here rather than reading component source. Then fetch the entries with ${getToolName(ctx)(DOCS_METHOD_REFS.show)}, referencing only IDs returned here \u2014 never guess IDs. When multiple Storybook sources are configured, entries from every source are included; scope follow-up calls to one source via their \`storybookId\` input. Pass \`withStoryIds: true\` when you need story IDs for other tools.`;
}
function describeShow(ctx) {
return `Get documentation for a UI component or docs entry.
Returns the first ${MAX_STORIES_TO_SHOW} stories (including story IDs) with code snippets showing how props are used, plus TypeScript prop definitions. Call this before using a component to avoid hallucinating prop names, types, or valid combinations, and to answer any question about a component's props, API, or usage \u2014 reading or grepping the component source is not a substitute. Stories reveal real prop usage patterns, interactions, and edge cases that type definitions alone don't show. If the example stories don't show the prop you need, use the ${getToolName(ctx)(DOCS_METHOD_REFS.showStory)} tool to fetch the story documentation for the specific story variant you need \u2014 its story ID can be passed directly as \`storyId\`.
Example: id="button" returns Primary, Secondary, Large stories with code like <Button variant="primary" size="large"> showing actual prop combinations.`;
}
function formatEntryNotFound(id, storybookId, ctx) {
let suffix = storybookId ? ` in source "${storybookId}"` : "";
return `Component or Docs Entry not found: "${id}"${suffix}. Use the ${getToolName(ctx)(DOCS_METHOD_REFS.list)} tool to see available components and documentation entries.`;
}
function renderShow(data, ctx) {
let resolution = resolveShow(data);
switch (resolution.kind) {
case "source-error":
return resolution.message;
case "entry-missing":
return formatEntryNotFound(data.id, data.storybookId, ctx);
case "found":
return resolution.entry.kind === "doc" ? formatDocsManifest(resolution.entry.doc) : formatComponentManifest(resolution.entry.component);
default:
return resolution;
}
}
function formatAvailableStories(stories) {
return stories?.map((story) => story.id ? `${story.name} (${story.id})` : story.name).join(", ") || "none";
}
function renderShowStory(data, ctx) {
let resolution = resolveShowStory(data);
switch (resolution.kind) {
case "input-invalid":
return `Provide either \`storyId\`, or both \`componentId\` and \`storyName\`. Story ids are listed by the ${getToolName(ctx)(DOCS_METHOD_REFS.list)} tool with \`withStoryIds: true\` and in ${getToolName(ctx)(DOCS_METHOD_REFS.show)} output.`;
case "source-error":
return resolution.message;
case "component-missing":
return data.storyId !== void 0 ? `Story not found: "${data.storyId}". Use the ${getToolName(ctx)(DOCS_METHOD_REFS.list)} tool with \`withStoryIds: true\` to see available stories and their ids.` : `Component not found: "${data.componentId}". Use the ${getToolName(ctx)(DOCS_METHOD_REFS.list)} tool to see available components.`;
case "story-missing": {
let availableStories = formatAvailableStories(resolution.component.stories);
return data.storyId !== void 0 ? `Story not found: "${data.storyId}" for component "${resolution.component.id}". Available stories: ${availableStories}` : `Story "${data.storyName}" not found for component "${data.componentId}". Available stories: ${availableStories}`;
}
case "found":
return formatStoryDocumentation(resolution.component, resolution.storyName);
default:
return resolution;
}
}
var storybookIdField = {
storybookId: pipe(
string(),
description('The ID of the Storybook source to query (e.g., "local", "design-system")')
)
};
function selectSource(sources, storybookId, ctx) {
if (!sources?.length)
return {};
let available = sources.map(({ source }) => source.id).join(", "), listRef = `Use the ${getToolName(ctx)(DOCS_METHOD_REFS.list)} tool to see available sources.`;
if (!storybookId)
return { sourceError: `storybookId is required. Available sources: ${available}. ${listRef}` };
let match = sources.find(({ source }) => source.id === storybookId);
return match ? { access: match.access } : {
sourceError: `Storybook source not found: "${storybookId}". Available sources: ${available}. ${listRef}`
};
}
function createDocsToolset(options) {
let { docsAccess, sources } = options, multiSource = !!sources?.length;
if (!multiSource && !docsAccess)
throw new Error("createDocsToolset requires a docsAccess or at least one source.");
let showSchema = multiSource ? object({
id: pipe(string(), description('The component or docs entry ID (e.g., "button")')),
...storybookIdField
}) : object({
id: pipe(string(), description('The component or docs entry ID (e.g., "button")'))
}), showStoryFields = {
storyId: pipe(
optional(string()),
description(
'The story ID, as listed by the docs list tool with withStoryIds: true and shown next to each story in the component documentation (e.g., "button--primary"). Prefer this over componentId + storyName whenever you have a story ID.'
)
),
componentId: pipe(
optional(string()),
description(
'The component ID (e.g., "button"). Use together with storyName, and only when you have no story ID.'
)
),
storyName: pipe(
optional(string()),
description(
'The human-readable story name (e.g., "Primary"). Use together with componentId.'
)
)
}, showStorySchema = multiSource ? object({ ...showStoryFields, ...storybookIdField }) : object(showStoryFields), access = (storybookId, ctx) => multiSource ? selectSource(sources, storybookId, ctx) : { access: docsAccess };
return {
id: DOCS_TOOLSET_ID,
description: "Storybook component and docs documentation.",
methods: {
[DOCS_METHOD_NAMES.list]: {
input: object({
withStoryIds: optional(
pipe(
boolean(),
description(
"When true, includes story sub-bullets under each component with story name and story ID. Use this to discover IDs for downstream story-focused workflows without filesystem lookup."
)
),
!1
)
}),
title: "List All Documentation",
description: describeList,
handler: async (input, ctx) => {
let { withStoryIds } = input, data = multiSource ? { withStoryIds, sources: await listSources(sources, { withStoryIds }) } : { withStoryIds, manifests: await docsAccess.list({ withStoryIds }) }, markdown = data.sources ? formatMultiSourceManifestsToLists(data.sources, { withStoryIds }) : formatManifestsToLists(data.manifests, { withStoryIds }), counted = selectReportedManifests(data);
return counted && await reportToolsetTelemetry(ctx, "tool:listAllDocumentation", {
toolset: "docs",
componentCount: Object.keys(counted.componentManifest.components).length,
docsCount: Object.keys(counted.docsManifest?.docs ?? {}).length,
resultTokenCount: estimateTokens(markdown),
sourceCount: data.sources?.length
}), { ok: !0, data, markdown };
}
},
[DOCS_METHOD_NAMES.show]: {
input: showSchema,
title: "Get Documentation",
description: describeShow,
handler: async (input, ctx) => {
let { id, storybookId } = input, selected = access(storybookId, ctx), data = selected.sourceError ? { id, storybookId, sourceError: selected.sourceError } : { id, storybookId, entry: await selected.access.resolve(id) }, markdown = renderShow(data, ctx);
return await reportToolsetTelemetry(ctx, "tool:getDocumentation", {
toolset: "docs",
componentId: id,
found: data.entry !== void 0,
resultTokenCount: estimateTokens(markdown)
}), isDocsShowError(data) ? { ok: !1, data, markdown } : { ok: !0, data, markdown };
}
},
[DOCS_METHOD_NAMES.showStory]: {
input: showStorySchema,
title: "Get Documentation for Story",
description: "Get detailed documentation for a specific story variant of a UI component. Use this when you need to see more usage examples of a component, via the stories written for it. Identify the story by its story ID (preferred), or by componentId plus storyName.",
handler: async (input, ctx) => {
let { storyId, componentId, storyName, storybookId } = input, request = { storyId, componentId, storyName, storybookId }, resolveId = storyId !== void 0 ? componentIdOfStoryId(storyId) : componentId !== void 0 && storyName !== void 0 ? componentId : void 0, selected = resolveId !== void 0 ? access(storybookId, ctx) : {}, data = selected.access && resolveId !== void 0 ? { ...request, entry: await selected.access.resolve(resolveId) } : { ...request, sourceError: selected.sourceError }, markdown = renderShowStory(data, ctx);
return isDocsShowStoryError(data) ? { ok: !1, data, markdown } : { ok: !0, data, markdown };
}
}
}
};
}
// src/shared/open-service/toolsets/docs/map.ts
function selectAttachedDocs(classification, id, mdx) {
let attached = classification.attachedDocsByComponent.get(id) ?? [];
if (attached.length === 0 || !mdx?.docs)
return;
let docs = {};
for (let docsId of attached) {
let doc = mdx.docs[docsId];
doc && (docs[docsId] = doc);
}
return docs;
}
// src/shared/open-service/toolsets/docs/access-service.ts
var MDX_SERVICE_ID2 = "addon-docs/mdx";
function classifyIndex(index) {
let entries = Object.values(index.entries).filter(
(entry) => entry.tags?.includes(Tag.MANIFEST) ?? !1
), selected = selectComponentEntriesByComponentId(entries), storyBasedIds = /* @__PURE__ */ new Set();
for (let [id, entry] of selected)
entry.type === "story" && storyBasedIds.add(id);
let attachedDocsByComponent = /* @__PURE__ */ new Map(), unattachedDocs = /* @__PURE__ */ new Map();
for (let entry of entries)
if (entry.type === "docs") {
if (entry.tags?.includes(Tag.UNATTACHED_MDX))
unattachedDocs.set(entry.id, entry.name);
else if (entry.tags?.includes(Tag.ATTACHED_MDX)) {
let componentId = getComponentIdFromEntry(entry);
attachedDocsByComponent.set(componentId, [
...attachedDocsByComponent.get(componentId) ?? [],
entry.id
]);
}
}
return {
componentIds: [...selected.keys()],
storyBasedIds,
unattachedDocs,
attachedDocsByComponent
};
}
function tryGetService(getService3, serviceId) {
try {
return getService3(serviceId, { internal: !0 });
} catch (error) {
if (error instanceof OpenServiceMissingServiceError)
return;
throw error;
}
}
async function loadOptionalComponentPayload(load) {
try {
return await load;
} catch (error) {
if (error instanceof OpenServiceDocgenMissingComponentError)
return;
throw error;
}
}
function createServiceDocsAccess({
storyIndex,
getService: getService3
}) {
let classify = async () => classifyIndex(await storyIndex.getIndex()), getDocgen = () => getService3("core/docgen", { internal: !0 }), getStoryDocs = () => getService3("core/story-docs", { internal: !0 }), getMdx = () => tryGetService(getService3, MDX_SERVICE_ID2);
async function listComponents(classification, withStoryIds) {
let allDocgen = await getDocgen().queries.docgenForAllComponents.loaded(), storyDocs = getStoryDocs(), storyBasedIds = withStoryIds ? classification.componentIds.filter((id) => classification.storyBasedIds.has(id)) : [], storiesById = new Map(
await Promise.all(
storyBasedIds.map(
async (id) => [
id,
await loadOptionalComponentPayload(storyDocs.queries.storyDocs.loaded({ id }))
]
)
)
), components = {};
for (let id of classification.componentIds) {
let payload = allDocgen[id];
components[id] = {
id,
name: payload?.name ?? id,
...payload?.description !== void 0 ? { description: payload.description } : {},
...payload?.summary !== void 0 ? { summary: payload.summary } : {},
...classification.storyBasedIds.has(id) && withStoryIds ? { stories: adaptCoreStories(storiesById.get(id)?.stories) ?? [] } : {}
};
}
return components;
}
async function listDocs(classification) {
if (classification.unattachedDocs.size === 0)
return {};
let allMdx = await getMdx()?.queries.mdxForAllComponents.loaded() ?? {}, docs = {};
for (let [docId, name] of classification.unattachedDocs) {
let payload = allMdx[docId]?.docs?.[docId];
docs[docId] = {
id: docId,
name,
...payload?.summary !== void 0 ? { summary: payload.summary } : {}
};
}
return docs;
}
async function resolveComponent(id, classification) {
let mdx = getMdx(), [docgenPayload, storyDocsPayload] = await Promise.all([
loadOptionalComponentPayload(getDocgen().queries.docgen.loaded({ id })),
loadOptionalComponentPayload(getStoryDocs().queries.storyDocs.loaded({ id }))
]), mdxPayload = (classification.attachedDocsByComponent.get(id)?.length ?? 0) > 0 && mdx ? await loadOptionalComponentPayload(mdx.queries.mdxForComponent.loaded({ id })) : void 0, docs = selectAttachedDocs(classification, id, mdxPayload), core = {
...docgenPayload,
id,
name: docgenPayload?.name ?? id,
...storyDocsPayload?.stories ? { stories: storyDocsPayload.stories } : {},
...storyDocsPayload?.import ? { import: storyDocsPayload.import } : {},
...docs ? { docs } : {}
};
return { kind: "component", component: adaptCoreComponent(core) };
}
async function resolveStandaloneDoc(id) {
let mdx = getMdx(), doc = (mdx ? await loadOptionalComponentPayload(mdx.queries.mdxForComponent.loaded({ id })) : void 0)?.docs?.[id];
return doc ? { kind: "doc", doc: adaptCoreDoc(doc) } : void 0;
}
return {
async list({ withStoryIds }) {
let classification = await classify(), [components, docs] = await Promise.all([
listComponents(classification, withStoryIds),
listDocs(classification)
]);
return toShallowManifests(components, docs);
},
async resolve(id) {
let classification = await classify();
if (classification.unattachedDocs.has(id))
return resolveStandaloneDoc(id);
if (classification.componentIds.includes(id))
return resolveComponent(id, classification);
}
};
}
// src/shared/open-service/toolsets/docs/access-manifest.ts
function isRecord(value) {
return typeof value == "object" && value !== null;
}
function isShallow(manifest) {
return manifest.v === 1;
}
function toComponentManifest(value) {
if (!(!isRecord(value) || !isRecord(value.components)))
return isShallow(value) ? { v: 1, components: value.components } : { v: 0, components: value.components };
}
function toDocsManifest(value) {
if (!(!isRecord(value) || !isRecord(value.docs)))
return isShallow(value) ? { v: 1, docs: value.docs } : { v: 0, docs: value.docs };
}
function withoutStories(manifest) {
let components = Object.fromEntries(
Object.entries(manifest.components).map(([id, { stories: _stories, ...rest2 }]) => [id, rest2])
);
return manifest.v === 1 ? { v: 1, components } : { v: 0, components };
}
function createManifestDocsAccess({ getManifests: getManifests2 }) {
return {
async list({ withStoryIds }) {
let raw = await getManifests2(), componentManifest = toComponentManifest(raw.components), docsManifest = toDocsManifest(raw.docs);
return {
...emptyManifests(),
...componentManifest ? {
componentManifest: withStoryIds ? componentManifest : withoutStories(componentManifest)
} : {},
...docsManifest ? { docsManifest } : {}
};
},
async resolve(id) {
let raw = await getManifests2(), components = toComponentManifest(raw.components)?.components, component = components && Object.hasOwn(components, id) ? components[id] : void 0;
if (component)
return { kind: "component", component };
let docs = toDocsManifest(raw.docs)?.docs, doc = docs && Object.hasOwn(docs, id) ? docs[id] : void 0;
if (doc)
return { kind: "doc", doc };
}
};
}
// src/shared/open-service/toolsets/docs/access-local.ts
function createLocalDocsAccess({
storyIndex,
getManifests: getManifests2
}) {
let serviceAccess = createServiceDocsAccess({
storyIndex,
getService
}), manifestAccess = createManifestDocsAccess({ getManifests: getManifests2 }), requiredServiceIds = [DOCGEN_SERVICE_ID, STORY_DOCS_SERVICE_ID], pick = () => {
let registered = new Set(getRegisteredServices().map((service) => service.id));
return requiredServiceIds.every((id) => registered.has(id)) ? serviceAccess : manifestAccess;
};
return {
list: (options) => pick().list(options),
resolve: (id) => pick().resolve(id)
};
}
// src/shared/open-service/toolsets/stories/changed.ts
var INCLUDED_STATUS_VALUES = /* @__PURE__ */ new Set([
"status-value:new",
"status-value:modified",
"status-value:affected"
]);
function isChangeStatusValue(value) {
return INCLUDED_STATUS_VALUES.has(value);
}
function statusPriority(statusValue) {
return statusValue === "status-value:new" ? 0 : statusValue === "status-value:modified" ? 1 : 2;
}
function getChangedStories({ statuses, index }) {
let changedFromStatusStore = [];
for (let byType of Object.values(statuses)) {
let status = byType?.[CHANGE_DETECTION_STATUS_TYPE_ID];
status?.value && isChangeStatusValue(status.value) && changedFromStatusStore.push(status);
}
let stories = changedFromStatusStore.flatMap(({ storyId, value }) => {
let entry = index.entries[storyId];
return !entry || !isChangeStatusValue(value) ? [] : [
{
storyId,
statusValue: value,
title: entry.title,
name: entry.name,
importPath: entry.importPath
}
];
});
stories.sort((a, b) => {
let priorityDelta = statusPriority(a.statusValue) - statusPriority(b.statusValue);
return priorityDelta !== 0 ? priorityDelta : a.storyId.localeCompare(b.storyId);
});
let counts = {
new: stories.filter((story) => story.statusValue === "status-value:new").length,
modified: stories.filter((story) => story.statusValue === "status-value:modified").length,
affected: stories.filter((story) => story.statusValue === "status-value:affected").length
};
return { stories, counts };
}
// src/shared/open-service/toolsets/stories/resolve-component-stories.ts
import { existsSync as existsSync4, realpathSync } from "node:fs";
var SOURCE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"], INDEX_BASENAMES = SOURCE_EXTENSIONS.map((ext) => `index${ext}`);
function slash(filePath) {
return filePath.replace(/\\/g, "/");
}
function expandBarrelTargets(absoluteComponentPath) {
let targets = /* @__PURE__ */ new Set([absoluteComponentPath]), dir = dirname(absoluteComponentPath), ext = extname(absoluteComponentPath), base = basename(absoluteComponentPath, ext), dirName = basename(dir);
if (base.toLowerCase() === "index")
for (let candidateExt of SOURCE_EXTENSIONS) {
let candidate = join(dir, `${dirName}${candidateExt}`);
existsSync4(candidate) && targets.add(candidate);
}
else if (base.toLowerCase() === dirName.toLowerCase())
for (let indexBasename of INDEX_BASENAMES) {
let candidate = join(dir, indexBasename);
existsSync4(candidate) && targets.add(candidate);
}
return [...targets].map((target) => normalize(target));
}
function canonicalise(absolutePath) {
try {
return realpathSync.native(absolutePath);
} catch (err) {
if (err.code === "ENOENT")
return;
throw err;
}
}
function toStoryFileKey(importPath) {
let normalized = slash(importPath);
return normalized.startsWith("./") || normalized.startsWith("../") ? normalized : `./${normalized}`;
}
function buildStoryIdsByFile(storyIndex) {
let storyIdsByFile = /* @__PURE__ */ new Map();
for (let entry of Object.values(storyIndex.entries)) {
if (entry.type !== "story" || entry.importPath.startsWith("virtual:"))
continue;
let filePath = toStoryFileKey(entry.importPath), ids = storyIdsByFile.get(filePath);
ids || (ids = /* @__PURE__ */ new Set(), storyIdsByFile.set(filePath, ids)), ids.add(entry.id);
}
return storyIdsByFile;
}
function reasonForStatus(status) {
switch (status.value) {
case "booting":
return "Storybook's story module graph hasn't built yet \u2014 it is still being constructed. Retry shortly.";
case "unavailable":
return `Storybook's story dependency graph is unavailable: ${status.reason}. Make sure the dev server is running with a builder that supports change detection.`;
case "error":
return `Storybook's story module graph failed to build: ${status.error.message}`;
case "ready":
return "Storybook's story module graph is ready.";
default:
return status;
}
}
async function resolveComponentStories(request, deps) {
let { moduleGraph } = deps;
if (!moduleGraph)
return {
available: !1,
reason: "Storybook's story module graph is unavailable. This Storybook version may not ship the open-service API, the module-graph service isn't registered (e.g. a builder without change detection), or the dev server is not running."
};
let status = await moduleGraph.queries.status.loaded(void 0);
if (status.value !== "ready")
return { available: !1, reason: reasonForStatus(status) };
let workingDir = deps.workingDir ?? process.cwd(), storyIndex = await deps.getStoryIndex(), storyIdsByFile = buildStoryIdsByFile(storyIndex), resolved = [...new Set(request.componentPaths)].map((componentPath) => {
let absolute = resolve(workingDir, componentPath), canonical = canonicalise(absolute), echo = normalize(componentPath);
if (!canonical)
return { componentPath: echo, targets: [], pathNotFound: !0 };
let targets = new Set(expandBarrelTargets(absolute));
if (canonical !== absolute)
for (let target of expandBarrelTargets(canonical))
targets.add(target);
return { componentPath: echo, targets: [...targets] };
}), allTargets = [...new Set(resolved.flatMap((component) => component.targets))], hitsByTarget = /* @__PURE__ */ new Map();
if (allTargets.length > 0) {
let batched = await moduleGraph.queries.storiesForFiles.loaded({ files: allTargets });
allTargets.forEach((target, i) => {
hitsByTarget.set(target, batched[i] ?? []);
});
}
return { available: !0, results: resolved.map((component) => {
if (component.pathNotFound)
return { componentPath: component.componentPath, matches: [], pathNotFound: !0 };
let byStoryId = /* @__PURE__ */ new Map();
for (let target of component.targets) {
let hits = hitsByTarget.get(target) ?? [];
for (let { storyFile, depth } of hits) {
let storyIds = storyIdsByFile.get(storyFile);
if (storyIds)
for (let storyId of storyIds) {
let existing = byStoryId.get(storyId);
(existing === void 0 || depth < existing) && byStoryId.set(storyId, depth);
}
}
}
let matches = [...byStoryId.entries()].map(([storyId, depth]) => ({ storyId, depth })).sort((a, b) => a.depth !== b.depth ? a.depth - b.depth : a.storyId.localeCompare(b.storyId));
return { componentPath: component.componentPath, matches };
}) };
}
// src/shared/open-service/toolsets/stories/find-by-component.ts
var DEFAULT_MAX_DISTANCE = 3;
function applyMaxDistance(depths, maxDistance) {
let kept = [], clippedDistances = /* @__PURE__ */ new Set(), clippedCount = 0;
for (let depth of depths)
depth.depth <= maxDistance ? kept.push(depth) : (clippedCount++, clippedDistances.add(depth.depth));
let clipped = clippedCount > 0 ? {
count: clippedCount,
distances: [...clippedDistances].sort((a, b) => a - b)
} : void 0;
return { kept, clipped };
}
async function findStoriesByComponent({
componentPaths,
maxDistance = DEFAULT_MAX_DISTANCE,
index,
moduleGraph
}) {
let lookup = await resolveComponentStories(
{ componentPaths },
{ getStoryIndex: async () => index, moduleGraph }
);
return lookup.available ? { available: !0, results: (lookup.results ?? []).map((entry) => {
if (entry.pathNotFound)
return { componentPath: entry.componentPath, matches: [], pathNotFound: !0 };
let { kept, clipped } = applyMaxDistance(entry.matches, maxDistance), matches = [];
for (let { storyId, depth } of kept) {
let indexEntry = index.entries[storyId];
!indexEntry || indexEntry.type !== "story" || matches.push({
storyId: indexEntry.id,
title: indexEntry.title,
name: indexEntry.name,
importPath: indexEntry.importPath,
distance: depth
});
}
return clipped ? { componentPath: entry.componentPath, matches, clipped } : { componentPath: entry.componentPath, matches };
}) } : {
available: !1,
reason: lookup.reason ?? "Storybook's story module graph is unavailable."
};
}
// src/shared/open-service/toolsets/stories/format.ts
function pluralize(count, singular, plural2 = `${singular}s`) {
return count === 1 ? singular : plural2;
}
function previewReviewNudge(ctx) {
let reviewTool = getToolName(ctx)("review.create");
return `These preview links are for iterating or sharing a specific story \u2014 they are not how visual work or a browse request ends. The ${reviewTool} tool is available in this session: if you are finishing visually observable work or showing a set of stories, publish the review with **${reviewTool}** and link that instead.`;
}
function formatPreviewStories({ stories }, ctx, { reviewEnabled = !1 } = {}) {
let blocks = stories.map((story) => "error" in story ? story.error : story.previewUrl);
return reviewEnabled && stories.some((story) => "previewUrl" in story) && blocks.push(previewReviewNudge(ctx)), blocks;
}
var BANNER_INLINE_LIMIT = 3;
function formatPartialCoverageBanner(unreachable) {
if (unreachable.length === 0)
return "";
let fileList = unreachable.length <= BANNER_INLINE_LIMIT ? unreachable.join(", ") : `${unreachable.slice(0, BANNER_INLINE_LIMIT).join(", ")}, +${unreachable.length - BANNER_INLINE_LIMIT} more`;
return `\u26A0 Coverage gap: ${unreachable.length} modified ${pluralize(unreachable.length, "file")} unreachable from any story (${fileList}) \u2014 full sanity-check note at end of this response.
`;
}
function formatUnreachableHint(unreachable, ctx) {
return unreachable.length === 0 ? "" : `
The following working-tree file(s) are modified but unreachable from any story (no static import path connects them \u2014 they are likely theme tokens, decorators, or other Storybook-preview-runtime files):
${unreachable.map((file) => `- ${file}`).join(`
`)}
For these, grep the codebase for their exports (e.g. specific tokens or symbols) to find runtime consumers, then call \`${getToolName(ctx)("stories.findByComponent")}\` with those consumer file paths.`;
}
function formatPartialCoverageHint(unreachable, ctx) {
return unreachable.length === 0 ? "" : `
Coverage sanity check: the working tree also contains modified file(s) that aren't reachable from any story above (no static import path connects them \u2014 typically theme tokens, decorators, or other preview-runtime files):
${unreachable.map((file) => `- ${file}`).join(`
`)}
The list above is real but may be stale w.r.t. these files \u2014 they're often left over from an earlier sub-change in the same diff. Before composing a review, grep the codebase for their exports and call \`${getToolName(ctx)("stories.findByComponent")}\` with the runtime consumers' file paths. Do not assume the list above already covers them, and never invent story IDs to fill the gap.`;
}
function formatChangedStories({ stories, counts, unreachableFiles }, ctx, { reviewEnabled = !1 } = {}) {
if (stories.length === 0)
return `No new, modified, or related stories detected.${formatUnreachableHint(unreachableFiles, ctx)}`;
let buckets = {
new: stories.filter((story) => story.statusValue === "status-value:new"),
modified: stories.filter((story) => story.statusValue === "status-value:modified"),
affected: stories.filter((story) => story.statusValue === "status-value:affected")
}, text = `${formatPartialCoverageBanner(unreachableFiles)}Detected ${stories.length} changed stor${pluralize(stories.length, "y", "ies")} (${counts.new} new, ${counts.modified} modified, ${counts.affected} related).`;
reviewEnabled && (text += `
Next: if the change is visually observable, publish the review now \u2014 call **${getToolName(ctx)("review.create")}** curating these story IDs. That review link is how you finish; do not substitute individual preview URLs for it.`);
let serializeStory = ({
storyId,
title,
name,
importPath
}) => `- \`${storyId}\`: ${title} / ${name} (\`${importPath}\`)`;
return buckets.new.length > 0 && (text += `
New stories:
${buckets.new.map(serializeStory).join(`
`)}`), buckets.modified.length > 0 && (text += `
Modified stories:
${buckets.modified.map(serializeStory).join(`
`)}`), buckets.affected.length > 0 && (text += `
Related stories:
${buckets.affected.map(serializeStory).join(`
`)}`), text + formatPartialCoverageHint(unreachableFiles, ctx);
}
function formatClippedTail(clipped, maxDistance) {
let { distances } = clipped, rangeText = distances.length === 1 ? `distance ${distances[0]}` : `distances ${distances[0]}..${distances[distances.length - 1]}`;
return `+${clipped.count} more ${pluralize(clipped.count, "story", "stories")} at ${rangeText} hidden by \`maxDistance: ${maxDistance}\``;
}
function serializeComponentSection({ componentPath, matches, clipped, pathNotFound }, maxDistance) {
if (pathNotFound)
return `${componentPath}: path does not exist on disk \u2014 re-check the path you sent.`;
if (matches.length === 0)
return clipped && clipped.count > 0 ? `${componentPath}: no stories within \`maxDistance: ${maxDistance}\` \u2014 ${formatClippedTail(clipped, maxDistance)}.` : `${componentPath}: no stories found`;
let byDistance = /* @__PURE__ */ new Map();
for (let match of matches) {
let bucket = byDistance.get(match.distance) ?? [];
bucket.push(match), byDistance.set(match.distance, bucket);
}
let distances = [...byDistance.keys()].sort((a, b) => a - b), componentCount = new Set(matches.map((match) => match.title)).size, bucketSummary = distances.map((d) => `d${d}=${byDistance.get(d).length}`).join(", "), lines = [
`${componentPath}:`,
`\u2192 ${matches.length} ${pluralize(matches.length, "story", "stories")} across ${componentCount} ${pluralize(componentCount, "component")}, distances ${distances[0]}..${distances[distances.length - 1]} (${bucketSummary})`
];
for (let distance of distances) {
lines.push(`distance ${distance}:`);
for (let match of byDistance.get(distance))
lines.push(
` - \`${match.storyId}\`: ${match.title} / ${match.name} (\`${match.importPath}\`)`
);
}
return clipped && clipped.count > 0 && lines.push(` (${formatClippedTail(clipped, maxDistance)}.)`), lines.join(`
`);
}
function formatFindByComponent({ results, maxDistance }) {
return results.length === 0 ? "No component paths provided." : results.map((result) => serializeComponentSection(result, maxDistance)).join(`
`);
}
// src/router/utils.ts
import { once as once2 } from "storybook/internal/client-logger";
var import_memoizerific = __toESM(require_memoizerific(), 1);
// ../../node_modules/picoquery/lib/string-util.js
var hexTable = Array.from({ length: 256 }, (_, i) => "%" + ((i < 16 ? "0" : "") + i.toString(16)).toUpperCase()), noEscape = new Int8Array([
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
0,
1,
1,
1,
1,
0,
0,
1,
1,
0,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
0,
0,
0,
0,
0,
0,
0,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
0,
0,
0,
0,
1,
0,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
0,
0,
0,
1,
0
]);
function encodeString(str) {
let len = str.length;
if (len === 0)
return "";
let out = "", lastPos = 0, i = 0;
outer: for (; i < len; i++) {
let c = str.charCodeAt(i);
for (; c < 128; ) {
if (noEscape[c] !== 1 && (lastPos < i && (out += str.slice(lastPos, i)), lastPos = i + 1, out += hexTable[c]), ++i === len)
break outer;
c = str.charCodeAt(i);
}
if (lastPos < i && (out += str.slice(lastPos, i)), c < 2048) {
lastPos = i + 1, out += hexTable[192 | c >> 6] + hexTable[128 | c & 63];
continue;
}
if (c < 55296 || c >= 57344) {
lastPos = i + 1, out += hexTable[224 | c >> 12] + hexTable[128 | c >> 6 & 63] + hexTable[128 | c & 63];
continue;
}
if (++i, i >= len)
throw new Error("URI malformed");
let c2 = str.charCodeAt(i) & 1023;
lastPos = i + 1, c = 65536 + ((c & 1023) << 10 | c2), out += hexTable[240 | c >> 18] + hexTable[128 | c >> 12 & 63] + hexTable[128 | c >> 6 & 63] + hexTable[128 | c & 63];
}
return lastPos === 0 ? str : lastPos < len ? out + str.slice(lastPos) : out;
}
// ../../node_modules/picoquery/lib/shared.js
var defaultValueSerializer = (value) => {
switch (typeof value) {
case "string":
return encodeString(value);
case "bigint":
case "boolean":
return "" + value;
case "number":
if (Number.isFinite(value))
return value < 1e21 ? "" + value : encodeString("" + value);
break;
}
return value instanceof Date ? encodeString(value.toISOString()) : "";
}, defaultShouldSerializeObject = (val) => val instanceof Date, identityFunc = (v) => v, defaultOptions = {
nesting: !0,
nestingSyntax: "dot",
arrayRepeat: !1,
arrayRepeatSyntax: "repeat",
delimiter: 38,
valueDeserializer: identityFunc,
valueSerializer: defaultValueSerializer,
keyDeserializer: identityFunc,
shouldSerializeObject: defaultShouldSerializeObject
};
// ../../node_modules/picoquery/lib/object-util.js
function isPrototypeKey(value) {
return value === "__proto__" || value === "constructor" || value === "prototype";
}
function getDeepObject(obj, key, nextKey, forceObject, forceArray) {
if (isPrototypeKey(key))
return obj;
let currObj = obj[key];
return typeof currObj == "object" && currObj !== null ? currObj : !forceObject && (forceArray || typeof nextKey == "number" || typeof nextKey == "string" && nextKey * 0 === 0 && nextKey.indexOf(".") === -1) ? obj[key] = [] : obj[key] = {};
}
var MAX_DEPTH = 20, strBracketPair = "[]", strBracketLeft = "[", strBracketRight = "]", strDot = ".";
function stringifyObject(obj, options, depth = 0, parentKey, isProbableArray) {
let { nestingSyntax = defaultOptions.nestingSyntax, arrayRepeat = defaultOptions.arrayRepeat, arrayRepeatSyntax = defaultOptions.arrayRepeatSyntax, nesting = defaultOptions.nesting, delimiter = defaultOptions.delimiter, valueSerializer = defaultOptions.valueSerializer, shouldSerializeObject = defaultOptions.shouldSerializeObject } = options, strDelimiter = typeof delimiter == "number" ? String.fromCharCode(delimiter) : delimiter, useArrayRepeatKey = isProbableArray === !0 && arrayRepeat, shouldUseDot = nestingSyntax === "dot" || nestingSyntax === "js" && !isProbableArray;
if (depth > MAX_DEPTH)
return "";
let result = "", firstKey = !0, valueIsProbableArray = !1;
for (let key in obj) {
let value = obj[key];
if (value === void 0)
continue;
let path3;
parentKey ? (path3 = parentKey, useArrayRepeatKey ? arrayRepeatSyntax === "bracket" && (path3 += strBracketPair) : shouldUseDot ? (path3 += strDot, path3 += key) : (path3 += strBracketLeft, path3 += key, path3 += strBracketRight)) : path3 = key, firstKey || (result += strDelimiter), typeof value == "object" && value !== null && !shouldSerializeObject(value) ? (valueIsProbableArray = value.pop !== void 0, (nesting || arrayRepeat && valueIsProbableArray) && (result += stringifyObject(value, options, depth + 1, path3, valueIsProbableArray))) : (result += encodeString(path3), result += "=", result += valueSerializer(value, key)), firstKey && (firstKey = !1);
}
return result;
}
// ../../node_modules/picoquery/lib/decode-uri-component.js
var UTF8_DATA = [
// The first part of the table maps bytes to character to a transition.
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
3,
3,
3,
3,
3,
3,
3,
3,
3,
3,
3,
3,
3,
3,
3,
3,
3,
3,
3,
3,
3,
3,
3,
3,
3,
3,
3,
3,
3,
3,
3,
3,
4,
4,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
6,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
8,
7,
7,
10,
9,
9,
9,
11,
4,
4,
4,
4,
4,
4,
4,
4,
4,
4,
4,
// The second part of the table maps a state to a new state when adding a
// transition.
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
12,
0,
0,
0,
0,
24,
36,
48,
60,
72,
84,
96,
0,
12,
12,
12,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
24,
0,
0,
0,
0,
0,
0,
0,
0,
0,
24,
24,
24,
0,
0,
0,
0,
0,
0,
0,
0,
0,
24,
24,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
48,
48,
48,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
48,
48,
0,
0,
0,
0,
0,
0,
0,
0,
0,
48,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
// The third part maps the current transition to a mask that needs to apply
// to the byte.
127,
63,
63,
63,
0,
31,
15,
15,
15,
7,
7,
7
];
function decodeURIComponent(uri) {
let percentPosition = uri.indexOf("%");
if (percentPosition === -1)
return uri;
let length = uri.length, decoded = "", last = 0, codepoint = 0, startOfOctets = percentPosition, state = 12;
for (; percentPosition > -1 && percentPosition < length; ) {
let high = hexCodeToInt(uri[percentPosition + 1], 4), low = hexCodeToInt(uri[percentPosition + 2], 0), byte = high | low, type = UTF8_DATA[byte];
if (state = UTF8_DATA[256 + state + type], codepoint = codepoint << 6 | byte & UTF8_DATA[364 + type], state === 12)
decoded += uri.slice(last, startOfOctets), decoded += codepoint <= 65535 ? String.fromCharCode(codepoint) : String.fromCharCode(55232 + (codepoint >> 10), 56320 + (codepoint & 1023)), codepoint = 0, last = percentPosition + 3, percentPosition = startOfOctets = uri.indexOf("%", last);
else {
if (state === 0)
return null;
if (percentPosition += 3, percentPosition < length && uri.charCodeAt(percentPosition) === 37)
continue;
return null;
}
}
return decoded + uri.slice(last);
}
var HEX = {
0: 0,
1: 1,
2: 2,
3: 3,
4: 4,
5: 5,
6: 6,
7: 7,
8: 8,
9: 9,
a: 10,
A: 10,
b: 11,
B: 11,
c: 12,
C: 12,
d: 13,
D: 13,
e: 14,
E: 14,
f: 15,
F: 15
};
function hexCodeToInt(c, shift) {
let i = HEX[c];
return i === void 0 ? 255 : i << shift;
}
// ../../node_modules/picoquery/lib/parse.js
var regexPlus = /\+/g, Empty = function() {
};
Empty.prototype = /* @__PURE__ */ Object.create(null);
function computeKeySlice(input, startIndex, endIndex, keyHasPlus, shouldDecodeKey) {
let chunk = input.substring(startIndex, endIndex);
return keyHasPlus && (chunk = chunk.replace(regexPlus, " ")), shouldDecodeKey && (chunk = decodeURIComponent(chunk) || chunk), chunk;
}
function parse2(input, options) {
let { valueDeserializer = defaultOptions.valueDeserializer, keyDeserializer = defaultOptions.keyDeserializer, arrayRepeatSyntax = defaultOptions.arrayRepeatSyntax, nesting = defaultOptions.nesting, arrayRepeat = defaultOptions.arrayRepeat, nestingSyntax = defaultOptions.nestingSyntax, delimiter = defaultOptions.delimiter } = options ?? {}, charDelimiter = typeof delimiter == "string" ? delimiter.charCodeAt(0) : delimiter, isJsNestingSyntax = nestingSyntax === "js", result = new Empty();
if (typeof input != "string")
return result;
let inputLength = input.length, value = "", startingIndex = -1, equalityIndex = -1, keySeparatorIndex = -1, currentObj = result, lastKey, currentKey = "", keyChunk = "", shouldDecodeKey = !1, shouldDecodeValue = !1, keyHasPlus = !1, valueHasPlus = !1, keyIsDot = !1, hasBothKeyValuePair = !1, c = 0, arrayRepeatBracketIndex = -1, prevIndex = -1, prevChar = -1;
for (let i = 0; i < inputLength + 1; i++) {
if (c = i !== inputLength ? input.charCodeAt(i) : charDelimiter, c === charDelimiter) {
if (hasBothKeyValuePair = equalityIndex > startingIndex, hasBothKeyValuePair || (equalityIndex = i), keySeparatorIndex !== equalityIndex - 1 && (keyChunk = computeKeySlice(input, keySeparatorIndex + 1, arrayRepeatBracketIndex > -1 ? arrayRepeatBracketIndex : equalityIndex, keyHasPlus, shouldDecodeKey), currentKey = keyDeserializer(keyChunk), lastKey !== void 0 && (currentObj = getDeepObject(currentObj, lastKey, currentKey, isJsNestingSyntax && keyIsDot, void 0))), hasBothKeyValuePair || currentKey !== "") {
hasBothKeyValuePair && (value = input.slice(equalityIndex + 1, i), valueHasPlus && (value = value.replace(regexPlus, " ")), shouldDecodeValue && (value = decodeURIComponent(value) || value));
let newValue = valueDeserializer(value, currentKey);
if (arrayRepeat) {
let currentValue = currentObj[currentKey];
currentValue === void 0 ? arrayRepeatBracketIndex > -1 ? currentObj[currentKey] = [newValue] : currentObj[currentKey] = newValue : currentValue.pop ? currentValue.push(newValue) : currentObj[currentKey] = [currentValue, newValue];
} else
currentObj[currentKey] = newValue;
}
value = "", startingIndex = i, equalityIndex = i, shouldDecodeKey = !1, shouldDecodeValue = !1, keyHasPlus = !1, valueHasPlus = !1, keyIsDot = !1, arrayRepeatBracketIndex = -1, keySeparatorIndex = i, currentObj = result, lastKey = void 0, currentKey = "";
} else c === 93 ? (arrayRepeat && arrayRepeatSyntax === "bracket" && prevChar === 91 && (arrayRepeatBracketIndex = prevIndex), nesting && (nestingSyntax === "index" || isJsNestingSyntax) && equalityIndex <= startingIndex && (keySeparatorIndex !== prevIndex && (keyChunk = computeKeySlice(input, keySeparatorIndex + 1, i, keyHasPlus, shouldDecodeKey), currentKey = keyDeserializer(keyChunk), lastKey !== void 0 && (currentObj = getDeepObject(currentObj, lastKey, currentKey, void 0, void 0)), lastKey = currentKey, keyHasPlus = !1, shouldDecodeKey = !1), keySeparatorIndex = i, keyIsDot = !1)) : c === 46 ? nesting && (nestingSyntax === "dot" || isJsNestingSyntax) && equalityIndex <= startingIndex && (keySeparatorIndex !== prevIndex && (keyChunk = computeKeySlice(input, keySeparatorIndex + 1, i, keyHasPlus, shouldDecodeKey), currentKey = keyDeserializer(keyChunk), lastKey !== void 0 && (currentObj = getDeepObject(currentObj, lastKey, currentKey, isJsNestingSyntax)), lastKey = currentKey, keyHasPlus = !1, shouldDecodeKey = !1), keyIsDot = !0, keySeparatorIndex = i) : c === 91 ? nesting && (nestingSyntax === "index" || isJsNestingSyntax) && equalityIndex <= startingIndex && (keySeparatorIndex !== prevIndex && (keyChunk = computeKeySlice(input, keySeparatorIndex + 1, i, keyHasPlus, shouldDecodeKey), currentKey = keyDeserializer(keyChunk), isJsNestingSyntax && lastKey !== void 0 && (currentObj = getDeepObject(currentObj, lastKey, currentKey, isJsNestingSyntax)), lastKey = currentKey, keyHasPlus = !1, shouldDecodeKey = !1, keyIsDot = !1), keySeparatorIndex = i) : c === 61 ? equalityIndex <= startingIndex ? equalityIndex = i : shouldDecodeValue = !0 : c === 43 ? equalityIndex > startingIndex ? valueHasPlus = !0 : keyHasPlus = !0 : c === 37 && (equalityIndex > startingIndex ? shouldDecodeValue = !0 : shouldDecodeKey = !0);
prevIndex = i, prevChar = c;
}
return result;
}
// ../../node_modules/picoquery/lib/stringify.js
function stringify(input, options) {
return input === null || typeof input != "object" ? "" : stringifyObject(input, options ?? {});
}
// src/router/utils.ts
var import_ts_dedent6 = __toESM(require_dist(), 1);
// src/shared/review/routes.ts
var isReviewSummaryPath = (path3) => path3 === "/review/" || path3 === "/review";
// src/router/utils.ts
var splitPathRegex = /\/([^/]+)\/(?:(.*)_)?([^/]+)?/, parsePath = (0, import_memoizerific.default)(1e3)((path3) => {
let result = {
viewMode: void 0,
storyId: void 0,
refId: void 0
};
if (path3) {
let [, viewMode, refId, storyId] = path3.toLowerCase().match(splitPathRegex) || [];
viewMode ? Object.assign(result, {
viewMode,
storyId,
refId
}) : isReviewSummaryPath(path3) && (result.viewMode = "review");
}
return result;
}), DEEPLY_EQUAL = /* @__PURE__ */ Symbol("Deeply equal"), deepDiff = (value, update) => {
if (typeof value != typeof update)
return update;
if (isEqual(value, update))
return DEEPLY_EQUAL;
if (Array.isArray(value) && Array.isArray(update)) {
let res = update.reduce((acc, upd, index) => {
let diff = deepDiff(value[index], upd);
return diff !== DEEPLY_EQUAL && (acc[index] = diff), acc;
}, new Array(update.length));
return update.length >= value.length ? res : res.concat(new Array(value.length - update.length).fill(void 0));
}
return isPlainObject(value) && isPlainObject(update) ? Object.keys({ ...value, ...update }).reduce((acc, key) => {
let diff = deepDiff(value?.[key], update?.[key]);
return diff === DEEPLY_EQUAL ? acc : Object.assign(acc, { [key]: diff });
}, {}) : update;
}, VALIDATION_REGEXP = /^[a-zA-Z0-9 _-]*$/, NUMBER_REGEXP = /^-?[0-9]+(\.[0-9]+)?$/, HEX_REGEXP = /^#([a-f0-9]{3,4}|[a-f0-9]{6}|[a-f0-9]{8})$/i, COLOR_REGEXP = /^(rgba?|hsla?)\(([0-9]{1,3}),\s?([0-9]{1,3})%?,\s?([0-9]{1,3})%?,?\s?([0-9](\.[0-9]{1,2})?)?\)$/i, validateArgs = (key = "", value) => key === null || key === "" || !VALIDATION_REGEXP.test(key) ? !1 : value == null || value instanceof Date || typeof value == "number" || typeof value == "boolean" ? !0 : typeof value == "string" ? VALIDATION_REGEXP.test(value) || NUMBER_REGEXP.test(value) || HEX_REGEXP.test(value) || COLOR_REGEXP.test(value) : Array.isArray(value) ? value.every((v) => validateArgs(key, v)) : isPlainObject(value) ? Object.entries(value).every(([k, v]) => validateArgs(k, v)) : !1, encodeSpecialValues = (value) => value === void 0 ? "!undefined" : value === null ? "!null" : typeof value == "string" ? HEX_REGEXP.test(value) ? `!hex(${value.slice(1)})` : COLOR_REGEXP.test(value) ? `!${value.replace(/[\s%]/g, "")}` : value : typeof value == "boolean" ? `!${value}` : value instanceof Date ? `!date(${value.toISOString()})` : Array.isArray(value) ? value.map(encodeSpecialValues) : isPlainObject(value) ? Object.entries(value).reduce(
(acc, [key, val]) => Object.assign(acc, { [key]: encodeSpecialValues(val) }),
{}
) : value, decodeKnownQueryChar = (chr) => {
switch (chr) {
case "%20":
return "+";
case "%5B":
return "[";
case "%5D":
return "]";
case "%2C":
return ",";
case "%3A":
return ":";
}
return chr;
}, knownQueryChar = /%[0-9A-F]{2}/g, buildArgsParam = (initialArgs, args) => {
let update = deepDiff(initialArgs, args);
if (!update || update === DEEPLY_EQUAL)
return "";
let object2 = Object.entries(update).reduce((acc, [key, value]) => validateArgs(key, value) ? Object.assign(acc, { [key]: value }) : (once2.warn(import_ts_dedent6.dedent`
Omitted potentially unsafe URL args.
More info: https://storybook.js.org/docs/writing-stories/args?ref=error#setting-args-through-the-url
`), acc), {});
return stringify(encodeSpecialValues(object2), {
delimiter: ";",
// we don't actually create multiple query params
nesting: !0,
nestingSyntax: "js"
// encode objects using dot notation: obj.key=val
}).replace(knownQueryChar, decodeKnownQueryChar).split(";").map((part) => part.replace("=", ":")).join(";");
}, queryFromString = (0, import_memoizerific.default)(1e3)((s) => s !== void 0 ? parse2(s) : {});
var getMatch = (0, import_memoizerific.default)(1e3)((current, target, startsWith = !0) => {
if (startsWith) {
if (typeof target != "string")
throw new Error("startsWith only works with string targets");
return current && current.startsWith(target) ? { path: current } : null;
}
let currentIsTarget = typeof target == "string" && current === target, matchTarget = current && target && current.match(target);
return currentIsTarget || matchTarget ? { path: current } : null;
});
// src/shared/open-service/toolsets/stories/preview-stories.ts
function previewStories({
origin,
index,
stories
}) {
let resolvedStories = findStoryIds(index, stories), result = [];
for (let story of resolvedStories) {
if ("errorMessage" in story) {
result.push({
input: story.input,
error: story.errorMessage
});
continue;
}
let indexEntry = story.entry, previewUrl = `${origin}/?path=/story/${indexEntry.id}`, argsParam = buildArgsParam({}, story.input.props ?? {});
argsParam && (previewUrl += `&args=${argsParam}`);
let globalsParam = buildArgsParam({}, story.input.globals ?? {});
globalsParam && (previewUrl += `&globals=${globalsParam}`), result.push({
title: indexEntry.title,
name: indexEntry.name,
previewUrl
});
}
return { stories: result };
}
// src/shared/open-service/toolsets/stories/unreachable-files.ts
import { logger as logger8 } from "storybook/internal/node-logger";
var SOURCE_EXT_RE = /\.(?:tsx?|jsx?|mjs|cjs)$/i, CHUNK_SIZE = 50, DEFAULT_MAX_FILES = 10;
async function detectUnreachableFiles({
git,
moduleGraph,
maxFiles = DEFAULT_MAX_FILES
}) {
if ((await moduleGraph.queries.status.loaded(void 0)).value !== "ready")
return [];
let changedFiles, repoRoot;
try {
[changedFiles, repoRoot] = await Promise.all([git.getChangedFiles(), git.getRepoRoot()]);
} catch (error) {
return logger8.debug(`Unreachable-file detection skipped, git is unavailable: ${error}`), [];
}
let relativeFiles = [.../* @__PURE__ */ new Set([...changedFiles.changed, ...changedFiles.new])].filter(
(file) => SOURCE_EXT_RE.test(file)
), unreachable = [];
for (let start = 0; start < relativeFiles.length && unreachable.length < maxFiles; start += CHUNK_SIZE) {
let chunk = relativeFiles.slice(start, start + CHUNK_SIZE), hits = await moduleGraph.queries.storiesForFiles.loaded({
files: chunk.map((file) => resolve(repoRoot, file))
});
for (let [position, file] of chunk.entries()) {
if (unreachable.length >= maxFiles)
break;
(hits[position]?.length ?? 0) === 0 && unreachable.push(file);
}
}
return unreachable;
}
// src/shared/open-service/toolsets/stories/definition.ts
var previewSuccessSchema = object({
title: string(),
name: string(),
previewUrl: pipe(
string(),
description(
"Direct URL to open the story preview. Include this URL in the final user-facing response so users can open it directly."
)
)
}), previewFailureSchema = object({
input: storyInputSchema,
error: string()
}), previewOutputSchema = object({
stories: array(union([previewSuccessSchema, previewFailureSchema]))
}), changeStatusSchema = union([
literal("status-value:new"),
literal("status-value:modified"),
literal("status-value:affected")
]), changedStorySchema = object({
storyId: string(),
statusValue: changeStatusSchema,
title: string(),
name: string(),
importPath: string()
}), changedOutputSchema = object({
stories: array(changedStorySchema),
counts: object({
new: number(),
modified: number(),
affected: number()
}),
unreachableFiles: array(string())
}), storyMatchSchema = object({
storyId: string(),
title: string(),
name: string(),
importPath: string(),
distance: pipe(
number(),
description(
"Import-graph depth from the story file to the component (lower = stronger). 0: the path you passed is itself a story file (self-match). 1: story file directly imports the component. 2+: reached through N hops."
)
)
}), clippedByMaxDistanceSchema = pipe(
object({
count: number(),
distances: array(number())
}),
description(
"Present only when `maxDistance` filtered out one or more matches. `count` is how many were dropped; `distances` lists the (sorted, distinct) distances those dropped matches sat at \u2014 widen `maxDistance` to include them."
)
), findByComponentOutputSchema = object({
results: array(
object({
componentPath: string(),
matches: array(storyMatchSchema),
clipped: optional(clippedByMaxDistanceSchema),
pathNotFound: pipe(
optional(boolean()),
description(
'`true` when no file exists at the resolved absolute path. Distinguishes a typo from "this component has no stories yet". The agent should re-check the path it sent.'
)
)
})
)
}), GIT_UNUSABLE_REASONS = /* @__PURE__ */ new Set(["not a git repository", "git is not available"]);
function emptyChangedStories() {
return {
stories: [],
counts: { new: 0, modified: 0, affected: 0 },
unreachableFiles: []
};
}
function reasonForChangeDetectionReadiness(readiness) {
switch (readiness.status) {
case "unavailable":
return readiness.reason === "disabled" ? "Storybook change detection is disabled, so changed-story statuses are unavailable. Enable the changeDetection feature and retry." : `Storybook change detection is unavailable: ${readiness.reason}.`;
case "error":
return `Storybook change detection failed: ${readiness.error.message}`;
case "pending":
return "Storybook change detection has not finished its initial scan.";
default:
throw readiness;
}
}
function isGitUnusableReadiness(readiness) {
return readiness.status === "unavailable" && GIT_UNUSABLE_REASONS.has(readiness.reason);
}
function describePreview(ctx, reviewEnabled) {
return reviewEnabled ? `Use this tool to get Storybook preview URLs while iterating on a specific story, or when the user asks for a direct link to one.
Do not end visual work or browse requests with these links \u2014 publish a curated review with ${getToolName(ctx)("review.create")} instead (passing changedFiles: [] when no code changed) and link that.` : `Use this tool to get one or more Storybook preview URLs.
Call it after editing anything that changes how the UI looks \u2014 components, stories, styles, CSS, themes, colors, or design tokens \u2014 no exceptions. A shared file has no stories of its own: preview the stories of the components that consume it.
Include each returned preview URL in your final user-facing response so users can open them directly.`;
}
function describeChanged(ctx) {
return `Get Storybook stories marked as new, modified, or related. Returns story metadata only (no URLs).
The result reflects the cumulative working-tree diff, not just your latest edit \u2014 after multiple edits in one session, a non-empty result may cover an earlier sub-change and miss your most recent one. Check that every file you touched is represented; for any that isn't, find its consumer components and pass their paths to ${getToolName(ctx)("stories.findByComponent")} instead. The response surfaces this gap with a "coverage sanity check" hint when it detects unreachable working-tree files.`;
}
function describeFindByComponent(ctx, reviewEnabled) {
let ref = getToolName(ctx), handOffTargets = reviewEnabled ? `${ref("stories.preview")} or ${ref("review.create")}` : ref("stories.preview"), inputShapes = reviewEnabled ? 'files you just edited, a feature/domain/topic the user named, a query like "all consumers of X", or an autonomous review after a UI change' : 'files you just edited, a feature/domain/topic the user named, or a query like "all consumers of X"', cascadeGuidance = reviewEnabled ? `For ${ref("review.create")}, the distance buckets map onto the visual cascade (the component itself \u2192 direct importers \u2192 page-level context) \u2014 one collection per layer; when several stories of a component share a distance, prefer the variant whose name signals it renders the changed surface.` : "The distance buckets map onto the visual cascade (the component itself \u2192 direct importers \u2192 page-level context) \u2014 use them to decide which stories to preview; when several stories of a component share a distance, prefer the variant whose name signals it renders the changed surface.";
return `Map component source files to the stories that render them, returning grounded \`storyId\` values from the live Storybook index \u2014 hand these to ${handOffTargets} instead of guessing.
Reach for this whenever you need story IDs, whatever shape the input has: ${inputShapes}. First resolve the input to a list of absolute component file paths using filesystem search (grep / Glob / find) and code reading \u2014 that bridge is yours to build; this tool starts where it ends. One common trap: when the changed file is _shared_ infrastructure (theme token, design token, util, hook, CSS module) it isn't itself a component \u2014 grep for its consumers and pass _their_ paths, not the shared file's. If the symbol you grepped looks like one member of a related group (sibling tokens, neighboring exports), widen to the rest of the group too \u2014 a too-narrow grep silently drops stories. Try \`${ref("stories.changed")}\` first for "I just edited X" when it's available; if a file you touched is missing from its response, treat that file as the shared-infrastructure case and route its consumers through this tool.
Results are sorted by \`distance\` (0 = the path you passed is itself a story file, 1 = direct importer, 2+ = transitive; lower = stronger). Shared primitives are usually consumed through wrapper components, so the distance-1 bucket is often empty \u2014 the default \`maxDistance: ${DEFAULT_MAX_DISTANCE}\` keeps that cascade visible while capping noise from wide decorators; raise it to widen recall, lower it to tighten precision. ${cascadeGuidance}
Never invent IDs from file names, feature names, or memory; title strings can be overridden by story authors, so only IDs returned by discovery tools resolve. If a component has no matches here, it has no stories yet (say so, don't fabricate).
Backed by Storybook's live reverse dependency graph, available only when the dev server runs a builder that supports change detection (e.g. Vite) \u2014 otherwise returns a typed error.`;
}
function moduleGraphAccessFromCtx(ctx, moduleGraph = ctx.getService("core/module-graph", {
internal: !0
})) {
let moduleGraphIndex = ctx.getService("core/module-graph-index", {
internal: !0
});
return {
queries: {
status: {
loaded: () => moduleGraph.queries.status.loaded(void 0)
},
storiesForFiles: {
loaded: (files2) => moduleGraphIndex.queries.storiesForFiles.loaded(files2)
}
}
};
}
function createStoriesToolset({
storyIndex,
git,
changeStatuses,
reviewEnabled = !1
}) {
return {
id: "stories",
description: "Story discovery, change detection, and preview URL generation.",
methods: {
preview: {
input: object({
stories: pipe(
storyInputArraySchema,
description(
`Stories to preview.
Prefer { storyId } when you don't already have story file context, since this avoids filesystem discovery.
Use { storyId } when IDs were discovered from documentation tools.
Use { absoluteStoryPath + exportName } only when you're already working in a specific .stories.* file and already have that context.`
)
)
}),
output: previewOutputSchema,
title: "Get story preview URLs",
// Preview URLs only work when they point at a live origin.
requiresDevServer: !0,
description: (ctx) => describePreview(ctx, reviewEnabled),
handler: async (input, ctx) => {
if (!ctx.origin)
throw new OpenServiceMissingOriginError({
toolsetId: "stories",
methodName: "preview"
});
let data = previewStories({
origin: ctx.origin,
index: await storyIndex.getIndex(),
stories: input.stories
});
return await reportToolsetTelemetry(ctx, "tool:previewStories", {
toolset: "dev",
inputStoryCount: input.stories.length,
outputStoryCount: data.stories.length
}), { ok: !0, data, markdown: formatPreviewStories(data, ctx, { reviewEnabled }) };
}
},
changed: {
input: object({}),
title: "Get changed stories metadata",
description: describeChanged,
handler: async (_input, ctx) => {
let graphService = ctx.getService("core/module-graph", {
internal: !0
}), moduleGraph = moduleGraphAccessFromCtx(ctx, graphService), graphStatus = await moduleGraph.queries.status.loaded(void 0);
if (graphStatus.value !== "ready")
throw new OpenServiceModuleGraphUnavailableError({
reason: reasonForStatus(graphStatus)
});
let changeDetection = await graphService.queries.changeDetectionReadiness.loaded(void 0);
if (changeDetection.status !== "ready") {
if (isGitUnusableReadiness(changeDetection)) {
let data2 = emptyChangedStories();
return await reportToolsetTelemetry(ctx, "tool:getChangedStories", {
toolset: "dev",
storyCount: 0,
newStoryCount: 0,
modifiedStoryCount: 0,
affectedStoryCount: 0
}), {
ok: !0,
data: data2,
markdown: formatChangedStories(data2, ctx, { reviewEnabled })
};
}
throw new OpenServiceModuleGraphUnavailableError({
reason: reasonForChangeDetectionReadiness(changeDetection)
});
}
let [statuses, index] = await Promise.all([
Promise.resolve(changeStatuses.getAll()),
storyIndex.getIndex()
]), data = {
...getChangedStories({ statuses, index }),
// Files outside the story graph are why an empty or partial result can still be wrong,
// so they are part of the answer rather than a separate lookup.
unreachableFiles: await detectUnreachableFiles({ git, moduleGraph })
};
return await reportToolsetTelemetry(ctx, "tool:getChangedStories", {
toolset: "dev",
storyCount: data.stories.length,
newStoryCount: data.counts.new,
modifiedStoryCount: data.counts.modified,
affectedStoryCount: data.counts.affected
}), { ok: !0, data, markdown: formatChangedStories(data, ctx, { reviewEnabled }) };
}
},
findByComponent: {
input: object({
componentPaths: pipe(
array(string()),
minLength(1),
description(
`Absolute paths to component source files (e.g. "/repo/src/Button.tsx").
Pass the components you actually want stories for \u2014 typically files you just read, edited, or that the user mentioned.
Relative paths are also accepted and resolved against the Storybook working directory, but absolute paths are preferred for unambiguous results.
Story files (\`*.stories.*\`) are accepted too: they appear at distance 0 as self-matches, plus any reverse-graph hits (other stories that import them).`
)
),
maxDistance: pipe(
optional(pipe(number(), minValue(1), integer())),
description(
`Ceiling on the import depth to include in results. Must be a positive integer.
- 1: only stories that directly import the component.
- 2+: also include stories that reach the component through N hops.
Defaults to ${DEFAULT_MAX_DISTANCE}; raise it to widen recall, lower it to tighten precision. Shared components (Button, Icon, \u2026) accumulate noisy indirect matches at distance \u2265 3, so the default cap protects against runaway results.`
)
)
}),
output: findByComponentOutputSchema,
title: "Get stories for component files",
description: (ctx) => describeFindByComponent(ctx, reviewEnabled),
handler: async (input, ctx) => {
let maxDistance = input.maxDistance ?? DEFAULT_MAX_DISTANCE, lookup = await findStoriesByComponent({
componentPaths: input.componentPaths,
maxDistance,
index: await storyIndex.getIndex(),
moduleGraph: moduleGraphAccessFromCtx(ctx)
});
if (!lookup.available)
throw new OpenServiceModuleGraphUnavailableError({ reason: lookup.reason });
let unmatchedCount = lookup.results.filter(
(result) => !result.pathNotFound && result.matches.length === 0
).length;
await reportToolsetTelemetry(ctx, "tool:getStoriesByComponent", {
toolset: "dev",
componentCount: input.componentPaths.length,
matchedComponentCount: input.componentPaths.length - unmatchedCount,
totalMatchCount: lookup.results.reduce(
(total, result) => total + result.matches.length,
0
),
maxDistance
});
let data = { results: lookup.results, maxDistance };
return { ok: !0, data, markdown: formatFindByComponent(data) };
}
}
}
};
}
// src/shared/open-service/toolsets/review/definition.ts
var REVIEW_PAGE_PATH = "/review/", reviewCollectionSchema = object({
title: pipe(
string(),
description(
"Title describing **what** this collection consists of, phrased the way a person would say it. Avoid typographic marks and CamelCase. Plain text, no markdown."
)
),
rationale: pipe(
string(),
description(
"Rationale explaining **why** this collection is relevant to the user. Shown alongside the title. One or two sentences. Plain text, no markdown."
)
),
storyIds: pipe(
array(string()),
description(
'Story IDs that represent this collection (e.g. "button--primary"). The page renders exactly these.'
)
)
}), reviewCreateInputSchema = object({
title: pipe(
string(),
description(
"Terse, human-readable title for the overall review. What is this review about? Avoid typographic marks and CamelCase. Plain text, no markdown."
)
),
description: pipe(
string(),
description(
"Description of the review scope, including what's there, why it's relevant, and what to look for. Preferably one or two sentences. At most 2 paragraphs for reviews spanning multiple topics. Markdown formatting restricted to **bold**, _italic_, and `code` (backticks). Use emphasis for the key **what** and _why_, and backticks for literal source code references like component or token names."
)
),
collections: pipe(
array(reviewCollectionSchema),
description(
"Groups of stories to show in the review, most relevant first. Prefer 2-5 groups."
)
),
changedFiles: pipe(
array(string()),
description(
"Paths of the files you changed, most central first. Pass an empty array `[]` only when no code changed (browse requests, Trigger 2)."
)
)
}), reviewCreateOutputSchema = object({
reviewUrl: pipe(
string(),
description(
"URL of the Storybook review page. Always include this URL in your final user-facing response so the user can open it directly."
)
)
});
function describeCreate(ctx) {
let ref = getToolName(ctx);
return `Publish a curated review to Storybook's review page for spot-checking **visual impact**. Each call replaces the single active review \u2014 call it again whenever the user iterates on the changes.
## When to call
- **Trigger 1 \u2014 visual change** (components, stories, CSS, themes, colors, design tokens, i18n \u2014 anything that changes how the UI looks): when the user should spot-check rendering. A shared file (token, style, util) has no stories of its own \u2014 review its consumers' stories. Skip non-visual refactors unless side-effects are plausible. Start from \`${ref("stories.changed")}\`; fall back to \`${ref("stories.findByComponent")}\` if change detection is unavailable. Include \`changedFiles\`.
- **Trigger 2 \u2014 browse request** ("show me the Badge component"): resolve via \`${ref("stories.findByComponent")}\` / \`${ref("docs.list")}\`; you may consult other sources to interpret the ask, but IDs must still come from those tools. Pass \`changedFiles: []\` \u2014 no code changed.
## Hard rules
1. Every \`storyId\` MUST come from those tools. Reject IDs derived from file paths, story names, or memory. Unknown IDs cause a runtime error; obtain real IDs via \`${ref("stories.findByComponent")}\` or \`${ref("docs.list")}\`, then retry.
2. Every story you CREATED in this change MUST appear in the review \u2014 including interaction/play-function stories. Showing the stories you modified is encouraged too. Curate by grouping, never by omission.
3. Prefer 2-5 collections; avoid one-story collections unless truly isolated.
4. Follow-up reviews: stabilize collection/story order to avoid disorientation from reshuffling.
5. Apply the field formatting rules from each schema property. Do not use em-dashes in review payload field values (title, rationale, description, etc.).
6. Do not instruct or tell the user what to do unless they explicitly ask for guidance.
7. "Collection" and "trigger" are internal terms for this tool's mechanics and mean nothing to users. Never use them in user-facing text unless the user used them first; say "group of stories" or just describe the contents in plain language.
## Curating (Trigger 1)
Trace the **visual cascade** up the **import graph** to **page-level UI surfaces** \u2014 one collection per layer (\`distance 0\` \u2192 direct importers \u2192 page context). Include **control stories** where the change is **not supposed to be visible**. **Theme tokens**, **shared styles**, and **layout primitives** need page-level coverage even from a single-file edit. **Localized changes:** affected component \u2192 **usage locations** \u2192 outer surfaces. **Larger features:** central page/module \u2192 lower-level pieces \u2192 outer **usage locations**.
## Curating (Trigger 2)
Exactly what the user asked for \u2014 **no more, no less**. Group logically or follow **story index hierarchy**.`;
}
function formatUnknownStoryIdsError(unknownIds, ctx) {
let ref = getToolName(ctx), list = unknownIds.map((id) => `- \`${id}\``).join(`
`);
return `${describeUnknownStoryIds(unknownIds)}
${list}
This usually means the IDs were inferred from file paths or naming conventions rather than returned by a tool. Resolve real IDs by calling \`${ref("stories.findByComponent")}\` (for components you've edited or want covered) or \`${ref("docs.list")}\` (to browse the index), then retry \`${ref("review.create")}\` with the verified IDs. Do not invent IDs to satisfy this check.`;
}
function formatReviewApplied({ reviewUrl, collectionCount, storyCount }, ctx) {
let storyNoun = storyCount === 1 ? "y" : "ies";
return `${`Review applied: ${collectionCount} collection${collectionCount === 1 ? "" : "s"}, ${storyCount} stor${storyNoun}.`} Storybook is already running at ${ctx.origin} \u2014 reuse it. Do NOT start another Storybook or change its port to view this review; the running instance already serves it.
Two things you must do now, both of them:
1. **Open ${reviewUrl} yourself in your preview browser.** If you have any browser-preview or navigate tool in this session (e.g. preview_eval or an equivalent), call it on this URL so the review opens in your preview window immediately. Don't merely print the link and stop \u2014 actually open it.
2. **Show the link to the user too.** End your final response with a dedicated review section as the very last thing: its own heading on a line by itself (e.g. \`## \u{1F440} Review your changes\`), then a one-line explanation of what the review is, then on the next line the review page as a markdown link prefixed with a \u{1F449} so it's easy to spot: \`\u{1F449} [Open the Storybook review page](${reviewUrl})\`. For the explanation, use something like: "The review shows the ${storyCount} stor${storyNoun} most relevant for you to review right now. Because this is AI-curated, results may be inaccurate or incomplete." Put nothing after the link \u2014 not a trailing sentence the user has to hunt for. The user needs to see this link even after you've opened it yourself.`;
}
var reviewToolset = {
id: "review",
description: "Create a curated Storybook review.",
methods: {
create: {
input: reviewCreateInputSchema,
output: reviewCreateOutputSchema,
title: "Create Storybook review",
// Reviews publish into the running Storybook's review service and link into its UI.
requiresDevServer: !0,
description: describeCreate,
handler: async (review, ctx) => {
if (!ctx.origin)
throw new OpenServiceMissingOriginError({
toolsetId: "review",
methodName: "create"
});
try {
await ctx.getService("core/review", { internal: !0 }).commands.setReview(review);
} catch (error) {
throw error instanceof OpenServiceUnknownStoryIdsError ? new Error(formatUnknownStoryIdsError(error.data.unknownIds, ctx)) : error;
}
let collectionCount = review.collections.length, storyCount = review.collections.reduce(
(total, collection) => total + collection.storyIds.length,
0
);
await reportToolsetTelemetry(ctx, "tool:displayReview", {
toolset: "dev",
collectionCount,
storyCount,
changedFileCount: review.changedFiles.length
});
let data = {
reviewUrl: `${ctx.origin.replace(/\/$/, "")}/?path=${REVIEW_PAGE_PATH}`,
collectionCount,
storyCount
};
return { ok: !0, data, markdown: formatReviewApplied(data, ctx) };
}
}
}
};
// 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
function createServerTestProviderStore(leader) {
return createTestProviderStore({
universalTestProviderStore: UniversalStore.create({
...UNIVERSAL_TEST_PROVIDER_STORE_OPTIONS,
leader
})
});
}
var cache3 = {};
function getTestProviderStoreBundle() {
return getOrRecreateStore(
UNIVERSAL_TEST_PROVIDER_STORE_OPTIONS.id,
cache3,
createServerTestProviderStore
);
}
var getTestProviderStoreById = (id) => getTestProviderStoreBundle().getTestProviderStoreById(id), fullTestProviderStore = new Proxy(
{},
{
get(_target, prop) {
let store = getTestProviderStoreBundle().fullTestProviderStore, value = Reflect.get(store, prop, store);
return typeof value == "function" ? value.bind(store) : value;
}
}
), universalTestProviderStore = new Proxy({}, {
get(_target, prop) {
let store = getTestProviderStoreBundle().universalTestProviderStore, value = Reflect.get(store, prop, store);
return typeof value == "function" ? value.bind(store) : value;
}
});
// src/core-server/utils/ghost-stories/get-candidates.ts
import { readFile as readFile7 } 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(path3) {
hasJSX = !0, hasExport && path3.stop();
},
JSXFragment(path3) {
hasJSX = !0, hasExport && path3.stop();
},
ExportNamedDeclaration(path3) {
hasExport = !0, hasJSX && path3.stop();
},
ExportDefaultDeclaration(path3) {
hasExport = !0, hasJSX && path3.stop();
},
ExportAllDeclaration(path3) {
hasExport = !0, hasJSX && path3.stop();
}
}), hasJSX && hasExport;
}
async function getCandidatesForStorybook(files2, sampleCount) {
let simpleCandidates = [], analyzedCandidates = [];
for (let file of files2) {
let source;
try {
if (source = await readFile7(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 existsSync5 } from "node:fs";
import { mkdir as mkdir2, readFile as readFile8 } 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 mkdir2(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 (!existsSync5(outputFile))
return {
duration,
runError: "JSON report not found"
};
let vitestReport;
try {
let resultsJson = await readFile8(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;
export {
useStatics,
parseStaticDir,
mapStaticDir,
getManagerBuilder,
getPreviewBuilder,
getBuilders,
debounce,
throttle,
docgenQueryStaticPath,
storyDocsQueryStaticPath,
MDX_SERVICE_ID,
mdxQueryStaticPath,
mdxStaticStorePath,
mdxManifestRef,
loadManifests,
writeManifests,
registerManifests,
defineService,
getStoryIdsByAbsolutePath,
resolveChangeDetectionAdapter,
registerModuleGraphService,
ChangeDetectionUnavailableError,
ChangeDetectionFailureError,
GitDiffProvider,
setChangeDetectionHost,
getChangeDetectionReadiness,
setChangeDetectionReadiness,
resetChangeDetectionReadiness,
getStatusStoreByTypeId,
fullStatusStore,
universalStatusStore,
getWsToken,
getStoryMetadata,
doesStoryFileExist,
generateStoryFile,
findStoryIds,
storyInputArraySchema,
emptyManifests,
createDocsToolset,
createServiceDocsAccess,
createManifestDocsAccess,
createLocalDocsAccess,
createStoriesToolset,
reviewToolset,
getTestProviderStoreById,
fullTestProviderStore,
universalTestProviderStore,
getComponentCandidates,
analyzeTestResults,
toStoryTestResult,
runStoryTests
};