@reticular/speakable 1.5.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,57 @@
1
+ import { I as InteractionSequence, a as AnalysisResult, A as AccessibilityTimeline } from '../types-Blh0EnZF.js';
2
+
3
+ /**
4
+ * Shared message protocol for the iframe harness.
5
+ *
6
+ * The parent-page controller and the in-iframe agent exchange these
7
+ * request/response envelopes over `postMessage`. Every request carries a
8
+ * unique `id` so responses can be correlated.
9
+ */
10
+
11
+ /** Serializable subset of CaptureOptions safe to send over postMessage. */
12
+ interface SerializableCaptureOptions {
13
+ componentName: string;
14
+ storyName?: string;
15
+ sequence: InteractionSequence;
16
+ settlePeriod?: number;
17
+ awaitUpgrade?: boolean;
18
+ upgradeTimeoutMs?: number;
19
+ }
20
+
21
+ /**
22
+ * Iframe harness — parent-page controller.
23
+ *
24
+ * Runs in a parent page, mounts a component (URL or HTML string) into an
25
+ * iframe, injects the browser bundle, and drives analysis/capture over
26
+ * `postMessage`. This is the Storybook-independent runtime testing surface.
27
+ */
28
+
29
+ interface HarnessOptions {
30
+ /** An existing iframe to drive, or a container to create one inside. */
31
+ target: HTMLIFrameElement | {
32
+ container: HTMLElement;
33
+ };
34
+ /** Load + inject + request timeout in ms (default: 10000). */
35
+ timeoutMs?: number;
36
+ /** Origins allowed for postMessage. Defaults to same-origin only. */
37
+ allowedOrigins?: string[];
38
+ /**
39
+ * URL to the injectable IIFE bundle. When omitted, the harness assumes the
40
+ * bundle is already present in the iframe (e.g. bundled into the page).
41
+ */
42
+ bundleUrl?: string;
43
+ }
44
+ type LoadSource = {
45
+ url: string;
46
+ } | {
47
+ html: string;
48
+ };
49
+ interface Harness {
50
+ load(source: LoadSource): Promise<void>;
51
+ analyze(selector?: string): Promise<AnalysisResult>;
52
+ captureTimeline(options: SerializableCaptureOptions): Promise<AccessibilityTimeline>;
53
+ destroy(): void;
54
+ }
55
+ declare function createHarness(options: HarnessOptions): Harness;
56
+
57
+ export { type Harness, type HarnessOptions, type LoadSource, type SerializableCaptureOptions, createHarness };
@@ -0,0 +1,176 @@
1
+ // src/harness/protocol.ts
2
+ var HARNESS_CHANNEL = "speakable-harness";
3
+ function isHarnessResponse(data) {
4
+ if (!data || typeof data !== "object") return false;
5
+ const d = data;
6
+ return d.channel === HARNESS_CHANNEL && typeof d.id === "string" && typeof d.ok === "boolean";
7
+ }
8
+ function isHarnessReady(data) {
9
+ if (!data || typeof data !== "object") return false;
10
+ const d = data;
11
+ return d.channel === HARNESS_CHANNEL && d.kind === "ready";
12
+ }
13
+
14
+ // src/harness/index.ts
15
+ var DEFAULT_TIMEOUT = 1e4;
16
+ var idCounter = 0;
17
+ function nextId() {
18
+ idCounter += 1;
19
+ return `req-${Date.now()}-${idCounter}`;
20
+ }
21
+ function isCrossOrigin(url) {
22
+ try {
23
+ const resolved = new URL(url, window.location.href);
24
+ return resolved.origin !== window.location.origin;
25
+ } catch {
26
+ return false;
27
+ }
28
+ }
29
+ function createHarness(options) {
30
+ const { timeoutMs = DEFAULT_TIMEOUT, allowedOrigins, bundleUrl } = options;
31
+ let ownsIframe = false;
32
+ let iframe;
33
+ if (options.target instanceof HTMLIFrameElement) {
34
+ iframe = options.target;
35
+ } else {
36
+ iframe = document.createElement("iframe");
37
+ iframe.setAttribute("title", "speakable-harness");
38
+ options.target.container.appendChild(iframe);
39
+ ownsIframe = true;
40
+ }
41
+ const pending = /* @__PURE__ */ new Map();
42
+ function onMessage(event) {
43
+ if (event.source !== iframe.contentWindow) return;
44
+ if (allowedOrigins && allowedOrigins.length > 0 && !allowedOrigins.includes("*")) {
45
+ if (!allowedOrigins.includes(event.origin)) return;
46
+ }
47
+ const data = event.data;
48
+ if (!isHarnessResponse(data)) return;
49
+ const entry = pending.get(data.id);
50
+ if (!entry) return;
51
+ clearTimeout(entry.timer);
52
+ pending.delete(data.id);
53
+ entry.resolve(data);
54
+ }
55
+ window.addEventListener("message", onMessage);
56
+ function targetOrigin() {
57
+ if (allowedOrigins && allowedOrigins.includes("*")) return "*";
58
+ try {
59
+ return iframe.contentWindow ? new URL(iframe.src || window.location.href).origin : "*";
60
+ } catch {
61
+ return "*";
62
+ }
63
+ }
64
+ function request(payload) {
65
+ const id = nextId();
66
+ const win = iframe.contentWindow;
67
+ if (!win) {
68
+ return Promise.reject(new Error("Harness iframe has no content window; call load() first."));
69
+ }
70
+ return new Promise((resolve, reject) => {
71
+ const timer = setTimeout(() => {
72
+ pending.delete(id);
73
+ reject(new Error(`Harness request "${payload.kind}" timed out after ${timeoutMs}ms`));
74
+ }, timeoutMs);
75
+ pending.set(id, { resolve, reject, timer });
76
+ win.postMessage({ channel: HARNESS_CHANNEL, id, ...payload }, targetOrigin());
77
+ });
78
+ }
79
+ function waitForReady() {
80
+ return new Promise((resolve, reject) => {
81
+ const timer = setTimeout(() => {
82
+ window.removeEventListener("message", onReady);
83
+ reject(new Error(`Harness bundle did not signal ready within ${timeoutMs}ms`));
84
+ }, timeoutMs);
85
+ function onReady(event) {
86
+ if (event.source !== iframe.contentWindow) return;
87
+ if (!isHarnessReady(event.data)) return;
88
+ clearTimeout(timer);
89
+ window.removeEventListener("message", onReady);
90
+ resolve();
91
+ }
92
+ window.addEventListener("message", onReady);
93
+ });
94
+ }
95
+ function waitForLoad() {
96
+ return new Promise((resolve, reject) => {
97
+ const timer = setTimeout(() => {
98
+ iframe.removeEventListener("load", onLoad);
99
+ reject(new Error(`Harness iframe failed to load within ${timeoutMs}ms`));
100
+ }, timeoutMs);
101
+ function onLoad() {
102
+ clearTimeout(timer);
103
+ iframe.removeEventListener("load", onLoad);
104
+ resolve();
105
+ }
106
+ iframe.addEventListener("load", onLoad);
107
+ });
108
+ }
109
+ function injectBundle() {
110
+ const doc = iframe.contentDocument;
111
+ if (!doc) {
112
+ throw new Error(
113
+ "Cannot access iframe document for bundle injection. Cross-origin content is not supported; use { html } (srcdoc) or a same-origin URL."
114
+ );
115
+ }
116
+ const script = doc.createElement("script");
117
+ if (bundleUrl) {
118
+ script.src = bundleUrl;
119
+ } else {
120
+ const injected = iframe.contentWindow?.__SPEAKABLE__;
121
+ if (!injected) {
122
+ throw new Error(
123
+ "No bundleUrl provided and the iframe does not already include the Speakable bundle. Pass options.bundleUrl."
124
+ );
125
+ }
126
+ return;
127
+ }
128
+ doc.head?.appendChild(script) ?? doc.documentElement.appendChild(script);
129
+ }
130
+ return {
131
+ async load(source) {
132
+ if ("url" in source && isCrossOrigin(source.url)) {
133
+ throw new Error(
134
+ `Cannot inject into cross-origin URL "${source.url}". Use { html } (srcdoc) or a same-origin URL.`
135
+ );
136
+ }
137
+ const loaded = waitForLoad();
138
+ if ("url" in source) {
139
+ iframe.src = source.url;
140
+ } else {
141
+ iframe.srcdoc = source.html;
142
+ }
143
+ await loaded;
144
+ const ready = waitForReady();
145
+ injectBundle();
146
+ const alreadyPresent = iframe.contentWindow?.__SPEAKABLE__;
147
+ if (alreadyPresent) return;
148
+ await ready;
149
+ },
150
+ async analyze(selector) {
151
+ const res = await request({ kind: "analyze", ...selector && { selector } });
152
+ if (res.ok && res.kind === "analyze") return res.result;
153
+ throw new Error(res.ok ? "Unexpected response kind" : res.error);
154
+ },
155
+ async captureTimeline(opts) {
156
+ const res = await request({ kind: "capture", options: opts });
157
+ if (res.ok && res.kind === "capture") return res.result;
158
+ throw new Error(res.ok ? "Unexpected response kind" : res.error);
159
+ },
160
+ destroy() {
161
+ window.removeEventListener("message", onMessage);
162
+ for (const [, entry] of pending) {
163
+ clearTimeout(entry.timer);
164
+ entry.reject(new Error("Harness destroyed"));
165
+ }
166
+ pending.clear();
167
+ if (ownsIframe && iframe.parentNode) {
168
+ iframe.parentNode.removeChild(iframe);
169
+ }
170
+ }
171
+ };
172
+ }
173
+ export {
174
+ createHarness
175
+ };
176
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/harness/protocol.ts","../../src/harness/index.ts"],"sourcesContent":["/**\n * Shared message protocol for the iframe harness.\n *\n * The parent-page controller and the in-iframe agent exchange these\n * request/response envelopes over `postMessage`. Every request carries a\n * unique `id` so responses can be correlated.\n */\n\nimport type { AnalysisResult } from '../browser/analyze.js';\nimport type { AccessibilityTimeline, InteractionSequence } from '../runtime/types.js';\n\n/** Namespace tag stamped on every harness message to avoid collisions. */\nexport const HARNESS_CHANNEL = 'speakable-harness';\n\n/** Serializable subset of CaptureOptions safe to send over postMessage. */\nexport interface SerializableCaptureOptions {\n componentName: string;\n storyName?: string;\n sequence: InteractionSequence;\n settlePeriod?: number;\n awaitUpgrade?: boolean;\n upgradeTimeoutMs?: number;\n}\n\nexport type HarnessRequest =\n | { channel: typeof HARNESS_CHANNEL; id: string; kind: 'analyze'; selector?: string }\n | { channel: typeof HARNESS_CHANNEL; id: string; kind: 'capture'; options: SerializableCaptureOptions };\n\nexport type HarnessResponse =\n | { channel: typeof HARNESS_CHANNEL; id: string; ok: true; kind: 'analyze'; result: AnalysisResult }\n | { channel: typeof HARNESS_CHANNEL; id: string; ok: true; kind: 'capture'; result: AccessibilityTimeline }\n | { channel: typeof HARNESS_CHANNEL; id: string; ok: false; error: string };\n\n/** Sent by the agent once it has installed its message listener. */\nexport interface HarnessReadyMessage {\n channel: typeof HARNESS_CHANNEL;\n kind: 'ready';\n}\n\nexport function isHarnessRequest(data: unknown): data is HarnessRequest {\n if (!data || typeof data !== 'object') return false;\n const d = data as Record<string, unknown>;\n return d.channel === HARNESS_CHANNEL && typeof d.id === 'string' &&\n (d.kind === 'analyze' || d.kind === 'capture');\n}\n\nexport function isHarnessResponse(data: unknown): data is HarnessResponse {\n if (!data || typeof data !== 'object') return false;\n const d = data as Record<string, unknown>;\n return d.channel === HARNESS_CHANNEL && typeof d.id === 'string' && typeof d.ok === 'boolean';\n}\n\nexport function isHarnessReady(data: unknown): data is HarnessReadyMessage {\n if (!data || typeof data !== 'object') return false;\n const d = data as Record<string, unknown>;\n return d.channel === HARNESS_CHANNEL && d.kind === 'ready';\n}\n","/**\n * Iframe harness — parent-page controller.\n *\n * Runs in a parent page, mounts a component (URL or HTML string) into an\n * iframe, injects the browser bundle, and drives analysis/capture over\n * `postMessage`. This is the Storybook-independent runtime testing surface.\n */\n\nimport type { AnalysisResult } from '../browser/analyze.js';\nimport type { AccessibilityTimeline } from '../runtime/types.js';\nimport {\n HARNESS_CHANNEL,\n isHarnessReady,\n isHarnessResponse,\n type HarnessResponse,\n type SerializableCaptureOptions,\n} from './protocol.js';\n\nexport interface HarnessOptions {\n /** An existing iframe to drive, or a container to create one inside. */\n target: HTMLIFrameElement | { container: HTMLElement };\n /** Load + inject + request timeout in ms (default: 10000). */\n timeoutMs?: number;\n /** Origins allowed for postMessage. Defaults to same-origin only. */\n allowedOrigins?: string[];\n /**\n * URL to the injectable IIFE bundle. When omitted, the harness assumes the\n * bundle is already present in the iframe (e.g. bundled into the page).\n */\n bundleUrl?: string;\n}\n\nexport type LoadSource = { url: string } | { html: string };\n\nexport interface Harness {\n load(source: LoadSource): Promise<void>;\n analyze(selector?: string): Promise<AnalysisResult>;\n captureTimeline(options: SerializableCaptureOptions): Promise<AccessibilityTimeline>;\n destroy(): void;\n}\n\nconst DEFAULT_TIMEOUT = 10000;\n\nlet idCounter = 0;\nfunction nextId(): string {\n idCounter += 1;\n return `req-${Date.now()}-${idCounter}`;\n}\n\nfunction isCrossOrigin(url: string): boolean {\n try {\n const resolved = new URL(url, window.location.href);\n return resolved.origin !== window.location.origin;\n } catch {\n // Relative or malformed — treat as same-origin.\n return false;\n }\n}\n\nexport function createHarness(options: HarnessOptions): Harness {\n const { timeoutMs = DEFAULT_TIMEOUT, allowedOrigins, bundleUrl } = options;\n\n // Resolve or create the iframe.\n let ownsIframe = false;\n let iframe: HTMLIFrameElement;\n if (options.target instanceof HTMLIFrameElement) {\n iframe = options.target;\n } else {\n iframe = document.createElement('iframe');\n iframe.setAttribute('title', 'speakable-harness');\n options.target.container.appendChild(iframe);\n ownsIframe = true;\n }\n\n const pending = new Map<\n string,\n { resolve: (r: HarnessResponse) => void; reject: (e: Error) => void; timer: ReturnType<typeof setTimeout> }\n >();\n\n function onMessage(event: MessageEvent) {\n // Only accept messages originating from our iframe's window.\n if (event.source !== iframe.contentWindow) return;\n if (allowedOrigins && allowedOrigins.length > 0 && !allowedOrigins.includes('*')) {\n if (!allowedOrigins.includes(event.origin)) return;\n }\n const data = event.data;\n if (!isHarnessResponse(data)) return;\n const entry = pending.get(data.id);\n if (!entry) return;\n clearTimeout(entry.timer);\n pending.delete(data.id);\n entry.resolve(data);\n }\n\n window.addEventListener('message', onMessage);\n\n function targetOrigin(): string {\n if (allowedOrigins && allowedOrigins.includes('*')) return '*';\n try {\n return iframe.contentWindow ? new URL(iframe.src || window.location.href).origin : '*';\n } catch {\n return '*';\n }\n }\n\n function request(payload: Record<string, unknown>): Promise<HarnessResponse> {\n const id = nextId();\n const win = iframe.contentWindow;\n if (!win) {\n return Promise.reject(new Error('Harness iframe has no content window; call load() first.'));\n }\n return new Promise<HarnessResponse>((resolve, reject) => {\n const timer = setTimeout(() => {\n pending.delete(id);\n reject(new Error(`Harness request \"${payload.kind}\" timed out after ${timeoutMs}ms`));\n }, timeoutMs);\n pending.set(id, { resolve, reject, timer });\n win.postMessage({ channel: HARNESS_CHANNEL, id, ...payload }, targetOrigin());\n });\n }\n\n function waitForReady(): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n const timer = setTimeout(() => {\n window.removeEventListener('message', onReady);\n reject(new Error(`Harness bundle did not signal ready within ${timeoutMs}ms`));\n }, timeoutMs);\n function onReady(event: MessageEvent) {\n if (event.source !== iframe.contentWindow) return;\n if (!isHarnessReady(event.data)) return;\n clearTimeout(timer);\n window.removeEventListener('message', onReady);\n resolve();\n }\n window.addEventListener('message', onReady);\n });\n }\n\n function waitForLoad(): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n const timer = setTimeout(() => {\n iframe.removeEventListener('load', onLoad);\n reject(new Error(`Harness iframe failed to load within ${timeoutMs}ms`));\n }, timeoutMs);\n function onLoad() {\n clearTimeout(timer);\n iframe.removeEventListener('load', onLoad);\n resolve();\n }\n iframe.addEventListener('load', onLoad);\n });\n }\n\n function injectBundle(): void {\n const doc = iframe.contentDocument;\n if (!doc) {\n throw new Error(\n 'Cannot access iframe document for bundle injection. Cross-origin content is not supported; use { html } (srcdoc) or a same-origin URL.'\n );\n }\n const script = doc.createElement('script');\n if (bundleUrl) {\n script.src = bundleUrl;\n } else {\n // No bundle URL provided and no bundle detected — surface a clear error\n // rather than hanging on waitForReady.\n const injected = (iframe.contentWindow as unknown as { __SPEAKABLE__?: unknown })?.__SPEAKABLE__;\n if (!injected) {\n throw new Error(\n 'No bundleUrl provided and the iframe does not already include the Speakable bundle. Pass options.bundleUrl.'\n );\n }\n return;\n }\n doc.head?.appendChild(script) ?? doc.documentElement.appendChild(script);\n }\n\n return {\n async load(source: LoadSource): Promise<void> {\n // Validate before starting any timers so we don't leak a pending\n // load-wait rejection when we bail out synchronously.\n if ('url' in source && isCrossOrigin(source.url)) {\n throw new Error(\n `Cannot inject into cross-origin URL \"${source.url}\". Use { html } (srcdoc) or a same-origin URL.`\n );\n }\n\n const loaded = waitForLoad();\n if ('url' in source) {\n iframe.src = source.url;\n } else {\n iframe.srcdoc = source.html;\n }\n await loaded;\n\n const ready = waitForReady();\n injectBundle();\n // If the bundle was already present, it may have signaled ready before we\n // attached the listener; fall back to a short probe.\n const alreadyPresent = (iframe.contentWindow as unknown as { __SPEAKABLE__?: unknown })?.__SPEAKABLE__;\n if (alreadyPresent) return;\n await ready;\n },\n\n async analyze(selector?: string): Promise<AnalysisResult> {\n const res = await request({ kind: 'analyze', ...(selector && { selector }) });\n if (res.ok && res.kind === 'analyze') return res.result;\n throw new Error(res.ok ? 'Unexpected response kind' : res.error);\n },\n\n async captureTimeline(opts: SerializableCaptureOptions): Promise<AccessibilityTimeline> {\n const res = await request({ kind: 'capture', options: opts });\n if (res.ok && res.kind === 'capture') return res.result;\n throw new Error(res.ok ? 'Unexpected response kind' : res.error);\n },\n\n destroy(): void {\n window.removeEventListener('message', onMessage);\n for (const [, entry] of pending) {\n clearTimeout(entry.timer);\n entry.reject(new Error('Harness destroyed'));\n }\n pending.clear();\n if (ownsIframe && iframe.parentNode) {\n iframe.parentNode.removeChild(iframe);\n }\n },\n };\n}\n\nexport type { SerializableCaptureOptions } from './protocol.js';\n"],"mappings":"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","names":[]}
package/dist/index.js CHANGED
@@ -2521,17 +2521,54 @@ function processChildNode(node, children, warnings) {
2521
2521
  }
2522
2522
  }
2523
2523
  function collectChildrenFromRolelessElement(element, warnings) {
2524
+ return collectAccessibleChildren(element, warnings);
2525
+ }
2526
+ function collectAccessibleChildren(element, warnings) {
2524
2527
  const children = [];
2525
2528
  if (element.shadowRoot) {
2526
2529
  for (const child of Array.from(element.shadowRoot.childNodes)) {
2527
- processChildNode(child, children, warnings);
2530
+ processShadowNode(child, children, warnings);
2528
2531
  }
2532
+ return children;
2533
+ }
2534
+ if (element.localName && element.localName.includes("-") && element.childNodes.length === 0) {
2535
+ warnings.push({
2536
+ message: `Custom element <${element.localName}> has no accessible children and may have a closed shadow root; its content could not be analyzed.`,
2537
+ element
2538
+ });
2529
2539
  }
2530
2540
  for (const child of Array.from(element.childNodes)) {
2531
2541
  processChildNode(child, children, warnings);
2532
2542
  }
2533
2543
  return children;
2534
2544
  }
2545
+ function processShadowNode(node, children, warnings) {
2546
+ if (node.nodeType === 1) {
2547
+ const element = node;
2548
+ if (element.localName === "slot") {
2549
+ const slot = element;
2550
+ let assigned = [];
2551
+ if (typeof slot.assignedNodes === "function") {
2552
+ try {
2553
+ assigned = Array.from(slot.assignedNodes({ flatten: true }));
2554
+ } catch {
2555
+ assigned = [];
2556
+ }
2557
+ }
2558
+ if (assigned.length > 0) {
2559
+ for (const assignedNode of assigned) {
2560
+ processChildNode(assignedNode, children, warnings);
2561
+ }
2562
+ } else {
2563
+ for (const child of Array.from(slot.childNodes)) {
2564
+ processShadowNode(child, children, warnings);
2565
+ }
2566
+ }
2567
+ return;
2568
+ }
2569
+ }
2570
+ processChildNode(node, children, warnings);
2571
+ }
2535
2572
  function buildAccessibilityTree(rootElement, sourceHash) {
2536
2573
  const warnings = [];
2537
2574
  if (isHidden(rootElement)) {
@@ -2624,15 +2661,7 @@ function buildNodeRecursive(element, warnings) {
2624
2661
  element: w.element
2625
2662
  })));
