@runtypelabs/persona 3.21.2 → 3.22.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.
Files changed (59) hide show
  1. package/README.md +67 -0
  2. package/dist/animations/glyph-cycle.d.cts +1 -1
  3. package/dist/animations/glyph-cycle.d.ts +1 -1
  4. package/dist/animations/{types-CWPIj66R.d.cts → types-BZVr1YOV.d.cts} +10 -0
  5. package/dist/animations/{types-CWPIj66R.d.ts → types-BZVr1YOV.d.ts} +10 -0
  6. package/dist/animations/wipe.d.cts +1 -1
  7. package/dist/animations/wipe.d.ts +1 -1
  8. package/dist/index.cjs +50 -43
  9. package/dist/index.cjs.map +1 -1
  10. package/dist/index.d.cts +474 -6
  11. package/dist/index.d.ts +474 -6
  12. package/dist/index.global.js +98 -88
  13. package/dist/index.global.js.map +1 -1
  14. package/dist/index.js +48 -41
  15. package/dist/index.js.map +1 -1
  16. package/dist/smart-dom-reader.cjs +1875 -0
  17. package/dist/smart-dom-reader.d.cts +4521 -0
  18. package/dist/smart-dom-reader.d.ts +4521 -0
  19. package/dist/smart-dom-reader.js +1848 -0
  20. package/dist/theme-editor.cjs +2282 -90
  21. package/dist/theme-editor.d.cts +348 -1
  22. package/dist/theme-editor.d.ts +348 -1
  23. package/dist/theme-editor.js +2267 -90
  24. package/package.json +9 -2
  25. package/src/client.test.ts +165 -0
  26. package/src/client.ts +144 -23
  27. package/src/components/composer-parts.test.ts +34 -0
  28. package/src/components/composer-parts.ts +9 -6
  29. package/src/index.ts +26 -0
  30. package/src/session.test.ts +258 -0
  31. package/src/session.ts +886 -30
  32. package/src/session.webmcp.test.ts +815 -0
  33. package/src/smart-dom-reader.test.ts +135 -0
  34. package/src/smart-dom-reader.ts +135 -0
  35. package/src/theme-editor/color-utils.test.ts +59 -0
  36. package/src/theme-editor/color-utils.ts +38 -2
  37. package/src/theme-editor/index.ts +35 -0
  38. package/src/theme-editor/webmcp/coerce.test.ts +86 -0
  39. package/src/theme-editor/webmcp/coerce.ts +286 -0
  40. package/src/theme-editor/webmcp/index.ts +45 -0
  41. package/src/theme-editor/webmcp/summary.ts +324 -0
  42. package/src/theme-editor/webmcp/tools.test.ts +205 -0
  43. package/src/theme-editor/webmcp/tools.ts +795 -0
  44. package/src/theme-editor/webmcp/types.ts +87 -0
  45. package/src/types.ts +186 -0
  46. package/src/ui.composer-keyboard.test.ts +229 -0
  47. package/src/ui.ts +127 -5
  48. package/src/utils/composer-history.test.ts +128 -0
  49. package/src/utils/composer-history.ts +113 -0
  50. package/src/utils/message-fingerprint.test.ts +20 -0
  51. package/src/utils/message-fingerprint.ts +2 -0
  52. package/src/utils/smart-dom-adapter.test.ts +257 -0
  53. package/src/utils/smart-dom-adapter.ts +217 -0
  54. package/{LICENSE → src/vendor/smart-dom-reader/LICENSE} +2 -2
  55. package/src/vendor/smart-dom-reader/README.md +61 -0
  56. package/src/vendor/smart-dom-reader/index.d.ts +476 -0
  57. package/src/vendor/smart-dom-reader/index.js +1618 -0
  58. package/src/webmcp-bridge.test.ts +429 -0
  59. package/src/webmcp-bridge.ts +547 -0
