storybook
Version:
Storybook: Develop, document, and test UI components in isolation
1,136 lines (1,121 loc) • 46.6 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 {
formatAttachFallback
} from "./chunk-4CTPDGB7.js";
import {
AttachUnavailableError,
EnvironmentMismatchError,
SpawnFailedError,
ToolsRuntimeError,
attachGateReasonFromError,
isAttachGateError,
projectPathsEqual
} from "./chunk-F5UFNM2U.js";
import {
resolveToolsetDescription
} from "./chunk-EEWXQXZP.js";
import {
parseToolsetMethodId,
toCliMethodName
} from "./chunk-52XR72NR.js";
import {
deserializeError
} from "./chunk-6WZPWJR4.js";
import {
StorybookDevServerDisconnectedError,
formatIssues
} from "./chunk-DW727PJG.js";
// src/cli/tools/sdk/child-protocol.ts
function isChildMessage(value) {
return typeof value == "object" && value !== null && "type" in value;
}
// src/cli/tools/sdk/command-telemetry.ts
import { telemetry } from "storybook/internal/telemetry";
function toolsCommandDimensions(args) {
return {
client: args.clientInfo.kind,
requestedMode: args.requestedMode,
attachMode: args.resolvedMode ?? args.requestedMode,
...args.resolvedMode ? { resolvedMode: args.resolvedMode } : {},
...args.host ? { host: args.host } : {},
...args.fallbackReason ? { attachGate: args.fallbackReason } : {}
};
}
function commandNameFromRef(ref) {
try {
let { toolsetId, methodName } = parseToolsetMethodId(ref);
return `${toolsetId} ${toCliMethodName(methodName)}`;
} catch {
return "(invalid)";
}
}
function wrapMethodTelemetry(sink, dimensions) {
return async (event, payload) => {
await sink(event, { ...dimensions, ...payload });
};
}
function defaultMethodTelemetrySink(configDir) {
return async (event, payload) => {
await telemetry("tools-command", { event, ...payload }, { configDir });
};
}
function resolveCallTelemetry(options, dimensions, args) {
let isChildHost = process.env.STORYBOOK_TOOLS_CHILD_HOST === "true", sink = options.telemetry ?? (!isChildHost && shouldReportSdkInvocation(args.clientInfo.kind) ? defaultMethodTelemetrySink(args.configDir) : void 0);
if (sink)
return isChildHost ? sink : wrapMethodTelemetry(sink, dimensions);
}
async function reportToolsCommandEvent(payload, options) {
try {
await telemetry("tools-command", payload, options);
} catch {
}
}
function shouldReportSdkInvocation(kind) {
return kind === "sdk" && process.env.STORYBOOK_TOOLS_CHILD_HOST !== "true";
}
async function reportSdkAttachGate(args) {
if (!shouldReportSdkInvocation(args.clientInfo.kind))
return;
let attachGate = attachGateReasonFromError(args.error);
await reportToolsCommandEvent(
{
command: "(none)",
success: !1,
outcome: "attach-gate",
...toolsCommandDimensions({
clientInfo: args.clientInfo,
requestedMode: args.requestedMode,
fallbackReason: attachGate
})
},
{ configDir: args.configDir }
);
}
async function reportSdkInvocation(args) {
if (!shouldReportSdkInvocation(args.clientInfo.kind))
return;
let dimensions = toolsCommandDimensions(args);
if (!("ok" in args.result)) {
let attachGate = attachGateReasonFromError(args.result.error);
await reportToolsCommandEvent(
{
command: commandNameFromRef(args.ref),
success: !1,
outcome: attachGate ? "attach-gate" : "error",
duration: args.duration,
...dimensions,
...attachGate ? { attachGate } : {}
},
{ configDir: args.configDir }
);
return;
}
let success = args.result.ok;
await reportToolsCommandEvent(
{
command: commandNameFromRef(args.ref),
success,
outcome: success ? "success" : "failure",
duration: args.duration,
...dimensions
},
{ configDir: args.configDir }
);
}
// src/cli/tools/sdk/create-tools.ts
import { resolve } from "node:path";
import { versions } from "storybook/internal/common";
// ../../node_modules/@valibot/to-json-schema/dist/index.mjs
function addError(errors, message) {
return errors ? (errors.push(message), errors) : [message];
}
var ESCAPE_REGEX = /[.*+?^${}()|[\]\\]/g;
function escapeRegExp(string) {
return string.replace(ESCAPE_REGEX, "\\$&");
}
function handleError(message, config) {
switch (config?.errorMode) {
case "ignore":
break;
case "warn":
console.warn(message);
break;
default:
throw new Error(message);
}
}
function isJsonConstValue(value) {
return typeof value == "boolean" || typeof value == "number" && Number.isFinite(value) || typeof value == "string";
}
function isJsonEnumValues(values) {
return values.every(isJsonConstValue);
}
function convertAction(jsonSchema, valibotAction, config) {
if (config?.ignoreActions?.includes(valibotAction.type)) return jsonSchema;
let errors;
switch (valibotAction.type) {
case "base64":
jsonSchema.contentEncoding = "base64";
break;
case "bic":
case "cuid2":
case "decimal":
case "digits":
case "domain":
case "emoji":
case "hash":
case "hexadecimal":
case "hex_color":
case "isrc":
case "iso_time_second":
case "iso_week":
case "mac":
case "mac48":
case "mac64":
case "nanoid":
case "octal":
case "slug":
case "ulid":
jsonSchema.pattern ? errors = addError(errors, `The "${valibotAction.type}" action is not supported in combination with another regex action.`) : jsonSchema.pattern = valibotAction.requirement.source;
break;
case "description":
jsonSchema.description = valibotAction.description;
break;
case "email":
case "rfc_email":
jsonSchema.format = "email";
break;
case "ends_with":
jsonSchema.pattern ? errors = addError(errors, `The "${valibotAction.type}" action is not supported in combination with another regex action.`) : jsonSchema.pattern = `${escapeRegExp(valibotAction.requirement)}$`;
break;
case "empty":
jsonSchema.type === "array" ? jsonSchema.maxItems = 0 : (jsonSchema.type !== "string" && (errors = addError(errors, `The "${valibotAction.type}" action is not supported on type "${jsonSchema.type}".`)), jsonSchema.maxLength = 0);
break;
case "entries":
jsonSchema.minProperties = valibotAction.requirement, jsonSchema.maxProperties = valibotAction.requirement;
break;
case "examples":
Array.isArray(jsonSchema.examples) ? jsonSchema.examples = [...jsonSchema.examples, ...valibotAction.examples] : jsonSchema.examples = valibotAction.examples;
break;
case "gt_value":
if (jsonSchema.type !== "number" && jsonSchema.type !== "integer" && (errors = addError(errors, `The "gt_value" action is not supported on type "${jsonSchema.type}".`)), config?.target === "openapi-3.0") {
errors = addError(errors, 'The "gt_value" action is not supported for OpenAPI 3.0.');
break;
}
jsonSchema.exclusiveMinimum = valibotAction.requirement;
break;
case "includes":
jsonSchema.pattern ? errors = addError(errors, `The "${valibotAction.type}" action is not supported in combination with another regex action.`) : jsonSchema.pattern = escapeRegExp(valibotAction.requirement);
break;
case "integer":
jsonSchema.type = "integer";
break;
case "ipv4":
jsonSchema.format = "ipv4";
break;
case "ipv6":
jsonSchema.format = "ipv6";
break;
case "iso_date":
jsonSchema.format = "date";
break;
case "iso_date_time":
case "iso_timestamp":
jsonSchema.format = "date-time";
break;
case "iso_time":
jsonSchema.format = "time";
break;
case "jws_compact":
jsonSchema.pattern ? errors = addError(errors, `The "${valibotAction.type}" action is not supported in combination with another regex action.`) : jsonSchema.pattern = valibotAction.requirement.source;
break;
case "length":
jsonSchema.type === "array" ? (jsonSchema.minItems = valibotAction.requirement, jsonSchema.maxItems = valibotAction.requirement) : (jsonSchema.type !== "string" && (errors = addError(errors, `The "${valibotAction.type}" action is not supported on type "${jsonSchema.type}".`)), jsonSchema.minLength = valibotAction.requirement, jsonSchema.maxLength = valibotAction.requirement);
break;
case "lt_value":
if (jsonSchema.type !== "number" && jsonSchema.type !== "integer" && (errors = addError(errors, `The "lt_value" action is not supported on type "${jsonSchema.type}".`)), config?.target === "openapi-3.0") {
errors = addError(errors, 'The "lt_value" action is not supported for OpenAPI 3.0.');
break;
}
jsonSchema.exclusiveMaximum = valibotAction.requirement;
break;
case "max_entries":
jsonSchema.maxProperties = valibotAction.requirement;
break;
case "max_length":
jsonSchema.type === "array" ? jsonSchema.maxItems = valibotAction.requirement : (jsonSchema.type !== "string" && (errors = addError(errors, `The "${valibotAction.type}" action is not supported on type "${jsonSchema.type}".`)), jsonSchema.maxLength = valibotAction.requirement);
break;
case "max_value":
jsonSchema.type !== "number" && jsonSchema.type !== "integer" && (errors = addError(errors, `The "max_value" action is not supported on type "${jsonSchema.type}".`)), jsonSchema.maximum = valibotAction.requirement;
break;
case "metadata":
typeof valibotAction.metadata.title == "string" && (jsonSchema.title = valibotAction.metadata.title), typeof valibotAction.metadata.description == "string" && (jsonSchema.description = valibotAction.metadata.description), Array.isArray(valibotAction.metadata.examples) && (Array.isArray(jsonSchema.examples) ? jsonSchema.examples = [...jsonSchema.examples, ...valibotAction.metadata.examples] : jsonSchema.examples = valibotAction.metadata.examples);
break;
case "min_entries":
jsonSchema.minProperties = valibotAction.requirement;
break;
case "min_length":
jsonSchema.type === "array" ? jsonSchema.minItems = valibotAction.requirement : (jsonSchema.type !== "string" && (errors = addError(errors, `The "${valibotAction.type}" action is not supported on type "${jsonSchema.type}".`)), jsonSchema.minLength = valibotAction.requirement);
break;
case "min_value":
jsonSchema.type !== "number" && jsonSchema.type !== "integer" && (errors = addError(errors, `The "min_value" action is not supported on type "${jsonSchema.type}".`)), jsonSchema.minimum = valibotAction.requirement;
break;
case "multiple_of":
jsonSchema.multipleOf = valibotAction.requirement;
break;
case "non_empty":
jsonSchema.type === "array" ? jsonSchema.minItems = 1 : (jsonSchema.type !== "string" && (errors = addError(errors, `The "${valibotAction.type}" action is not supported on type "${jsonSchema.type}".`)), jsonSchema.minLength = 1);
break;
case "not_value":
if (!isJsonConstValue(valibotAction.requirement)) {
errors = addError(errors, 'The requirement of the "not_value" action is not JSON compatible.');
break;
}
config?.target === "openapi-3.0" ? jsonSchema.not = { enum: [valibotAction.requirement] } : jsonSchema.not = { const: valibotAction.requirement };
break;
case "not_values":
if (!isJsonEnumValues(valibotAction.requirement)) {
errors = addError(errors, 'A requirement of the "not_values" action is not JSON compatible.');
break;
}
jsonSchema.not = { enum: valibotAction.requirement };
break;
case "regex":
valibotAction.requirement.flags && (errors = addError(errors, "RegExp flags are not supported by JSON Schema.")), jsonSchema.pattern ? errors = addError(errors, `The "${valibotAction.type}" action is not supported in combination with another regex action.`) : jsonSchema.pattern = valibotAction.requirement.source;
break;
case "safe_integer":
jsonSchema.type = "integer", (typeof jsonSchema.minimum != "number" || jsonSchema.minimum < Number.MIN_SAFE_INTEGER) && (jsonSchema.minimum = Number.MIN_SAFE_INTEGER), (typeof jsonSchema.maximum != "number" || jsonSchema.maximum > Number.MAX_SAFE_INTEGER) && (jsonSchema.maximum = Number.MAX_SAFE_INTEGER);
break;
case "starts_with":
jsonSchema.pattern ? errors = addError(errors, `The "${valibotAction.type}" action is not supported in combination with another regex action.`) : jsonSchema.pattern = `^${escapeRegExp(valibotAction.requirement)}`;
break;
case "title":
jsonSchema.title = valibotAction.title;
break;
case "url":
jsonSchema.format = "uri";
break;
case "uuid":
jsonSchema.format = "uuid";
break;
case "value":
if (!isJsonConstValue(valibotAction.requirement)) {
errors = addError(errors, 'The requirement of the "value" action is not JSON compatible.');
break;
}
config?.target === "openapi-3.0" ? jsonSchema.enum = [valibotAction.requirement] : jsonSchema.const = valibotAction.requirement;
break;
case "values":
if (!isJsonEnumValues(valibotAction.requirement)) {
errors = addError(errors, 'A requirement of the "values" action is not JSON compatible.');
break;
}
jsonSchema.enum = valibotAction.requirement;
break;
default:
errors = addError(errors, `The "${valibotAction.type}" action cannot be converted to JSON Schema.`);
}
if (config?.overrideAction) {
let actionOverride = config.overrideAction({
valibotAction,
jsonSchema,
errors
});
if (actionOverride) return { ...actionOverride };
}
if (errors) for (let message of errors) handleError(message, config);
return jsonSchema;
}
function flattenPipe(pipe) {
return pipe.flatMap((item) => "pipe" in item ? flattenPipe(item.pipe) : item);
}
function getDefinitionRef(referenceId) {
return `#/$defs/${referenceId.replaceAll("~", "~0").replaceAll("/", "~1")}`;
}
var refCount = 0;
function convertSchema(jsonSchema, valibotSchema, config, context, skipRef = !1) {
if (!skipRef) {
let referenceId = context.referenceMap.get(valibotSchema);
if (referenceId) {
if (jsonSchema.$ref = getDefinitionRef(referenceId), config?.overrideRef) {
let refOverride = config.overrideRef({
...context,
referenceId,
valibotSchema,
jsonSchema
});
refOverride && (jsonSchema.$ref = refOverride);
}
return jsonSchema;
}
}
if ("pipe" in valibotSchema) {
let flatPipe = flattenPipe(valibotSchema.pipe), startIndex = 0, stopIndex = flatPipe.length - 1;
if (config?.typeMode === "input") {
let inputStopIndex = flatPipe.slice(1).findIndex((item) => item.kind === "schema" || item.kind === "transformation" && (item.type === "find_item" || item.type === "parse_json" || item.type === "raw_transform" || item.type === "reduce_items" || item.type === "stringify_json" || item.type === "to_bigint" || item.type === "to_boolean" || item.type === "to_date" || item.type === "to_number" || item.type === "to_string" || item.type === "transform"));
inputStopIndex !== -1 && (stopIndex = inputStopIndex);
} else if (config?.typeMode === "output") {
let outputStartIndex = flatPipe.findLastIndex((item) => item.kind === "schema");
outputStartIndex !== -1 && (startIndex = outputStartIndex);
}
for (let index = startIndex; index <= stopIndex; index++) {
let valibotPipeItem = flatPipe[index];
valibotPipeItem.kind === "schema" ? (index > startIndex && handleError('Set the "typeMode" config to "input" or "output" to convert pipelines with multiple schemas.', config), jsonSchema = convertSchema(jsonSchema, valibotPipeItem, config, context, !0)) : jsonSchema = convertAction(jsonSchema, valibotPipeItem, config);
}
return jsonSchema;
}
let errors;
switch (valibotSchema.type) {
case "boolean":
jsonSchema.type = "boolean";
break;
case "null":
config?.target === "openapi-3.0" ? jsonSchema.enum = [null] : jsonSchema.type = "null";
break;
case "number":
jsonSchema.type = "number";
break;
case "string":
jsonSchema.type = "string";
break;
case "array":
jsonSchema.type = "array", jsonSchema.items = convertSchema({}, valibotSchema.item, config, context);
break;
case "tuple":
case "tuple_with_rest":
case "loose_tuple":
case "strict_tuple":
if (jsonSchema.type = "array", config?.target === "openapi-3.0") {
jsonSchema.items = { anyOf: [] }, jsonSchema.minItems = valibotSchema.items.length;
for (let item of valibotSchema.items) jsonSchema.items.anyOf.push(convertSchema({}, item, config, context));
valibotSchema.type === "tuple_with_rest" ? jsonSchema.items.anyOf.push(convertSchema({}, valibotSchema.rest, config, context)) : (valibotSchema.type === "strict_tuple" || valibotSchema.type === "tuple") && (jsonSchema.maxItems = valibotSchema.items.length);
} else if (config?.target === "draft-2020-12") {
jsonSchema.prefixItems = [], jsonSchema.minItems = valibotSchema.items.length;
for (let item of valibotSchema.items) jsonSchema.prefixItems.push(convertSchema({}, item, config, context));
valibotSchema.type === "tuple_with_rest" ? jsonSchema.items = convertSchema({}, valibotSchema.rest, config, context) : valibotSchema.type === "strict_tuple" && (jsonSchema.items = !1);
} else {
jsonSchema.items = [], jsonSchema.minItems = valibotSchema.items.length;
for (let item of valibotSchema.items) jsonSchema.items.push(convertSchema({}, item, config, context));
valibotSchema.type === "tuple_with_rest" ? jsonSchema.additionalItems = convertSchema({}, valibotSchema.rest, config, context) : valibotSchema.type === "strict_tuple" && (jsonSchema.additionalItems = !1);
}
break;
case "object":
case "object_with_rest":
case "loose_object":
case "strict_object":
jsonSchema.type = "object", jsonSchema.properties = {}, jsonSchema.required = [];
for (let key in valibotSchema.entries) {
let entry = valibotSchema.entries[key];
jsonSchema.properties[key] = convertSchema({}, entry, config, context), entry.type !== "exact_optional" && entry.type !== "nullish" && entry.type !== "optional" && jsonSchema.required.push(key);
}
valibotSchema.type === "object_with_rest" ? jsonSchema.additionalProperties = convertSchema({}, valibotSchema.rest, config, context) : valibotSchema.type === "strict_object" && (jsonSchema.additionalProperties = !1);
break;
case "record":
config?.target === "openapi-3.0" && "pipe" in valibotSchema.key && (errors = addError(errors, 'The "record" schema with a schema for the key that contains a "pipe" cannot be converted to JSON Schema.')), valibotSchema.key.type !== "string" && (errors = addError(errors, `The "record" schema with the "${valibotSchema.key.type}" schema for the key cannot be converted to JSON Schema.`)), jsonSchema.type = "object", config?.target !== "openapi-3.0" && (jsonSchema.propertyNames = convertSchema({}, valibotSchema.key, config, context)), jsonSchema.additionalProperties = convertSchema({}, valibotSchema.value, config, context);
break;
case "any":
case "unknown":
break;
case "never":
jsonSchema.not = {};
break;
case "nullable":
case "nullish":
if (config?.target === "openapi-3.0") {
let innerSchema = convertSchema({}, valibotSchema.wrapped, config, context);
Object.assign(jsonSchema, innerSchema), jsonSchema.nullable = !0;
} else jsonSchema.anyOf = [convertSchema({}, valibotSchema.wrapped, config, context), { type: "null" }];
valibotSchema.default !== void 0 && (jsonSchema.default = typeof valibotSchema.default == "function" ? valibotSchema.default() : valibotSchema.default);
break;
case "exact_optional":
case "optional":
case "undefinedable":
jsonSchema = convertSchema(jsonSchema, valibotSchema.wrapped, config, context), valibotSchema.default !== void 0 && (jsonSchema.default = typeof valibotSchema.default == "function" ? valibotSchema.default() : valibotSchema.default);
break;
case "literal":
typeof valibotSchema.literal != "boolean" && typeof valibotSchema.literal != "number" && typeof valibotSchema.literal != "string" && (errors = addError(errors, 'The value of the "literal" schema is not JSON compatible.')), config?.target === "openapi-3.0" ? jsonSchema.enum = [valibotSchema.literal] : jsonSchema.const = valibotSchema.literal;
break;
case "enum":
jsonSchema.enum = valibotSchema.options, valibotSchema.options.every((option) => typeof option == "string") ? jsonSchema.type = "string" : valibotSchema.options.every((option) => typeof option == "number") ? jsonSchema.type = "number" : config?.target !== "openapi-3.0" && (jsonSchema.type = ["string", "number"]);
break;
case "picklist": {
let hasInvalidOption = valibotSchema.options.some((option) => typeof option != "number" && typeof option != "string");
hasInvalidOption && (errors = addError(errors, 'An option of the "picklist" schema is not JSON compatible.')), jsonSchema.enum = valibotSchema.options, valibotSchema.options.every((option) => typeof option == "string") ? jsonSchema.type = "string" : valibotSchema.options.every((option) => typeof option == "number") ? jsonSchema.type = "number" : !hasInvalidOption && config?.target !== "openapi-3.0" && (jsonSchema.type = ["string", "number"]);
break;
}
case "union":
jsonSchema.anyOf = valibotSchema.options.map((option) => convertSchema({}, option, config, context));
break;
case "variant":
jsonSchema.oneOf = valibotSchema.options.map((option) => convertSchema({}, option, config, context));
break;
case "intersect":
jsonSchema.allOf = valibotSchema.options.map((option) => convertSchema({}, option, config, context));
break;
case "lazy": {
let wrappedValibotSchema = context.getterMap.get(valibotSchema.getter);
wrappedValibotSchema || (wrappedValibotSchema = valibotSchema.getter(void 0), context.getterMap.set(valibotSchema.getter, wrappedValibotSchema));
let referenceId = context.referenceMap.get(wrappedValibotSchema);
if (referenceId || (referenceId = `${refCount++}`, context.referenceMap.set(wrappedValibotSchema, referenceId), context.definitions[referenceId] = convertSchema({}, wrappedValibotSchema, config, context, !0)), jsonSchema.$ref = getDefinitionRef(referenceId), config?.overrideRef) {
let refOverride = config.overrideRef({
...context,
referenceId,
valibotSchema: wrappedValibotSchema,
jsonSchema
});
refOverride && (jsonSchema.$ref = refOverride);
}
break;
}
default:
errors = addError(errors, `The "${valibotSchema.type}" schema cannot be converted to JSON Schema.`);
}
if (config?.overrideSchema) {
let schemaOverride = config.overrideSchema({
...context,
referenceId: context.referenceMap.get(valibotSchema),
valibotSchema,
jsonSchema,
errors
});
if (schemaOverride) return { ...schemaOverride };
}
if (errors) for (let message of errors) handleError(message, config);
return jsonSchema;
}
var store;
function getGlobalDefs() {
return store;
}
function toJsonSchema(schema, config) {
let context = {
definitions: {},
referenceMap: /* @__PURE__ */ new Map(),
getterMap: /* @__PURE__ */ new Map()
}, definitions = config?.definitions ?? getGlobalDefs();
if (definitions) {
for (let key in definitions) context.referenceMap.set(definitions[key], key);
for (let key in definitions) context.definitions[key] = convertSchema({}, definitions[key], config, context, !0);
}
let jsonSchema = convertSchema({}, schema, config, context), target = config?.target ?? "draft-07";
return target === "draft-2020-12" ? jsonSchema.$schema = "https://json-schema.org/draft/2020-12/schema" : target === "draft-07" && (jsonSchema.$schema = "http://json-schema.org/draft-07/schema#"), context.referenceMap.size && (jsonSchema.$defs = context.definitions), jsonSchema;
}
// src/cli/tools/sdk/json-schema.ts
function toToolsetJsonSchema(schema) {
if (schema["~standard"].vendor === "valibot")
try {
return toJsonSchema(schema, { errorMode: "ignore" });
} catch {
return;
}
}
// src/cli/tools/sdk/catalog.ts
function toCatalogEntry(toolset, ctx) {
return {
id: toolset.id,
description: toolset.description,
methods: Object.entries(toolset.methods).map(([methodName, method]) => ({
ref: `${toolset.id}.${methodName}`,
title: method.title,
description: resolveToolsetDescription(method.description, ctx).trim(),
requiresDevServer: method.requiresDevServer === !0,
input: toToolsetJsonSchema(method.input),
...method.output ? { output: toToolsetJsonSchema(method.output) } : {}
}))
};
}
// src/cli/tools/sdk/child-client.ts
import { fork } from "node:child_process";
import { logger } from "storybook/internal/node-logger";
// src/cli/tools/sdk/resolve-project-storybook.ts
import { createRequire } from "node:module";
import { join } from "node:path";
var CHILD_HOST_ENTRY = "storybook/internal/tools/child-host";
function resolveChildHostScript(resolutionRoot) {
return createRequire(join(resolutionRoot, "package.json")).resolve(CHILD_HOST_ENTRY, { paths: [resolutionRoot] });
}
// src/cli/tools/sdk/child-client.ts
async function spawnChildHost(args, deps = {}) {
let log = deps.logger ?? logger, forkChild = deps.fork ?? fork, cwd = args.cwd, resolvedMode = args.options.mode === "local" ? "local" : "attached", resolutionRoot = args.installationPath ?? cwd, scriptPath;
try {
scriptPath = (deps.resolveScript ?? resolveChildHostScript)(resolutionRoot);
} catch (cause) {
throw new SpawnFailedError({
reason: args.installationPath ? `The running Storybook's installation at ${resolutionRoot} can no longer resolve \`storybook/internal/tools/child-host\`. From your project directory, restart Storybook (for example \`npx storybook dev\`) and re-run this command from there.` : `Could not resolve \`storybook/internal/tools/child-host\` from ${resolutionRoot}. Install Storybook in that project, then retry.`,
cause
});
}
let env = {
...process.env,
STORYBOOK_TOOLS_CHILD_HOST: "true"
};
resolvedMode === "attached" ? env.STORYBOOK_ATTACHED_TOOLS = "true" : delete env.STORYBOOK_ATTACHED_TOOLS;
let child;
try {
child = forkChild(scriptPath, [], {
cwd,
stdio: ["ignore", "pipe", "pipe", "ipc"],
env
});
} catch (cause) {
throw new SpawnFailedError({
reason: `Could not start a tools child host for ${cwd}.`,
cause
});
}
child.stdout?.on("data", (chunk) => {
log.log(String(chunk));
}), child.stderr?.on("data", (chunk) => {
log.warn(String(chunk));
});
let pending = /* @__PURE__ */ new Map(), pendingTelemetry = /* @__PURE__ */ new Map(), closed = !1, nextId = 0, failPending = (error) => {
for (let [, waiter] of pending)
waiter.reject(error);
pending.clear();
}, disconnected = new Promise((_, reject) => {
child.once("exit", (code, signal) => {
if (closed)
return;
let error = new ToolsRuntimeError({
reason: "connection-lost",
message: `The tools child host for ${cwd} exited${signal ? ` (${signal})` : code != null ? ` (code ${code})` : ""}.`
});
failPending(error), reject(error);
}), child.once("error", (cause) => {
if (closed)
return;
let error = new SpawnFailedError({
reason: `The tools child host for ${cwd} failed.`,
cause
});
failPending(error), reject(error);
});
});
disconnected.catch(() => {
}), child.on("message", (raw) => {
if (!isChildMessage(raw) || raw.type === "hello")
return;
if (raw.type === "telemetry") {
Promise.resolve().then(() => pendingTelemetry.get(raw.id)?.(raw.event, raw.payload)).catch(() => {
});
return;
}
let waiter = pending.get(raw.id);
if (waiter) {
if (pending.delete(raw.id), raw.type === "result") {
waiter.resolve(raw.value);
return;
}
waiter.reject(rehydrateSerializedToolsError(raw.error));
}
});
let send = (message) => {
if (!child.send)
throw new SpawnFailedError({
reason: `The tools child host for ${cwd} has no IPC channel.`
});
child.send(message);
}, request = (message) => closed ? Promise.reject(
new ToolsRuntimeError({
reason: "closed",
message: "This tools host is closed. Create a new one with `createTools`."
})
) : Promise.race([
new Promise((resolve2, reject) => {
pending.set(message.id, { resolve: resolve2, reject }), send(message);
}),
disconnected
]), hello;
try {
hello = await waitForHello(child, send, {
cwd,
options: args.options,
clientInfo: args.clientInfo,
resolvedMode
});
} catch (error) {
throw closed = !0, child.kill(), error;
}
if (hello.version !== 1)
throw closed = !0, child.kill(), new SpawnFailedError({
reason: `The tools child host at ${cwd} speaks protocol ${hello.version}, but this process expects ${1}. Restart your Storybook so both sides match.`
});
let runtime = {
configDir: hello.storybook.configDir,
toolsets: [],
getService: () => {
throw new ToolsRuntimeError({
reason: "command-unhandled",
message: "Toolset services are served by the project-local child host."
});
},
close: async () => {
}
}, assertOpen = () => {
if (closed)
throw new ToolsRuntimeError({
reason: "closed",
message: "This tools host is closed. Create a new one with `createTools`."
});
}, dimensions = toolsCommandDimensions({
clientInfo: args.clientInfo,
requestedMode: args.requestedMode,
resolvedMode,
host: "child"
});
return {
mode: resolvedMode,
host: "child",
requestedMode: args.requestedMode,
clientInfo: hello.clientInfo,
storybook: hello.storybook,
runtime,
async describe(options = {}) {
return assertOpen(), await request({ type: "describe", id: String(++nextId), options });
},
async call(ref, input = {}, options = {}) {
assertOpen(), options.signal?.throwIfAborted();
let id = String(++nextId), telemetry2 = resolveCallTelemetry(options, dimensions, {
clientInfo: args.clientInfo,
configDir: hello.storybook.configDir
});
telemetry2 && pendingTelemetry.set(id, telemetry2);
let onAbort, aborted = options.signal ? new Promise((_, reject) => {
onAbort = () => {
try {
send({ type: "cancel", id });
} catch {
}
reject(options.signal.reason);
}, options.signal.addEventListener("abort", onAbort, { once: !0 });
}) : void 0, start = Date.now();
try {
let work = request({ type: "call", id, ref, input }), outcome = await (aborted ? Promise.race([work, aborted]) : work);
return await reportSdkInvocation({
ref,
clientInfo: args.clientInfo,
requestedMode: args.requestedMode,
resolvedMode,
host: "child",
result: outcome,
duration: Date.now() - start,
configDir: hello.storybook.configDir
}), outcome;
} catch (error) {
throw await reportSdkInvocation({
ref,
clientInfo: args.clientInfo,
requestedMode: args.requestedMode,
resolvedMode,
host: "child",
result: { error },
duration: Date.now() - start,
configDir: hello.storybook.configDir
}), error;
} finally {
pendingTelemetry.delete(id), onAbort && options.signal?.removeEventListener("abort", onAbort);
}
},
async close() {
if (!closed) {
closed = !0, failPending(
new ToolsRuntimeError({
reason: "closed",
message: "This tools host is closed. Create a new one with `createTools`."
})
);
try {
send({ type: "close" });
} catch {
}
child.kill();
}
}
};
}
var HELLO_TIMEOUT_MS = 1e4;
async function waitForHello(child, send, args) {
let { cwd, options, clientInfo, resolvedMode } = args;
return new Promise((resolve2, reject) => {
let onMessage = (raw) => {
if (isChildMessage(raw) && raw.type === "hello") {
cleanup(), resolve2(raw);
return;
}
isChildMessage(raw) && raw.type === "error" && raw.id === "init" && (cleanup(), reject(rehydrateSerializedToolsError(raw.error)));
}, onExit = (code, signal) => {
cleanup(), reject(
new SpawnFailedError({
reason: `The tools child host for ${cwd} exited before it was ready${signal ? ` (${signal})` : code != null ? ` (code ${code})` : ""}.`
})
);
}, onError = (cause) => {
cleanup(), reject(
new SpawnFailedError({
reason: `The tools child host for ${cwd} failed to start.`,
cause
})
);
}, cleanup = () => {
clearTimeout(timer), child.off("message", onMessage), child.off("exit", onExit), child.off("error", onError);
}, timer = setTimeout(() => {
cleanup(), reject(
new SpawnFailedError({
reason: `The tools child host for ${cwd} did not become ready in time.`
})
);
}, HELLO_TIMEOUT_MS);
child.on("message", onMessage), child.once("exit", onExit), child.once("error", onError);
try {
send({
type: "init",
options: {
...options,
cwd,
mode: resolvedMode,
autoSpawn: !1,
clientInfo
}
});
} catch (cause) {
cleanup(), reject(
new SpawnFailedError({
reason: `Could not initialize a tools child host for ${cwd}.`,
cause
})
);
}
});
}
function rehydrateSerializedToolsError(serialized) {
let plain = deserializeError(serialized), data = plain.data;
if (data == null || typeof data != "object")
return plain;
switch (toolsErrorKind(serialized)) {
case "AttachUnavailableError":
return new AttachUnavailableError(data);
case "EnvironmentMismatchError":
return new EnvironmentMismatchError(data);
case "SpawnFailedError":
return new SpawnFailedError(data);
case "ToolsRuntimeError":
return new ToolsRuntimeError(data);
default:
return plain;
}
}
function toolsErrorKind(serialized) {
let fromProps = serialized.properties?._name;
return typeof fromProps == "string" ? fromProps : serialized.name.match(/\(([^)]+)\)$/)?.[1] ?? serialized.name;
}
// src/cli/tools/sdk/create-tools.ts
async function createTools(options = {}, deps = {}) {
let mode = options.mode ?? "auto", clientInfo = {
name: options.clientInfo?.name ?? "storybook-tools-sdk",
version: options.clientInfo?.version ?? versions.storybook,
kind: options.clientInfo?.kind ?? "sdk"
};
switch (mode) {
case "attached":
try {
return await createAttachedTools(options, deps, clientInfo, mode);
} catch (error) {
throw isAttachGateError(error) && await reportSdkAttachGate({
error,
clientInfo,
requestedMode: mode,
configDir: options.configDir
}), error;
}
case "auto":
try {
return await createAttachedTools(options, deps, clientInfo, mode);
} catch (error) {
if (!isAttachGateError(error))
throw error;
let fallbackReason = attachGateReasonFromError(error), notice = fallbackReason === "no-instance" ? void 0 : formatAttachFallback(error.message);
try {
return await createLocalTools(options, deps, clientInfo, mode, {
...notice ? { fallbackNotice: notice } : {},
fallbackReason
});
} catch (localError) {
throw shouldWrapAutoLocalFailure(localError) ? wrapAutoLocalFailure(notice ?? formatAttachFallback(error.message), localError) : localError;
}
}
case "local":
return createLocalTools(options, deps, clientInfo, mode);
default:
throw mode;
}
}
async function createAttachedTools(options, deps, clientInfo, requestedMode) {
process.env.STORYBOOK_ATTACHED_TOOLS = "true";
let { bootstrapAttachedRuntime } = await import("./attached-runtime-OD2MHCQ2.js"), autoSpawn = process.env.STORYBOOK_TOOLS_CHILD_HOST === "true" ? !1 : options.autoSpawn ?? !0, attached = await (deps.attach ?? ((target) => bootstrapAttachedRuntime({ ...target, autoSpawn })))({
cwd: options.cwd,
configDir: options.configDir,
port: options.port
});
if ("kind" in attached && attached.kind === "spawn")
return (deps.spawnChild ?? spawnChildHost)({
cwd: attached.record.cwd,
installationPath: attached.storybookPath,
options: {
...options,
mode: "attached",
autoSpawn: !1,
cwd: attached.record.cwd,
port: attached.record.port
},
clientInfo,
requestedMode
});
let inProcess = attached, siblings = inProcess.siblings?.length ? inProcess.siblings.map(toSiblingInstance) : void 0;
return createToolsHost({
mode: "attached",
host: "in-process",
requestedMode,
runtime: inProcess.runtime,
clientInfo,
storybook: {
version: versions.storybook,
configDir: inProcess.runtime.configDir,
url: inProcess.record.url,
pid: inProcess.record.pid,
...inProcess.record.port != null ? { port: inProcess.record.port } : {},
...inProcess.record.cwd ? { cwd: inProcess.record.cwd } : {},
...siblings ? { siblings } : {}
},
close: () => inProcess.connection.close(),
disconnected: inProcess.connection.disconnected
});
}
function toSiblingInstance(record) {
return {
url: record.url,
port: record.port,
pid: record.pid,
cwd: record.cwd,
...record.configDir ? { configDir: record.configDir } : {}
};
}
async function createLocalTools(options, deps, clientInfo, requestedMode, fallback) {
delete process.env.STORYBOOK_ATTACHED_TOOLS;
let cwd = resolve(options.cwd ?? process.cwd()), autoSpawn = process.env.STORYBOOK_TOOLS_CHILD_HOST === "true" ? !1 : options.autoSpawn ?? !0;
if (!projectPathsEqual(cwd, process.cwd())) {
if (!autoSpawn)
throw new ToolsRuntimeError({
reason: "mode-unavailable",
message: `This process is running from ${process.cwd()}, but the target Storybook is at ${cwd}. Re-run from that directory, or omit \`autoSpawn: false\` so a child host can load the project.`
});
let child = await (deps.spawnChild ?? spawnChildHost)({
cwd,
options: { ...options, mode: "local", autoSpawn: !1, cwd },
clientInfo,
requestedMode
});
return fallback ? { ...child, ...fallback } : child;
}
let { bootstrapToolsRuntime } = await import("./local-runtime-QL5INIZ2.js"), runtime;
try {
runtime = await (deps.bootstrap ?? bootstrapToolsRuntime)({
cwd,
configDir: options.configDir
});
} catch (error) {
throw new ToolsRuntimeError({
reason: "config-load-failed",
message: `Could not load the Storybook configuration for this project: ${error instanceof Error ? error.message : String(error)}`,
cause: error
});
}
return createToolsHost({
mode: "local",
host: "in-process",
requestedMode,
...fallback,
runtime,
clientInfo,
storybook: { version: versions.storybook, configDir: runtime.configDir }
});
}
function shouldWrapAutoLocalFailure(error) {
return error instanceof SpawnFailedError ? !0 : error instanceof ToolsRuntimeError && (error.data.reason === "config-load-failed" || error.data.reason === "mode-unavailable");
}
function wrapAutoLocalFailure(notice, localError) {
let message = `${notice}
${localError.message}`;
if (localError instanceof SpawnFailedError)
return new SpawnFailedError({
reason: message,
cause: localError.data.cause ?? localError
});
if (localError instanceof ToolsRuntimeError)
return new ToolsRuntimeError({
reason: localError.data.reason,
message,
cause: localError.data.cause
});
throw localError;
}
function transportFor(kind) {
return kind === "cli" ? "cli" : "sdk";
}
function createToolsHost(args) {
let { mode, host, requestedMode, runtime, clientInfo, storybook } = args, baseCtx = {
transport: transportFor(clientInfo.kind),
getService: runtime.getService,
...storybook.url ? { origin: storybook.url } : {}
}, closed = !1, assertOpen = () => {
if (closed)
throw new ToolsRuntimeError({
reason: "closed",
message: "This tools host is closed. Create a new one with `createTools`."
});
}, invoke = async (ref, input, options) => {
options.signal?.throwIfAborted();
let { toolsetId, methodName } = splitRef(ref), method = findMethod(findToolset(runtime, toolsetId), methodName);
if (mode === "local" && method.requiresDevServer)
throw new AttachUnavailableError({
reason: "no-instance",
instances: [],
remediation: `\`${ref}\` needs a running Storybook dev server, and this tools host loaded the project's configuration on its own. Start Storybook (for example \`npm run storybook\`), then retry.`
});
let validation = await method.input["~standard"].validate(input);
if (validation.issues)
throw new ToolsRuntimeError({
reason: "invalid-input",
message: `Invalid input for \`${ref}\`:
${formatIssues(validation.issues)}`,
issues: validation.issues
});
return raceAbort(
options.signal,
method.handler(validation.value, {
...baseCtx,
...options.origin !== void 0 ? { origin: options.origin } : {},
...options.telemetry ? { telemetry: options.telemetry } : {}
})
);
}, dimensions = toolsCommandDimensions({
clientInfo,
requestedMode,
resolvedMode: mode,
host,
fallbackReason: args.fallbackReason
});
return {
mode,
host,
requestedMode,
clientInfo,
runtime,
storybook,
...args.fallbackNotice ? { fallbackNotice: args.fallbackNotice } : {},
...args.fallbackReason ? { fallbackReason: args.fallbackReason } : {},
async describe(options = {}) {
assertOpen();
let toolsets = options.toolset === void 0 ? runtime.toolsets : [findToolset(runtime, options.toolset)];
return {
configDir: runtime.configDir,
toolsets: toolsets.map((toolset) => toCatalogEntry(toolset, baseCtx))
};
},
async call(ref, input = {}, options = {}) {
assertOpen();
let telemetry2 = resolveCallTelemetry(options, dimensions, {
clientInfo,
configDir: runtime.configDir
}), callOptions = {
...options,
...telemetry2 ? { telemetry: telemetry2 } : {}
}, start = Date.now();
try {
let outcome = args.disconnected ? await Promise.race([invoke(ref, input, callOptions), args.disconnected]) : await invoke(ref, input, callOptions);
return await reportSdkInvocation({
ref,
clientInfo,
requestedMode,
resolvedMode: mode,
host,
fallbackReason: args.fallbackReason,
result: outcome,
duration: Date.now() - start,
configDir: runtime.configDir
}), outcome;
} catch (error) {
let mapped = error instanceof StorybookDevServerDisconnectedError ? new ToolsRuntimeError({
reason: "connection-lost",
message: error.message,
cause: error
}) : error;
throw await reportSdkInvocation({
ref,
clientInfo,
requestedMode,
resolvedMode: mode,
host,
fallbackReason: args.fallbackReason,
result: { error: mapped },
duration: Date.now() - start,
configDir: runtime.configDir
}), mapped;
}
},
async close() {
closed || (closed = !0, args.close?.(), await runtime.close());
}
};
}
function raceAbort(signal, work) {
let pending = Promise.resolve(work);
if (!signal)
return pending;
signal.throwIfAborted();
let onAbort, aborted = new Promise((_, reject) => {
onAbort = () => reject(signal.reason), signal.addEventListener("abort", onAbort, { once: !0 });
});
return Promise.race([pending, aborted]).finally(() => {
signal.removeEventListener("abort", onAbort);
});
}
function splitRef(ref) {
try {
return parseToolsetMethodId(ref);
} catch {
throw new ToolsRuntimeError({
reason: "unknown-method",
message: `Invalid tool reference \`${ref}\`. Expected \`toolsetId.methodName\`.`
});
}
}
function findToolset(runtime, toolsetId) {
let toolset = runtime.toolsets.find((candidate) => candidate.id === toolsetId);
if (!toolset)
throw new ToolsRuntimeError({
reason: "unknown-toolset",
message: `Unknown toolset \`${toolsetId}\`. The Storybook configuration at ${runtime.configDir} provides: ${runtime.toolsets.map((candidate) => candidate.id).join(", ")}.`
});
return toolset;
}
function findMethod(toolset, methodName) {
if (!Object.hasOwn(toolset.methods, methodName))
throw new ToolsRuntimeError({
reason: "unknown-method",
message: `Unknown tool \`${toolset.id}.${methodName}\`. The \`${toolset.id}\` toolset provides: ${Object.keys(toolset.methods).join(", ")}.`
});
return toolset.methods[methodName];
}
export {
toolsCommandDimensions,
wrapMethodTelemetry,
createTools
};