@shadow-js/devtools 0.0.0-stage → 1.0.1

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 (112) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/LICENSE +21 -0
  3. package/README.md +576 -2
  4. package/dist/dev/agent.d.ts +239 -0
  5. package/dist/dev/agent.js +626 -0
  6. package/dist/dev/agent.js.map +1 -0
  7. package/dist/dev/backend.d.ts +53 -0
  8. package/dist/dev/backend.js +53 -0
  9. package/dist/dev/backend.js.map +1 -0
  10. package/dist/dev/hook.d.ts +83 -0
  11. package/dist/dev/hook.js +136 -0
  12. package/dist/dev/hook.js.map +1 -0
  13. package/dist/dev/index.d.ts +17 -0
  14. package/dist/dev/index.js +17 -0
  15. package/dist/dev/inspection.d.ts +226 -0
  16. package/dist/dev/inspection.js +263 -0
  17. package/dist/dev/inspection.js.map +1 -0
  18. package/dist/dev/inspector.d.ts +331 -0
  19. package/dist/dev/inspector.js +429 -0
  20. package/dist/dev/inspector.js.map +1 -0
  21. package/dist/dev/model.d.ts +418 -0
  22. package/dist/dev/model.js +559 -0
  23. package/dist/dev/model.js.map +1 -0
  24. package/dist/dev/mount.d.ts +207 -0
  25. package/dist/dev/mount.js +256 -0
  26. package/dist/dev/mount.js.map +1 -0
  27. package/dist/dev/overlay.d.ts +102 -0
  28. package/dist/dev/overlay.js +176 -0
  29. package/dist/dev/overlay.js.map +1 -0
  30. package/dist/dev/preview.d.ts +74 -0
  31. package/dist/dev/preview.js +122 -0
  32. package/dist/dev/preview.js.map +1 -0
  33. package/dist/dev/protocol.d.ts +288 -0
  34. package/dist/dev/protocol.js +109 -0
  35. package/dist/dev/protocol.js.map +1 -0
  36. package/dist/dev/ring.d.ts +50 -0
  37. package/dist/dev/ring.js +94 -0
  38. package/dist/dev/ring.js.map +1 -0
  39. package/dist/dev/transport.d.ts +347 -0
  40. package/dist/dev/transport.js +394 -0
  41. package/dist/dev/transport.js.map +1 -0
  42. package/dist/dev/ui/details.js +294 -0
  43. package/dist/dev/ui/details.js.map +1 -0
  44. package/dist/dev/ui/inspection-state.d.ts +43 -0
  45. package/dist/dev/ui/inspection-state.js +62 -0
  46. package/dist/dev/ui/inspection-state.js.map +1 -0
  47. package/dist/dev/ui/issues.js +85 -0
  48. package/dist/dev/ui/issues.js.map +1 -0
  49. package/dist/dev/ui/panel.d.ts +55 -0
  50. package/dist/dev/ui/panel.js +160 -0
  51. package/dist/dev/ui/panel.js.map +1 -0
  52. package/dist/dev/ui/rows.d.ts +44 -0
  53. package/dist/dev/ui/rows.js +132 -0
  54. package/dist/dev/ui/rows.js.map +1 -0
  55. package/dist/dev/ui/state-tab.js +88 -0
  56. package/dist/dev/ui/state-tab.js.map +1 -0
  57. package/dist/dev/ui/state.d.ts +192 -0
  58. package/dist/dev/ui/state.js +147 -0
  59. package/dist/dev/ui/state.js.map +1 -0
  60. package/dist/dev/ui/styles.d.ts +31 -0
  61. package/dist/dev/ui/styles.js +113 -0
  62. package/dist/dev/ui/styles.js.map +1 -0
  63. package/dist/dev/ui/timeline.js +135 -0
  64. package/dist/dev/ui/timeline.js.map +1 -0
  65. package/dist/dev/ui/tree.js +186 -0
  66. package/dist/dev/ui/tree.js.map +1 -0
  67. package/dist/prod/agent.js +2 -0
  68. package/dist/prod/agent.js.map +1 -0
  69. package/dist/prod/backend.js +2 -0
  70. package/dist/prod/backend.js.map +1 -0
  71. package/dist/prod/hook.js +2 -0
  72. package/dist/prod/hook.js.map +1 -0
  73. package/dist/prod/index.js +1 -0
  74. package/dist/prod/inspection.js +2 -0
  75. package/dist/prod/inspection.js.map +1 -0
  76. package/dist/prod/inspector.js +2 -0
  77. package/dist/prod/inspector.js.map +1 -0
  78. package/dist/prod/model.js +2 -0
  79. package/dist/prod/model.js.map +1 -0
  80. package/dist/prod/mount.js +2 -0
  81. package/dist/prod/mount.js.map +1 -0
  82. package/dist/prod/overlay.js +11 -0
  83. package/dist/prod/overlay.js.map +1 -0
  84. package/dist/prod/preview.js +2 -0
  85. package/dist/prod/preview.js.map +1 -0
  86. package/dist/prod/protocol.js +2 -0
  87. package/dist/prod/protocol.js.map +1 -0
  88. package/dist/prod/ring.js +2 -0
  89. package/dist/prod/ring.js.map +1 -0
  90. package/dist/prod/transport.js +2 -0
  91. package/dist/prod/transport.js.map +1 -0
  92. package/dist/prod/ui/details.js +2 -0
  93. package/dist/prod/ui/details.js.map +1 -0
  94. package/dist/prod/ui/inspection-state.js +2 -0
  95. package/dist/prod/ui/inspection-state.js.map +1 -0
  96. package/dist/prod/ui/issues.js +2 -0
  97. package/dist/prod/ui/issues.js.map +1 -0
  98. package/dist/prod/ui/panel.js +2 -0
  99. package/dist/prod/ui/panel.js.map +1 -0
  100. package/dist/prod/ui/rows.js +2 -0
  101. package/dist/prod/ui/rows.js.map +1 -0
  102. package/dist/prod/ui/state-tab.js +2 -0
  103. package/dist/prod/ui/state-tab.js.map +1 -0
  104. package/dist/prod/ui/state.js +2 -0
  105. package/dist/prod/ui/state.js.map +1 -0
  106. package/dist/prod/ui/styles.js +82 -0
  107. package/dist/prod/ui/styles.js.map +1 -0
  108. package/dist/prod/ui/timeline.js +2 -0
  109. package/dist/prod/ui/timeline.js.map +1 -0
  110. package/dist/prod/ui/tree.js +2 -0
  111. package/dist/prod/ui/tree.js.map +1 -0
  112. package/package.json +69 -4