2626
2663
  const focusInfo = extractFocusInfo(element);
2627
- const children = [];
2628
- if (element.shadowRoot) {
2629
- for (const child of Array.from(element.shadowRoot.childNodes)) {
2630
- processChildNode(child, children, warnings);
2631
- }
2632
- }
2633
- for (const child of Array.from(element.childNodes)) {
2634
- processChildNode(child, children, warnings);
2635
- }
2664
+ const children = collectAccessibleChildren(element, warnings);
2636
2665
  const node = {
2637
2666
  role: roleResult.role,
2638
2667
  name: nameResult.name,
@@ -2645,15 +2674,7 @@ function buildNodeRecursive(element, warnings) {
2645
2674
  return node;
2646
2675
  }
2647
2676
  function createGenericContainer(rootElement, warnings) {
2648
- const children = [];
2649
- if (rootElement.shadowRoot) {
2650
- for (const child of Array.from(rootElement.shadowRoot.childNodes)) {
2651
- processChildNode(child, children, warnings);
2652
- }
2653
- }
2654
- for (const child of Array.from(rootElement.childNodes)) {
2655
- processChildNode(child, children, warnings);
2656
- }
2677
+ const children = collectAccessibleChildren(rootElement, warnings);
2657
2678
  return {
2658
2679
  role: "generic",
2659
2680
  name: "",
@@ -4365,8 +4386,10 @@ function createHeuristicAnalyzer(config) {
4365
4386
  };
4366
4387
  }
4367
4388
 
4389
+ // src/version.ts
4390
+ var SPEAKABLE_VERSION = "1.7.0" ? "1.7.0" : "dev";
4391
+
4368
4392
  // src/runtime/timeline-generator.ts
4369
- var SPEAKABLE_VERSION = "1.3.0";
4370
4393
  var TIMELINE_FORMAT_VERSION = "1.0";
4371
4394
  var DEFAULT_SETTLE_PERIOD = 500;
4372
4395
  var DEFAULT_LOAD_TIMEOUT = 1e4;
@@ -5123,7 +5146,6 @@ function classifyModified(entry) {
5123
5146
  // src/runtime/baseline-storage.ts
5124
5147
  import { readFile, writeFile, mkdir, access } from "fs/promises";
5125
5148
  import { join, dirname } from "path";
5126
- var SPEAKABLE_VERSION2 = "1.3.0";
5127
5149
  function createBaselineStorage(baseDir) {
5128
5150
  function sanitize(name) {
5129
5151
  return name.toLowerCase().replace(/[/\\]/g, "-").replace(/[^a-z0-9._-]/g, "-");
@@ -5143,7 +5165,7 @@ function createBaselineStorage(baseDir) {
5143
5165
  const baselineFile = {
5144
5166
  baseline: {
5145
5167
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
5146
- speakableVersion: SPEAKABLE_VERSION2,
5168
+ speakableVersion: SPEAKABLE_VERSION,
5147
5169
  storybookUrl: meta?.storybookUrl,
5148
5170
  interactionSequence: timeline.interactionSequence
5149
5171
  },