UNPKG

storybook

Version:

Storybook: Develop, document, and test UI components in isolation

193 lines (185 loc) • 6.94 kB
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 { projectPathsEqual } from "./chunk-F5UFNM2U.js"; import { CLAUDE_AGENT_NAME, CLAUDE_PREVIEW_AGENT_NAME } from "./chunk-UF3LYQOO.js"; import { integer, literal, maxValue, minValue, number, object, optional, picklist, pipe, safeParse, string } from "./chunk-5EYYAT2W.js"; // src/cli/tools/instances/registry.ts import * as fs from "node:fs/promises"; import { homedir } from "node:os"; import { join } from "node:path"; // src/cli/tools/instances/types.ts var McpStatusSchema = picklist(["not-installed", "starting", "ready", "error"]), StorybookInstanceRecordSchema = object({ schemaVersion: literal(1), instanceId: string(), pid: pipe(number(), minValue(1), integer()), cwd: string(), /** * Resolved config directory of the running Storybook, used as a second matching key so * monorepo instances are found from a different cwd (storybookjs/storybook#35359). Optional: * records written by Storybooks older than 10.5 lack it. */ configDir: optional(string()), url: string(), port: pipe(number(), minValue(1), maxValue(65535), integer()), /** * Token authenticating clients against the instance's WebSocket channel. Optional: records * written by older Storybooks lack it. */ token: optional(string()), agent: optional(string()), storybookVersion: optional(string()), /** * Realpathed root of the `storybook` package the dev server actually runs, recorded by the * server from its own module location. The caller attaches in-process when this equals its own * root, and spawns its child host from this root otherwise. Optional: records written by older * Storybooks lack it, which makes attach refuse. */ storybookPath: optional(string()), startedAt: optional(string()), updatedAt: optional(string()), mcp: object({ status: McpStatusSchema, endpoint: optional(string()) }) }); // src/cli/tools/instances/registry.ts var DEFAULT_REGISTRY_DIR = join(homedir(), ".storybook", "instances"), SOFT_REGISTRY_ERRORS = /* @__PURE__ */ new Set(["ENOENT", "EACCES", "EPERM", "ENOTDIR"]); async function readRegistry(registryDir = DEFAULT_REGISTRY_DIR) { let entries; try { entries = await fs.readdir(registryDir); } catch (error) { if (SOFT_REGISTRY_ERRORS.has(error.code ?? "")) return []; throw error; } return (await Promise.all( entries.filter((name) => name.endsWith(".json")).map(async (name) => { try { let raw = await fs.readFile(join(registryDir, name), "utf-8"), parsed = safeParse(StorybookInstanceRecordSchema, JSON.parse(raw)); return parsed.success ? isProcessAlive(parsed.output.pid) ? parsed.output : (await fs.rm(join(registryDir, name), { force: !0 }).catch(() => { }), null) : null; } catch { return null; } }) )).filter((r) => r !== null); } function isProcessAlive(pid) { if (!Number.isInteger(pid) || pid <= 0) return !1; try { return process.kill(pid, 0), !0; } catch (error) { return error.code === "EPERM"; } } // src/cli/tools/instances/resolve.ts function resolveInstance(records, target) { let selection = selectInstances(records, target); if (selection.kind === "port-mismatch") return { kind: "intercept", reason: "port-mismatch", records: selection.candidates, matches: [] }; if (selection.kind === "no-instance") return { kind: "intercept", reason: "no-instance", records: selection.records, matches: [] }; let sortedMatches = selection.matches, selected = sortedMatches.find((r) => r.mcp.status === "ready") ?? sortedMatches[0]; switch (selected.mcp.status) { case "ready": return { kind: "instance", record: selected, matches: sortedMatches }; case "starting": return { kind: "intercept", reason: "mcp-starting", matches: sortedMatches }; case "not-installed": return { kind: "intercept", reason: "addon-missing", matches: sortedMatches }; case "error": return { kind: "intercept", reason: "mcp-error", matches: sortedMatches }; default: { let unhandled = selected.mcp.status; throw new Error(`Unhandled MCP status: ${unhandled}`); } } } function selectInstances(records, target) { let { port: targetPort, agent: currentAgent } = target; if (targetPort != null) { let candidates = target.configDirExplicit ? records.filter((record) => matchesTargetConfigDir(record, target)) : records, matches = candidates.filter((record) => record.port === targetPort); return matches.length === 0 ? candidates.length > 0 ? { kind: "port-mismatch", port: targetPort, candidates } : { kind: "no-instance", records } : { kind: "match", matches: [...matches].sort(byMostRecentlyStarted) }; } let projectMatches = listProjectMatches(records, target); return projectMatches.length === 0 ? { kind: "no-instance", records } : { kind: "match", matches: selectCompetingBucket(projectMatches, currentAgent) }; } function listProjectMatches(records, target) { return target.configDirExplicit ? records.filter((record) => matchesTargetConfigDir(record, target)) : records.filter( (record) => projectPathsEqual(record.cwd, target.cwd) || matchesTargetConfigDir(record, target) ); } function matchesTargetConfigDir(record, target) { return target.configDir != null && record.configDir != null && projectPathsEqual(record.configDir, target.configDir); } function selectCompetingBucket(matches, currentAgent) { let selectedAgent = (currentAgent === CLAUDE_AGENT_NAME ? [CLAUDE_PREVIEW_AGENT_NAME, CLAUDE_AGENT_NAME] : currentAgent ? [currentAgent] : []).find((agent) => matches.some((r) => r.agent === agent)); return [...selectedAgent ? matches.filter((r) => r.agent === selectedAgent) : matches].sort(byMostRecentlyStarted); } function startedAtMs(r) { if (!r.startedAt) return Number.NEGATIVE_INFINITY; let t = Date.parse(r.startedAt); return Number.isNaN(t) ? Number.NEGATIVE_INFINITY : t; } function byMostRecentlyStarted(a, b) { let ta = startedAtMs(a), tb = startedAtMs(b); return ta !== tb ? tb > ta ? 1 : -1 : a.pid - b.pid; } export { readRegistry, resolveInstance, selectInstances };