@@ -0,0 +1,176 @@
1
+ import "./protocol.js";
2
+ import { nodeLabel } from "./inspector.js";
3
+ //#region src/overlay.ts
4
+ const STYLE = `
5
+ :host { all: initial; }
6
+ .layer { position: fixed; left: 0; top: 0; width: 0; height: 0; z-index: 2147483646; pointer-events: none; }
7
+ .box { position: fixed; box-sizing: border-box; pointer-events: none; }
8
+ .highlight { background: rgba(86, 156, 214, 0.25); outline: 1px solid rgba(86, 156, 214, 0.9); }
9
+ .label { position: absolute; left: 0; top: -18px; padding: 1px 6px; font: 11px/16px ui-monospace, monospace; color: #fff; background: rgba(40, 90, 150, 0.95); border-radius: 3px; white-space: nowrap; }
10
+ .flash { outline: 2px solid rgba(255, 193, 7, 0.95); background: rgba(255, 193, 7, 0.18); animation: shadowjs-flash 600ms ease-out forwards; }
11
+ @keyframes shadowjs-flash { from { opacity: 1; } to { opacity: 0; } }
12
+ @media (prefers-reduced-motion: reduce) { .flash { animation-duration: 1ms; } }
13
+ `;
14
+ /**
15
+ * The union of the boxes of `nodes`: the smallest rectangle that contains all of them. Elements are
16
+ * measured with their bounding box, text nodes with a range. Nodes with no layout (zero width and
17
+ * height, or neither an element nor a text node) are skipped.
18
+ *
19
+ * @param nodes The DOM nodes to measure.
20
+ * @param doc The document, used to create ranges for text nodes.
21
+ * @returns The box in viewport coordinates, or `null` when none is laid out.
22
+ * @example
23
+ * import { boxOf } from "@shadow-js/devtools";
24
+ *
25
+ * const box = boxOf([document.body], document);
26
+ * @since 1.0.0
27
+ */
28
+ function boxOf(nodes, doc) {
29
+ let left = Number.POSITIVE_INFINITY;
30
+ let top = Number.POSITIVE_INFINITY;
31
+ let right = Number.NEGATIVE_INFINITY;
32
+ let bottom = Number.NEGATIVE_INFINITY;
33
+ for (const node of nodes) {
34
+ let rect = null;
35
+ if (node instanceof Element) rect = node.getBoundingClientRect();
36
+ else if (node.nodeType === 3 && typeof doc.createRange === "function") {
37
+ const range = doc.createRange();
38
+ range.selectNodeContents(node);
39
+ rect = range.getBoundingClientRect();
40
+ }
41
+ if (rect === null || rect.width === 0 && rect.height === 0) continue;
42
+ left = Math.min(left, rect.left);
43
+ top = Math.min(top, rect.top);
44
+ right = Math.max(right, rect.right);
45
+ bottom = Math.max(bottom, rect.bottom);
46
+ }
47
+ if (!Number.isFinite(left) || !Number.isFinite(top)) return null;
48
+ return {
49
+ left,
50
+ top,
51
+ width: right - left,
52
+ height: bottom - top
53
+ };
54
+ }
55
+ function place(element, box) {
56
+ element.style.left = `${box.left}px`;
57
+ element.style.top = `${box.top}px`;
58
+ element.style.width = `${box.width}px`;
59
+ element.style.height = `${box.height}px`;
60
+ }
61
+ /**
62
+ * Creates the in-page overlay: a shadow-root layer above the page that never takes pointer events.
63
+ * It draws the hover highlight and, when enabled, flashes a component's box each time one of its
64
+ * effects or memos re-runs after its first run. It reads positions from the DOM nodes the agent
65
+ * holds weakly. Dev only: `mountDevtools` creates it, and that call does nothing outside `__DEV__`
66
+ * builds, so this layer never ships.
67
+ *
68
+ * @param agent The agent that knows which DOM nodes a component rendered.
69
+ * @param options Document, flash switch and flash limit.
70
+ * @returns The overlay. Call `dispose` on it when it is no longer needed.
71
+ * @example
72
+ * import { componentStats, createAgent, createOverlay } from "@shadow-js/devtools";
73
+ *
74
+ * const agent = createAgent();
75
+ * const overlay = createOverlay(agent);
76
+ * const top = componentStats(agent.model)[0];
77
+ * if (top !== undefined) overlay.highlight(top.id);
78
+ * @see boxOf
79
+ * @since 1.0.0
80
+ */
81
+ function createOverlay(agent, options = {}) {
82
+ const doc = options.document ?? globalThis.document;
83
+ const limit = options.maxFlashes ?? 40;
84
+ let flashEnabled = options.flash !== false;
85
+ const host = doc.createElement("div");
86
+ host.setAttribute("data-shadowjs-devtools-overlay", "");
87
+ const root = host.attachShadow({ mode: "open" });
88
+ const style = doc.createElement("style");
89
+ style.textContent = STYLE;
90
+ const layer = doc.createElement("div");
91
+ layer.className = "layer";
92
+ root.append(style, layer);
93
+ doc.body.appendChild(host);
94
+ let hover = null;
95
+ const flashBoxes = [];
96
+ const removeFlash = (element) => {
97
+ const index = flashBoxes.indexOf(element);
98
+ if (index !== -1) flashBoxes.splice(index, 1);
99
+ element.remove();
100
+ };
101
+ const boxFor = (id) => {
102
+ const nodes = agent.nodesOf(id);
103
+ return nodes.length === 0 ? null : boxOf(nodes, doc);
104
+ };
105
+ const flash = (id) => {
106
+ if (!flashEnabled) return;
107
+ const box = boxFor(id);
108
+ if (box === null) return;
109
+ const element = doc.createElement("div");
110
+ element.className = "box flash";
111
+ place(element, box);
112
+ element.addEventListener("animationend", () => removeFlash(element), { once: true });
113
+ layer.appendChild(element);
114
+ flashBoxes.push(element);
115
+ while (flashBoxes.length > limit) {
116
+ const oldest = flashBoxes[0];
117
+ if (oldest === void 0) break;
118
+ removeFlash(oldest);
119
+ }
120
+ };
121
+ const stop = agent.subscribe((events) => {
122
+ if (!flashEnabled) return;
123
+ const flashed = /* @__PURE__ */ new Set();
124
+ for (const event of events) {
125
+ if (event.k !== "run") continue;
126
+ const record = agent.model.node(event.id);
127
+ if (record === void 0 || record.runs < 2) continue;
128
+ const component = agent.model.componentOf(event.id);
129
+ if (component === 0 || flashed.has(component)) continue;
130
+ flashed.add(component);
131
+ flash(component);
132
+ }
133
+ });
134
+ return {
135
+ host,
136
+ get flashes() {
137
+ return flashBoxes.length;
138
+ },
139
+ highlight(id) {
140
+ hover?.remove();
141
+ hover = null;
142
+ if (id === null) return;
143
+ const component = agent.model.node(id)?.isComponent === true ? id : agent.model.componentOf(id);
144
+ const target = component === 0 ? id : component;
145
+ const box = boxFor(target);
146
+ if (box === null) return;
147
+ const element = doc.createElement("div");
148
+ element.className = "box highlight";
149
+ place(element, box);
150
+ const record = agent.model.node(target);
151
+ const label = doc.createElement("span");
152
+ label.className = "label";
153
+ label.textContent = record === void 0 ? `#${target}` : `<${nodeLabel(record)}>`;
154
+ element.appendChild(label);
155
+ layer.appendChild(element);
156
+ hover = element;
157
+ },
158
+ flash,
159
+ clearFlashes() {
160
+ for (const element of [...flashBoxes]) removeFlash(element);
161
+ },
162
+ setFlash(on) {
163
+ flashEnabled = on;
164
+ },
165
+ dispose() {
166
+ stop();
167
+ for (const element of [...flashBoxes]) removeFlash(element);
168
+ hover = null;
169
+ host.remove();
170
+ }
171
+ };
172
+ }
173
+ //#endregion
174
+ export { boxOf, createOverlay };
175
+
176
+ //# sourceMappingURL=overlay.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"overlay.js","names":[],"sources":["../../src/overlay.ts"],"sourcesContent":["import type { Agent } from \"./agent\";\nimport { nodeLabel } from \"./inspector\";\nimport { NO_ID } from \"./protocol\";\n\n/**\n * A rectangle in viewport coordinates, from {@link boxOf}.\n *\n * @since 1.0.0\n */\nexport interface Box {\n /** Distance from the left edge of the viewport, in pixels. */\n left: number;\n /** Distance from the top edge of the viewport, in pixels. */\n top: number;\n /** Width in pixels. */\n width: number;\n /** Height in pixels. */\n height: number;\n}\n\n/**\n * Options of {@link createOverlay}.\n *\n * @since 1.0.0\n */\nexport interface OverlayOptions {\n /** The document to draw in. @default globalThis.document */\n document?: Document;\n /** Flash a component's box each time one of its effects or memos re-runs after its first run. @default true */\n flash?: boolean;\n /** Most flash boxes alive at once; older ones are removed first. @default 40 */\n maxFlashes?: number;\n}\n\n/**\n * The in-page layer: the hover highlight and the update flashes. Created by {@link createOverlay}.\n *\n * @since 1.0.0\n */\nexport interface Overlay {\n /** The shadow host element that holds the layer. It is appended to `document.body`. */\n readonly host: HTMLElement;\n /**\n * Draws a labeled box around the component of this node (the node itself when it is a component),\n * replacing the previous highlight. `null` removes the highlight; a node with nothing laid out draws\n * nothing.\n */\n highlight(id: number | null): void;\n /**\n * Flashes the rendered DOM of the component with this id once. Does nothing while flashing is off or\n * when none of its DOM is laid out.\n */\n flash(id: number): void;\n /** Number of flash boxes currently in the page. */\n readonly flashes: number;\n /** Removes all flash boxes now (they also remove themselves when their animation ends). */\n clearFlashes(): void;\n /**\n * Turns the update flashes on or off. Turning them off does not remove the boxes already shown; use\n * {@link Overlay.clearFlashes} for that.\n */\n setFlash(on: boolean): void;\n /**\n * Stops listening to the agent and removes the layer, the flash boxes and the highlight. Do not use\n * the overlay afterward.\n */\n dispose(): void;\n}\n\nconst STYLE = `\n:host { all: initial; }\n.layer { position: fixed; left: 0; top: 0; width: 0; height: 0; z-index: 2147483646; pointer-events: none; }\n.box { position: fixed; box-sizing: border-box; pointer-events: none; }\n.highlight { background: rgba(86, 156, 214, 0.25); outline: 1px solid rgba(86, 156, 214, 0.9); }\n.label { position: absolute; left: 0; top: -18px; padding: 1px 6px; font: 11px/16px ui-monospace, monospace; color: #fff; background: rgba(40, 90, 150, 0.95); border-radius: 3px; white-space: nowrap; }\n.flash { outline: 2px solid rgba(255, 193, 7, 0.95); background: rgba(255, 193, 7, 0.18); animation: shadowjs-flash 600ms ease-out forwards; }\n@keyframes shadowjs-flash { from { opacity: 1; } to { opacity: 0; } }\n@media (prefers-reduced-motion: reduce) { .flash { animation-duration: 1ms; } }\n`;\n\n/**\n * The union of the boxes of `nodes`: the smallest rectangle that contains all of them. Elements are\n * measured with their bounding box, text nodes with a range. Nodes with no layout (zero width and\n * height, or neither an element nor a text node) are skipped.\n *\n * @param nodes The DOM nodes to measure.\n * @param doc The document, used to create ranges for text nodes.\n * @returns The box in viewport coordinates, or `null` when none is laid out.\n * @example\n * import { boxOf } from \"@shadow-js/devtools\";\n *\n * const box = boxOf([document.body], document);\n * @since 1.0.0\n */\nexport function boxOf(nodes: readonly Node[], doc: Document): Box | null {\n let left = Number.POSITIVE_INFINITY;\n let top = Number.POSITIVE_INFINITY;\n let right = Number.NEGATIVE_INFINITY;\n let bottom = Number.NEGATIVE_INFINITY;\n for (const node of nodes) {\n let rect: DOMRect | null = null;\n if (node instanceof Element) {\n rect = node.getBoundingClientRect();\n } else if (node.nodeType === 3 && typeof doc.createRange === \"function\") {\n const range = doc.createRange();\n range.selectNodeContents(node);\n rect = range.getBoundingClientRect();\n }\n if (rect === null || (rect.width === 0 && rect.height === 0)) continue;\n left = Math.min(left, rect.left);\n top = Math.min(top, rect.top);\n right = Math.max(right, rect.right);\n bottom = Math.max(bottom, rect.bottom);\n }\n if (!Number.isFinite(left) || !Number.isFinite(top)) return null;\n return { left, top, width: right - left, height: bottom - top };\n}\n\nfunction place(element: HTMLElement, box: Box): void {\n element.style.left = `${box.left}px`;\n element.style.top = `${box.top}px`;\n element.style.width = `${box.width}px`;\n element.style.height = `${box.height}px`;\n}\n\n/**\n * Creates the in-page overlay: a shadow-root layer above the page that never takes pointer events.\n * It draws the hover highlight and, when enabled, flashes a component's box each time one of its\n * effects or memos re-runs after its first run. It reads positions from the DOM nodes the agent\n * holds weakly. Dev only: `mountDevtools` creates it, and that call does nothing outside `__DEV__`\n * builds, so this layer never ships.\n *\n * @param agent The agent that knows which DOM nodes a component rendered.\n * @param options Document, flash switch and flash limit.\n * @returns The overlay. Call `dispose` on it when it is no longer needed.\n * @example\n * import { componentStats, createAgent, createOverlay } from \"@shadow-js/devtools\";\n *\n * const agent = createAgent();\n * const overlay = createOverlay(agent);\n * const top = componentStats(agent.model)[0];\n * if (top !== undefined) overlay.highlight(top.id);\n * @see boxOf\n * @since 1.0.0\n */\nexport function createOverlay(agent: Agent, options: OverlayOptions = {}): Overlay {\n const doc = options.document ?? globalThis.document;\n const limit = options.maxFlashes ?? 40;\n let flashEnabled = options.flash !== false;\n const host = doc.createElement(\"div\");\n host.setAttribute(\"data-shadowjs-devtools-overlay\", \"\");\n const root = host.attachShadow({ mode: \"open\" });\n const style = doc.createElement(\"style\");\n style.textContent = STYLE;\n const layer = doc.createElement(\"div\");\n layer.className = \"layer\";\n root.append(style, layer);\n doc.body.appendChild(host);\n\n let hover: HTMLElement | null = null;\n const flashBoxes: HTMLElement[] = [];\n\n const removeFlash = (element: HTMLElement): void => {\n const index = flashBoxes.indexOf(element);\n if (index !== -1) flashBoxes.splice(index, 1);\n element.remove();\n };\n\n const boxFor = (id: number): Box | null => {\n const nodes = agent.nodesOf(id);\n return nodes.length === 0 ? null : boxOf(nodes, doc);\n };\n\n const flash = (id: number): void => {\n if (!flashEnabled) return;\n const box = boxFor(id);\n if (box === null) return;\n const element = doc.createElement(\"div\");\n element.className = \"box flash\";\n place(element, box);\n element.addEventListener(\"animationend\", () => removeFlash(element), { once: true });\n layer.appendChild(element);\n flashBoxes.push(element);\n while (flashBoxes.length > limit) {\n const oldest = flashBoxes[0];\n if (oldest === undefined) break;\n removeFlash(oldest);\n }\n };\n\n // Flash the component of every effect or memo that re-runs (its second and later runs).\n const stop = agent.subscribe((events) => {\n if (!flashEnabled) return;\n const flashed = new Set<number>();\n for (const event of events) {\n if (event.k !== \"run\") continue;\n const record = agent.model.node(event.id);\n if (record === undefined || record.runs < 2) continue;\n const component = agent.model.componentOf(event.id);\n if (component === NO_ID || flashed.has(component)) continue;\n flashed.add(component);\n flash(component);\n }\n });\n\n return {\n host,\n get flashes(): number {\n return flashBoxes.length;\n },\n highlight(id): void {\n hover?.remove();\n hover = null;\n if (id === null) return;\n const component =\n agent.model.node(id)?.isComponent === true ? id : agent.model.componentOf(id);\n const target = component === NO_ID ? id : component;\n const box = boxFor(target);\n if (box === null) return;\n const element = doc.createElement(\"div\");\n element.className = \"box highlight\";\n place(element, box);\n const record = agent.model.node(target);\n const label = doc.createElement(\"span\");\n label.className = \"label\";\n label.textContent = record === undefined ? `#${target}` : `<${nodeLabel(record)}>`;\n element.appendChild(label);\n layer.appendChild(element);\n hover = element;\n },\n flash,\n clearFlashes(): void {\n for (const element of [...flashBoxes]) removeFlash(element);\n },\n setFlash(on): void {\n flashEnabled = on;\n },\n dispose(): void {\n stop();\n for (const element of [...flashBoxes]) removeFlash(element);\n hover = null;\n host.remove();\n },\n };\n}\n"],"mappings":";;;AAqEA,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;AAyBd,SAAgB,MAAM,OAAwB,KAA2B;CACvE,IAAI,OAAO,OAAO;CAClB,IAAI,MAAM,OAAO;CACjB,IAAI,QAAQ,OAAO;CACnB,IAAI,SAAS,OAAO;CACpB,KAAK,MAAM,QAAQ,OAAO;EACxB,IAAI,OAAuB;EAC3B,IAAI,gBAAgB,SAClB,OAAO,KAAK,sBAAsB;OAC7B,IAAI,KAAK,aAAa,KAAK,OAAO,IAAI,gBAAgB,YAAY;GACvE,MAAM,QAAQ,IAAI,YAAY;GAC9B,MAAM,mBAAmB,IAAI;GAC7B,OAAO,MAAM,sBAAsB;EACrC;EACA,IAAI,SAAS,QAAS,KAAK,UAAU,KAAK,KAAK,WAAW,GAAI;EAC9D,OAAO,KAAK,IAAI,MAAM,KAAK,IAAI;EAC/B,MAAM,KAAK,IAAI,KAAK,KAAK,GAAG;EAC5B,QAAQ,KAAK,IAAI,OAAO,KAAK,KAAK;EAClC,SAAS,KAAK,IAAI,QAAQ,KAAK,MAAM;CACvC;CACA,IAAI,CAAC,OAAO,SAAS,IAAI,KAAK,CAAC,OAAO,SAAS,GAAG,GAAG,OAAO;CAC5D,OAAO;EAAE;EAAM;EAAK,OAAO,QAAQ;EAAM,QAAQ,SAAS;CAAI;AAChE;AAEA,SAAS,MAAM,SAAsB,KAAgB;CACnD,QAAQ,MAAM,OAAO,GAAG,IAAI,KAAK;CACjC,QAAQ,MAAM,MAAM,GAAG,IAAI,IAAI;CAC/B,QAAQ,MAAM,QAAQ,GAAG,IAAI,MAAM;CACnC,QAAQ,MAAM,SAAS,GAAG,IAAI,OAAO;AACvC;;;;;;;;;;;;;;;;;;;;;AAsBA,SAAgB,cAAc,OAAc,UAA0B,CAAC,GAAY;CACjF,MAAM,MAAM,QAAQ,YAAY,WAAW;CAC3C,MAAM,QAAQ,QAAQ,cAAc;CACpC,IAAI,eAAe,QAAQ,UAAU;CACrC,MAAM,OAAO,IAAI,cAAc,KAAK;CACpC,KAAK,aAAa,kCAAkC,EAAE;CACtD,MAAM,OAAO,KAAK,aAAa,EAAE,MAAM,OAAO,CAAC;CAC/C,MAAM,QAAQ,IAAI,cAAc,OAAO;CACvC,MAAM,cAAc;CACpB,MAAM,QAAQ,IAAI,cAAc,KAAK;CACrC,MAAM,YAAY;CAClB,KAAK,OAAO,OAAO,KAAK;CACxB,IAAI,KAAK,YAAY,IAAI;CAEzB,IAAI,QAA4B;CAChC,MAAM,aAA4B,CAAC;CAEnC,MAAM,eAAe,YAA+B;EAClD,MAAM,QAAQ,WAAW,QAAQ,OAAO;EACxC,IAAI,UAAU,IAAI,WAAW,OAAO,OAAO,CAAC;EAC5C,QAAQ,OAAO;CACjB;CAEA,MAAM,UAAU,OAA2B;EACzC,MAAM,QAAQ,MAAM,QAAQ,EAAE;EAC9B,OAAO,MAAM,WAAW,IAAI,OAAO,MAAM,OAAO,GAAG;CACrD;CAEA,MAAM,SAAS,OAAqB;EAClC,IAAI,CAAC,cAAc;EACnB,MAAM,MAAM,OAAO,EAAE;EACrB,IAAI,QAAQ,MAAM;EAClB,MAAM,UAAU,IAAI,cAAc,KAAK;EACvC,QAAQ,YAAY;EACpB,MAAM,SAAS,GAAG;EAClB,QAAQ,iBAAiB,sBAAsB,YAAY,OAAO,GAAG,EAAE,MAAM,KAAK,CAAC;EACnF,MAAM,YAAY,OAAO;EACzB,WAAW,KAAK,OAAO;EACvB,OAAO,WAAW,SAAS,OAAO;GAChC,MAAM,SAAS,WAAW;GAC1B,IAAI,WAAW,KAAA,GAAW;GAC1B,YAAY,MAAM;EACpB;CACF;CAGA,MAAM,OAAO,MAAM,WAAW,WAAW;EACvC,IAAI,CAAC,cAAc;EACnB,MAAM,0BAAU,IAAI,IAAY;EAChC,KAAK,MAAM,SAAS,QAAQ;GAC1B,IAAI,MAAM,MAAM,OAAO;GACvB,MAAM,SAAS,MAAM,MAAM,KAAK,MAAM,EAAE;GACxC,IAAI,WAAW,KAAA,KAAa,OAAO,OAAO,GAAG;GAC7C,MAAM,YAAY,MAAM,MAAM,YAAY,MAAM,EAAE;GAClD,IAAI,cAAA,KAAuB,QAAQ,IAAI,SAAS,GAAG;GACnD,QAAQ,IAAI,SAAS;GACrB,MAAM,SAAS;EACjB;CACF,CAAC;CAED,OAAO;EACL;EACA,IAAI,UAAkB;GACpB,OAAO,WAAW;EACpB;EACA,UAAU,IAAU;GAClB,OAAO,OAAO;GACd,QAAQ;GACR,IAAI,OAAO,MAAM;GACjB,MAAM,YACJ,MAAM,MAAM,KAAK,EAAE,CAAC,EAAE,gBAAgB,OAAO,KAAK,MAAM,MAAM,YAAY,EAAE;GAC9E,MAAM,SAAS,cAAA,IAAsB,KAAK;GAC1C,MAAM,MAAM,OAAO,MAAM;GACzB,IAAI,QAAQ,MAAM;GAClB,MAAM,UAAU,IAAI,cAAc,KAAK;GACvC,QAAQ,YAAY;GACpB,MAAM,SAAS,GAAG;GAClB,MAAM,SAAS,MAAM,MAAM,KAAK,MAAM;GACtC,MAAM,QAAQ,IAAI,cAAc,MAAM;GACtC,MAAM,YAAY;GAClB,MAAM,cAAc,WAAW,KAAA,IAAY,IAAI,WAAW,IAAI,UAAU,MAAM,EAAE;GAChF,QAAQ,YAAY,KAAK;GACzB,MAAM,YAAY,OAAO;GACzB,QAAQ;EACV;EACA;EACA,eAAqB;GACnB,KAAK,MAAM,WAAW,CAAC,GAAG,UAAU,GAAG,YAAY,OAAO;EAC5D;EACA,SAAS,IAAU;GACjB,eAAe;EACjB;EACA,UAAgB;GACd,KAAK;GACL,KAAK,MAAM,WAAW,CAAC,GAAG,UAAU,GAAG,YAAY,OAAO;GAC1D,QAAQ;GACR,KAAK,OAAO;EACd;CACF;AACF"}
@@ -0,0 +1,74 @@
1
+ //#region src/preview.d.ts
2
+ /**
3
+ * Short, side-effect-free text for a value. It never walks an object (a store view or a proxy could
4
+ * run user code or be huge): objects show their kind and size only.
5
+ *
6
+ * @param value Any value.
7
+ * @param limit Maximum length of the result (default 80).
8
+ * @returns Text such as `42`, `"hello"`, `Array(3)`, `Map(2)`, `{…}` or `<div>`. Longer text is cut to
9
+ * `limit` characters and ends with `…`.
10
+ * @example
11
+ * import { preview } from "@shadow-js/devtools";
12
+ *
13
+ * preview([1, 2, 3]); // "Array(3)"
14
+ * @since 1.0.0
15
+ */
16
+ export declare function preview(value: unknown, limit?: number): string;
17
+ /**
18
+ * A readable, bounded message for anything thrown: `name: message` for an `Error`, otherwise the
19
+ * {@link preview} of the value. Use it to show a caught value to a person.
20
+ *
21
+ * @param error The thrown value.
22
+ * @param limit Maximum length of the result; a longer message is cut and ends with `…` (default 300).
23
+ * @returns The message, cut to `limit` characters when it is longer.
24
+ * @example
25
+ * import { errorMessage } from "@shadow-js/devtools";
26
+ *
27
+ * errorMessage(new TypeError("bad")); // "TypeError: bad"
28
+ * errorMessage("plain"); // "\"plain\""
29
+ * @see preview
30
+ * @since 1.0.0
31
+ */
32
+ export declare function errorMessage(error: unknown, limit?: number): string;
33
+ /**
34
+ * Pretty JSON for the value editor. Values JSON cannot hold (cycles, BigInt) are shown as text
35
+ * instead of throwing.
36
+ *
37
+ * @param value Any value.
38
+ * @returns Indented JSON. `undefined` is spelled `undefined`. A value JSON has no text for (a function
39
+ * or a symbol) gives its {@link preview}. A value that makes `JSON.stringify` throw gives its preview
40
+ * followed by the reason in parentheses, such as `{…} (not JSON: ...)`.
41
+ * @example
42
+ * import { toEditableText } from "@shadow-js/devtools";
43
+ *
44
+ * toEditableText([1, 2]); // "[\n 1,\n 2\n]"
45
+ * @see parseEditableText
46
+ * @since 1.0.0
47
+ */
48
+ export declare function toEditableText(value: unknown): string;
49
+ /** The result of {@link parseEditableText}. @since 1.0.0 */
50
+ export type ParsedValue = {
51
+ ok: true;
52
+ value: unknown;
53
+ } | {
54
+ ok: false;
55
+ error: string;
56
+ };
57
+ /**
58
+ * Parses what a user typed in the value editor: JSON, or the literal `undefined`. Surrounding
59
+ * whitespace is ignored. It never throws: bad input comes back as `{ ok: false }` with a message.
60
+ *
61
+ * @param text The editor text.
62
+ * @returns `{ ok: true, value }` for valid input, or `{ ok: false, error }` where `error` is a message
63
+ * cut to 120 characters.
64
+ * @example
65
+ * import { parseEditableText } from "@shadow-js/devtools";
66
+ *
67
+ * parseEditableText('{"count": 2}'); // { ok: true, value: { count: 2 } }
68
+ * parseEditableText("{oops").ok; // false
69
+ * @see toEditableText
70
+ * @since 1.0.0
71
+ */
72
+ export declare function parseEditableText(text: string): ParsedValue;
73
+ //#endregion
74
+ //# sourceMappingURL=preview.d.ts.map
@@ -0,0 +1,122 @@
1
+ //#region src/preview.ts
2
+ /**
3
+ * Short, side-effect-free text for a value. It never walks an object (a store view or a proxy could
4
+ * run user code or be huge): objects show their kind and size only.
5
+ *
6
+ * @param value Any value.
7
+ * @param limit Maximum length of the result (default 80).
8
+ * @returns Text such as `42`, `"hello"`, `Array(3)`, `Map(2)`, `{…}` or `<div>`. Longer text is cut to
9
+ * `limit` characters and ends with `…`.
10
+ * @example
11
+ * import { preview } from "@shadow-js/devtools";
12
+ *
13
+ * preview([1, 2, 3]); // "Array(3)"
14
+ * @since 1.0.0
15
+ */
16
+ function preview(value, limit = 80) {
17
+ const text = describe(value);
18
+ return text.length > limit ? `${text.slice(0, Math.max(limit - 1, 1))}…` : text;
19
+ }
20
+ function describe(value) {
21
+ switch (typeof value) {
22
+ case "string": return JSON.stringify(value);
23
+ case "number":
24
+ case "boolean":
25
+ case "undefined": return String(value);
26
+ case "bigint": return `${value}n`;
27
+ case "symbol": return value.toString();
28
+ case "function": return "ƒ()";
29
+ }
30
+ if (value === null) return "null";
31
+ if (Array.isArray(value)) return `Array(${value.length})`;
32
+ if (value instanceof Map) return `Map(${value.size})`;
33
+ if (value instanceof Set) return `Set(${value.size})`;
34
+ if (value instanceof Date) return Number.isNaN(value.getTime()) ? "Invalid Date" : value.toISOString();
35
+ if (value instanceof Error) return `${value.name}: ${value.message}`;
36
+ if (typeof Node !== "undefined" && value instanceof Node) return value instanceof Element ? `<${value.localName}>` : `#${value.nodeName}`;
37
+ const prototype = Object.getPrototypeOf(value);
38
+ if (prototype === null || prototype === Object.prototype) return "{…}";
39
+ const ctor = typeof prototype === "object" && prototype !== null ? Reflect.get(prototype, "constructor") : void 0;
40
+ const name = typeof ctor === "function" ? ctor.name : "";
41
+ return name === "" ? "{…}" : `${name} {…}`;
42
+ }
43
+ /**
44
+ * A readable, bounded message for anything thrown: `name: message` for an `Error`, otherwise the
45
+ * {@link preview} of the value. Use it to show a caught value to a person.
46
+ *
47
+ * @param error The thrown value.
48
+ * @param limit Maximum length of the result; a longer message is cut and ends with `…` (default 300).
49
+ * @returns The message, cut to `limit` characters when it is longer.
50
+ * @example
51
+ * import { errorMessage } from "@shadow-js/devtools";
52
+ *
53
+ * errorMessage(new TypeError("bad")); // "TypeError: bad"
54
+ * errorMessage("plain"); // "\"plain\""
55
+ * @see preview
56
+ * @since 1.0.0
57
+ */
58
+ function errorMessage(error, limit = 300) {
59
+ const text = error instanceof Error ? `${error.name}: ${error.message}` : preview(error, limit);
60
+ return text.length > limit ? `${text.slice(0, limit - 1)}…` : text;
61
+ }
62
+ /**
63
+ * Pretty JSON for the value editor. Values JSON cannot hold (cycles, BigInt) are shown as text
64
+ * instead of throwing.
65
+ *
66
+ * @param value Any value.
67
+ * @returns Indented JSON. `undefined` is spelled `undefined`. A value JSON has no text for (a function
68
+ * or a symbol) gives its {@link preview}. A value that makes `JSON.stringify` throw gives its preview
69
+ * followed by the reason in parentheses, such as `{…} (not JSON: ...)`.
70
+ * @example
71
+ * import { toEditableText } from "@shadow-js/devtools";
72
+ *
73
+ * toEditableText([1, 2]); // "[\n 1,\n 2\n]"
74
+ * @see parseEditableText
75
+ * @since 1.0.0
76
+ */
77
+ function toEditableText(value) {
78
+ if (value === void 0) return "undefined";
79
+ try {
80
+ const json = JSON.stringify(value, null, 2);
81
+ return json === void 0 ? preview(value, 500) : json;
82
+ } catch (error) {
83
+ return `${preview(value, 200)} (not JSON: ${errorMessage(error, 80)})`;
84
+ }
85
+ }
86
+ /**
87
+ * Parses what a user typed in the value editor: JSON, or the literal `undefined`. Surrounding
88
+ * whitespace is ignored. It never throws: bad input comes back as `{ ok: false }` with a message.
89
+ *
90
+ * @param text The editor text.
91
+ * @returns `{ ok: true, value }` for valid input, or `{ ok: false, error }` where `error` is a message
92
+ * cut to 120 characters.
93
+ * @example
94
+ * import { parseEditableText } from "@shadow-js/devtools";
95
+ *
96
+ * parseEditableText('{"count": 2}'); // { ok: true, value: { count: 2 } }
97
+ * parseEditableText("{oops").ok; // false
98
+ * @see toEditableText
99
+ * @since 1.0.0
100
+ */
101
+ function parseEditableText(text) {
102
+ const trimmed = text.trim();
103
+ if (trimmed === "undefined") return {
104
+ ok: true,
105
+ value: void 0
106
+ };
107
+ try {
108
+ return {
109
+ ok: true,
110
+ value: JSON.parse(trimmed)
111
+ };
112
+ } catch (error) {
113
+ return {
114
+ ok: false,
115
+ error: errorMessage(error, 120)
116
+ };
117
+ }
118
+ }
119
+ //#endregion
120
+ export { errorMessage, parseEditableText, preview, toEditableText };
121
+
122
+ //# sourceMappingURL=preview.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preview.js","names":[],"sources":["../../src/preview.ts"],"sourcesContent":["/**\n * Short, side-effect-free text for a value. It never walks an object (a store view or a proxy could\n * run user code or be huge): objects show their kind and size only.\n *\n * @param value Any value.\n * @param limit Maximum length of the result (default 80).\n * @returns Text such as `42`, `\"hello\"`, `Array(3)`, `Map(2)`, `{…}` or `<div>`. Longer text is cut to\n * `limit` characters and ends with `…`.\n * @example\n * import { preview } from \"@shadow-js/devtools\";\n *\n * preview([1, 2, 3]); // \"Array(3)\"\n * @since 1.0.0\n */\nexport function preview(value: unknown, limit = 80): string {\n const text = describe(value);\n return text.length > limit ? `${text.slice(0, Math.max(limit - 1, 1))}…` : text;\n}\n\nfunction describe(value: unknown): string {\n switch (typeof value) {\n case \"string\":\n return JSON.stringify(value);\n case \"number\":\n case \"boolean\":\n case \"undefined\":\n return String(value);\n case \"bigint\":\n return `${value}n`;\n case \"symbol\":\n return value.toString();\n case \"function\":\n return \"ƒ()\";\n default:\n break;\n }\n if (value === null) return \"null\";\n if (Array.isArray(value)) return `Array(${value.length})`;\n if (value instanceof Map) return `Map(${value.size})`;\n if (value instanceof Set) return `Set(${value.size})`;\n if (value instanceof Date)\n return Number.isNaN(value.getTime()) ? \"Invalid Date\" : value.toISOString();\n if (value instanceof Error) return `${value.name}: ${value.message}`;\n if (typeof Node !== \"undefined\" && value instanceof Node) {\n return value instanceof Element ? `<${value.localName}>` : `#${value.nodeName}`;\n }\n const prototype: unknown = Object.getPrototypeOf(value);\n if (prototype === null || prototype === Object.prototype) return \"{…}\";\n const ctor: unknown =\n typeof prototype === \"object\" && prototype !== null\n ? Reflect.get(prototype, \"constructor\")\n : undefined;\n const name = typeof ctor === \"function\" ? ctor.name : \"\";\n return name === \"\" ? \"{…}\" : `${name} {…}`;\n}\n\n/**\n * A readable, bounded message for anything thrown: `name: message` for an `Error`, otherwise the\n * {@link preview} of the value. Use it to show a caught value to a person.\n *\n * @param error The thrown value.\n * @param limit Maximum length of the result; a longer message is cut and ends with `…` (default 300).\n * @returns The message, cut to `limit` characters when it is longer.\n * @example\n * import { errorMessage } from \"@shadow-js/devtools\";\n *\n * errorMessage(new TypeError(\"bad\")); // \"TypeError: bad\"\n * errorMessage(\"plain\"); // \"\\\"plain\\\"\"\n * @see preview\n * @since 1.0.0\n */\nexport function errorMessage(error: unknown, limit = 300): string {\n const text = error instanceof Error ? `${error.name}: ${error.message}` : preview(error, limit);\n return text.length > limit ? `${text.slice(0, limit - 1)}…` : text;\n}\n\n/**\n * Pretty JSON for the value editor. Values JSON cannot hold (cycles, BigInt) are shown as text\n * instead of throwing.\n *\n * @param value Any value.\n * @returns Indented JSON. `undefined` is spelled `undefined`. A value JSON has no text for (a function\n * or a symbol) gives its {@link preview}. A value that makes `JSON.stringify` throw gives its preview\n * followed by the reason in parentheses, such as `{…} (not JSON: ...)`.\n * @example\n * import { toEditableText } from \"@shadow-js/devtools\";\n *\n * toEditableText([1, 2]); // \"[\\n 1,\\n 2\\n]\"\n * @see parseEditableText\n * @since 1.0.0\n */\nexport function toEditableText(value: unknown): string {\n if (value === undefined) return \"undefined\";\n try {\n const json = JSON.stringify(value, null, 2);\n return json === undefined ? preview(value, 500) : json;\n } catch (error) {\n // Circular structures and BigInt values cannot be JSON: show what they are instead.\n return `${preview(value, 200)} (not JSON: ${errorMessage(error, 80)})`;\n }\n}\n\n/** The result of {@link parseEditableText}. @since 1.0.0 */\nexport type ParsedValue = { ok: true; value: unknown } | { ok: false; error: string };\n\n/**\n * Parses what a user typed in the value editor: JSON, or the literal `undefined`. Surrounding\n * whitespace is ignored. It never throws: bad input comes back as `{ ok: false }` with a message.\n *\n * @param text The editor text.\n * @returns `{ ok: true, value }` for valid input, or `{ ok: false, error }` where `error` is a message\n * cut to 120 characters.\n * @example\n * import { parseEditableText } from \"@shadow-js/devtools\";\n *\n * parseEditableText('{\"count\": 2}'); // { ok: true, value: { count: 2 } }\n * parseEditableText(\"{oops\").ok; // false\n * @see toEditableText\n * @since 1.0.0\n */\nexport function parseEditableText(text: string): ParsedValue {\n const trimmed = text.trim();\n if (trimmed === \"undefined\") return { ok: true, value: undefined };\n try {\n return { ok: true, value: JSON.parse(trimmed) as unknown };\n } catch (error) {\n return { ok: false, error: errorMessage(error, 120) };\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;AAcA,SAAgB,QAAQ,OAAgB,QAAQ,IAAY;CAC1D,MAAM,OAAO,SAAS,KAAK;CAC3B,OAAO,KAAK,SAAS,QAAQ,GAAG,KAAK,MAAM,GAAG,KAAK,IAAI,QAAQ,GAAG,CAAC,CAAC,EAAE,KAAK;AAC7E;AAEA,SAAS,SAAS,OAAwB;CACxC,QAAQ,OAAO,OAAf;EACE,KAAK,UACH,OAAO,KAAK,UAAU,KAAK;EAC7B,KAAK;EACL,KAAK;EACL,KAAK,aACH,OAAO,OAAO,KAAK;EACrB,KAAK,UACH,OAAO,GAAG,MAAM;EAClB,KAAK,UACH,OAAO,MAAM,SAAS;EACxB,KAAK,YACH,OAAO;CAGX;CACA,IAAI,UAAU,MAAM,OAAO;CAC3B,IAAI,MAAM,QAAQ,KAAK,GAAG,OAAO,SAAS,MAAM,OAAO;CACvD,IAAI,iBAAiB,KAAK,OAAO,OAAO,MAAM,KAAK;CACnD,IAAI,iBAAiB,KAAK,OAAO,OAAO,MAAM,KAAK;CACnD,IAAI,iBAAiB,MACnB,OAAO,OAAO,MAAM,MAAM,QAAQ,CAAC,IAAI,iBAAiB,MAAM,YAAY;CAC5E,IAAI,iBAAiB,OAAO,OAAO,GAAG,MAAM,KAAK,IAAI,MAAM;CAC3D,IAAI,OAAO,SAAS,eAAe,iBAAiB,MAClD,OAAO,iBAAiB,UAAU,IAAI,MAAM,UAAU,KAAK,IAAI,MAAM;CAEvE,MAAM,YAAqB,OAAO,eAAe,KAAK;CACtD,IAAI,cAAc,QAAQ,cAAc,OAAO,WAAW,OAAO;CACjE,MAAM,OACJ,OAAO,cAAc,YAAY,cAAc,OAC3C,QAAQ,IAAI,WAAW,aAAa,IACpC,KAAA;CACN,MAAM,OAAO,OAAO,SAAS,aAAa,KAAK,OAAO;CACtD,OAAO,SAAS,KAAK,QAAQ,GAAG,KAAK;AACvC;;;;;;;;;;;;;;;;AAiBA,SAAgB,aAAa,OAAgB,QAAQ,KAAa;CAChE,MAAM,OAAO,iBAAiB,QAAQ,GAAG,MAAM,KAAK,IAAI,MAAM,YAAY,QAAQ,OAAO,KAAK;CAC9F,OAAO,KAAK,SAAS,QAAQ,GAAG,KAAK,MAAM,GAAG,QAAQ,CAAC,EAAE,KAAK;AAChE;;;;;;;;;;;;;;;;AAiBA,SAAgB,eAAe,OAAwB;CACrD,IAAI,UAAU,KAAA,GAAW,OAAO;CAChC,IAAI;EACF,MAAM,OAAO,KAAK,UAAU,OAAO,MAAM,CAAC;EAC1C,OAAO,SAAS,KAAA,IAAY,QAAQ,OAAO,GAAG,IAAI;CACpD,SAAS,OAAO;EAEd,OAAO,GAAG,QAAQ,OAAO,GAAG,EAAE,cAAc,aAAa,OAAO,EAAE,EAAE;CACtE;AACF;;;;;;;;;;;;;;;;AAoBA,SAAgB,kBAAkB,MAA2B;CAC3D,MAAM,UAAU,KAAK,KAAK;CAC1B,IAAI,YAAY,aAAa,OAAO;EAAE,IAAI;EAAM,OAAO,KAAA;CAAU;CACjE,IAAI;EACF,OAAO;GAAE,IAAI;GAAM,OAAO,KAAK,MAAM,OAAO;EAAa;CAC3D,SAAS,OAAO;EACd,OAAO;GAAE,IAAI;GAAO,OAAO,aAAa,OAAO,GAAG;EAAE;CACtD;AACF"}