@@ -0,0 +1,257 @@
1
+ // Pure mapper test — no DOM, no installed/vendored library at runtime (types only).
2
+ import { describe, it, expect } from "vitest";
3
+ import { smartDomResultToEnriched } from "./smart-dom-adapter";
4
+ import { formatEnrichedContext } from "./dom-context";
5
+ import type {
6
+ SmartDOMResult,
7
+ ExtractedElement,
8
+ ElementSelector
9
+ } from "../vendor/smart-dom-reader";
10
+
11
+ function makeSelector(partial: Partial<ElementSelector> = {}): ElementSelector {
12
+ return { css: "div", xpath: "/html/body/div", ...partial };
13
+ }
14
+
15
+ function makeEl(partial: Partial<ExtractedElement> = {}): ExtractedElement {
16
+ return {
17
+ tag: partial.tag ?? "div",
18
+ text: partial.text ?? "",
19
+ selector: partial.selector ?? makeSelector(),
20
+ attributes: partial.attributes ?? {},
21
+ context: {
22
+ parentChain: [],
23
+ ...(partial.context ?? {})
24
+ },
25
+ interaction: partial.interaction ?? {},
26
+ children: partial.children
27
+ };
28
+ }
29
+
30
+ function makeResult(
31
+ interactive: Partial<SmartDOMResult["interactive"]> = {},
32
+ semantic?: SmartDOMResult["semantic"]
33
+ ): SmartDOMResult {
34
+ return {
35
+ mode: semantic ? "full" : "interactive",
36
+ timestamp: 0,
37
+ page: {
38
+ url: "https://example.com",
39
+ title: "Example",
40
+ hasErrors: false,
41
+ isLoading: false,
42
+ hasModals: false
43
+ },
44
+ landmarks: {
45
+ navigation: [],
46
+ main: [],
47
+ forms: [],
48
+ headers: [],
49
+ footers: [],
50
+ articles: [],
51
+ sections: []
52
+ },
53
+ interactive: {
54
+ buttons: [],
55
+ links: [],
56
+ inputs: [],
57
+ forms: [],
58
+ clickable: [],
59
+ ...interactive
60
+ },
61
+ semantic
62
+ };
63
+ }
64
+
65
+ describe("smartDomResultToEnriched", () => {
66
+ it("prefers the highest-scoring plain-CSS candidate and skips XPath/text", () => {
67
+ const result = makeResult({
68
+ buttons: [
69
+ makeEl({
70
+ tag: "button",
71
+ text: "Add to cart",
72
+ interaction: { click: true },
73
+ selector: makeSelector({
74
+ css: "button.add",
75
+ xpath: "/html/body/button[1]",
76
+ candidates: [
77
+ { type: "xpath", value: "/html/body/button[1]", score: 100 },
78
+ { type: "text", value: "text=Add to cart", score: 90 },
79
+ { type: "data-testid", value: '[data-testid="add"]', score: 80 },
80
+ { type: "class-path", value: "button.add", score: 50 }
81
+ ]
82
+ })
83
+ })
84
+ ]
85
+ });
86
+
87
+ const enriched = smartDomResultToEnriched(result);
88
+ expect(enriched).toHaveLength(1);
89
+ // best plain-CSS candidate by score (data-testid:80 > class-path:50; xpath/text skipped)
90
+ expect(enriched[0].selector).toBe('[data-testid="add"]');
91
+ expect(enriched[0].interactivity).toBe("clickable");
92
+ });
93
+
94
+ it("falls back to selector.css when no candidates qualify", () => {
95
+ const result = makeResult({
96
+ buttons: [
97
+ makeEl({
98
+ tag: "button",
99
+ text: "Go",
100
+ interaction: { click: true },
101
+ selector: makeSelector({ css: "button.go", candidates: [] })
102
+ })
103
+ ]
104
+ });
105
+ const enriched = smartDomResultToEnriched(result);
106
+ expect(enriched[0].selector).toBe("button.go");
107
+ });
108
+
109
+ it("classifies interactivity from tag/role/interaction", () => {
110
+ const result = makeResult({
111
+ buttons: [
112
+ makeEl({
113
+ tag: "button",
114
+ text: "Buy",
115
+ interaction: { click: true },
116
+ selector: makeSelector({ css: "button.buy" })
117
+ })
118
+ ],
119
+ links: [
120
+ makeEl({
121
+ tag: "a",
122
+ text: "Home",
123
+ attributes: { href: "/home" },
124
+ interaction: { nav: true },
125
+ selector: makeSelector({ css: "a.home" })
126
+ })
127
+ ],
128
+ inputs: [
129
+ makeEl({
130
+ tag: "input",
131
+ attributes: { type: "text", name: "q" },
132
+ interaction: { change: true },
133
+ selector: makeSelector({ css: "input.q" })
134
+ })
135
+ ]
136
+ });
137
+
138
+ const enriched = smartDomResultToEnriched(result);
139
+ const bySel = Object.fromEntries(enriched.map((e) => [e.selector, e]));
140
+ expect(bySel["button.buy"].interactivity).toBe("clickable");
141
+ expect(bySel["a.home"].interactivity).toBe("navigable");
142
+ expect(bySel["input.q"].interactivity).toBe("input");
143
+ });
144
+
145
+ it("excludes elements under the host (.persona-host) via parentChain", () => {
146
+ const result = makeResult({
147
+ buttons: [
148
+ makeEl({
149
+ tag: "button",
150
+ text: "Widget send",
151
+ interaction: { click: true },
152
+ selector: makeSelector({ css: "button.send" }),
153
+ context: { parentChain: ["div.persona-host", "div.panel"] }
154
+ }),
155
+ makeEl({
156
+ tag: "button",
157
+ text: "Page button",
158
+ interaction: { click: true },
159
+ selector: makeSelector({ css: "button.page" }),
160
+ context: { parentChain: ["main", "section"] }
161
+ })
162
+ ]
163
+ });
164
+
165
+ const enriched = smartDomResultToEnriched(result);
166
+ expect(enriched.map((e) => e.selector)).toEqual(["button.page"]);
167
+ });
168
+
169
+ it("includes semantic elements only when present and not disabled", () => {
170
+ const semantic: SmartDOMResult["semantic"] = {
171
+ headings: [
172
+ makeEl({
173
+ tag: "h1",
174
+ text: "Title",
175
+ selector: makeSelector({ css: "h1.title" })
176
+ })
177
+ ],
178
+ images: [],
179
+ tables: [],
180
+ lists: [],
181
+ articles: []
182
+ };
183
+ const result = makeResult(
184
+ {
185
+ buttons: [
186
+ makeEl({
187
+ tag: "button",
188
+ text: "Go",
189
+ interaction: { click: true },
190
+ selector: makeSelector({ css: "button.go" })
191
+ })
192
+ ]
193
+ },
194
+ semantic
195
+ );
196
+
197
+ const withSemantic = smartDomResultToEnriched(result);
198
+ expect(withSemantic.map((e) => e.selector)).toContain("h1.title");
199
+ const heading = withSemantic.find((e) => e.selector === "h1.title");
200
+ expect(heading?.interactivity).toBe("static");
201
+
202
+ const withoutSemantic = smartDomResultToEnriched(result, {
203
+ includeSemantic: false
204
+ });
205
+ expect(withoutSemantic.map((e) => e.selector)).not.toContain("h1.title");
206
+ });
207
+
208
+ it("deduplicates by selector and honors maxElements", () => {
209
+ const dup = makeEl({
210
+ tag: "button",
211
+ text: "Dup",
212
+ interaction: { click: true },
213
+ selector: makeSelector({ css: "button.dup" })
214
+ });
215
+ const result = makeResult({
216
+ buttons: [dup, dup],
217
+ clickable: [dup]
218
+ });
219
+ expect(smartDomResultToEnriched(result)).toHaveLength(1);
220
+
221
+ const many = makeResult({
222
+ buttons: [
223
+ makeEl({ tag: "button", text: "1", interaction: { click: true }, selector: makeSelector({ css: "button.a" }) }),
224
+ makeEl({ tag: "button", text: "2", interaction: { click: true }, selector: makeSelector({ css: "button.b" }) }),
225
+ makeEl({ tag: "button", text: "3", interaction: { click: true }, selector: makeSelector({ css: "button.c" }) })
226
+ ]
227
+ });
228
+ expect(smartDomResultToEnriched(many, { maxElements: 2 })).toHaveLength(2);
229
+ });
230
+
231
+ it("produces output that feeds formatEnrichedContext", () => {
232
+ const result = makeResult({
233
+ buttons: [
234
+ makeEl({
235
+ tag: "button",
236
+ text: "Checkout",
237
+ interaction: { click: true },
238
+ selector: makeSelector({ css: "button.checkout" })
239
+ })
240
+ ],
241
+ links: [
242
+ makeEl({
243
+ tag: "a",
244
+ text: "Cart",
245
+ attributes: { href: "/cart" },
246
+ interaction: { nav: true },
247
+ selector: makeSelector({ css: "a.cart" })
248
+ })
249
+ ]
250
+ });
251
+
252
+ const formatted = formatEnrichedContext(smartDomResultToEnriched(result));
253
+ expect(formatted).toContain("button.checkout");
254
+ expect(formatted).toContain("a.cart");
255
+ expect(formatted).toContain("Checkout");
256
+ });
257
+ });
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Pure mapper: `@mcp-b/smart-dom-reader` output → Persona's {@link EnrichedPageElement}[].
3
+ *
4
+ * This module imports the smart-dom-reader types with `import type` ONLY, so the
5
+ * library's runtime value is never pulled in here. That keeps the mapper — and its
6
+ * unit test — free of any DOM and free of the (vendored) library at runtime; the
7
+ * types are erased during compilation.
8
+ *
9
+ * The smart-dom-reader returns a structured {@link SmartDOMResult} JSON object with
10
+ * rich selectors (CSS + XPath + ranked candidates). Persona's collect → reason → act
11
+ * loop drives clicks through `document.querySelector` (see `utils/actions.ts`), which
12
+ * cannot pierce shadow roots or evaluate XPath. So this mapper deliberately prefers
13
+ * the **best plain-CSS candidate selector** for each element and skips XPath / text
14
+ * pseudo-selectors, keeping results actionable.
15
+ */
16
+
17
+ import type {
18
+ SmartDOMResult,
19
+ ExtractedElement,
20
+ ElementSelectorCandidate
21
+ } from "../vendor/smart-dom-reader";
22
+ import type { EnrichedPageElement } from "./dom-context";
23
+
24
+ /** Options for {@link smartDomResultToEnriched}. */
25
+ export interface SmartDomAdapterOptions {
26
+ /**
27
+ * Include non-interactive `semantic` groups (headings, images, tables, lists,
28
+ * articles) when the result has them (i.e. full-mode extraction). Default: true.
29
+ */
30
+ includeSemantic?: boolean;
31
+ /**
32
+ * Skip elements whose own selector / ancestor chain contains this selector string,
33
+ * so the widget never reports its own shadow-DOM UI. Matched as a substring against
34
+ * the element's candidate selectors and `context.parentChain`. Default: ".persona-host".
35
+ * Pass "" to disable.
36
+ */
37
+ excludeSelector?: string;
38
+ /** Truncate each element's text to this many characters. Default: 200. */
39
+ maxTextLength?: number;
40
+ /** Optional cap on the number of mapped elements returned. */
41
+ maxElements?: number;
42
+ }
43
+
44
+ /**
45
+ * Candidate selector types that resolve through `document.querySelector` (plain CSS).
46
+ * Everything else smart-dom-reader can emit — `xpath` and text pseudo-selectors —
47
+ * is not actionable via the current click loop, so it is skipped here.
48
+ */
49
+ const PLAIN_CSS_CANDIDATE_TYPES: ReadonlySet<ElementSelectorCandidate["type"]> =
50
+ new Set(["id", "data-testid", "role-aria", "name", "class-path", "css-path"]);
51
+
52
+ /** Looks like an XPath expression rather than a CSS selector. */
53
+ function looksLikeXPath(value: string): boolean {
54
+ const v = value.trim();
55
+ return v.startsWith("/") || v.startsWith("(") || v.startsWith("./");
56
+ }
57
+
58
+ /**
59
+ * Pick the best `document.querySelector`-able selector for an extracted element:
60
+ * the highest-scoring plain-CSS candidate, falling back to the primary `css` field
61
+ * when no candidate qualifies. Returns null when only XPath-like selectors exist.
62
+ */
63
+ function bestPlainCssSelector(el: ExtractedElement): string | null {
64
+ const candidates = el.selector.candidates;
65
+ if (candidates && candidates.length > 0) {
66
+ let best: ElementSelectorCandidate | null = null;
67
+ for (const c of candidates) {
68
+ if (!PLAIN_CSS_CANDIDATE_TYPES.has(c.type)) continue;
69
+ if (!c.value || looksLikeXPath(c.value)) continue;
70
+ if (!best || c.score > best.score) best = c;
71
+ }
72
+ if (best) return best.value;
73
+ }
74
+ const css = el.selector.css;
75
+ if (css && !looksLikeXPath(css)) return css;
76
+ return null;
77
+ }
78
+
79
+ /** Classify an extracted element into Persona's interactivity buckets. */
80
+ function classifyInteractivity(
81
+ el: ExtractedElement
82
+ ): EnrichedPageElement["interactivity"] {
83
+ const tag = el.tag.toLowerCase();
84
+ const role = el.interaction.role ?? el.attributes.role;
85
+
86
+ if (tag === "a" && (el.interaction.nav || el.attributes.href != null)) {
87
+ return "navigable";
88
+ }
89
+ if (tag === "input" || tag === "select" || tag === "textarea") return "input";
90
+ if (
91
+ role === "textbox" ||
92
+ role === "combobox" ||
93
+ role === "listbox" ||
94
+ role === "spinbutton"
95
+ ) {
96
+ return "input";
97
+ }
98
+ if (el.interaction.change && !el.interaction.click) return "input";
99
+ if (
100
+ tag === "button" ||
101
+ role === "button" ||
102
+ el.interaction.click ||
103
+ el.interaction.submit
104
+ ) {
105
+ return "clickable";
106
+ }
107
+ return "static";
108
+ }
109
+
110
+ /** Build the relevant-attribute bag, mirroring `dom-context.collectAttributes`. */
111
+ function collectAttributes(el: ExtractedElement): Record<string, string> {
112
+ const attrs: Record<string, string> = { ...el.attributes };
113
+ const role = el.interaction.role;
114
+ if (role && !attrs.role) attrs.role = role;
115
+ return attrs;
116
+ }
117
+
118
+ /**
119
+ * Returns true when the element sits under `excludeSelector` (e.g. the widget host).
120
+ * smart-dom-reader has no exclude option and pierces shadow DOM by default, so this
121
+ * guards against the widget reading its own UI. The check is a substring match across
122
+ * the element's candidate selectors and ancestor chain — robust for the default
123
+ * `.persona-host` class guard.
124
+ */
125
+ function isExcluded(el: ExtractedElement, excludeSelector: string): boolean {
126
+ if (!excludeSelector) return false;
127
+ const haystacks: Array<string | undefined> = [
128
+ el.selector.css,
129
+ el.selector.xpath,
130
+ el.context.nearestForm,
131
+ el.context.nearestSection,
132
+ el.context.nearestMain,
133
+ el.context.nearestNav,
134
+ ...(el.selector.candidates?.map((c) => c.value) ?? []),
135
+ ...el.context.parentChain
136
+ ];
137
+ return haystacks.some((h) => !!h && h.includes(excludeSelector));
138
+ }
139
+
140
+ /**
141
+ * Map a {@link SmartDOMResult} into Persona's {@link EnrichedPageElement}[] shape so it
142
+ * can be formatted by `formatEnrichedContext` and consumed wherever the default
143
+ * `collectEnrichedPageContext` output is.
144
+ *
145
+ * - Maps `interactive.{buttons, links, inputs, clickable}` and, when present and
146
+ * `includeSemantic` is not false, `semantic.{headings, images, tables, lists, articles}`.
147
+ * - Chooses the best plain-CSS selector per element (skipping XPath / shadow-piercing
148
+ * selectors) so results stay actionable via `document.querySelector`.
149
+ * - Drops elements under `excludeSelector` (default `.persona-host`).
150
+ * - Deduplicates by selector and preserves discovery order (interactive before semantic).
151
+ */
152
+ export function smartDomResultToEnriched(
153
+ result: SmartDOMResult,
154
+ opts: SmartDomAdapterOptions = {}
155
+ ): EnrichedPageElement[] {
156
+ const includeSemantic = opts.includeSemantic ?? true;
157
+ const excludeSelector = opts.excludeSelector ?? ".persona-host";
158
+ const maxTextLength = opts.maxTextLength ?? 200;
159
+
160
+ const groups: ExtractedElement[][] = [
161
+ result.interactive.buttons,
162
+ result.interactive.links,
163
+ result.interactive.inputs,
164
+ result.interactive.clickable
165
+ ];
166
+
167
+ if (includeSemantic && result.semantic) {
168
+ groups.push(
169
+ result.semantic.headings,
170
+ result.semantic.images,
171
+ result.semantic.tables,
172
+ result.semantic.lists,
173
+ result.semantic.articles
174
+ );
175
+ }
176
+
177
+ const out: EnrichedPageElement[] = [];
178
+ const seen = new Set<string>();
179
+
180
+ // Walk an element and any nested children. In full mode the library attaches
181
+ // shadow-DOM descendants as `children` of semantic containers, so recursing here
182
+ // is how those pierced elements surface. Returns false once the optional
183
+ // maxElements cap is reached.
184
+ const visit = (el: ExtractedElement): boolean => {
185
+ if (isExcluded(el, excludeSelector)) return true;
186
+
187
+ const selector = bestPlainCssSelector(el);
188
+ if (selector && !seen.has(selector)) {
189
+ seen.add(selector);
190
+ out.push({
191
+ selector,
192
+ tagName: el.tag.toLowerCase(),
193
+ text: (el.text ?? "").trim().substring(0, maxTextLength),
194
+ role: el.interaction.role ?? el.attributes.role ?? null,
195
+ interactivity: classifyInteractivity(el),
196
+ attributes: collectAttributes(el)
197
+ });
198
+ if (opts.maxElements && out.length >= opts.maxElements) return false;
199
+ }
200
+
201
+ if (el.children) {
202
+ for (const child of el.children) {
203
+ if (!visit(child)) return false;
204
+ }
205
+ }
206
+ return true;
207
+ };
208
+
209
+ for (const group of groups) {
210
+ if (!group) continue;
211
+ for (const el of group) {
212
+ if (!visit(el)) return out;
213
+ }
214
+ }
215
+
216
+ return out;
217
+ }
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Runtype
3
+ Copyright (c) 2025 mcp-b contributors
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
18
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
19
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
20
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
21
+ SOFTWARE.
@@ -0,0 +1,61 @@
1
+ # Vendored: `@mcp-b/smart-dom-reader`
2
+
3
+ This directory contains a **vendored copy** of [`@mcp-b/smart-dom-reader`](https://github.com/WebMCP-org/npm-packages/tree/main/packages/smart-dom-reader)
4
+ (v2.3.1, MIT, © 2025 mcp-b contributors), consumed only by the optional
5
+ `@runtypelabs/persona/smart-dom-reader` entry point (`src/smart-dom-reader.ts`)
6
+ and the pure mapper (`src/utils/smart-dom-adapter.ts`, type-only).
7
+
8
+ ## Why vendored instead of a dependency
9
+
10
+ Every published version of the package (2.3.1, 2.3.2, 3.0.0) is **mis-published**:
11
+ its `package.json` declares
12
+
13
+ ```json
14
+ "main": "./dist/index.js",
15
+ "types": "./dist/index.d.ts",
16
+ "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" } }
17
+ ```
18
+
19
+ but the build tool (`vp pack`, vite-plus/rolldown) only emits `dist/index.mjs` +
20
+ `dist/index.d.mts`. The files referenced by `package.json` are **absent from the
21
+ tarball**, so the package cannot be imported by name in Node or any bundler
22
+ (`ERR_MODULE_NOT_FOUND`), and TypeScript cannot resolve its types. This affects
23
+ the package itself and every downstream consumer, so making it an optional peer
24
+ dependency would ship a feature that no integrator could actually load.
25
+
26
+ Vendoring the built artifact into this opt-in entry sidesteps the broken module
27
+ resolution entirely: the code is bundled into `dist/smart-dom-reader.{js,cjs}`
28
+ and never touches a package resolver. Consumers who never import the
29
+ `/smart-dom-reader` entry pay nothing.
30
+
31
+ > **Follow-up:** raise the packaging bug with the upstream maintainer. Once a
32
+ > corrected release exists, this vendor dir can be replaced with a normal
33
+ > optional peer dependency (see the original plan).
34
+
35
+ ## Files
36
+
37
+ - `index.js` — upstream `dist/index.mjs`, with two local edits (see header comment).
38
+ - `index.d.ts` — upstream `dist/index.d.mts` verbatim (sourceMappingURL stripped).
39
+ - `LICENSE` — upstream MIT license.
40
+
41
+ ## Local modifications to `index.js`
42
+
43
+ The **only** changes from upstream `dist/index.mjs` are:
44
+
45
+ 1. Removed the top-level `import { createRequire } from "node:module";` — a
46
+ Node-only builtin that breaks browser bundling.
47
+ 2. Replaced `var __require = createRequire(import.meta.url);` with
48
+ `var __require = void 0;`. `__require` is referenced only inside a guarded
49
+ `typeof __require === "function"` Node fallback in `resolveSmartDomReader()`;
50
+ the browser path (`typeof window !== "undefined"`) returns before reaching it,
51
+ so this is a runtime no-op in the browser.
52
+
53
+ ## How to update
54
+
55
+ 1. `npm pack @mcp-b/smart-dom-reader@<version>` and extract the tarball.
56
+ 2. Copy `dist/index.mjs` → `index.js` and `dist/index.d.mts` → `index.d.ts`.
57
+ 3. Re-apply the two edits above (strip the `node:module` import; neutralize
58
+ `__require`) and the provenance headers. Strip trailing `sourceMappingURL`
59
+ comments.
60
+ 4. Copy the upstream `LICENSE`.
61
+ 5. Re-run `pnpm --filter @runtypelabs/persona build typecheck test:run`.