@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.
- package/CHANGELOG.md +21 -0
- package/LICENSE +21 -0
- package/README.md +576 -2
- package/dist/dev/agent.d.ts +239 -0
- package/dist/dev/agent.js +626 -0
- package/dist/dev/agent.js.map +1 -0
- package/dist/dev/backend.d.ts +53 -0
- package/dist/dev/backend.js +53 -0
- package/dist/dev/backend.js.map +1 -0
- package/dist/dev/hook.d.ts +83 -0
- package/dist/dev/hook.js +136 -0
- package/dist/dev/hook.js.map +1 -0
- package/dist/dev/index.d.ts +17 -0
- package/dist/dev/index.js +17 -0
- package/dist/dev/inspection.d.ts +226 -0
- package/dist/dev/inspection.js +263 -0
- package/dist/dev/inspection.js.map +1 -0
- package/dist/dev/inspector.d.ts +331 -0
- package/dist/dev/inspector.js +429 -0
- package/dist/dev/inspector.js.map +1 -0
- package/dist/dev/model.d.ts +418 -0
- package/dist/dev/model.js +559 -0
- package/dist/dev/model.js.map +1 -0
- package/dist/dev/mount.d.ts +207 -0
- package/dist/dev/mount.js +256 -0
- package/dist/dev/mount.js.map +1 -0
- package/dist/dev/overlay.d.ts +102 -0
- package/dist/dev/overlay.js +176 -0
- package/dist/dev/overlay.js.map +1 -0
- package/dist/dev/preview.d.ts +74 -0
- package/dist/dev/preview.js +122 -0
- package/dist/dev/preview.js.map +1 -0
- package/dist/dev/protocol.d.ts +288 -0
- package/dist/dev/protocol.js +109 -0
- package/dist/dev/protocol.js.map +1 -0
- package/dist/dev/ring.d.ts +50 -0
- package/dist/dev/ring.js +94 -0
- package/dist/dev/ring.js.map +1 -0
- package/dist/dev/transport.d.ts +347 -0
- package/dist/dev/transport.js +394 -0
- package/dist/dev/transport.js.map +1 -0
- package/dist/dev/ui/details.js +294 -0
- package/dist/dev/ui/details.js.map +1 -0
- package/dist/dev/ui/inspection-state.d.ts +43 -0
- package/dist/dev/ui/inspection-state.js +62 -0
- package/dist/dev/ui/inspection-state.js.map +1 -0
- package/dist/dev/ui/issues.js +85 -0
- package/dist/dev/ui/issues.js.map +1 -0
- package/dist/dev/ui/panel.d.ts +55 -0
- package/dist/dev/ui/panel.js +160 -0
- package/dist/dev/ui/panel.js.map +1 -0
- package/dist/dev/ui/rows.d.ts +44 -0
- package/dist/dev/ui/rows.js +132 -0
- package/dist/dev/ui/rows.js.map +1 -0
- package/dist/dev/ui/state-tab.js +88 -0
- package/dist/dev/ui/state-tab.js.map +1 -0
- package/dist/dev/ui/state.d.ts +192 -0
- package/dist/dev/ui/state.js +147 -0
- package/dist/dev/ui/state.js.map +1 -0
- package/dist/dev/ui/styles.d.ts +31 -0
- package/dist/dev/ui/styles.js +113 -0
- package/dist/dev/ui/styles.js.map +1 -0
- package/dist/dev/ui/timeline.js +135 -0
- package/dist/dev/ui/timeline.js.map +1 -0
- package/dist/dev/ui/tree.js +186 -0
- package/dist/dev/ui/tree.js.map +1 -0
- package/dist/prod/agent.js +2 -0
- package/dist/prod/agent.js.map +1 -0
- package/dist/prod/backend.js +2 -0
- package/dist/prod/backend.js.map +1 -0
- package/dist/prod/hook.js +2 -0
- package/dist/prod/hook.js.map +1 -0
- package/dist/prod/index.js +1 -0
- package/dist/prod/inspection.js +2 -0
- package/dist/prod/inspection.js.map +1 -0
- package/dist/prod/inspector.js +2 -0
- package/dist/prod/inspector.js.map +1 -0
- package/dist/prod/model.js +2 -0
- package/dist/prod/model.js.map +1 -0
- package/dist/prod/mount.js +2 -0
- package/dist/prod/mount.js.map +1 -0
- package/dist/prod/overlay.js +11 -0
- package/dist/prod/overlay.js.map +1 -0
- package/dist/prod/preview.js +2 -0
- package/dist/prod/preview.js.map +1 -0
- package/dist/prod/protocol.js +2 -0
- package/dist/prod/protocol.js.map +1 -0
- package/dist/prod/ring.js +2 -0
- package/dist/prod/ring.js.map +1 -0
- package/dist/prod/transport.js +2 -0
- package/dist/prod/transport.js.map +1 -0
- package/dist/prod/ui/details.js +2 -0
- package/dist/prod/ui/details.js.map +1 -0
- package/dist/prod/ui/inspection-state.js +2 -0
- package/dist/prod/ui/inspection-state.js.map +1 -0
- package/dist/prod/ui/issues.js +2 -0
- package/dist/prod/ui/issues.js.map +1 -0
- package/dist/prod/ui/panel.js +2 -0
- package/dist/prod/ui/panel.js.map +1 -0
- package/dist/prod/ui/rows.js +2 -0
- package/dist/prod/ui/rows.js.map +1 -0
- package/dist/prod/ui/state-tab.js +2 -0
- package/dist/prod/ui/state-tab.js.map +1 -0
- package/dist/prod/ui/state.js +2 -0
- package/dist/prod/ui/state.js.map +1 -0
- package/dist/prod/ui/styles.js +82 -0
- package/dist/prod/ui/styles.js.map +1 -0
- package/dist/prod/ui/timeline.js +2 -0
- package/dist/prod/ui/timeline.js.map +1 -0
- package/dist/prod/ui/tree.js +2 -0
- package/dist/prod/ui/tree.js.map +1 -0
- package/package.json +69 -4
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model.js","names":["DEFAULT_LIMITS","blank","seq","t","sameIds","a","b","i","InspectorModel","limits","RingBuffer","id","record","event","type","label","owner","list","alive","leaf","stack","next","observer","reader","oldest","owned","leafId","previous","keep","source","causes","open","cached","path","component","currentId","limit","steps","hit","current","entry","visited","plain","flush","revive","state","run","write","copy","error","mismatch","foldEvents","model","events"],"sources":["../../src/model.ts"],"sourcesContent":["import { NO_ID, type WireEvent } from \"./protocol\";\nimport { RingBuffer } from \"./ring\";\n\n/**\n * The kinds of node the inspector tracks: an owner, a signal, a memo, an effect, a resource or a store.\n *\n * @since 1.0.0\n */\nexport type NodeType = \"owner\" | \"signal\" | \"memo\" | \"effect\" | \"resource\" | \"store\";\n\n/**\n * Everything the model knows about one node. Live nodes are in {@link InspectorModel.nodes}; disposed\n * ones are kept in {@link InspectorModel.graveyard} for a while.\n *\n * @since 1.0.0\n */\nexport interface NodeRecord {\n /** Id assigned by the agent. Never {@link NO_ID}. */\n id: number;\n /** What kind of node this is. */\n type: NodeType;\n /**\n * How the node is labeled: `owner` or `root` for owners, `memo` or `effect` for computations,\n * `signal`, `resource` or `store`, and `component` once a component was mounted on the owner.\n */\n label: string;\n /** The name the code gave the node, or `undefined`. */\n name: string | undefined;\n /** Owner id, or {@link NO_ID} for roots and detached nodes. */\n parent: number;\n /** False once the node left the live set: disposed, cascaded from its owner, or garbage collected. */\n alive: boolean;\n /** Sequence number of the event that created the node. */\n createdSeq: number;\n /** Time of the creation event, in milliseconds on the agent's clock. */\n createdAt: number;\n /** Sequence number of the event that removed the node, or 0 while it is alive. */\n disposedSeq: number;\n /** Computations: created in the render phase (the runtime's render flag). False otherwise. */\n render: boolean;\n /** True once a component was mounted on this owner. */\n isComponent: boolean;\n /** Time the component was mounted (agent clock, in milliseconds), or 0 when it never was. */\n mountedAt: number;\n /** Computations: ids of the nodes read by the last run. Empty for other nodes. */\n sources: number[];\n /** Ids of the computations that currently read this node. `null` when none ever did. */\n observers: Set<number> | null;\n /** Number of runs recorded. 0 for nodes that never run. */\n runs: number;\n /** Sum of the durations of all runs, in milliseconds. */\n totalDuration: number;\n /** Duration of the last run, in milliseconds. */\n lastDuration: number;\n /** Start of the last run, in milliseconds as the runtime reported it. */\n lastStart: number;\n /** Sequence number of the last run, or 0 when it never ran. */\n lastRunSeq: number;\n /**\n * Sequence number of the last change: a write for a signal, a run that changed the value for a\n * computation, or a state report for a resource. 0 when there was none.\n */\n lastChangeSeq: number;\n /** Signals: a short text of the current value. Empty for other nodes. */\n preview: string;\n /** Number of writes to a signal. 0 for other nodes. */\n writes: number;\n /** Sequence number of the last write, or 0 when never written. */\n lastWriteSeq: number;\n /**\n * Id of the computation that performed the last write, or {@link NO_ID} for a write from outside\n * any computation (an event handler, for example) and when the node was never written.\n */\n lastWriter: number;\n /** Resources: the last reported state, for example `\"pending\"` or `\"ready\"`. Empty for other nodes. */\n state: string;\n}\n\n/**\n * Why a computation ran. A run has one cause per source that changed since the computation's previous\n * run; see {@link RunRecord.causes}.\n *\n * @since 1.0.0\n */\nexport interface Cause {\n /**\n * `write` when the source is a signal that was written, `run` when the source is a memo or a\n * resource that changed, `initial` for the first run, and `unknown` when no recorded source changed.\n */\n kind: \"write\" | \"run\" | \"initial\" | \"unknown\";\n /** The source that changed: a signal or memo ({@link NO_ID} for `initial` and `unknown`). */\n id: number;\n /**\n * Sequence number of that change: the write or the run of the source, the creation for `initial`,\n * or the last run for `unknown`.\n */\n seq: number;\n}\n\n/**\n * One run of an effect or a memo, as recorded.\n *\n * @since 1.0.0\n */\nexport interface RunRecord {\n /** Sequence number of the run event. */\n seq: number;\n /** Time the event was received (agent clock, in milliseconds). */\n t: number;\n /** The computation that ran. */\n id: number;\n /** When the run started, in milliseconds as the runtime reported it. */\n start: number;\n /** How long the run took, in milliseconds. */\n duration: number;\n /** True when the computation's value changed. */\n changed: boolean;\n /** Sequence number of the flush it belongs to ({@link FlushRecord.seq}), or 0 when it ran outside a flush. */\n flush: number;\n /** Why it ran: see {@link Cause}. */\n causes: Cause[];\n}\n\n/**\n * One signal write, as recorded.\n *\n * @since 1.0.0\n */\nexport interface WriteRecord {\n /** Sequence number of the write event. */\n seq: number;\n /** Time the event was received (agent clock, in milliseconds). */\n t: number;\n /** The signal that was written. */\n id: number;\n /** A short text of the value written. */\n preview: string;\n /** Computation that wrote ({@link NO_ID}: outside code such as an event handler). */\n writer: number;\n /** Ids of the computations that read the signal when it was written. */\n observers: number[];\n}\n\n/**\n * One flush of the effect queues. A flush is recorded only when at least one of its runs is recorded.\n *\n * @since 1.0.0\n */\nexport interface FlushRecord {\n /** Sequence number of the flush-start event. */\n seq: number;\n /** The same value as {@link FlushRecord.seq}: the sequence number of the flush-start event. */\n startSeq: number;\n /** When the flush started (agent clock, in milliseconds). */\n start: number;\n /** When the flush ended (agent clock, in milliseconds). Equal to `start` while the flush is open. */\n end: number;\n /** Effects in the render queue when the flush started. */\n render: number;\n /** Effects in the user queue when the flush started. */\n user: number;\n /**\n * Runs recorded during the flush. The model counts them itself, so runs the agent ignored (the\n * tools' own) are not included. 0 while the flush is open.\n */\n runs: number;\n /** True until the flush-end event arrives. */\n open: boolean;\n}\n\n/**\n * An error the runtime reported.\n *\n * @since 1.0.0\n */\nexport interface ErrorRecord {\n /** Sequence number of the error event. */\n seq: number;\n /** Time the event was received (agent clock, in milliseconds). */\n t: number;\n /** The node the error came from, or {@link NO_ID} when it had no known owner. */\n id: number;\n /** The thrown value as text (see `errorMessage`). */\n message: string;\n /** A diagnostic code the runtime attached, for example `WRITE_IN_MEMO`, or `undefined`. */\n code: string | undefined;\n}\n\n/**\n * A hydration mismatch the runtime reported.\n *\n * @since 1.0.0\n */\nexport interface HydrationRecord {\n /** Sequence number of the mismatch event. */\n seq: number;\n /** Time the event was received (agent clock, in milliseconds). */\n t: number;\n /** What the runtime reported, as text. */\n message: string;\n /** The expected side, as text: a string, or the HTML of an element. Cut to 200 characters. */\n expected: string;\n /** The actual side, as text: a string, or the HTML of an element. Cut to 200 characters. */\n actual: string;\n}\n\n/**\n * Capacities of the model's bounded buffers. When passed to the constructor each field is optional;\n * the values left out are the defaults in {@link DEFAULT_LIMITS}.\n *\n * @since 1.0.0\n */\nexport interface ModelLimits {\n /**\n * Run records kept for timelines and causal chains.\n *\n * @default 20000\n */\n runs: number;\n /**\n * Write records kept for causal chains.\n *\n * @default 10000\n */\n writes: number;\n /**\n * Flush records kept.\n *\n * @default 1000\n */\n flushes: number;\n /**\n * Error records kept, and hydration records kept (each buffer on its own).\n *\n * @default 200\n */\n errors: number;\n /**\n * Disposed nodes kept so that old runs can still be named.\n *\n * @default 4000\n */\n graveyard: number;\n /**\n * Live nodes tracked. A node created beyond this count is counted in {@link InspectorModel.overflow}\n * and ignored; later events about it count as unknown.\n *\n * @default 100000\n */\n maxNodes: number;\n}\n\n/**\n * The default capacities of the model's buffers, used for each limit that the caller leaves out.\n *\n * @example\n * import { DEFAULT_LIMITS, foldEvents } from \"@shadow-js/devtools\";\n * import type { WireEvent } from \"@shadow-js/devtools\";\n *\n * const events: WireEvent[] = [];\n * const model = foldEvents(events, { ...DEFAULT_LIMITS, runs: 500 });\n * @since 1.0.0\n */\nexport const DEFAULT_LIMITS: Readonly<ModelLimits> = {\n /** Run records kept. @default 20000 */\n runs: 20_000,\n /** Write records kept. @default 10000 */\n writes: 10_000,\n /** Flush records kept. @default 1000 */\n flushes: 1_000,\n /** Error records kept, and hydration records kept. @default 200 */\n errors: 200,\n /** Disposed nodes kept. @default 4000 */\n graveyard: 4_000,\n /** Live nodes tracked. @default 100000 */\n maxNodes: 100_000,\n};\n\n/**\n * The JSON-safe state of a model: what the agent's `state()` returns (for a remote panel) and what\n * {@link InspectorModel.importState} accepts.\n *\n * @since 1.0.0\n */\nexport interface ModelState {\n /** Sequence number of the last applied event. */\n lastSeq: number;\n /** Nodes ignored because `maxNodes` was reached. */\n overflow: number;\n /** Events that referred to a node the model does not know. */\n unknown: number;\n /** Live nodes, in insertion order, with observers as arrays. */\n nodes: Array<Omit<NodeRecord, \"observers\"> & { observers: number[] }>;\n /** Disposed nodes, oldest first, with observers as arrays. */\n graveyard: Array<Omit<NodeRecord, \"observers\"> & { observers: number[] }>;\n /** Run records, oldest first. */\n runs: RunRecord[];\n /** Write records, oldest first. */\n writes: WriteRecord[];\n /** Flush records, oldest first. */\n flushes: FlushRecord[];\n /** Error records, oldest first. */\n errors: ErrorRecord[];\n /** Hydration records, oldest first. */\n hydration: HydrationRecord[];\n}\n\nfunction blank(id: number, type: NodeType, label: string, seq: number, t: number): NodeRecord {\n return {\n id,\n type,\n label,\n name: undefined,\n parent: NO_ID,\n alive: true,\n createdSeq: seq,\n createdAt: t,\n disposedSeq: 0,\n render: false,\n isComponent: false,\n mountedAt: 0,\n sources: [],\n observers: null,\n runs: 0,\n totalDuration: 0,\n lastDuration: 0,\n lastStart: 0,\n lastRunSeq: 0,\n lastChangeSeq: 0,\n preview: \"\",\n writes: 0,\n lastWriteSeq: 0,\n lastWriter: NO_ID,\n state: \"\",\n };\n}\n\nfunction sameIds(a: readonly number[], b: readonly number[]): boolean {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i += 1) if (a[i] !== b[i]) return false;\n return true;\n}\n\n/**\n * The inspector's incremental model of the reactive graph: a pure fold over {@link WireEvent}s. The\n * same events always produce the same state, on the page or in a remote panel. `apply` is O(1) for\n * most events and O(sources) for a run; memory is bounded by {@link ModelLimits}.\n *\n * A development tool: its events come from the dev hook, which exists only in development builds.\n *\n * @example\n * import { InspectorModel, snapshotOf } from \"@shadow-js/devtools\";\n * import type { WireEvent } from \"@shadow-js/devtools\";\n *\n * const events: WireEvent[] = [];\n * const model = new InspectorModel();\n * for (const event of events) model.apply(event);\n * snapshotOf(model).counts.nodes;\n * @since 1.0.0\n */\nexport class InspectorModel {\n /** The capacities in effect: the defaults with the constructor's overrides applied. */\n readonly limits: Readonly<ModelLimits>;\n /** Live nodes by id. */\n readonly nodes: Map<number, NodeRecord> = new Map();\n /**\n * Recently disposed nodes, oldest first, kept so that history can still be named. Holds at most\n * `limits.graveyard` nodes.\n */\n readonly graveyard: Map<number, NodeRecord> = new Map();\n /** Run records, the newest kept (capacity `limits.runs`). */\n readonly runs: RingBuffer<RunRecord>;\n /** Write records, the newest kept (capacity `limits.writes`). */\n readonly writes: RingBuffer<WriteRecord>;\n /** Flush records, the newest kept (capacity `limits.flushes`). */\n readonly flushes: RingBuffer<FlushRecord>;\n /** Error records, the newest kept (capacity `limits.errors`). */\n readonly errors: RingBuffer<ErrorRecord>;\n /** Hydration records, the newest kept (capacity `limits.errors`). */\n readonly hydration: RingBuffer<HydrationRecord>;\n /** Sequence number of the last applied event. */\n lastSeq = 0;\n /** Nodes ignored because `maxNodes` was reached. */\n overflow = 0;\n /** Events that referred to a node the model does not know. */\n unknown = 0;\n /**\n * Changes whenever a component is mounted, unmounted or disposed. Cached component lookups from an\n * older epoch are recomputed.\n */\n componentEpoch = 0;\n /** Entries in the component lookup cache. Never more than `limits.maxNodes` after a lookup. */\n get componentCacheSize(): number {\n return this.componentCache.size;\n }\n\n /**\n * Called once for every node that leaves the live set, after it moved to the graveyard: when it is\n * disposed, or when its owner is disposed (cascade). The agent uses it to drop its own references.\n */\n onBury: ((record: NodeRecord) => void) | undefined;\n private openFlush: FlushRecord | null = null;\n private flushRuns = 0;\n /** Signals, stores and resources by owner: they have no lifecycle event of their own. */\n private readonly leaves: Map<number, number[]> = new Map();\n private readonly componentCache: Map<number, { epoch: number; component: number }> = new Map();\n\n /**\n * @param limits Overrides for the buffer capacities. Values left out take the defaults in\n * {@link DEFAULT_LIMITS}.\n */\n constructor(limits: Partial<ModelLimits> = {}) {\n this.limits = { ...DEFAULT_LIMITS, ...limits };\n this.runs = new RingBuffer(this.limits.runs);\n this.writes = new RingBuffer(this.limits.writes);\n this.flushes = new RingBuffer(this.limits.flushes);\n this.errors = new RingBuffer(this.limits.errors);\n this.hydration = new RingBuffer(this.limits.errors);\n }\n\n /**\n * The node with this id: alive, or recently disposed (in the graveyard).\n *\n * @param id The node id.\n * @returns The record, or `undefined` when the model does not know the id.\n */\n node(id: number): NodeRecord | undefined {\n return this.nodes.get(id) ?? this.graveyard.get(id);\n }\n\n private create(\n event: WireEvent & { id: number },\n type: NodeType,\n label: string,\n ): NodeRecord | null {\n if (this.nodes.size >= this.limits.maxNodes) {\n this.overflow += 1;\n return null;\n }\n const record = blank(event.id, type, label, event.seq, event.t);\n this.nodes.set(event.id, record);\n return record;\n }\n\n /** Remembers that a leaf node (signal, store, resource) belongs to `owner`. */\n private addLeaf(owner: number, id: number): void {\n if (owner === NO_ID) return;\n let list = this.leaves.get(owner);\n if (list === undefined) {\n list = [];\n this.leaves.set(owner, list);\n }\n list.push(id);\n // Leaves that died on their own (garbage collected) would pile up on a long-lived owner.\n if (list.length > 1024 && list.length % 1024 === 0) {\n const alive = list.filter((leaf) => this.nodes.has(leaf));\n list.length = 0;\n list.push(...alive);\n }\n }\n\n private bury(record: NodeRecord, seq: number): void {\n // Iterative: the cascade follows owner -> leaves only, but disposal order is user controlled.\n const stack: NodeRecord[] = [record];\n while (stack.length > 0) {\n const next = stack.pop();\n if (next === undefined) break;\n if (!this.nodes.has(next.id)) continue;\n next.alive = false;\n next.disposedSeq = seq;\n this.nodes.delete(next.id);\n if (next.sources.length > 0) this.setSources(next, []);\n if (next.observers !== null) {\n for (const observer of next.observers) {\n const reader = this.nodes.get(observer);\n if (reader !== undefined) reader.sources = reader.sources.filter((id) => id !== next.id);\n }\n }\n next.observers = null;\n this.graveyard.set(next.id, next);\n if (this.graveyard.size > this.limits.graveyard) {\n const oldest = this.graveyard.keys().next();\n if (oldest.done !== true) this.graveyard.delete(oldest.value);\n }\n this.componentCache.delete(next.id);\n const owned = this.leaves.get(next.id);\n if (owned !== undefined) {\n this.leaves.delete(next.id);\n for (const leafId of owned) {\n const leaf = this.nodes.get(leafId);\n if (leaf !== undefined) stack.push(leaf);\n }\n }\n this.onBury?.(next);\n }\n }\n\n private setSources(record: NodeRecord, next: readonly number[]): void {\n const previous = record.sources;\n if (sameIds(previous, next)) return;\n const keep = new Set(next);\n for (const id of previous) {\n if (keep.has(id)) continue;\n this.nodes.get(id)?.observers?.delete(record.id);\n }\n for (const id of next) {\n const source = this.nodes.get(id);\n if (source === undefined) continue;\n if (source.observers === null) source.observers = new Set();\n source.observers.add(record.id);\n }\n record.sources = next.slice();\n }\n\n /**\n * Applies one event. Events must arrive in `seq` order: an event whose `seq` is not greater than\n * {@link InspectorModel.lastSeq} is ignored, so a remote panel can receive events it already holds.\n * An event about a node the model does not know is counted in `unknown`. A node created beyond\n * `maxNodes` is counted in `overflow` and ignored.\n *\n * @param event The event, with the sequence number and time the agent gave it.\n */\n apply(event: WireEvent): void {\n if (event.seq <= this.lastSeq) return;\n this.lastSeq = event.seq;\n switch (event.k) {\n case \"owner+\": {\n const record = this.create(event, \"owner\", event.label);\n if (record === null) return;\n record.parent = event.parent;\n record.name = event.name;\n return;\n }\n case \"owner-\":\n case \"comp-\": {\n const record = this.nodes.get(event.id);\n if (record === undefined) {\n this.unknown += 1;\n return;\n }\n if (record.isComponent) this.componentEpoch += 1;\n this.bury(record, event.seq);\n return;\n }\n case \"signal+\": {\n const record = this.create(event, \"signal\", \"signal\");\n if (record === null) return;\n record.parent = event.owner;\n record.name = event.name;\n record.preview = event.preview;\n this.addLeaf(event.owner, event.id);\n return;\n }\n case \"write\": {\n const record = this.nodes.get(event.id);\n if (record === undefined) {\n this.unknown += 1;\n return;\n }\n record.writes += 1;\n record.preview = event.preview;\n record.lastWriteSeq = event.seq;\n record.lastChangeSeq = event.seq;\n record.lastWriter = event.writer;\n this.writes.push({\n seq: event.seq,\n t: event.t,\n id: event.id,\n preview: event.preview,\n writer: event.writer,\n observers: event.observers,\n });\n return;\n }\n case \"comp+\": {\n const record = this.create(event, event.kind, event.kind);\n if (record === null) return;\n record.parent = event.owner;\n record.name = event.name;\n record.render = event.render;\n return;\n }\n case \"run\": {\n const record = this.nodes.get(event.id);\n if (record === undefined) {\n this.unknown += 1;\n return;\n }\n const causes = this.causesOf(record);\n record.runs += 1;\n record.totalDuration += event.duration;\n record.lastDuration = event.duration;\n record.lastStart = event.start;\n record.lastRunSeq = event.seq;\n if (event.changed) record.lastChangeSeq = event.seq;\n this.setSources(record, event.sources);\n this.runs.push({\n seq: event.seq,\n t: event.t,\n id: event.id,\n start: event.start,\n duration: event.duration,\n changed: event.changed,\n flush: this.openFlush === null ? 0 : this.openFlush.seq,\n causes,\n });\n if (this.openFlush !== null) this.flushRuns += 1;\n return;\n }\n case \"mount\": {\n const record = this.nodes.get(event.id);\n if (record === undefined) {\n this.unknown += 1;\n return;\n }\n record.isComponent = true;\n record.label = \"component\";\n record.name = event.name;\n record.mountedAt = event.t;\n this.componentEpoch += 1;\n return;\n }\n case \"unmount\": {\n const record = this.nodes.get(event.id);\n if (record === undefined) {\n this.unknown += 1;\n return;\n }\n // The owner's disposal follows; keep the flag so the component stays attributable.\n this.componentEpoch += 1;\n return;\n }\n case \"resource\": {\n let record = this.nodes.get(event.id);\n if (record === undefined) {\n record = this.create(event, \"resource\", \"resource\") ?? undefined;\n if (record === undefined) return;\n record.parent = event.owner;\n this.addLeaf(event.owner, event.id);\n }\n record.state = event.state;\n if (event.name !== undefined) record.name = event.name;\n record.lastChangeSeq = event.seq;\n return;\n }\n case \"store+\": {\n const record = this.create(event, \"store\", \"store\");\n if (record === null) return;\n record.parent = event.owner;\n record.name = event.name;\n this.addLeaf(event.owner, event.id);\n return;\n }\n case \"error\":\n this.errors.push({\n seq: event.seq,\n t: event.t,\n id: event.id,\n message: event.message,\n code: event.code,\n });\n return;\n case \"hydration\":\n this.hydration.push({\n seq: event.seq,\n t: event.t,\n message: event.message,\n expected: event.expected,\n actual: event.actual,\n });\n return;\n case \"flush+\": {\n const record: FlushRecord = {\n seq: event.seq,\n startSeq: event.seq,\n start: event.t,\n end: event.t,\n render: event.render,\n user: event.user,\n runs: 0,\n open: true,\n };\n this.flushes.push(record);\n this.openFlush = record;\n this.flushRuns = 0;\n return;\n }\n case \"flush-\": {\n const open = this.openFlush;\n if (open === null) return;\n open.end = event.t;\n open.runs = this.flushRuns;\n open.open = false;\n this.openFlush = null;\n return;\n }\n }\n }\n\n /** Why a computation is about to run again: sources that changed since its last run. */\n private causesOf(record: NodeRecord): Cause[] {\n if (record.runs === 0) return [{ kind: \"initial\", id: NO_ID, seq: record.createdSeq }];\n const causes: Cause[] = [];\n for (const id of record.sources) {\n const source = this.nodes.get(id);\n if (source === undefined || source.lastChangeSeq <= record.lastRunSeq) continue;\n causes.push({\n kind: source.type === \"signal\" ? \"write\" : \"run\",\n id,\n seq: source.lastChangeSeq,\n });\n }\n if (causes.length === 0) causes.push({ kind: \"unknown\", id: NO_ID, seq: record.lastRunSeq });\n causes.sort((a, b) => a.seq - b.seq);\n return causes;\n }\n\n /**\n * The nearest component that owns `id`: the node itself when it is a component, otherwise the first\n * component above it in the owner chain (the graveyard included). The answer is cached until the next\n * component mount, unmount or disposal.\n *\n * @param id The node id.\n * @returns The component's id, or {@link NO_ID} when no component owns the node.\n */\n componentOf(id: number): number {\n const cached = this.componentCache.get(id);\n if (cached !== undefined && cached.epoch === this.componentEpoch) return cached.component;\n // Walk up to the first component; every node on the way shares the answer, so cache them all.\n const path: number[] = [];\n let component: number = NO_ID;\n let currentId = id;\n const limit = this.nodes.size + this.graveyard.size + 1;\n for (let steps = 0; steps < limit; steps += 1) {\n const hit = this.componentCache.get(currentId);\n if (hit !== undefined && hit.epoch === this.componentEpoch) {\n component = hit.component;\n break;\n }\n const current = this.node(currentId);\n if (current === undefined) break;\n path.push(currentId);\n if (current.isComponent) {\n component = current.id;\n break;\n }\n if (current.parent === NO_ID) break;\n currentId = current.parent;\n }\n const entry = { epoch: this.componentEpoch, component };\n for (const visited of path) this.componentCache.set(visited, entry);\n if (this.componentCache.size > this.limits.maxNodes) this.componentCache.clear();\n return component;\n }\n\n /**\n * A serializable copy of the whole state, safe to send as JSON. {@link InspectorModel.importState}\n * rebuilds an equal model from it.\n *\n * @returns The state.\n */\n exportState(): ModelState {\n const plain = (record: NodeRecord): ModelState[\"nodes\"][number] => ({\n ...record,\n sources: record.sources.slice(),\n observers: record.observers === null ? [] : [...record.observers],\n });\n return {\n lastSeq: this.lastSeq,\n overflow: this.overflow,\n unknown: this.unknown,\n nodes: [...this.nodes.values()].map(plain),\n graveyard: [...this.graveyard.values()].map(plain),\n runs: this.runs.toArray(),\n writes: this.writes.toArray(),\n flushes: this.flushes.toArray().map((flush) => ({ ...flush })),\n errors: this.errors.toArray(),\n hydration: this.hydration.toArray(),\n };\n }\n\n /**\n * Replaces the whole state with `state`. A remote panel does this with the agent's state, and the\n * agent does it with an empty state to implement `clear`. The capacities of this model do not change:\n * when `state` holds more records than a buffer can keep, the newest are kept.\n *\n * @param state The state to load.\n */\n importState(state: ModelState): void {\n this.nodes.clear();\n this.graveyard.clear();\n this.runs.clear();\n this.writes.clear();\n this.flushes.clear();\n this.errors.clear();\n this.hydration.clear();\n this.componentCache.clear();\n this.openFlush = null;\n this.flushRuns = 0;\n const revive = (plain: ModelState[\"nodes\"][number]): NodeRecord => ({\n ...plain,\n sources: plain.sources.slice(),\n observers: plain.observers.length === 0 ? null : new Set(plain.observers),\n });\n this.leaves.clear();\n for (const plain of state.nodes) {\n const record = revive(plain);\n this.nodes.set(plain.id, record);\n if (record.type === \"signal\" || record.type === \"store\" || record.type === \"resource\") {\n this.addLeaf(record.parent, record.id);\n }\n }\n for (const plain of state.graveyard) this.graveyard.set(plain.id, revive(plain));\n for (const run of state.runs) this.runs.push(run);\n for (const write of state.writes) this.writes.push(write);\n for (const flush of state.flushes) {\n const copy = { ...flush };\n this.flushes.push(copy);\n if (copy.open) this.openFlush = copy;\n }\n for (const error of state.errors) this.errors.push(error);\n for (const mismatch of state.hydration) this.hydration.push(mismatch);\n this.lastSeq = state.lastSeq;\n this.overflow = state.overflow;\n this.unknown = state.unknown;\n this.componentEpoch += 1;\n }\n}\n\n/**\n * Builds a model from a finished event stream. A pure function: the same input always gives the same\n * model. A development tool, like {@link InspectorModel}.\n *\n * @param events Events in `seq` order.\n * @param limits Buffer capacities. Values left out take the defaults in {@link DEFAULT_LIMITS}.\n * @returns The folded model.\n * @example\n * import { foldEvents } from \"@shadow-js/devtools\";\n * import type { WireEvent } from \"@shadow-js/devtools\";\n *\n * const events: WireEvent[] = [];\n * const model = foldEvents(events, { runs: 1000 });\n * @since 1.0.0\n */\nexport function foldEvents(\n events: Iterable<WireEvent>,\n limits?: Partial<ModelLimits>,\n): InspectorModel {\n const model = new InspectorModel(limits);\n for (const event of events) model.apply(event);\n return model;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import"@shadow-js/web";import"@shadow-js/core";import"@shadow-js/core/extend";function e(e){let t=e.split(`+`).map(e=>e.trim().toLowerCase()).filter(e=>e!==``),n=t.pop();if(n===void 0)return null;let r=(...e)=>t.some(t=>e.includes(t));return{alt:r(`alt`,`option`),ctrl:r(`ctrl`,`control`),shift:r(`shift`),meta:r(`meta`,`cmd`,`command`),key:n}}function t(e,t){return e.altKey!==t.alt||e.ctrlKey!==t.ctrl||e.shiftKey!==t.shift||e.metaKey!==t.meta?!1:e.key.toLowerCase()===t.key||t.key.length===1&&e.code.toLowerCase()===`key${t.key}`}function n(){}const r={host:null,root:null,agent:null,backend:null,isOpen:!1,open:n,close:n,toggle:n,moveTo:n,setTab:n,select:n,flush:n,dispose:n};function i(e={}){return r}export{t as matchesShortcut,i as mountDevtools,e as parseShortcut};
|
|
2
|
+
//# sourceMappingURL=mount.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mount.js","names":["parseShortcut","parts","spec","part","key","has","names","matchesShortcut","event","shortcut","noop","OFF","mountDevtools"],"sources":["../../src/mount.tsx"],"sourcesContent":["import { flushSync, useSignal } from \"@shadow-js/core\";\nimport { writeSignal } from \"@shadow-js/core/extend\";\nimport { render } from \"@shadow-js/web\";\nimport { type Agent, createAgent } from \"./agent\";\nimport { type Backend, createLocalBackend } from \"./backend\";\nimport { installHook } from \"./hook\";\nimport { createOverlay } from \"./overlay\";\nimport type { DevtoolsHook } from \"./protocol\";\nimport { serveAgent, type Transport } from \"./transport\";\nimport { Panel } from \"./ui/panel\";\nimport { createPanelState, type PanelState, type TabId } from \"./ui/state\";\nimport { LAUNCHER_CSS, PANEL_CSS } from \"./ui/styles\";\n\n/**\n * Options of {@link mountDevtools}. Every field is optional.\n *\n * @since 1.0.0\n */\nexport interface MountOptions {\n /**\n * The hook to listen to. When unset, the package uses the hook {@link installHook} returns.\n *\n * @default installHook()\n */\n hook?: DevtoolsHook;\n /**\n * Use this agent instead of creating one. It must already be attached to the hook, or be attached by\n * you: the package attaches only the agent it creates.\n *\n * @default createAgent({ writeSignal })\n */\n agent?: Agent;\n /**\n * Where the overlay host element is appended.\n *\n * @default document.body\n */\n container?: Element;\n /**\n * The document to use.\n *\n * @default globalThis.document\n */\n document?: Document;\n /**\n * Start with the panel open. When false, only the launcher button shows.\n *\n * @default false\n */\n open?: boolean;\n /**\n * Keyboard shortcut that toggles the panel, such as `\"Alt+Shift+D\"`. Pass `false` to turn the shortcut\n * off.\n *\n * @default \"Alt+Shift+D\"\n */\n toggleKey?: string | false;\n /**\n * Panel position in pixels from the top-left corner of the window.\n *\n * @default { x: 16, y: 16 }\n */\n position?: { x: number; y: number };\n /**\n * Flash components when they update. The panel's Flash button changes this at runtime.\n *\n * @default true\n */\n flash?: boolean;\n /**\n * The tab shown first.\n *\n * @default \"tree\"\n */\n tab?: TabId;\n /**\n * Also serve the agent over this transport, for a remote panel in an extension or another tab. See\n * {@link serveAgent}.\n */\n serve?: Transport;\n /**\n * Show the launcher button that opens the panel.\n *\n * @default true\n */\n launcher?: boolean;\n}\n\n/**\n * The handle `mountDevtools()` returns. Every member is a no-op in production builds, where `host`,\n * `root`, `agent` and `backend` are `null`.\n *\n * @since 1.0.0\n */\nexport interface DevtoolsHandle {\n /**\n * The shadow host element the panel is appended to, or `null` when the tools are off.\n */\n readonly host: HTMLElement | null;\n /**\n * The shadow root holding the panel, or `null` when the tools are off.\n */\n readonly root: ShadowRoot | null;\n /**\n * The agent the panel reads from, or `null` when the tools are off.\n */\n readonly agent: Agent | null;\n /**\n * The backend the panel reads from, or `null` when the tools are off.\n */\n readonly backend: Backend | null;\n /**\n * Whether the panel is open. Each read returns the current value.\n */\n readonly isOpen: boolean;\n /**\n * Opens the panel and applies the change now.\n */\n open(): void;\n /**\n * Closes the panel and applies the change now.\n */\n close(): void;\n /**\n * Opens the panel when it is closed, and closes it when it is open. Applies the change now.\n */\n toggle(): void;\n /**\n * Moves the panel to `x` and `y` pixels from the top-left corner. The position is clamped to the\n * window.\n */\n moveTo(x: number, y: number): void;\n /**\n * Shows a tab and applies the change now.\n */\n setTab(tab: TabId): void;\n /**\n * Selects a node in the tree.\n */\n select(id: number): void;\n /**\n * Applies pending model changes to the panel now. The panel normally refreshes once per frame.\n */\n flush(): void;\n /**\n * Removes the panel, the overlay and every listener, and stops serving the agent. When\n * `mountDevtools()` created the agent, it is disposed too. Calling this again does nothing.\n */\n dispose(): void;\n}\n\n/** A parsed keyboard shortcut. */\ninterface Shortcut {\n alt: boolean;\n ctrl: boolean;\n shift: boolean;\n meta: boolean;\n key: string;\n}\n\n/**\n * Parses `\"Alt+Shift+D\"` style shortcuts. Modifier names are case-insensitive: `alt` or `option`,\n * `ctrl` or `control`, `shift`, and `meta`, `cmd` or `command`. The last part is the key, compared\n * case-insensitively with `event.key`.\n *\n * @param spec The shortcut text.\n * @returns The parsed shortcut, or `null` when it has no key.\n * @example\n * import { parseShortcut } from \"@shadow-js/devtools\";\n * const shortcut = parseShortcut(\"Ctrl+Shift+K\"); // { ctrl: true, shift: true, alt: false, meta: false, key: \"k\" }\n * @since 1.0.0\n */\nexport function parseShortcut(spec: string): Shortcut | null {\n const parts = spec\n .split(\"+\")\n .map((part) => part.trim().toLowerCase())\n .filter((part) => part !== \"\");\n const key = parts.pop();\n if (key === undefined) return null;\n const has = (...names: string[]): boolean => parts.some((part) => names.includes(part));\n return {\n alt: has(\"alt\", \"option\"),\n ctrl: has(\"ctrl\", \"control\"),\n shift: has(\"shift\"),\n meta: has(\"meta\", \"cmd\", \"command\"),\n key,\n };\n}\n\n/**\n * True when a keyboard event is the shortcut. The modifiers must match exactly. The key matches\n * `event.key`, or, for a single letter, `event.code` (such as `KeyD`), because Alt and Shift change the\n * character on some keyboard layouts.\n *\n * @param event The event.\n * @param shortcut From {@link parseShortcut}.\n * @returns True when the event is the shortcut.\n * @example\n * import { matchesShortcut, parseShortcut } from \"@shadow-js/devtools\";\n * const toggle = parseShortcut(\"Alt+Shift+D\");\n * document.addEventListener(\"keydown\", (event) => {\n * if (toggle !== null && matchesShortcut(event, toggle)) console.log(\"toggle the panel\");\n * });\n * @since 1.0.0\n */\nexport function matchesShortcut(\n event: Pick<KeyboardEvent, \"key\" | \"code\" | \"altKey\" | \"ctrlKey\" | \"shiftKey\" | \"metaKey\">,\n shortcut: Shortcut,\n): boolean {\n if (\n event.altKey !== shortcut.alt ||\n event.ctrlKey !== shortcut.ctrl ||\n event.shiftKey !== shortcut.shift ||\n event.metaKey !== shortcut.meta\n ) {\n return false;\n }\n const key = event.key.toLowerCase();\n if (key === shortcut.key) return true;\n return shortcut.key.length === 1 && event.code.toLowerCase() === `key${shortcut.key}`;\n}\n\nfunction noop(): void {\n // Production and server builds: the tools do nothing.\n}\n\nconst OFF: DevtoolsHandle = {\n host: null,\n root: null,\n agent: null,\n backend: null,\n isOpen: false,\n open: noop,\n close: noop,\n toggle: noop,\n moveTo: noop,\n setTab: noop,\n select: noop,\n flush: noop,\n dispose: noop,\n};\n\n/**\n * Shows the devtools: a floating, draggable panel in a shadow root (so page styles cannot touch it)\n * with the component tree, a flame timeline of flushes, a state viewer with live editing, and an\n * issues list. Hovering a row highlights the component on the page; components flash when they\n * update. Toggle with `Alt+Shift+D`, or with the `toggleKey` option.\n *\n * This is a development tool: the panel is for development builds and never ships. In production\n * builds (`__DEV__` false), on the server, or when another protocol version owns the hook, this does\n * nothing and returns a handle of no-ops. Call it before your application renders so no event is\n * missed.\n *\n * @param options See {@link MountOptions}.\n * @returns A handle to control or remove the panel.\n * @example\n * import { mountDevtools } from \"@shadow-js/devtools\";\n * // Development builds only: production builds ignore this call.\n * const devtools = mountDevtools({ open: true });\n * devtools.toggle();\n * @since 1.0.0\n */\nexport function mountDevtools(options: MountOptions = {}): DevtoolsHandle {\n if (!__DEV__) return OFF;\n const doc = options.document ?? (typeof document === \"undefined\" ? undefined : document);\n if (doc === undefined) return OFF;\n const hook = options.hook ?? installHook();\n if (hook === undefined) return OFF;\n\n const ownsAgent = options.agent === undefined;\n const agent = options.agent ?? createAgent({ writeSignal });\n const detach = ownsAgent ? agent.attach(hook) : noop;\n const overlay = createOverlay(agent, { document: doc, flash: options.flash !== false });\n const backend = createLocalBackend(agent, { highlight: (id) => overlay.highlight(id) });\n const stopServing =\n options.serve === undefined\n ? noop\n : serveAgent(agent, options.serve, { highlight: (id) => overlay.highlight(id) });\n\n const host = doc.createElement(\"div\");\n host.setAttribute(\"data-shadowjs-devtools\", \"\");\n const root = host.attachShadow({ mode: \"open\" });\n const style = doc.createElement(\"style\");\n style.textContent = `${PANEL_CSS}${LAUNCHER_CSS}`;\n root.appendChild(style);\n (options.container ?? doc.body).appendChild(host);\n\n let state: PanelState | undefined;\n let setOpen: (value: boolean) => void = noop;\n let setPosition: (x: number, y: number) => void = noop;\n let isOpen = options.open === true;\n const start = options.position ?? { x: 16, y: 16 };\n\n // Everything the panel creates is invisible to the agent: it must not inspect itself.\n const unmount = agent.quiet(() =>\n render(() => {\n const [open, setOpenSignal] = useSignal(isOpen);\n const [position, setPositionSignal] = useSignal({ x: start.x, y: start.y });\n setOpen = (value) => {\n isOpen = value;\n setOpenSignal(value);\n };\n setPosition = (x, y) => {\n const maxX = Math.max((doc.defaultView?.innerWidth ?? 1024) - 80, 0);\n const maxY = Math.max((doc.defaultView?.innerHeight ?? 768) - 40, 0);\n setPositionSignal({ x: Math.min(Math.max(x, 0), maxX), y: Math.min(Math.max(y, 0), maxY) });\n };\n state = createPanelState(backend, {\n ...(options.tab === undefined ? {} : { tab: options.tab }),\n flash: options.flash !== false,\n onFlash: (on) => overlay.setFlash(on),\n onPause: (paused) => {\n if (paused) agent.pause();\n else agent.resume();\n },\n });\n return (\n <>\n <Panel\n state={state}\n open={open()}\n x={position().x}\n y={position().y}\n onMove={(x, y) => setPosition(x, y)}\n onClose={() => setOpen(false)}\n />\n <button\n type=\"button\"\n class=\"launcher\"\n aria-label=\"Open ShadowJS devtools\"\n hidden={open() || options.launcher === false}\n on:click={() => setOpen(true)}\n >\n S\n </button>\n </>\n );\n }, root),\n );\n\n // The model changes far more often than anyone can read it: refresh the views once per frame.\n let refreshPending = false;\n let frame: number | undefined;\n const refresh = (): void => {\n refreshPending = false;\n frame = undefined;\n state?.refresh();\n };\n const stopSubscription = backend.subscribe(() => {\n if (refreshPending) return;\n refreshPending = true;\n if (typeof doc.defaultView?.requestAnimationFrame === \"function\") {\n frame = doc.defaultView.requestAnimationFrame(refresh);\n } else {\n queueMicrotask(refresh);\n }\n });\n\n const shortcut =\n options.toggleKey === false ? null : parseShortcut(options.toggleKey ?? \"Alt+Shift+D\");\n const onKey = (event: KeyboardEvent): void => {\n if (shortcut === null || !matchesShortcut(event, shortcut)) return;\n event.preventDefault();\n setOpen(!isOpen);\n flushSync();\n };\n doc.addEventListener(\"keydown\", onKey, true);\n\n const flush = (): void => {\n // Deliver the agent's pending events first: they are what schedules the refresh.\n agent.flush();\n if (refreshPending) {\n if (frame !== undefined) doc.defaultView?.cancelAnimationFrame(frame);\n refresh();\n }\n flushSync();\n };\n\n let disposed = false;\n return {\n host,\n root,\n agent,\n backend,\n get isOpen(): boolean {\n return isOpen;\n },\n open(): void {\n setOpen(true);\n flushSync();\n },\n close(): void {\n setOpen(false);\n flushSync();\n },\n toggle(): void {\n setOpen(!isOpen);\n flushSync();\n },\n moveTo(x, y): void {\n setPosition(x, y);\n flushSync();\n },\n setTab(tab): void {\n state?.setTab(tab);\n flushSync();\n },\n select(id): void {\n state?.select(id);\n flushSync();\n },\n flush,\n dispose(): void {\n if (disposed) return;\n disposed = true;\n doc.removeEventListener(\"keydown\", onKey, true);\n stopSubscription();\n if (frame !== undefined) doc.defaultView?.cancelAnimationFrame(frame);\n stopServing();\n unmount();\n overlay.dispose();\n backend.dispose();\n host.remove();\n detach();\n if (ownsAgent) agent.dispose();\n },\n };\n}\n"],"mappings":"8EA4KA,SAAgBA,EAAc,EAA+B,CAC3D,IAAMC,EAAQC,EACX,MAAM,GAAG,CAAA,CACT,IAAK,GAASC,EAAK,KAAK,CAAC,CAAC,YAAY,CAAC,CAAA,CACvC,OAAQ,GAASA,IAAS,EAAE,EACzBC,EAAMH,EAAM,IAAI,EACtB,GAAIG,IAAQ,IAAA,GAAW,OAAO,KAC9B,IAAMC,GAAO,GAAGC,IAA6BL,EAAM,KAAM,GAASK,EAAM,SAASH,CAAI,CAAC,EACtF,MAAO,CACL,IAAKE,EAAI,MAAO,QAAQ,EACxB,KAAMA,EAAI,OAAQ,SAAS,EAC3B,MAAOA,EAAI,OAAO,EAClB,KAAMA,EAAI,OAAQ,MAAO,SAAS,EAClC,KACF,CACF,CAkBA,SAAgBE,EACd,EACA,EACS,CAWT,OATEC,EAAM,SAAWC,EAAS,KAC1BD,EAAM,UAAYC,EAAS,MAC3BD,EAAM,WAAaC,EAAS,OAC5BD,EAAM,UAAYC,EAAS,KAEpB,GAEGD,EAAM,IAAI,YAClB,IAAQC,EAAS,KACdA,EAAS,IAAI,SAAW,GAAKD,EAAM,KAAK,YAAY,IAAM,MAAMC,EAAS,KAClF,CAEA,SAASC,GAAa,CAEtB,CAEA,MAAMC,EAAsB,CAC1B,KAAM,KACN,KAAM,KACN,MAAO,KACP,QAAS,KACT,OAAQ,GACR,KAAMD,EACN,MAAOA,EACP,OAAQA,EACR,OAAQA,EACR,OAAQA,EACR,OAAQA,EACR,MAAOA,EACP,QAASA,CACX,EAsBA,SAAgBE,EAAc,EAAwB,CAAC,EAAmB,CAC1D,OAAOD,CAoKvB"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import"./protocol.js";import{nodeLabel as e}from"./inspector.js";function t(e,t){let n=1/0,r=1/0,i=-1/0,a=-1/0;for(let o of e){let e=null;if(o instanceof Element)e=o.getBoundingClientRect();else if(o.nodeType===3&&typeof t.createRange==`function`){let n=t.createRange();n.selectNodeContents(o),e=n.getBoundingClientRect()}e===null||e.width===0&&e.height===0||(n=Math.min(n,e.left),r=Math.min(r,e.top),i=Math.max(i,e.right),a=Math.max(a,e.bottom))}return!Number.isFinite(n)||!Number.isFinite(r)?null:{left:n,top:r,width:i-n,height:a-r}}function n(e,t){e.style.left=`${t.left}px`,e.style.top=`${t.top}px`,e.style.width=`${t.width}px`,e.style.height=`${t.height}px`}function r(r,i={}){let a=i.document??globalThis.document,o=i.maxFlashes??40,s=i.flash!==!1,c=a.createElement(`div`);c.setAttribute(`data-shadowjs-devtools-overlay`,``);let l=c.attachShadow({mode:`open`}),u=a.createElement(`style`);u.textContent=`
|
|
2
|
+
:host { all: initial; }
|
|
3
|
+
.layer { position: fixed; left: 0; top: 0; width: 0; height: 0; z-index: 2147483646; pointer-events: none; }
|
|
4
|
+
.box { position: fixed; box-sizing: border-box; pointer-events: none; }
|
|
5
|
+
.highlight { background: rgba(86, 156, 214, 0.25); outline: 1px solid rgba(86, 156, 214, 0.9); }
|
|
6
|
+
.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; }
|
|
7
|
+
.flash { outline: 2px solid rgba(255, 193, 7, 0.95); background: rgba(255, 193, 7, 0.18); animation: shadowjs-flash 600ms ease-out forwards; }
|
|
8
|
+
@keyframes shadowjs-flash { from { opacity: 1; } to { opacity: 0; } }
|
|
9
|
+
@media (prefers-reduced-motion: reduce) { .flash { animation-duration: 1ms; } }
|
|
10
|
+
`;let d=a.createElement(`div`);d.className=`layer`,l.append(u,d),a.body.appendChild(c);let f=null,p=[],m=e=>{let t=p.indexOf(e);t!==-1&&p.splice(t,1),e.remove()},h=e=>{let n=r.nodesOf(e);return n.length===0?null:t(n,a)},g=e=>{if(!s)return;let t=h(e);if(t===null)return;let r=a.createElement(`div`);for(r.className=`box flash`,n(r,t),r.addEventListener(`animationend`,()=>m(r),{once:!0}),d.appendChild(r),p.push(r);p.length>o;){let e=p[0];if(e===void 0)break;m(e)}},_=r.subscribe(e=>{if(!s)return;let t=/* @__PURE__ */ new Set;for(let n of e){if(n.k!==`run`)continue;let e=r.model.node(n.id);if(e===void 0||e.runs<2)continue;let i=r.model.componentOf(n.id);i===0||t.has(i)||(t.add(i),g(i))}});return{host:c,get flashes(){return p.length},highlight(t){if(f?.remove(),f=null,t===null)return;let i=r.model.node(t)?.isComponent===!0?t:r.model.componentOf(t),o=i===0?t:i,s=h(o);if(s===null)return;let c=a.createElement(`div`);c.className=`box highlight`,n(c,s);let l=r.model.node(o),u=a.createElement(`span`);u.className=`label`,u.textContent=l===void 0?`#${o}`:`<${e(l)}>`,c.appendChild(u),d.appendChild(c),f=c},flash:g,clearFlashes(){for(let e of[...p])m(e)},setFlash(e){s=e},dispose(){_();for(let e of[...p])m(e);f=null,c.remove()}}}export{t as boxOf,r as createOverlay};
|
|
11
|
+
//# sourceMappingURL=overlay.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"overlay.js","names":["boxOf","left","top","right","bottom","node","nodes","rect","doc","range","place","box","createOverlay","options","limit","flashEnabled","host","root","style","layer","hover","flashBoxes","removeFlash","index","element","boxFor","agent","id","flash","oldest","stop","flashed","event","events","record","component","target","label","nodeLabel","on"],"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":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
function e(e,n=80){let r=t(e);return r.length>n?`${r.slice(0,Math.max(n-1,1))}…`:r}function t(e){switch(typeof e){case`string`:return JSON.stringify(e);case`number`:case`boolean`:case`undefined`:return String(e);case`bigint`:return`${e}n`;case`symbol`:return e.toString();case`function`:return`ƒ()`}if(e===null)return`null`;if(Array.isArray(e))return`Array(${e.length})`;if(e instanceof Map)return`Map(${e.size})`;if(e instanceof Set)return`Set(${e.size})`;if(e instanceof Date)return Number.isNaN(e.getTime())?`Invalid Date`:e.toISOString();if(e instanceof Error)return`${e.name}: ${e.message}`;if(typeof Node<`u`&&e instanceof Node)return e instanceof Element?`<${e.localName}>`:`#${e.nodeName}`;let t=Object.getPrototypeOf(e);if(t===null||t===Object.prototype)return`{…}`;let n=typeof t==`object`&&t?Reflect.get(t,`constructor`):void 0,r=typeof n==`function`?n.name:``;return r===``?`{…}`:`${r} {…}`}function n(t,n=300){let r=t instanceof Error?`${t.name}: ${t.message}`:e(t,n);return r.length>n?`${r.slice(0,n-1)}…`:r}function r(t){if(t===void 0)return`undefined`;try{let n=JSON.stringify(t,null,2);return n===void 0?e(t,500):n}catch(r){return`${e(t,200)} (not JSON: ${n(r,80)})`}}function i(e){let t=e.trim();if(t===`undefined`)return{ok:!0,value:void 0};try{return{ok:!0,value:JSON.parse(t)}}catch(e){return{ok:!1,error:n(e,120)}}}export{n as errorMessage,i as parseEditableText,e as preview,r as toEditableText};
|
|
2
|
+
//# sourceMappingURL=preview.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preview.js","names":["preview","text","describe","value","limit","prototype","ctor","name","errorMessage","error","toEditableText","json","parseEditableText","trimmed"],"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,SAAgBA,EAAQ,EAAgB,EAAQ,GAAY,CAC1D,IAAMC,EAAOC,EAASC,CAAK,EAC3B,OAAOF,EAAK,OAASG,EAAQ,GAAGH,EAAK,MAAM,EAAG,KAAK,IAAIG,EAAQ,EAAG,CAAC,CAAC,EAAE,GAAKH,CAC7E,CAEA,SAASC,EAAS,EAAwB,CACxC,OAAQ,OAAOC,EAAf,CACE,IAAK,SACH,OAAO,KAAK,UAAUA,CAAK,EAC7B,IAAK,SACL,IAAK,UACL,IAAK,YACH,OAAO,OAAOA,CAAK,EACrB,IAAK,SACH,MAAO,GAAGA,EAAM,GAClB,IAAK,SACH,OAAOA,EAAM,SAAS,EACxB,IAAK,WACH,MAAO,KAGX,CACA,GAAIA,IAAU,KAAM,MAAO,OAC3B,GAAI,MAAM,QAAQA,CAAK,EAAG,MAAO,SAASA,EAAM,OAAO,GACvD,GAAIA,aAAiB,IAAK,MAAO,OAAOA,EAAM,KAAK,GACnD,GAAIA,aAAiB,IAAK,MAAO,OAAOA,EAAM,KAAK,GACnD,GAAIA,aAAiB,KACnB,OAAO,OAAO,MAAMA,EAAM,QAAQ,CAAC,EAAI,eAAiBA,EAAM,YAAY,EAC5E,GAAIA,aAAiB,MAAO,MAAO,GAAGA,EAAM,KAAK,IAAIA,EAAM,UAC3D,GAAI,OAAO,KAAS,KAAeA,aAAiB,KAClD,OAAOA,aAAiB,QAAU,IAAIA,EAAM,UAAU,GAAK,IAAIA,EAAM,WAEvE,IAAME,EAAqB,OAAO,eAAeF,CAAK,EACtD,GAAIE,IAAc,MAAQA,IAAc,OAAO,UAAW,MAAO,MACjE,IAAMC,EACJ,OAAOD,GAAc,UAAYA,EAC7B,QAAQ,IAAIA,EAAW,aAAa,EACpC,IAAA,GACAE,EAAO,OAAOD,GAAS,WAAaA,EAAK,KAAO,GACtD,OAAOC,IAAS,GAAK,MAAQ,GAAGA,EAAK,KACvC,CAiBA,SAAgBC,EAAa,EAAgB,EAAQ,IAAa,CAChE,IAAMP,EAAOQ,aAAiB,MAAQ,GAAGA,EAAM,KAAK,IAAIA,EAAM,UAAYT,EAAQS,EAAOL,CAAK,EAC9F,OAAOH,EAAK,OAASG,EAAQ,GAAGH,EAAK,MAAM,EAAGG,EAAQ,CAAC,EAAE,GAAKH,CAChE,CAiBA,SAAgBS,EAAe,EAAwB,CACrD,GAAIP,IAAU,IAAA,GAAW,MAAO,YAChC,GAAI,CACF,IAAMQ,EAAO,KAAK,UAAUR,EAAO,KAAM,CAAC,EAC1C,OAAOQ,IAAS,IAAA,GAAYX,EAAQG,EAAO,GAAG,EAAIQ,CACpD,OAASF,EAAO,CAEd,MAAO,GAAGT,EAAQG,EAAO,GAAG,EAAE,cAAcK,EAAaC,EAAO,EAAE,EAAE,EACtE,CACF,CAoBA,SAAgBG,EAAkB,EAA2B,CAC3D,IAAMC,EAAUZ,EAAK,KAAK,EAC1B,GAAIY,IAAY,YAAa,MAAO,CAAE,GAAI,GAAM,MAAO,IAAA,EAAU,EACjE,GAAI,CACF,MAAO,CAAE,GAAI,GAAM,MAAO,KAAK,MAAMA,CAAO,CAAa,CAC3D,OAASJ,EAAO,CACd,MAAO,CAAE,GAAI,GAAO,MAAOD,EAAaC,EAAO,GAAG,CAAE,CACtD,CACF"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=1,t=`__SHADOWJS_DEVTOOLS_HOOK__`,n={OWNER_CREATED:1,OWNER_DISPOSED:2,SIGNAL_CREATED:3,SIGNAL_WRITTEN:4,COMPUTATION_CREATED:5,COMPUTATION_RUN:6,COMPUTATION_DISPOSED:7,COMPONENT_MOUNTED:8,COMPONENT_UNMOUNTED:9,RESOURCE_STATE:10,ERROR:11,HYDRATION_MISMATCH:12,FLUSH_START:13,FLUSH_END:14,STORE_CREATED:15,TRANSITION_START:16,TRANSITION_END:17},r=500,i=16,a=0;export{n as EVENT,t as HOOK_KEY,i as MAX_INSPECTIONS,r as MAX_INSPECTION_ROWS,a as NO_ID,e as PROTOCOL_VERSION};
|
|
2
|
+
//# sourceMappingURL=protocol.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol.js","names":["PROTOCOL_VERSION","HOOK_KEY","EVENT","MAX_INSPECTION_ROWS","MAX_INSPECTIONS","NO_ID"],"sources":["../../src/protocol.ts"],"sourcesContent":["/**\n * The dev-hook protocol: how a ShadowJS runtime reports its reactive graph to developer tools.\n *\n * The runtime (core, web and query) calls `hook.report(kind, a, b, c, d, e)` for every event, in\n * development builds only (`if (__DEV__) { ... }`). The arguments are positional and the objects\n * are the runtime's own nodes: the hook never copies them, so reporting allocates nothing. An agent\n * (see {@link createAgent}) turns them into serializable, id-based {@link WireEvent}s.\n *\n * The protocol is versioned by {@link PROTOCOL_VERSION}. A runtime reports only to a hook whose\n * version it knows, and the agent ignores events with an unknown kind, so old runtimes and new\n * tools (and the other way round) degrade instead of breaking.\n *\n * @packageDocumentation\n */\n\nimport type { InspectionEntry, InspectionSource } from \"./inspection\";\n\n/**\n * Protocol version carried by the hook (`hook.version`). Bump it when the meaning of an existing\n * event argument changes; adding event kinds does not require a bump.\n *\n * @example\n * import { getHook, PROTOCOL_VERSION } from \"@shadow-js/devtools\";\n *\n * const speaks = getHook()?.version === PROTOCOL_VERSION; // true when a matching hook is installed\n * @since 1.0.0\n */\nexport const PROTOCOL_VERSION = 1;\n\n/**\n * Name of the global the runtime looks up: `globalThis.__SHADOWJS_DEVTOOLS_HOOK__`.\n *\n * @example\n * import { HOOK_KEY } from \"@shadow-js/devtools\";\n *\n * const installed: unknown = Reflect.get(globalThis, HOOK_KEY);\n * @since 1.0.0\n */\nexport const HOOK_KEY = \"__SHADOWJS_DEVTOOLS_HOOK__\";\n\n/**\n * Event kinds of `hook.report(kind, ...)`, with the meaning of the arguments. An argument the table\n * leaves blank is not sent for that kind. `null` means \"no node\" (for example, a computation outside\n * any owner); `undefined` means \"no name\".\n *\n * | kind | a | b | c | d | e |\n * | --- | --- | --- | --- | --- | --- |\n * | `OWNER_CREATED` | owner | parent owner, or `null` for a root | `\"root\"` or `\"owner\"` | name, when known (the core runtime sends none) | |\n * | `OWNER_DISPOSED` | owner | | | | |\n * | `SIGNAL_CREATED` | signal | owner or `null` | name or `undefined` | initial value (shown as a preview) | |\n * | `SIGNAL_WRITTEN` | signal | next value | writing computation, or `null` | observers (array of computations), or `null` | |\n * | `COMPUTATION_CREATED` | computation | owner or `null` | `\"memo\"` or `\"effect\"` | name or `undefined` | render flag |\n * | `COMPUTATION_RUN` | computation | start time (ms) | duration (ms) | value changed | sources (array of the nodes it read) |\n * | `COMPUTATION_DISPOSED` | computation | | | | |\n * | `COMPONENT_MOUNTED` | owner of the component | name or `undefined` | what the component returned: a DOM node, an array of them, or another value | | |\n * | `COMPONENT_UNMOUNTED` | owner of the component | | | | |\n * | `RESOURCE_STATE` | resource handle | state (string) | name or `undefined` | owner or `null` | |\n * | `ERROR` | source owner or `null` | the thrown value | diagnostic code or `undefined` | | |\n * | `HYDRATION_MISMATCH` | message | expected side (text or node; the web runtime sends the element key) | actual side (text or node; the web runtime sends none) | | |\n * | `FLUSH_START` | render queue size | user queue size | | | |\n * | `FLUSH_END` | computations run in the flush | | | | |\n * | `STORE_CREATED` | store handle ({@link StoreHandle}) | owner or `null` | | | |\n * | `TRANSITION_START` | | | | | |\n * | `TRANSITION_END` | transitions resolved (count) | | | | |\n *\n * The agent ignores `TRANSITION_START` and `TRANSITION_END` today, and any other unknown kind.\n *\n * @example\n * import { EVENT } from \"@shadow-js/devtools\";\n *\n * const kind = EVENT.COMPUTATION_RUN; // 6: a = computation, b = start, c = duration, d = changed, e = sources\n * @since 1.0.0\n */\nexport const EVENT: {\n readonly OWNER_CREATED: 1;\n readonly OWNER_DISPOSED: 2;\n readonly SIGNAL_CREATED: 3;\n readonly SIGNAL_WRITTEN: 4;\n readonly COMPUTATION_CREATED: 5;\n readonly COMPUTATION_RUN: 6;\n readonly COMPUTATION_DISPOSED: 7;\n readonly COMPONENT_MOUNTED: 8;\n readonly COMPONENT_UNMOUNTED: 9;\n readonly RESOURCE_STATE: 10;\n readonly ERROR: 11;\n readonly HYDRATION_MISMATCH: 12;\n readonly FLUSH_START: 13;\n readonly FLUSH_END: 14;\n readonly STORE_CREATED: 15;\n readonly TRANSITION_START: 16;\n readonly TRANSITION_END: 17;\n} = {\n OWNER_CREATED: 1,\n OWNER_DISPOSED: 2,\n SIGNAL_CREATED: 3,\n SIGNAL_WRITTEN: 4,\n COMPUTATION_CREATED: 5,\n COMPUTATION_RUN: 6,\n COMPUTATION_DISPOSED: 7,\n COMPONENT_MOUNTED: 8,\n COMPONENT_UNMOUNTED: 9,\n RESOURCE_STATE: 10,\n ERROR: 11,\n HYDRATION_MISMATCH: 12,\n FLUSH_START: 13,\n FLUSH_END: 14,\n STORE_CREATED: 15,\n TRANSITION_START: 16,\n TRANSITION_END: 17,\n};\n\n/**\n * One of the numeric event kinds.\n *\n * @since 1.0.0\n */\nexport type EventKind = (typeof EVENT)[keyof typeof EVENT];\n\n/**\n * What a runtime passes with `STORE_CREATED`, so tools can show and edit a store without knowing its\n * implementation.\n *\n * @since 1.0.0\n */\nexport interface StoreHandle {\n /** Label shown in the store viewer. */\n readonly name: string;\n /** A plain copy of the current value (what `snapshot(store)` returns). */\n snapshot(): unknown;\n /** Writes `value` at `path` (property names and array indices) in one batched update. */\n set(path: readonly (string | number)[], value: unknown): void;\n}\n\n/**\n * The receiver side of the hook: an agent implements it to be told about events.\n *\n * @since 1.0.0\n */\nexport interface HookSink {\n /**\n * Called synchronously for every reported event, with the arguments of {@link EVENT}. Must be cheap\n * and must not throw; a throw is caught by the hook and counted as a fault.\n */\n receive(kind: number, a: unknown, b: unknown, c: unknown, d: unknown, e: unknown): void;\n}\n\n/**\n * Most rows one inspection source may return. The rest are cut and a final row says so, so a panel\n * never receives an unbounded list.\n *\n * @example\n * import { MAX_INSPECTION_ROWS } from \"@shadow-js/devtools\";\n * const cap: number = MAX_INSPECTION_ROWS; // 500\n * @since 1.0.0\n */\nexport const MAX_INSPECTION_ROWS = 500;\n\n/**\n * Most sources an app may register with {@link registerInspection}. Registering one more is refused,\n * and the names already registered keep working.\n *\n * @example\n * import { MAX_INSPECTIONS } from \"@shadow-js/devtools\";\n * const cap: number = MAX_INSPECTIONS; // 16\n * @since 1.0.0\n */\nexport const MAX_INSPECTIONS = 16;\n\n/**\n * The object installed at `globalThis.__SHADOWJS_DEVTOOLS_HOOK__` by `installHook`. A runtime calls\n * `report` on it; a tool attaches a {@link HookSink} to receive the events.\n *\n * @example\n * import { EVENT, getHook } from \"@shadow-js/devtools\";\n *\n * const hook = getHook();\n * hook?.report(EVENT.FLUSH_END, 0); // a flush that ran no computations\n * @since 1.0.0\n */\nexport interface DevtoolsHook {\n /** Protocol version; compare with {@link PROTOCOL_VERSION} before reporting. */\n readonly version: number;\n /** Runtime to tools. Positional arguments, see {@link EVENT}. Never throws. */\n report(kind: number, a?: unknown, b?: unknown, c?: unknown, d?: unknown, e?: unknown): void;\n /**\n * Registers a sink; returns the function that removes it. Events buffered before the first sink\n * attached (up to the buffer size given to `installHook`) are replayed to that first sink only.\n */\n attach(sink: HookSink): () => void;\n /** Number of attached sinks. */\n readonly sinks: number;\n /**\n * Registers an inspection source under `name`; re-registering a name replaces it. Returns false when\n * refused (the limit of {@link MAX_INSPECTIONS} is reached). Hooks of this package provide it; a\n * hook made by hand need not.\n */\n inspect?(name: string, source: InspectionSource): boolean;\n /** The registered inspection sources, in registration order. Hooks of this package provide it. */\n inspections?(): readonly InspectionEntry[];\n}\n\n/**\n * Events as they cross a process boundary: plain, JSON-safe objects that refer to nodes by id.\n * `seq` is a gapless counter; `t` is the time in milliseconds on the reporting clock.\n *\n * @since 1.0.0\n */\nexport type WireEvent =\n | {\n k: \"owner+\";\n seq: number;\n t: number;\n id: number;\n parent: number;\n label: string;\n name?: string;\n }\n | { k: \"owner-\"; seq: number; t: number; id: number }\n | {\n k: \"signal+\";\n seq: number;\n t: number;\n id: number;\n owner: number;\n name?: string;\n preview: string;\n }\n | {\n k: \"write\";\n seq: number;\n t: number;\n id: number;\n preview: string;\n writer: number;\n observers: number[];\n }\n | {\n k: \"comp+\";\n seq: number;\n t: number;\n id: number;\n owner: number;\n kind: \"memo\" | \"effect\";\n render: boolean;\n name?: string;\n }\n | {\n k: \"run\";\n seq: number;\n t: number;\n id: number;\n start: number;\n duration: number;\n changed: boolean;\n sources: number[];\n }\n | { k: \"comp-\"; seq: number; t: number; id: number }\n | { k: \"mount\"; seq: number; t: number; id: number; name: string }\n | { k: \"unmount\"; seq: number; t: number; id: number }\n | {\n k: \"resource\";\n seq: number;\n t: number;\n id: number;\n owner: number;\n state: string;\n name?: string;\n }\n | { k: \"error\"; seq: number; t: number; id: number; message: string; code?: string }\n | { k: \"hydration\"; seq: number; t: number; message: string; expected: string; actual: string }\n | { k: \"flush+\"; seq: number; t: number; render: number; user: number }\n | { k: \"flush-\"; seq: number; t: number; runs: number }\n | { k: \"store+\"; seq: number; t: number; id: number; owner: number; name: string };\n\n/**\n * Discriminant of {@link WireEvent}.\n *\n * @since 1.0.0\n */\nexport type WireKind = WireEvent[\"k\"];\n\n/**\n * The sentinel used where a wire event refers to \"no node\". The agent assigns node ids from 1, so\n * `NO_ID` never names a node.\n *\n * @example\n * import { NO_ID } from \"@shadow-js/devtools\";\n *\n * const isRoot = (parent: number): boolean => parent === NO_ID;\n * @since 1.0.0\n */\nexport const NO_ID = 0;\n"],"mappings":"AA2BA,MAAaA,EAAmB,EAWnBC,EAAW,6BAmCXC,EAkBT,CACF,cAAe,EACf,eAAgB,EAChB,eAAgB,EAChB,eAAgB,EAChB,oBAAqB,EACrB,gBAAiB,EACjB,qBAAsB,EACtB,kBAAmB,EACnB,oBAAqB,EACrB,eAAgB,GAChB,MAAO,GACP,mBAAoB,GACpB,YAAa,GACb,UAAW,GACX,cAAe,GACf,iBAAkB,GAClB,eAAgB,EAClB,EA8CaC,EAAsB,IAWtBC,EAAkB,GA6HlBC,EAAQ"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var e=class{capacity;slots;head=0;count=0;overwritten=0;constructor(e){if(!Number.isInteger(e)||e<1)throw RangeError(`RingBuffer capacity must be a positive integer`);this.capacity=e,this.slots=Array(e).fill(void 0)}get size(){return this.count}get dropped(){return this.overwritten}push(e){let t=(this.head+this.count)%this.capacity,n;return this.count===this.capacity?(n=this.slots[this.head],this.slots[this.head]=e,this.head=(this.head+1)%this.capacity,this.overwritten+=1):(this.slots[t]=e,this.count+=1),n}at(e){if(!(e<0||e>=this.count))return this.slots[(this.head+e)%this.capacity]}last(){return this.count===0?void 0:this.at(this.count-1)}toArray(){let e=[];for(let t=0;t<this.count;t+=1){let n=this.slots[(this.head+t)%this.capacity];n!==void 0&&e.push(n)}return e}*[Symbol.iterator](){for(let e=0;e<this.count;e+=1){let t=this.slots[(this.head+e)%this.capacity];t!==void 0&&(yield t)}}clear(){this.slots.fill(void 0),this.head=0,this.count=0}};export{e as RingBuffer};
|
|
2
|
+
//# sourceMappingURL=ring.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ring.js","names":["RingBuffer","capacity","index","evicted","item","out","i"],"sources":["../../src/ring.ts"],"sourcesContent":["/**\n * A fixed-capacity ring buffer. `push` is O(1) and never allocates after construction; when full it\n * overwrites the oldest item, so memory stays bounded however many items are recorded. Items are kept\n * oldest first. Do not push `undefined`: `toArray` and the iterator skip such items, while `size` still\n * counts them.\n *\n * @typeParam T The item type.\n * @example\n * import { RingBuffer } from \"@shadow-js/devtools\";\n *\n * const ring = new RingBuffer<number>(3);\n * ring.push(1);\n * ring.push(2);\n * ring.push(3);\n * ring.push(4);\n * ring.toArray(); // [2, 3, 4]\n * @since 1.0.0\n */\nexport class RingBuffer<T> {\n /** Maximum number of items kept. Fixed at construction. */\n readonly capacity: number;\n private readonly slots: Array<T | undefined>;\n private head = 0;\n private count = 0;\n private overwritten = 0;\n\n /**\n * @param capacity Maximum number of items kept (a positive integer).\n * @throws {@link RangeError} When `capacity` is not a positive integer.\n */\n constructor(capacity: number) {\n if (!Number.isInteger(capacity) || capacity < 1) {\n throw new RangeError(\"RingBuffer capacity must be a positive integer\");\n }\n this.capacity = capacity;\n this.slots = new Array<T | undefined>(capacity).fill(undefined);\n }\n\n /** Number of items currently held (at most `capacity`). */\n get size(): number {\n return this.count;\n }\n\n /** How many items were overwritten because the buffer was full. */\n get dropped(): number {\n return this.overwritten;\n }\n\n /** Adds an item, evicting the oldest when full. Returns the evicted item, if any. */\n push(item: T): T | undefined {\n const index = (this.head + this.count) % this.capacity;\n let evicted: T | undefined;\n if (this.count === this.capacity) {\n evicted = this.slots[this.head];\n this.slots[this.head] = item;\n this.head = (this.head + 1) % this.capacity;\n this.overwritten += 1;\n } else {\n this.slots[index] = item;\n this.count += 1;\n }\n return evicted;\n }\n\n /** The item `index` positions after the oldest one, or `undefined` out of range. */\n at(index: number): T | undefined {\n if (index < 0 || index >= this.count) return undefined;\n return this.slots[(this.head + index) % this.capacity];\n }\n\n /** The newest item, or `undefined` when empty. */\n last(): T | undefined {\n return this.count === 0 ? undefined : this.at(this.count - 1);\n }\n\n /** Items oldest first, as a new array. */\n toArray(): T[] {\n const out: T[] = [];\n for (let i = 0; i < this.count; i += 1) {\n const item = this.slots[(this.head + i) % this.capacity];\n if (item !== undefined) out.push(item);\n }\n return out;\n }\n\n /** Iterates oldest first without allocating an array. */\n *[Symbol.iterator](): IterableIterator<T> {\n for (let i = 0; i < this.count; i += 1) {\n const item = this.slots[(this.head + i) % this.capacity];\n if (item !== undefined) yield item;\n }\n }\n\n /** Removes every item (the `dropped` counter is kept). */\n clear(): void {\n this.slots.fill(undefined);\n this.head = 0;\n this.count = 0;\n }\n}\n"],"mappings":"AAkBA,IAAaA,EAAb,KAA2B,CAEzB,SACA,MACA,KAAe,EACf,MAAgB,EAChB,YAAsB,EAMtB,YAAY,EAAkB,CAC5B,GAAI,CAAC,OAAO,UAAUC,CAAQ,GAAKA,EAAW,EAC5C,MAAU,WAAW,gDAAgD,EAEvE,KAAK,SAAWA,EAChB,KAAK,MAAY,MAAqBA,CAAQ,CAAC,CAAC,KAAK,IAAA,EAAS,CAChE,CAGA,IAAI,MAAe,CACjB,OAAO,KAAK,KACd,CAGA,IAAI,SAAkB,CACpB,OAAO,KAAK,WACd,CAGA,KAAK,EAAwB,CAC3B,IAAMC,GAAS,KAAK,KAAO,KAAK,OAAS,KAAK,SAC1CC,EAUJ,OATI,KAAK,QAAU,KAAK,UACtB,EAAU,KAAK,MAAM,KAAK,MAC1B,KAAK,MAAM,KAAK,MAAQC,EACxB,KAAK,MAAQ,KAAK,KAAO,GAAK,KAAK,SACnC,KAAK,aAAe,IAEpB,KAAK,MAAMF,GAASE,EACpB,KAAK,OAAS,GAETD,CACT,CAGA,GAAG,EAA8B,CAC/B,GAAI,IAAQ,GAAKD,GAAS,KAAK,OAC/B,OAAO,KAAK,OAAO,KAAK,KAAOA,GAAS,KAAK,SAC/C,CAGA,MAAsB,CACpB,OAAO,KAAK,QAAU,EAAI,IAAA,GAAY,KAAK,GAAG,KAAK,MAAQ,CAAC,CAC9D,CAGA,SAAe,CACb,IAAMG,EAAW,CAAC,EAClB,IAAK,IAAIC,EAAI,EAAGA,EAAI,KAAK,MAAO,GAAK,EAAG,CACtC,IAAMF,EAAO,KAAK,OAAO,KAAK,KAAOE,GAAK,KAAK,UAC3CF,IAAS,IAAA,IAAWC,EAAI,KAAKD,CAAI,CACvC,CACA,OAAOC,CACT,CAGA,EAAE,OAAO,WAAiC,CACxC,IAAK,IAAIC,EAAI,EAAGA,EAAI,KAAK,MAAO,GAAK,EAAG,CACtC,IAAMF,EAAO,KAAK,OAAO,KAAK,KAAOE,GAAK,KAAK,UAC3CF,IAAS,IAAA,KAAW,MAAMA,EAChC,CACF,CAGA,OAAc,CACZ,KAAK,MAAM,KAAK,IAAA,EAAS,EACzB,KAAK,KAAO,EACZ,KAAK,MAAQ,CACf,CACF"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import"./protocol.js";import{InspectorModel as e}from"./model.js";const t=`shadowjs-devtools`;function n(e){return typeof e==`object`&&!!e}function r(e,t){return n(e)&&e.channel===`shadowjs-devtools`&&e.from===t&&typeof e.v==`number`&&typeof e.type==`string`}function i(e){if(!r(e,`agent`))return!1;switch(e.type){case`hello`:return!0;case`events`:return Array.isArray(e.events);case`response`:return typeof e.id==`number`&&n(e.response);default:return!1}}function a(e){if(!r(e,`panel`))return!1;switch(e.type){case`hello`:return!0;case`request`:return typeof e.id==`number`&&n(e.request)&&typeof e.request.type==`string`;default:return!1}}function o(){let e=[/* @__PURE__ */ new Set,/* @__PURE__ */ new Set],t=!1,n=n=>({send(r){if(t)return;let i=structuredClone(r);for(let t of[...e[+(n===0)]])t(i)},listen(t){return e[n].add(t),()=>{e[n].delete(t)}},close(){t=!0,e[0].clear(),e[1].clear()}});return[n(0),n(1)]}function s(e){return{send(t){e.postMessage(t)},listen(t){let n=e=>t(e.data);return e.addEventListener(`message`,n),e.start?.(),()=>e.removeEventListener(`message`,n)},close(){e.close?.()}}}function c(e){if(e.origin===`*`)throw TypeError(`createWindowTransport: pass the real origin, not "*".`);let t=e.receiver??globalThis;return{send(t){e.target.postMessage(t,e.origin)},listen(n){let r=t=>{t.origin===e.origin&&t.source===e.target&&n(t.data)};return t.addEventListener(`message`,r),()=>t.removeEventListener(`message`,r)},close(){}}}function l(e,t=e=>new BroadcastChannel(e)){let n=t(e);return{send(e){n.postMessage(e)},listen(e){let t=t=>e(t.data);return n.addEventListener(`message`,t),()=>n.removeEventListener(`message`,t)},close(){n.close()}}}function u(e,n,r={}){let i=r.batch??2e3,o=e=>n.send(e),s=n.listen(n=>{if(!a(n))return;if(n.type===`hello`){o({channel:t,v:1,from:`agent`,type:`hello`});return}let{request:i}=n,s;i.type===`highlight`?(r.highlight?.(i.id),s={ok:!0,result:null}):s=e.handle(i),o({channel:t,v:1,from:`agent`,type:`response`,id:n.id,response:s})}),c=e.subscribe(e=>{for(let n=0;n<e.length;n+=i)o({channel:t,v:1,from:`agent`,type:`events`,events:e.slice(n,n+i)})});return()=>{s(),c()}}function d(n,r={}){let a=new e(r.limits),o=/* @__PURE__ */ new Set,s=/* @__PURE__ */ new Map,c=[],l=!1,u=1,d=!1,f=!1,p=()=>void 0,m=new Promise(e=>{p=e}),h=()=>{f||(f=!0,queueMicrotask(()=>{if(f=!1,!d)for(let e of o)e()}))},g=e=>n.send(e),_=e=>{a.importState(e),l=!0;let t=c;c=[];for(let e of t)a.apply(e);p(),h()},v=e=>{if(d)return Promise.reject(/* @__PURE__ */ Error(`The devtools backend was disposed.`));let n=u;return u+=1,new Promise((r,i)=>{s.set(n,{resolve:r,reject:i}),g({channel:t,v:1,from:`panel`,type:`request`,id:n,request:e})})},y=n.listen(e=>{if(i(e)&&e.v===1){if(e.type===`events`){if(!l){c.push(...e.events);return}for(let t of e.events)a.apply(t);h()}else if(e.type===`response`){let t=s.get(e.id);if(t===void 0)return;s.delete(e.id),t.resolve(e.response)}}}),b=()=>{v({type:`snapshot`}).then(e=>{e.ok&&_(e.result)},()=>{l=!1})};return g({channel:t,v:1,from:`panel`,type:`hello`}),b(),{model:a,ready:m,subscribe(e){return o.add(e),()=>{o.delete(e)}},request(e){return e.type===`clear`?v(e).then(e=>(e.ok&&(l=!1,b()),e)):v(e)},dispose(){d=!0,y(),o.clear();for(let e of s.values())e.reject(/* @__PURE__ */ Error(`The devtools backend was disposed.`));s.clear(),n.close()}}}export{t as CHANNEL,d as connectRemote,l as createBroadcastChannelTransport,o as createMemoryTransportPair,s as createPortTransport,c as createWindowTransport,i as isAgentMessage,a as isPanelMessage,u as serveAgent};
|
|
2
|
+
//# sourceMappingURL=transport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.js","names":["CHANNEL","isRecord","value","isEnvelope","\"channel\"","\"from\"","from","\"v\"","\"type\"","isAgentMessage","\"events\"","\"id\"","\"response\"","isPanelMessage","\"request\"","createMemoryTransportPair","handlers","closed","side","copy","message","handler","index","createPortTransport","port","listener","event","createWindowTransport","options","receiver","createBroadcastChannelTransport","n","channel","factory","name","serveAgent","limit","send","transport","stopListening","raw","response","request","agent","stopStreaming","i","events","connectRemote","model","InspectorModel","listeners","waiting","buffered","synced","nextRequest","disposed","notifyScheduled","resolveReady","ready","resolve","notify","applyState","state","pending","id","req","stop","entry","sync"],"sources":["../../src/transport.ts"],"sourcesContent":["import type { Agent, AgentRequest, AgentResponse } from \"./agent\";\nimport type { Backend, PageHooks } from \"./backend\";\nimport { InspectorModel, type ModelLimits, type ModelState } from \"./model\";\nimport { PROTOCOL_VERSION, type WireEvent } from \"./protocol\";\n\n/**\n * The `channel` field every devtools message carries. It lets the messages share a `postMessage`\n * or `BroadcastChannel` with other traffic: anything without this value is ignored.\n *\n * @example\n * import { CHANNEL } from \"@shadow-js/devtools\";\n * window.addEventListener(\"message\", (event: MessageEvent) => {\n * if (event.data?.channel === CHANNEL) console.log(\"devtools message\", event.data.type);\n * });\n * @since 1.0.0\n */\nexport const CHANNEL = \"shadowjs-devtools\";\n\n/**\n * A message the agent side sends to the panel: the answer to a `hello`, a batch of events, or the\n * answer to a request. All fields are plain data.\n *\n * @since 1.0.0\n */\nexport type AgentMessage =\n | { channel: typeof CHANNEL; v: number; from: \"agent\"; type: \"hello\" }\n | { channel: typeof CHANNEL; v: number; from: \"agent\"; type: \"events\"; events: WireEvent[] }\n | {\n channel: typeof CHANNEL;\n v: number;\n from: \"agent\";\n type: \"response\";\n id: number;\n response: AgentResponse;\n };\n\n/**\n * A message the panel side sends to the agent: a `hello`, or a request. The agent answers a request\n * with a `response` that carries the same `id`. All fields are plain data.\n *\n * @since 1.0.0\n */\nexport type PanelMessage =\n | { channel: typeof CHANNEL; v: number; from: \"panel\"; type: \"hello\" }\n | {\n channel: typeof CHANNEL;\n v: number;\n from: \"panel\";\n type: \"request\";\n id: number;\n request: AgentRequest;\n };\n\n/**\n * A duplex channel of plain messages. Implementations must deliver messages in order; they need not\n * be synchronous. `postMessage`, `MessagePort` and `BroadcastChannel` adapters are provided.\n *\n * @since 1.0.0\n */\nexport interface Transport {\n /**\n * Sends a message to the other end. The message must be plain data that can be structured-cloned.\n */\n send(message: unknown): void;\n /**\n * Starts delivering incoming messages to `handler`, in order.\n *\n * @returns A function that stops this listener.\n */\n listen(handler: (message: unknown) => void): () => void;\n /**\n * Releases what the transport holds. Closing a port or a `BroadcastChannel` closes it; a window\n * transport holds nothing and does nothing.\n */\n close(): void;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n\nfunction isEnvelope(value: unknown, from: \"agent\" | \"panel\"): value is Record<string, unknown> {\n return (\n isRecord(value) &&\n value[\"channel\"] === CHANNEL &&\n value[\"from\"] === from &&\n typeof value[\"v\"] === \"number\" &&\n typeof value[\"type\"] === \"string\"\n );\n}\n\n/**\n * True for a well-formed message sent by an agent. Anything else on the channel is ignored. The check\n * covers the envelope (`channel`, `from`, `v`, `type`) and the fields each message type needs. It does\n * not validate every event inside an `events` message.\n *\n * @param value A received message.\n * @returns True when `value` is an {@link AgentMessage}.\n * @example\n * import { CHANNEL, isAgentMessage } from \"@shadow-js/devtools\";\n * isAgentMessage({ channel: CHANNEL, v: 1, from: \"agent\", type: \"hello\" }); // true\n * isAgentMessage({ channel: CHANNEL, v: 1, from: \"panel\", type: \"hello\" }); // false\n * @since 1.0.0\n */\nexport function isAgentMessage(value: unknown): value is AgentMessage {\n if (!isEnvelope(value, \"agent\")) return false;\n switch (value[\"type\"]) {\n case \"hello\":\n return true;\n case \"events\":\n return Array.isArray(value[\"events\"]);\n case \"response\":\n return typeof value[\"id\"] === \"number\" && isRecord(value[\"response\"]);\n default:\n return false;\n }\n}\n\n/**\n * True for a well-formed message sent by a panel. Anything else on the channel is ignored. The check\n * covers the envelope and the fields each message type needs, including the request's `type`.\n *\n * @param value A received message.\n * @returns True when `value` is a {@link PanelMessage}.\n * @example\n * import { CHANNEL, isPanelMessage } from \"@shadow-js/devtools\";\n * isPanelMessage({ channel: CHANNEL, v: 1, from: \"panel\", type: \"hello\" }); // true\n * isPanelMessage({ channel: CHANNEL, v: 1, from: \"agent\", type: \"hello\" }); // false\n * @since 1.0.0\n */\nexport function isPanelMessage(value: unknown): value is PanelMessage {\n if (!isEnvelope(value, \"panel\")) return false;\n switch (value[\"type\"]) {\n case \"hello\":\n return true;\n case \"request\":\n return (\n typeof value[\"id\"] === \"number\" &&\n isRecord(value[\"request\"]) &&\n typeof value[\"request\"][\"type\"] === \"string\"\n );\n default:\n return false;\n }\n}\n\n/**\n * Two connected in-memory transports, for tests and for connecting an agent and a panel inside one\n * page. Messages are structured-cloned, so anything that is not plain serializable data fails here\n * exactly as it would over `postMessage`. Closing either end closes both.\n *\n * @returns `[agentSide, panelSide]`.\n * @example\n * import { connectRemote, createAgent, createMemoryTransportPair, serveAgent } from \"@shadow-js/devtools\";\n * const [agentSide, panelSide] = createMemoryTransportPair();\n * const stop = serveAgent(createAgent(), agentSide);\n * const backend = connectRemote(panelSide);\n * await backend.ready;\n * stop();\n * @since 1.0.0\n */\nexport function createMemoryTransportPair(): [Transport, Transport] {\n const handlers: [Set<(message: unknown) => void>, Set<(message: unknown) => void>] = [\n new Set(),\n new Set(),\n ];\n let closed = false;\n const side = (index: 0 | 1): Transport => ({\n send(message): void {\n if (closed) return;\n const copy = structuredClone(message);\n for (const handler of [...handlers[index === 0 ? 1 : 0]]) handler(copy);\n },\n listen(handler): () => void {\n handlers[index].add(handler);\n return () => {\n handlers[index].delete(handler);\n };\n },\n close(): void {\n closed = true;\n handlers[0].clear();\n handlers[1].clear();\n },\n });\n return [side(0), side(1)];\n}\n\n/**\n * The part of a `MessagePort` the adapter uses. A `MessageChannel` end, a worker port and an\n * extension port all fit this shape.\n *\n * @since 1.0.0\n */\nexport interface MessagePortLike {\n /**\n * Posts a message to the other end of the port.\n */\n postMessage(message: unknown): void;\n /**\n * Adds a listener for incoming messages.\n */\n addEventListener(type: \"message\", listener: (event: { data: unknown }) => void): void;\n /**\n * Removes a listener added with `addEventListener`.\n */\n removeEventListener(type: \"message\", listener: (event: { data: unknown }) => void): void;\n /**\n * Starts the port. The adapter calls it after it adds a listener. Optional.\n */\n start?(): void;\n /**\n * Closes the port. Optional.\n */\n close?(): void;\n}\n\n/**\n * A transport over a `MessagePort`: a `MessageChannel` end, a worker port or an extension port.\n * Incoming messages reach the listeners registered with `listen`. `close` closes the port when it\n * supports closing.\n *\n * @param port The port.\n * @returns The transport.\n * @example\n * import { connectRemote, createAgent, createPortTransport, serveAgent } from \"@shadow-js/devtools\";\n * const channel = new MessageChannel();\n * serveAgent(createAgent(), createPortTransport(channel.port1));\n * const backend = connectRemote(createPortTransport(channel.port2));\n * await backend.ready;\n * @since 1.0.0\n */\nexport function createPortTransport(port: MessagePortLike): Transport {\n return {\n send(message): void {\n port.postMessage(message);\n },\n listen(handler): () => void {\n const listener = (event: { data: unknown }): void => handler(event.data);\n port.addEventListener(\"message\", listener);\n port.start?.();\n return () => port.removeEventListener(\"message\", listener);\n },\n close(): void {\n port.close?.();\n },\n };\n}\n\n/**\n * The part of a `Window` the adapter uses: its `postMessage`. A `WindowProxy` such as\n * `iframe.contentWindow` fits this shape.\n *\n * @since 1.0.0\n */\nexport interface WindowLike {\n /**\n * Posts a message to the window. The adapter always passes a real origin as `targetOrigin`.\n */\n postMessage(message: unknown, targetOrigin: string): void;\n}\n\n/**\n * The part of the receiving window the adapter uses: its `message` listeners. `globalThis` in a page\n * or a frame fits this shape.\n *\n * @since 1.0.0\n */\nexport interface WindowEvents {\n /**\n * Adds a listener for `message` events.\n */\n addEventListener(type: \"message\", listener: (event: MessageEvent) => void): void;\n /**\n * Removes a listener added with `addEventListener`.\n */\n removeEventListener(type: \"message\", listener: (event: MessageEvent) => void): void;\n}\n\n/**\n * Options of {@link createWindowTransport}.\n *\n * @since 1.0.0\n */\nexport interface WindowTransportOptions {\n /**\n * The window to talk to: the page being inspected, or the panel's frame.\n */\n target: WindowLike;\n /**\n * The origin of `target`. It is used as `targetOrigin` and to check where messages come from. Never\n * `\"*\"`: {@link createWindowTransport} refuses it.\n */\n origin: string;\n /**\n * Where messages arrive.\n *\n * @default globalThis\n */\n receiver?: WindowEvents;\n}\n\n/**\n * A transport over `window.postMessage` between a page and a panel in another window or frame (a\n * DevTools extension page, a popup). Both sides call it, each naming the other window and its origin.\n * Messages are sent only to `target`, with `origin` as the target origin. Incoming messages are\n * delivered only when their origin equals `origin` and their source is `target`; all others are\n * ignored.\n *\n * @param options Target window, its origin and the receiving window.\n * @returns The transport.\n * @throws {TypeError} When `origin` is `\"*\"`: the messages would reach any origin.\n * @example\n * import { createAgent, createWindowTransport, serveAgent } from \"@shadow-js/devtools\";\n * const frame = document.querySelector(\"iframe\");\n * if (frame?.contentWindow) {\n * const transport = createWindowTransport({ target: frame.contentWindow, origin: \"https://panel.example\" });\n * serveAgent(createAgent(), transport);\n * }\n * @since 1.0.0\n */\nexport function createWindowTransport(options: WindowTransportOptions): Transport {\n if (options.origin === \"*\")\n throw new TypeError('createWindowTransport: pass the real origin, not \"*\".');\n const receiver: WindowEvents = options.receiver ?? (globalThis as unknown as WindowEvents);\n return {\n send(message): void {\n options.target.postMessage(message, options.origin);\n },\n listen(handler): () => void {\n const listener = (event: MessageEvent): void => {\n if (event.origin !== options.origin) return;\n if ((event.source as unknown) !== options.target) return;\n handler(event.data);\n };\n receiver.addEventListener(\"message\", listener);\n return () => receiver.removeEventListener(\"message\", listener);\n },\n close(): void {\n // The window belongs to the page; nothing to release.\n },\n };\n}\n\n/**\n * The part of `BroadcastChannel` the adapter uses.\n *\n * @since 1.0.0\n */\nexport interface BroadcastChannelLike {\n /**\n * Posts a message to every other channel object with the same name.\n */\n postMessage(message: unknown): void;\n /**\n * Adds a listener for incoming messages.\n */\n addEventListener(type: \"message\", listener: (event: { data: unknown }) => void): void;\n /**\n * Removes a listener added with `addEventListener`.\n */\n removeEventListener(type: \"message\", listener: (event: { data: unknown }) => void): void;\n /**\n * Closes the channel object.\n */\n close(): void;\n}\n\n/**\n * A transport over a `BroadcastChannel`, for a panel in another tab or window of the same origin.\n * Several panels can listen to one agent; each message goes to all of them.\n *\n * @param name Channel name. Both sides use the same.\n * @param factory Creates the channel (default `new BroadcastChannel(name)`).\n * @returns The transport.\n * @example\n * import { connectRemote, createBroadcastChannelTransport } from \"@shadow-js/devtools\";\n * const backend = connectRemote(createBroadcastChannelTransport(\"shadowjs-devtools\"));\n * await backend.ready;\n * @since 1.0.0\n */\nexport function createBroadcastChannelTransport(\n name: string,\n factory: (name: string) => BroadcastChannelLike = (n) => new BroadcastChannel(n),\n): Transport {\n const channel = factory(name);\n return {\n send(message): void {\n channel.postMessage(message);\n },\n listen(handler): () => void {\n const listener = (event: { data: unknown }): void => handler(event.data);\n channel.addEventListener(\"message\", listener);\n return () => channel.removeEventListener(\"message\", listener);\n },\n close(): void {\n channel.close();\n },\n };\n}\n\n/**\n * Options of {@link serveAgent}: the page-side hooks and the batch size.\n *\n * @since 1.0.0\n */\nexport interface ServeOptions extends PageHooks {\n /**\n * Largest event batch sent in one message.\n *\n * @default 2000\n */\n batch?: number;\n}\n\n/**\n * Exposes an agent over a transport: forwards its event stream to the panel and answers the panel's\n * requests. The agent keeps recording while no panel listens, and the stream is sent regardless. What\n * happens to events nobody receives depends on the transport. A `highlight` request is passed to\n * `options.highlight` and answered without reaching the agent. Stop serving with the returned function.\n *\n * @param agent The agent in the inspected page.\n * @param transport The channel to the panel.\n * @param options Page-side behavior (highlight overlay) and batch size.\n * @returns A function that stops serving.\n * @example\n * import { createAgent, createMemoryTransportPair, serveAgent } from \"@shadow-js/devtools\";\n * const [pageSide] = createMemoryTransportPair();\n * const stop = serveAgent(createAgent(), pageSide, { highlight: (id) => console.log(\"highlight\", id) });\n * stop();\n * @since 1.0.0\n */\nexport function serveAgent(\n agent: Agent,\n transport: Transport,\n options: ServeOptions = {},\n): () => void {\n const limit = options.batch ?? 2000;\n const send = (message: AgentMessage): void => transport.send(message);\n const stopListening = transport.listen((raw) => {\n if (!isPanelMessage(raw)) return;\n if (raw.type === \"hello\") {\n send({ channel: CHANNEL, v: PROTOCOL_VERSION, from: \"agent\", type: \"hello\" });\n return;\n }\n const { request } = raw;\n let response: AgentResponse;\n if (request.type === \"highlight\") {\n options.highlight?.(request.id);\n response = { ok: true, result: null };\n } else {\n response = agent.handle(request);\n }\n send({\n channel: CHANNEL,\n v: PROTOCOL_VERSION,\n from: \"agent\",\n type: \"response\",\n id: raw.id,\n response,\n });\n });\n const stopStreaming = agent.subscribe((events) => {\n for (let i = 0; i < events.length; i += limit) {\n send({\n channel: CHANNEL,\n v: PROTOCOL_VERSION,\n from: \"agent\",\n type: \"events\",\n events: events.slice(i, i + limit) as WireEvent[],\n });\n }\n });\n return () => {\n stopListening();\n stopStreaming();\n };\n}\n\n/**\n * Options of {@link connectRemote}.\n *\n * @since 1.0.0\n */\nexport interface RemoteOptions {\n /**\n * Capacities of the panel's copy of the model (see {@link ModelLimits}). Fields that are not set keep\n * their defaults.\n */\n limits?: Partial<ModelLimits>;\n}\n\n/**\n * Connects a panel to a remote agent: asks for the complete state, then applies the event stream\n * to a local model. Events that arrive before the state are buffered, and the ones the state already\n * contains are skipped (sequence numbers), so no event is lost or applied twice. Disposing the backend\n * closes the transport.\n *\n * @param transport The channel to the agent.\n * @param options Model limits.\n * @returns A {@link Backend}.\n * @example\n * import { connectRemote, createBroadcastChannelTransport } from \"@shadow-js/devtools\";\n * const backend = connectRemote(createBroadcastChannelTransport(\"shadowjs-devtools\"));\n * await backend.ready;\n * @since 1.0.0\n */\nexport function connectRemote(transport: Transport, options: RemoteOptions = {}): Backend {\n const model = new InspectorModel(options.limits);\n const listeners = new Set<() => void>();\n const waiting = new Map<\n number,\n { resolve: (response: AgentResponse) => void; reject: (error: Error) => void }\n >();\n let buffered: WireEvent[] = [];\n let synced = false;\n let nextRequest = 1;\n let disposed = false;\n let notifyScheduled = false;\n let resolveReady: () => void = () => undefined;\n const ready = new Promise<void>((resolve) => {\n resolveReady = resolve;\n });\n\n const notify = (): void => {\n if (notifyScheduled) return;\n notifyScheduled = true;\n queueMicrotask(() => {\n notifyScheduled = false;\n if (disposed) return;\n for (const listener of listeners) listener();\n });\n };\n\n const send = (message: PanelMessage): void => transport.send(message);\n\n const applyState = (state: ModelState): void => {\n model.importState(state);\n synced = true;\n const pending = buffered;\n buffered = [];\n for (const event of pending) model.apply(event);\n resolveReady();\n notify();\n };\n\n const request = (req: AgentRequest): Promise<AgentResponse> => {\n if (disposed) return Promise.reject(new Error(\"The devtools backend was disposed.\"));\n const id = nextRequest;\n nextRequest += 1;\n return new Promise<AgentResponse>((resolve, reject) => {\n waiting.set(id, { resolve, reject });\n send({\n channel: CHANNEL,\n v: PROTOCOL_VERSION,\n from: \"panel\",\n type: \"request\",\n id,\n request: req,\n });\n });\n };\n\n const stop = transport.listen((raw) => {\n if (!isAgentMessage(raw)) return;\n if (raw.v !== PROTOCOL_VERSION) return;\n if (raw.type === \"events\") {\n if (!synced) {\n buffered.push(...raw.events);\n return;\n }\n for (const event of raw.events) model.apply(event);\n notify();\n return;\n }\n if (raw.type === \"response\") {\n const entry = waiting.get(raw.id);\n if (entry === undefined) return;\n waiting.delete(raw.id);\n entry.resolve(raw.response);\n }\n });\n\n const sync = (): void => {\n void request({ type: \"snapshot\" }).then(\n (response) => {\n if (response.ok) applyState(response.result as ModelState);\n },\n () => {\n // The only rejection is dispose(): there is nothing left to synchronize.\n synced = false;\n },\n );\n };\n\n send({ channel: CHANNEL, v: PROTOCOL_VERSION, from: \"panel\", type: \"hello\" });\n sync();\n\n return {\n model,\n ready,\n subscribe(listener): () => void {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n request(req): Promise<AgentResponse> {\n if (req.type === \"clear\") {\n // A clear empties the agent; the panel's copy must follow.\n return request(req).then((response) => {\n if (response.ok) {\n synced = false;\n sync();\n }\n return response;\n });\n }\n return request(req);\n },\n dispose(): void {\n disposed = true;\n stop();\n listeners.clear();\n for (const entry of waiting.values())\n entry.reject(new Error(\"The devtools backend was disposed.\"));\n waiting.clear();\n transport.close();\n },\n };\n}\n"],"mappings":"kEAgBA,MAAaA,EAAU,oBA6DvB,SAASC,EAAS,EAAkD,CAClE,OAAO,OAAOC,GAAU,YAAYA,CACtC,CAEA,SAASC,EAAW,EAAgB,EAA2D,CAC7F,OACEF,EAASC,CAAK,GACdA,EAAME,UAAA,qBACNF,EAAMG,OAAYC,GAClB,OAAOJ,EAAMK,GAAS,UACtB,OAAOL,EAAMM,MAAY,QAE7B,CAeA,SAAgBC,EAAe,EAAuC,CACpE,GAAI,CAACN,EAAWD,EAAO,OAAO,EAAG,MAAO,GACxC,OAAQA,EAAMM,KAAd,CACE,IAAK,QACH,MAAO,GACT,IAAK,SACH,OAAO,MAAM,QAAQN,EAAMQ,MAAS,EACtC,IAAK,WACH,OAAO,OAAOR,EAAMS,IAAU,UAAYV,EAASC,EAAMU,QAAW,EACtE,QACE,MAAO,EACX,CACF,CAcA,SAAgBC,EAAe,EAAuC,CACpE,GAAI,CAACV,EAAWD,EAAO,OAAO,EAAG,MAAO,GACxC,OAAQA,EAAMM,KAAd,CACE,IAAK,QACH,MAAO,GACT,IAAK,UACH,OACE,OAAON,EAAMS,IAAU,UACvBV,EAASC,EAAMY,OAAU,GACzB,OAAOZ,EAAMY,QAAWN,MAAY,SAExC,QACE,MAAO,EACX,CACF,CAiBA,SAAgBO,GAAoD,CAClE,IAAMC,EAA+E,iBACnF,IAAI,oBACJ,IAAI,GACN,EACIC,EAAS,GACPC,EAAQ,IAA6B,CACzC,KAAK,EAAe,CAClB,GAAID,EAAQ,OACZ,IAAME,EAAO,gBAAgBC,CAAO,EACpC,IAAK,IAAMC,IAAW,CAAC,GAAGL,EAAS,MAAU,GAAU,EAAGK,EAAQF,CAAI,CACxE,EACA,OAAO,EAAqB,CAE1B,OADAH,EAASM,EAAM,CAAC,IAAID,CAAO,MACd,CACXL,EAASM,EAAM,CAAC,OAAOD,CAAO,CAChC,CACF,EACA,OAAc,CACZ,EAAS,GACTL,EAAS,EAAE,CAAC,MAAM,EAClBA,EAAS,EAAE,CAAC,MAAM,CACpB,CACF,GACA,MAAO,CAACE,EAAK,CAAC,EAAGA,EAAK,CAAC,CAAC,CAC1B,CA8CA,SAAgBK,EAAoB,EAAkC,CACpE,MAAO,CACL,KAAK,EAAe,CAClBC,EAAK,YAAYJ,CAAO,CAC1B,EACA,OAAO,EAAqB,CAC1B,IAAMK,EAAY,GAAmCJ,EAAQK,EAAM,IAAI,EAGvE,OAFAF,EAAK,iBAAiB,UAAWC,CAAQ,EACzCD,EAAK,QAAQ,MACAA,EAAK,oBAAoB,UAAWC,CAAQ,CAC3D,EACA,OAAc,CACZD,EAAK,QAAQ,CACf,CACF,CACF,CA0EA,SAAgBG,EAAsB,EAA4C,CAChF,GAAIC,EAAQ,SAAW,IACrB,MAAU,UAAU,uDAAuD,EAC7E,IAAMC,EAAyBD,EAAQ,UAAa,WACpD,MAAO,CACL,KAAK,EAAe,CAClBA,EAAQ,OAAO,YAAYR,EAASQ,EAAQ,MAAM,CACpD,EACA,OAAO,EAAqB,CAC1B,IAAMH,EAAY,GAA8B,CAC1CC,EAAM,SAAWE,EAAQ,QACxBF,EAAM,SAAuBE,EAAQ,QAC1CP,EAAQK,EAAM,IAAI,CACpB,EAEA,OADAG,EAAS,iBAAiB,UAAWJ,CAAQ,MAChCI,EAAS,oBAAoB,UAAWJ,CAAQ,CAC/D,EACA,OAAc,CAEd,CACF,CACF,CAuCA,SAAgBK,EACd,EACA,EAAmD,GAAM,IAAI,iBAAiBC,CAAC,EACpE,CACX,IAAMC,EAAUC,EAAQC,CAAI,EAC5B,MAAO,CACL,KAAK,EAAe,CAClBF,EAAQ,YAAYZ,CAAO,CAC7B,EACA,OAAO,EAAqB,CAC1B,IAAMK,EAAY,GAAmCJ,EAAQK,EAAM,IAAI,EAEvE,OADAM,EAAQ,iBAAiB,UAAWP,CAAQ,MAC/BO,EAAQ,oBAAoB,UAAWP,CAAQ,CAC9D,EACA,OAAc,CACZO,EAAQ,MAAM,CAChB,CACF,CACF,CAiCA,SAAgBG,EACd,EACA,EACA,EAAwB,CAAC,EACb,CACZ,IAAMC,EAAQR,EAAQ,OAAS,IACzBS,EAAQ,GAAgCC,EAAU,KAAKlB,CAAO,EAC9DmB,EAAgBD,EAAU,OAAQ,GAAQ,CAC9C,GAAI,CAACzB,EAAe2B,CAAG,EAAG,OAC1B,GAAIA,EAAI,OAAS,QAAS,CACxBH,EAAK,CAAE,QAASrC,EAAS,EAAA,EAAqB,KAAM,QAAS,KAAM,OAAQ,CAAC,EAC5E,MACF,CACA,GAAM,CAAE,WAAYwC,EAChBC,EACAC,EAAQ,OAAS,aACnBd,EAAQ,YAAYc,EAAQ,EAAE,EAC9B,EAAW,CAAE,GAAI,GAAM,OAAQ,IAAK,GAEpC,EAAWC,EAAM,OAAOD,CAAO,EAEjCL,EAAK,CACH,QAASrC,EACT,EAAA,EACA,KAAM,QACN,KAAM,WACN,GAAIwC,EAAI,GACR,UACF,CAAC,CACH,CAAC,EACKI,EAAgBD,EAAM,UAAW,GAAW,CAChD,IAAK,IAAIE,EAAI,EAAGA,EAAIC,EAAO,OAAQ,GAAKV,EACtCC,EAAK,CACH,QAASrC,EACT,EAAA,EACA,KAAM,QACN,KAAM,SACN,OAAQ8C,EAAO,MAAMD,EAAGA,EAAIT,CAAK,CACnC,CAAC,CAEL,CAAC,EACD,UAAa,CACXG,EAAc,EACdK,EAAc,CAChB,CACF,CA8BA,SAAgBG,EAAc,EAAsB,EAAyB,CAAC,EAAY,CACxF,IAAMC,EAAQ,IAAIC,EAAerB,EAAQ,MAAM,EACzCsB,kBAAY,IAAI,IAChBC,kBAAU,IAAI,IAIhBC,EAAwB,CAAC,EACzBC,EAAS,GACTC,EAAc,EACdC,EAAW,GACXC,EAAkB,GAClBC,MAAiC,IAAA,GAC/BC,EAAQ,IAAI,QAAe,GAAY,CAC3C,EAAeC,CACjB,CAAC,EAEKC,MAAqB,CACrBJ,IACJ,EAAkB,GAClB,mBAAqB,CAEnB,GADA,EAAkB,GACd,GACJ,IAAK,IAAM/B,KAAYyB,EAAWzB,EAAS,CAC7C,CAAC,EACH,EAEMY,EAAQ,GAAgCC,EAAU,KAAKlB,CAAO,EAE9DyC,EAAc,GAA4B,CAC9Cb,EAAM,YAAYc,CAAK,EACvB,EAAS,GACT,IAAMC,EAAUX,EAChB,EAAW,CAAC,EACZ,IAAK,IAAM1B,KAASqC,EAASf,EAAM,MAAMtB,CAAK,EAC9C+B,EAAa,EACbG,EAAO,CACT,EAEMlB,EAAW,GAA8C,CAC7D,GAAIa,EAAU,OAAO,QAAQ,OAAO,gBAAI,MAAM,oCAAoC,CAAC,EACnF,IAAMS,EAAKV,EAEX,MADA,IAAe,EACR,IAAI,SAAwB,EAAS,IAAW,CACrDH,EAAQ,IAAIa,EAAI,CAAE,UAAS,QAAO,CAAC,EACnC3B,EAAK,CACH,QAASrC,EACT,EAAA,EACA,KAAM,QACN,KAAM,UACN,KACA,QAASiE,CACX,CAAC,CACH,CAAC,CACH,EAEMC,EAAO5B,EAAU,OAAQ,GAAQ,CACrC,GAAK7B,EAAe+B,CAAG,GACnBA,EAAI,IAAA,EACR,IAAIA,EAAI,OAAS,SAAU,CACzB,GAAI,CAACa,EAAQ,CACXD,EAAS,KAAK,GAAGZ,EAAI,MAAM,EAC3B,MACF,CACA,IAAK,IAAMd,KAASc,EAAI,OAAQQ,EAAM,MAAMtB,CAAK,EACjDkC,EAAO,CAET,MACA,GAAIpB,EAAI,OAAS,WAAY,CAC3B,IAAM2B,EAAQhB,EAAQ,IAAIX,EAAI,EAAE,EAChC,GAAI2B,IAAU,IAAA,GAAW,OACzBhB,EAAQ,OAAOX,EAAI,EAAE,EACrB2B,EAAM,QAAQ3B,EAAI,QAAQ,CAC5B,CANA,CAOF,CAAC,EAEK4B,MAAmB,CACvB1B,EAAa,CAAE,KAAM,UAAW,CAAC,CAAC,CAAC,KAChC,GAAa,CACRD,EAAS,IAAIoB,EAAWpB,EAAS,MAAoB,CAC3D,MACM,CAEJ,EAAS,EACX,CACF,CACF,EAKA,OAHAJ,EAAK,CAAE,QAASrC,EAAS,EAAA,EAAqB,KAAM,QAAS,KAAM,OAAQ,CAAC,EAC5EoE,EAAK,EAEE,CACL,QACA,QACA,UAAU,EAAsB,CAE9B,OADAlB,EAAU,IAAIzB,CAAQ,MACT,CACXyB,EAAU,OAAOzB,CAAQ,CAC3B,CACF,EACA,QAAQ,EAA6B,CAWnC,OAVIwC,EAAI,OAAS,QAERvB,EAAQuB,CAAG,CAAC,CAAC,KAAM,IACpBxB,EAAS,KACX,EAAS,GACT2B,EAAK,GAEA3B,EACR,EAEIC,EAAQuB,CAAG,CACpB,EACA,SAAgB,CACd,EAAW,GACXC,EAAK,EACLhB,EAAU,MAAM,EAChB,IAAK,IAAMiB,KAAShB,EAAQ,OAAO,EACjCgB,EAAM,OAAO,gBAAI,MAAM,oCAAoC,CAAC,EAC9DhB,EAAQ,MAAM,EACdb,EAAU,MAAM,CAClB,CACF,CACF"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{For as e,Show as t,addEventListener as n,anchor as r,className as i,createComponent as a,effect as o,insert as s,memo as c,setAttribute as l,template as u}from"@shadow-js/web";import{untrack as d,useEffect as f,useSignal as p}from"@shadow-js/core";const m=/*#__PURE__*/ u(`<button type="button" class="btn">Apply</button>`),h=/*#__PURE__*/ u(`<button type="button" class="btn">Revert</button>`),g=/*#__PURE__*/ u(`<div class="section"><h4>Value</h4><textarea class="textarea" aria-label="Value"></textarea><div><span role="status"></span></div></div>`),_=/*#__PURE__*/ u(`<span><button type="button" class="link"></button> </span>`),v=/*#__PURE__*/ u(`<li><strong></strong></li>`),y=/*#__PURE__*/ u(`<div class="status">Also changed: </div>`),b=/*#__PURE__*/ u(`<div class="status">Older history was dropped.</div>`),x=/*#__PURE__*/ u(`<div class="section" data-why><h4>Why did it run?</h4><ol class="chain"></ol></div>`),S=/*#__PURE__*/ u(`<div class="status"> runs · updates · last ms · total ms</div>`),C=/*#__PURE__*/ u(`<div class="section"><h4>Reads</h4></div>`),w=/*#__PURE__*/ u(`<div class="section"><h4>Read by</h4></div>`),T=/*#__PURE__*/ u(`<section class="detail" aria-label="Details"><div class="section"><strong></strong> <span class="status"> #</span></div></section>`),E=e=>{let[c,u]=p(``),[_,v]=p(!1),[y,b]=p(!1),[x,S]=p({kind:`idle`,text:``});f(()=>{e.state.version();let t=e.node.id,n=e.node.type===`store`;_()||d(()=>e.state.backend.request(n?{type:`store`,id:t}:{type:`value`,id:t})).then(e=>{if(!e.ok){S({kind:`bad`,text:e.error});return}let t=e.result;u(t.text),b(t.editable)})});let C=()=>{let t=e.node.id,n=e.node.type===`store`?{type:`store-set`,id:t,path:[],text:c()}:{type:`write`,id:t,text:c()};e.state.backend.request(n).then(t=>{t.ok?(v(!1),S({kind:`good`,text:`Applied.`}),e.state.refresh()):S({kind:`bad`,text:t.error})})};{let d=g(),f=d.firstChild.nextSibling,p=f.nextSibling,b=p.firstChild,w=r(p,b);return o(()=>{l(d,`data-editor`,e.node.type)}),l(f,`spellcheck`,!1),o(()=>{f.readOnly=!y()}),n(f,`input`,e=>{u(e.currentTarget.value),v(!0),S({kind:`idle`,text:``})}),o(()=>{f.value=c()??``}),s(p,a(t,{get when(){return y()},get children(){return[(()=>{let e=m();return o(()=>{e.disabled=!_()}),n(e,`click`,C),e})(),` `]}}),w),s(p,a(t,{get when(){return _()},get children(){return[(()=>{let t=h();return n(t,`click`,()=>{v(!1),e.state.refresh()}),t})(),` `]}}),b),o(()=>{i(b,`status ${x().kind}`)}),s(b,()=>x().text),d}},D=i=>a(t,{get when(){return i.state.selectedNode()},children:o=>{let l=T(),u=l.firstChild,d=u.firstChild,f=d.nextSibling.nextSibling,p=f.firstChild,m=r(l,null),h=r(l,null),g=r(l,null);return s(d,()=>i.state.nameOf(o().id)),s(f,()=>o().type,p),s(f,()=>o().id),s(u,a(t,{get when(){return o().type===`memo`||o().type===`effect`},get children(){let e=S(),t=e.firstChild,n=t.splitText(8),r=n.splitText(16),i=r.splitText(12);return s(e,()=>o().runs,t),s(e,()=>o().updates,n),s(e,()=>o().lastDuration.toFixed(2),r),s(e,()=>o().totalDuration.toFixed(2),i),e}})),s(l,a(t,{get when(){return o().sources.length>0},get children(){let t=C();return s(t,a(e,{get each(){return o().sources},children:e=>{let t=_(),r=t.firstChild;return n(r,`click`,()=>i.state.select(e)),s(r,()=>i.state.nameOf(e)),t}})),t}}),m),s(l,a(t,{get when(){return o().observers.length>0},get children(){let t=w();return s(t,a(e,{get each(){return o().observers},children:e=>{let t=_(),r=t.firstChild;return n(r,`click`,()=>i.state.select(e)),s(r,()=>i.state.nameOf(e)),t}})),t}}),h),s(l,a(t,{get when(){return i.state.explanation()},children:n=>{let i=x(),o=i.firstChild.nextSibling,l=r(i,null);return s(o,a(e,{get each(){return n().chain},children:e=>{let n=v(),i=n.firstChild,o=r(n,null);return s(n,()=>e.kind===`write`?`write `:e.kind===`run`?`ran `:``,i),s(i,()=>e.label),s(n,a(t,{get when(){return e.preview!==void 0},get children(){return[` = `,c(()=>e.preview)]}}),o),s(n,a(t,{get when(){return e.writer!==void 0},get children(){return[` (written by `,c(()=>e.writer),`)`]}})),n}})),s(i,a(t,{get when(){return n().alsoCausedBy.length>0},get children(){let e=y();return s(e,()=>n().alsoCausedBy.map(e=>e.label).join(`, `)),e}}),l),s(i,a(t,{get when(){return n().truncated},get children(){return b()}})),i}}),g),s(l,a(t,{get when(){return o().type===`signal`||o().type===`store`},get children(){return a(E,{get state(){return i.state},get node(){return o()}})}})),l}});export{D as Details,E as ValueEditor};
|
|
2
|
+
//# sourceMappingURL=details.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"details.js","names":["_$anchor","_$createComponent","_$memo","_tmpl$","_tmpl$2","_tmpl$3","_tmpl$4","_tmpl$5","_tmpl$6","_tmpl$7","_tmpl$8","_tmpl$9","_tmpl$10","_tmpl$11","_tmpl$12","ValueEditor","text","setText","useSignal","dirty","setDirty","editable","setEditable","status","setStatus","useEffect","props","id","store","untrack","response","result","apply","request","_el$","_el$2","_el$3","_el$4","_el$5","anchor","effect","setAttribute","addEventListener","event","insert","createComponent","Show","className","Details","_el$6","_el$7","_el$8","node","For","why","step","memo"],"sources":["../../../src/ui/details.tsx"],"sourcesContent":["import { untrack, useEffect, useSignal } from \"@shadow-js/core\";\nimport { For, Show } from \"@shadow-js/web\";\nimport type { Component } from \"@shadow-js/web/jsx-runtime\";\nimport type { AgentRequest } from \"../agent\";\nimport type { SnapshotNode } from \"../inspector\";\nimport type { PanelState } from \"./state\";\n\ninterface NodeProps {\n state: PanelState;\n node: SnapshotNode;\n}\n\n/**\n * Shows the value of a signal or store as indented JSON, and edits it. Props: `state` is the panel's\n * {@link PanelState} and `node` is the selected node. Apply writes the text back through the agent, and a\n * successful write re-renders the app like any other write. Revert discards the edit. While the text is\n * being edited, the view does not overwrite it. A resource or a memo has no editor here.\n *\n * Internal to the panel: the package index does not export this component. Used in development builds\n * only.\n *\n * @internal\n */\nexport const ValueEditor: Component<NodeProps> = (props) => {\n const [text, setText] = useSignal(\"\");\n const [dirty, setDirty] = useSignal(false);\n const [editable, setEditable] = useSignal(false);\n const [status, setStatus] = useSignal({ kind: \"idle\", text: \"\" });\n\n useEffect(() => {\n // Follow the value while it changes, unless the user is typing.\n props.state.version();\n const id = props.node.id;\n const store = props.node.type === \"store\";\n if (dirty()) return;\n // The request reads the value through the agent; the reads above are the only dependencies.\n const request = untrack(() =>\n props.state.backend.request(store ? { type: \"store\", id } : { type: \"value\", id }),\n );\n void request.then((response) => {\n if (!response.ok) {\n setStatus({ kind: \"bad\", text: response.error });\n return;\n }\n const result = response.result as { text: string; editable: boolean };\n setText(result.text);\n setEditable(result.editable);\n });\n });\n\n const apply = (): void => {\n const id = props.node.id;\n const request: AgentRequest =\n props.node.type === \"store\"\n ? { type: \"store-set\", id, path: [], text: text() }\n : { type: \"write\", id, text: text() };\n void props.state.backend.request(request).then((response) => {\n if (response.ok) {\n setDirty(false);\n setStatus({ kind: \"good\", text: \"Applied.\" });\n props.state.refresh();\n } else {\n setStatus({ kind: \"bad\", text: response.error });\n }\n });\n };\n\n return (\n <div class=\"section\" data-editor={props.node.type}>\n <h4>Value</h4>\n <textarea\n class=\"textarea\"\n aria-label=\"Value\"\n spellcheck={false}\n readOnly={!editable()}\n value={text()}\n on:input={(event) => {\n setText(event.currentTarget.value);\n setDirty(true);\n setStatus({ kind: \"idle\", text: \"\" });\n }}\n />\n <div>\n <Show when={editable()}>\n <button type=\"button\" class=\"btn\" disabled={!dirty()} on:click={apply}>\n Apply\n </button>{\" \"}\n </Show>\n <Show when={dirty()}>\n <button\n type=\"button\"\n class=\"btn\"\n on:click={() => {\n setDirty(false);\n props.state.refresh();\n }}\n >\n Revert\n </button>{\" \"}\n </Show>\n <span class={`status ${status().kind}`} role=\"status\">\n {status().text}\n </span>\n </div>\n </div>\n );\n};\n\n/** Props of {@link Details}. */\ninterface DetailsProps {\n state: PanelState;\n}\n\n/**\n * The selected node: its name and kind, its counters (runs, updates and durations for memos and effects),\n * what it reads, who reads it, why it last ran (memos and effects), and a {@link ValueEditor} for signals\n * and stores. Props: `state` is the panel's {@link PanelState}. It renders nothing when no node is\n * selected.\n *\n * Internal to the panel: the package index does not export this component. Used in development builds\n * only.\n *\n * @internal\n */\nexport const Details: Component<DetailsProps> = (props) => (\n <Show when={props.state.selectedNode()}>\n {(node) => (\n <section class=\"detail\" aria-label=\"Details\">\n <div class=\"section\">\n <strong>{props.state.nameOf(node().id)}</strong>{\" \"}\n <span class=\"status\">\n {node().type} #{node().id}\n </span>\n <Show when={node().type === \"memo\" || node().type === \"effect\"}>\n <div class=\"status\">\n {node().runs} runs · {node().updates} updates · last {node().lastDuration.toFixed(2)}{\" \"}\n ms · total {node().totalDuration.toFixed(2)} ms\n </div>\n </Show>\n </div>\n <Show when={node().sources.length > 0}>\n <div class=\"section\">\n <h4>Reads</h4>\n <For each={node().sources}>\n {(id) => (\n <span>\n <button type=\"button\" class=\"link\" on:click={() => props.state.select(id)}>\n {props.state.nameOf(id)}\n </button>{\" \"}\n </span>\n )}\n </For>\n </div>\n </Show>\n <Show when={node().observers.length > 0}>\n <div class=\"section\">\n <h4>Read by</h4>\n <For each={node().observers}>\n {(id) => (\n <span>\n <button type=\"button\" class=\"link\" on:click={() => props.state.select(id)}>\n {props.state.nameOf(id)}\n </button>{\" \"}\n </span>\n )}\n </For>\n </div>\n </Show>\n <Show when={props.state.explanation()}>\n {(why) => (\n <div class=\"section\" data-why>\n <h4>Why did it run?</h4>\n <ol class=\"chain\">\n <For each={why().chain}>\n {(step) => (\n <li>\n {step.kind === \"write\" ? \"write \" : step.kind === \"run\" ? \"ran \" : \"\"}\n <strong>{step.label}</strong>\n <Show when={step.preview !== undefined}> = {step.preview}</Show>\n <Show when={step.writer !== undefined}> (written by {step.writer})</Show>\n </li>\n )}\n </For>\n </ol>\n <Show when={why().alsoCausedBy.length > 0}>\n <div class=\"status\">\n Also changed:{\" \"}\n {why()\n .alsoCausedBy.map((step) => step.label)\n .join(\", \")}\n </div>\n </Show>\n <Show when={why().truncated}>\n <div class=\"status\">Older history was dropped.</div>\n </Show>\n </div>\n )}\n </Show>\n <Show when={node().type === \"signal\" || node().type === \"store\"}>\n <ValueEditor state={props.state} node={node()} />\n </Show>\n </section>\n )}\n </Show>\n);\n"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{parseInspectResult as e}from"../inspection.js";import{useSignal as t}from"@shadow-js/core";function n(n){let[r,i]=t([]),[a,o]=t(``),[s,c]=t(``),[l,u]=t(!1);return{sources:r,active:a,error:s,loading:l,select:e=>{r().some(t=>t.name===e)&&o(e)},refresh:async()=>{u(!0);try{let t=await n.request({type:`inspect`});if(!t.ok){c(t.error);return}let r=e(t.result);if(r===null){c(`The page answered the inspect request with an unexpected value.`);return}i(r),c(``),r.some(e=>e.name===a())||o(r[0]?.name??``)}catch(e){c(e instanceof Error?e.message:`The request failed.`)}finally{u(!1)}}}}export{n as createInspectionState};
|
|
2
|
+
//# sourceMappingURL=inspection-state.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inspection-state.js","names":["createInspectionState","sources","setSources","useSignal","active","setActive","error","setError","loading","setLoading","source","name","response","backend","parsed","parseInspectResult","failure"],"sources":["../../../src/ui/inspection-state.ts"],"sourcesContent":["import { type Accessor, useSignal } from \"@shadow-js/core\";\nimport type { Backend } from \"../backend\";\nimport { type InspectionSnapshot, parseInspectResult } from \"../inspection\";\n\n/**\n * What the inspection views of a remote panel read and write: the registered sources, the one on\n * screen, and the state of the last request. Built by {@link createInspectionState}.\n *\n * @since 1.0.0\n */\nexport interface InspectionState {\n /** The sources the page answered with at the last refresh (empty before the first one). */\n readonly sources: Accessor<readonly InspectionSnapshot[]>;\n /** The name of the source on screen, or `\"\"` when there is none. */\n readonly active: Accessor<string>;\n /** The reason the last refresh failed, or `\"\"` when it did not. */\n readonly error: Accessor<string>;\n /** True while a refresh is waiting for the page. */\n readonly loading: Accessor<boolean>;\n /** Shows the source registered as `name`. Ignored when no source has that name. */\n select(name: string): void;\n /** Asks the page for its sources now. Resolves when the answer arrived (or failed). */\n refresh(): Promise<void>;\n}\n\n/**\n * The state of the inspection views of a panel (the router and query tabs): it asks the agent for the\n * registered sources, keeps the answer, and remembers which source is shown. It talks only to a\n * {@link Backend}, so it works for a local agent and for a remote one. An answer that does not have\n * the expected shape is refused and reported as an error; the previous sources stay.\n *\n * @param backend The backend to ask.\n * @returns The inspection state.\n * @example\n * import { createInspectionState, createLocalBackend, createAgent } from \"@shadow-js/devtools\";\n *\n * const state = createInspectionState(createLocalBackend(createAgent()));\n * await state.refresh();\n * state.sources(); // the registered sources\n * @since 1.0.0\n */\nexport function createInspectionState(backend: Backend): InspectionState {\n const [sources, setSources] = useSignal<readonly InspectionSnapshot[]>([]);\n const [active, setActive] = useSignal(\"\");\n const [error, setError] = useSignal(\"\");\n const [loading, setLoading] = useSignal(false);\n\n const select = (name: string): void => {\n if (sources().some((source) => source.name === name)) setActive(name);\n };\n\n const refresh = async (): Promise<void> => {\n setLoading(true);\n try {\n const response = await backend.request({ type: \"inspect\" });\n if (!response.ok) {\n setError(response.error);\n return;\n }\n const parsed = parseInspectResult(response.result);\n if (parsed === null) {\n setError(\"The page answered the inspect request with an unexpected value.\");\n return;\n }\n setSources(parsed);\n setError(\"\");\n if (!parsed.some((source) => source.name === active())) {\n setActive(parsed[0]?.name ?? \"\");\n }\n } catch (failure) {\n setError(failure instanceof Error ? failure.message : \"The request failed.\");\n } finally {\n setLoading(false);\n }\n };\n\n return { sources, active, error, loading, select, refresh };\n}\n"],"mappings":"kGAyCA,SAAgBA,EAAsB,EAAmC,CACvE,GAAM,CAACC,EAASC,GAAcC,EAAyC,CAAC,CAAC,EACnE,CAACC,EAAQC,GAAaF,EAAU,EAAE,EAClC,CAACG,EAAOC,GAAYJ,EAAU,EAAE,EAChC,CAACK,EAASC,GAAcN,EAAU,EAAK,EA+B7C,MAAO,CAAE,UAAS,SAAQ,QAAO,UAAS,OA7B1B,GAAuB,CACjCF,EAAQ,CAAC,CAAC,KAAM,GAAWS,EAAO,OAASC,CAAI,GAAGN,EAAUM,CAAI,CACtE,EA2BkD,iBAzBP,CACzCF,EAAW,EAAI,EACf,GAAI,CACF,IAAMG,EAAW,MAAMC,EAAQ,QAAQ,CAAE,KAAM,SAAU,CAAC,EAC1D,GAAI,CAACD,EAAS,GAAI,CAChBL,EAASK,EAAS,KAAK,EACvB,MACF,CACA,IAAME,EAASC,EAAmBH,EAAS,MAAM,EACjD,GAAIE,IAAW,KAAM,CACnBP,EAAS,iEAAiE,EAC1E,MACF,CACAL,EAAWY,CAAM,EACjBP,EAAS,EAAE,EACNO,EAAO,KAAM,GAAWJ,EAAO,OAASN,EAAO,CAAC,GACnDC,EAAUS,EAAO,EAAE,EAAE,MAAQ,EAAE,CAEnC,OAASE,EAAS,CAChBT,EAASS,aAAmB,MAAQA,EAAQ,QAAU,qBAAqB,CAC7E,QAAU,CACRP,EAAW,EAAK,CAClB,CACF,CAE0D,CAC5D"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{For as e,Show as t,addEventListener as n,anchor as r,createComponent as i,insert as a,template as o}from"@shadow-js/web";const s=/*#__PURE__*/ o(`<div class="empty">No errors or hydration mismatches.</div>`),c=/*#__PURE__*/ o(`<code></code>`),l=/*#__PURE__*/ o(`<button type="button" class="link"></button>`),u=/*#__PURE__*/ o(`<div class="issue" data-issue="error"></div>`),d=/*#__PURE__*/ o(`<div class="issue" data-issue="hydration"><strong>Hydration mismatch:</strong> <div class="status">server: </div><div class="status">client: </div></div>`),f=/*#__PURE__*/ o(`<section class="list" aria-label="Issues"></section>`),p=o=>{let p=f();return a(p,i(t,{get when(){return o.state.errors().length+o.state.hydration().length>0},get fallback(){return s()},get children(){return[i(e,{get each(){return o.state.errors()},key:e=>e.seq,children:e=>{let s=u(),d=r(s,null),f=r(s,null);return a(s,i(t,{get when(){return e().code!==void 0},get children(){return[(()=>{let t=c();return a(t,()=>e().code),t})(),` `]}}),d),a(s,()=>e().message,f),a(s,i(t,{get when(){return e().id!==0},get children(){return[` `,(()=>{let t=l();return n(t,`click`,()=>o.state.select(e().id)),a(t,()=>o.state.nameOf(e().id)),t})()]}})),s}}),i(e,{get each(){return o.state.hydration()},key:e=>e.seq,children:e=>{let t=d(),n=t.firstChild.nextSibling.nextSibling,r=n.nextSibling;return a(t,()=>e().message,n),a(n,()=>e().expected),a(r,()=>e().actual),t}})]}})),p};export{p as IssuesTab};
|
|
2
|
+
//# sourceMappingURL=issues.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"issues.js","names":["_$createComponent","_$anchor","_tmpl$","_tmpl$2","_tmpl$3","_tmpl$4","_tmpl$5","_tmpl$6","IssuesTab","_el$","insert","createComponent","Show","props","For","error","_el$2","anchor","_el$3","addEventListener","mismatch"],"sources":["../../../src/ui/issues.tsx"],"sourcesContent":["import { For, Show } from \"@shadow-js/web\";\nimport type { Component } from \"@shadow-js/web/jsx-runtime\";\nimport type { TabProps } from \"./tree\";\n\n/**\n * The Issues tab. Props: `state` is the panel's {@link PanelState}. It lists the errors the runtime reported,\n * with their diagnostic code when there is one and a link to the node they belong to, then the hydration\n * mismatches: the runtime's message, the expected value (labeled server) and the actual value (labeled\n * client).\n *\n * Internal to the panel: the package index does not export this component. Used in development builds\n * only.\n *\n * @internal\n */\nexport const IssuesTab: Component<TabProps> = (props) => (\n <section class=\"list\" aria-label=\"Issues\">\n <Show\n when={props.state.errors().length + props.state.hydration().length > 0}\n fallback={<div class=\"empty\">No errors or hydration mismatches.</div>}\n >\n <For each={props.state.errors()} key={(error) => error.seq}>\n {(error) => (\n <div class=\"issue\" data-issue=\"error\">\n <Show when={error().code !== undefined}>\n <code>{error().code}</code>{\" \"}\n </Show>\n {error().message}\n <Show when={error().id !== 0}>\n {\" \"}\n <button type=\"button\" class=\"link\" on:click={() => props.state.select(error().id)}>\n {props.state.nameOf(error().id)}\n </button>\n </Show>\n </div>\n )}\n </For>\n <For each={props.state.hydration()} key={(mismatch) => mismatch.seq}>\n {(mismatch) => (\n <div class=\"issue\" data-issue=\"hydration\">\n <strong>Hydration mismatch:</strong> {mismatch().message}\n <div class=\"status\">server: {mismatch().expected}</div>\n <div class=\"status\">client: {mismatch().actual}</div>\n </div>\n )}\n </For>\n </Show>\n </section>\n);\n"],"mappings":"unBAeaQ,EAAkC,GAAU,CAAA,IAAAC,EAAAF,EAAA,EAAA,OAAAG,EAAAD,EAAAT,EAAAY,EAAA,CAAA,IAAA,MAAA,CAAA,OAAAC,EAAA,MAAA,OAAA,CAAA,CAAA,OAAAA,EAAA,MAAA,UAAA,CAAA,CAAA,OAAA,CAAA,EAAA,IAAA,UAAA,CAAA,OAAAX,EAAA,CAAA,EAAA,IAAA,UAAA,CAAA,MAAA,CAAAF,EAAAc,EAAA,CAAA,IAAA,MAAA,CAAA,OAAAD,EAAA,MAAA,OAAA,CAAA,EAAA,IAAA,GAAAE,EAAA,IAAA,SAAA,GAAA,CAAA,IAAAN,EAAAJ,EAAA,EAAAW,EAAAf,EAAAQ,EAAA,IAAA,EAAAS,EAAAjB,EAAAQ,EAAA,IAAA,EAAA,OAAAC,EAAAD,EAAAT,EAAAY,EAAA,CAAA,IAAA,MAAA,CAAA,OAAAG,EAAA,CAAA,CAAA,OAAA,IAAA,EAAA,EAAA,IAAA,UAAA,CAAA,MAAA,MAAA,CAAA,IAAAN,EAAAN,EAAA,EAAA,OAAAO,EAAAD,MAAAM,EAAA,CAAA,CAAA,IAAA,EAAAN,CAAA,EAAA,CAAA,EAAA,GAAA,CAAA,CAAA,CAAA,EAAAO,CAAA,EAAAN,EAAAD,MAAAM,EAAA,CAAA,CAAA,QAAAG,CAAA,EAAAR,EAAAD,EAAAT,EAAAY,EAAA,CAAA,IAAA,MAAA,CAAA,OAAAG,EAAA,CAAA,CAAA,KAAA,CAAA,EAAA,IAAA,UAAA,CAAA,MAAA,CAAA,SAAA,CAAA,IAAAN,EAAAL,EAAA,EAAA,OAAAe,EAAAV,EAAA,YAAAI,EAAA,MAAA,OAAAE,EAAA,CAAA,CAAA,EAAA,CAAA,EAAAL,EAAAD,MAAAI,EAAA,MAAA,OAAAE,EAAA,CAAA,CAAA,EAAA,CAAA,EAAAN,CAAA,EAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,EAAAA,CAAA,CAAA,CAAA,EAAAT,EAAAc,EAAA,CAAA,IAAA,MAAA,CAAA,OAAAD,EAAA,MAAA,UAAA,CAAA,EAAA,IAAA,GAAAO,EAAA,IAAA,SAAA,GAAA,CAAA,IAAAX,EAAAH,EAAA,EAAAU,EAAAP,EAAA,WAAA,YAAA,YAAAS,EAAAF,EAAA,YAAA,OAAAN,EAAAD,MAAAW,EAAA,CAAA,CAAA,QAAAJ,CAAA,EAAAN,EAAAM,MAAAI,EAAA,CAAA,CAAA,QAAA,EAAAV,EAAAQ,MAAAE,EAAA,CAAA,CAAA,MAAA,EAAAX,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,EAAAA,CAAA"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{IssuesTab as e}from"./issues.js";import{TABS as t}from"./state.js";import{StateTab as n}from"./state-tab.js";import{TimelineTab as r}from"./timeline.js";import{TreeTab as i}from"./tree.js";import{For as a,Match as o,Show as s,Switch as c,addEventListener as l,anchor as u,createComponent as d,effect as f,insert as p,setAttribute as m,style as h,template as g}from"@shadow-js/web";const _=/*#__PURE__*/ g(`<span class="badge bad"></span>`),v=/*#__PURE__*/ g(`<button type="button" class="tab" role="tab"></button>`),y=/*#__PURE__*/ g(`<section class="panel" role="dialog" aria-label="ShadowJS devtools"><header class="header"><span class="title">ShadowJS devtools</span><span class="spacer"></span><button type="button" class="btn" title="Pause or resume recording"></button><button type="button" class="btn" title="Flash components when they update">Flash</button><button type="button" class="btn" title="Forget everything recorded">Clear</button><button type="button" class="btn" aria-label="Close">×</button></header><div class="tabs" role="tablist"></div><div class="body"></div><footer class="footer"><span> components</span><span> signals</span><span> computations</span><span> runs</span></footer></section>`),b=g=>{let b=null,x=e=>{e.target instanceof HTMLElement&&e.target.closest(`button, input, label`)!==null||(b={pointer:e.pointerId,x:e.clientX,y:e.clientY,left:g.x,top:g.y},e.currentTarget.classList.add(`dragging`),typeof e.currentTarget.setPointerCapture==`function`&&e.currentTarget.setPointerCapture(e.pointerId))},S=e=>{b!==null&&e.pointerId===b.pointer&&g.onMove(b.left+e.clientX-b.x,b.top+e.clientY-b.y)},C=e=>{b!==null&&e.pointerId===b.pointer&&(b=null,e.currentTarget.classList.remove(`dragging`),typeof e.currentTarget.releasePointerCapture==`function`&&e.currentTarget.hasPointerCapture?.(e.pointerId)===!0&&e.currentTarget.releasePointerCapture(e.pointerId))},w=()=>{g.state.backend.request({type:`clear`}).then(()=>g.state.refresh())};{let b=y(),T=b.firstChild,E=T.firstChild.nextSibling.nextSibling,D=E.nextSibling,O=D.nextSibling,k=O.nextSibling,A=T.nextSibling,j=A.nextSibling,M=j.nextSibling.firstChild,N=M.firstChild,P=M.nextSibling,F=P.firstChild,I=P.nextSibling,L=I.firstChild,R=I.nextSibling,z=R.firstChild;return f(()=>{b.hidden=!g.open}),f(()=>({left:`${g.x}px`,top:`${g.y}px`}),(e,t)=>{h(b,e,t)}),l(T,`pointerdown`,x),l(T,`pointermove`,S),l(T,`pointerup`,C),l(T,`pointercancel`,C),f(()=>{m(E,`aria-pressed`,g.state.paused())}),l(E,`click`,()=>g.state.setPaused(!g.state.paused())),p(E,()=>g.state.paused()?`Resume`:`Pause`),f(()=>{m(D,`aria-pressed`,g.state.flash())}),l(D,`click`,()=>g.state.setFlash(!g.state.flash())),l(O,`click`,w),l(k,`click`,()=>g.onClose()),p(A,d(a,{each:t,children:e=>{let t=v(),n=u(t,null);return f(()=>{m(t,`aria-selected`,g.state.tab()===e.id)}),f(()=>{m(t,`data-tab`,e.id)}),l(t,`click`,()=>g.state.setTab(e.id)),p(t,()=>e.label,n),p(t,d(s,{get when(){return e.id===`issues`&&g.state.snapshot().counts.errors+g.state.snapshot().counts.hydration>0},get children(){let e=_();return p(e,()=>g.state.snapshot().counts.errors+g.state.snapshot().counts.hydration),e}})),t}})),p(j,d(c,{get children(){return[d(o,{get when(){return g.state.tab()===`tree`},get children(){return d(i,{get state(){return g.state}})}}),d(o,{get when(){return g.state.tab()===`timeline`},get children(){return d(r,{get state(){return g.state}})}}),d(o,{get when(){return g.state.tab()===`state`},get children(){return d(n,{get state(){return g.state}})}}),d(o,{get when(){return g.state.tab()===`issues`},get children(){return d(e,{get state(){return g.state}})}})]}})),p(M,()=>g.state.snapshot().counts.components,N),p(P,()=>g.state.snapshot().counts.signals,F),p(I,()=>g.state.snapshot().counts.memos+g.state.snapshot().counts.effects,L),p(R,()=>g.state.snapshot().counts.runs,z),b}};export{b as Panel};
|
|
2
|
+
//# sourceMappingURL=panel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"panel.js","names":["_$createComponent","_$anchor","_tmpl$","_tmpl$2","_tmpl$3","Panel","drag","begin","event","props","move","end","clear","_el$","_el$2","_el$6","_el$7","_el$8","_el$9","_el$3","_el$4","_el$10","_el$14","_el$11","_el$15","_el$12","_el$16","_el$13","_el$17","effect","style","_v$","_p$","addEventListener","setAttribute","insert","createComponent","For","TABS","anchor","tab","Show","Switch","Match","TreeTab","TimelineTab","StateTab","IssuesTab"],"sources":["../../../src/ui/panel.tsx"],"sourcesContent":["import { For, Match, Show, Switch } from \"@shadow-js/web\";\nimport type { Component } from \"@shadow-js/web/jsx-runtime\";\nimport { IssuesTab } from \"./issues\";\nimport { type PanelState, TABS } from \"./state\";\nimport { StateTab } from \"./state-tab\";\nimport { TimelineTab } from \"./timeline\";\nimport { TreeTab } from \"./tree\";\n\n/**\n * Props of {@link Panel}.\n *\n * @since 1.0.0\n */\nexport interface PanelProps {\n /**\n * The panel's state, from {@link createPanelState}.\n */\n state: PanelState;\n /**\n * Whether the panel is shown. A closed panel keeps its state and is hidden.\n */\n open: boolean;\n /**\n * Left edge of the panel in pixels.\n */\n x: number;\n /**\n * Top edge of the panel in pixels.\n */\n y: number;\n /**\n * Called while the header is dragged, with the new position in pixels. The panel does not move itself;\n * the owner sets `x` and `y`.\n */\n onMove(x: number, y: number): void;\n /**\n * Called when the close button is pressed.\n */\n onClose(): void;\n}\n\ninterface DragStart {\n pointer: number;\n x: number;\n y: number;\n left: number;\n top: number;\n}\n\n/**\n * The devtools panel: a header with the drag handle and the Pause, Flash, Clear and close controls, the\n * tabs (Components, Timeline, State, Issues), the active view and a footer with counts. It only talks to\n * {@link PanelState}, so it works the same on a local agent and on a remote one.\n *\n * Development tool: `mountDevtools()` renders it in development builds only, and it never ships in\n * production builds.\n *\n * @example\n * import { createAgent, createLocalBackend, createPanelState, Panel } from \"@shadow-js/devtools\";\n * const backend = createLocalBackend(createAgent());\n * export const Devtools = () => {\n * const state = createPanelState(backend);\n * return <Panel state={state} open={true} x={16} y={16} onMove={() => undefined} onClose={() => undefined} />;\n * };\n * @since 1.0.0\n */\nexport const Panel: Component<PanelProps> = (props) => {\n let drag: DragStart | null = null;\n\n const begin = (event: PointerEvent & { currentTarget: HTMLElement }): void => {\n if (\n event.target instanceof HTMLElement &&\n event.target.closest(\"button, input, label\") !== null\n )\n return;\n drag = {\n pointer: event.pointerId,\n x: event.clientX,\n y: event.clientY,\n left: props.x,\n top: props.y,\n };\n event.currentTarget.classList.add(\"dragging\");\n if (typeof event.currentTarget.setPointerCapture === \"function\") {\n event.currentTarget.setPointerCapture(event.pointerId);\n }\n };\n const move = (event: PointerEvent): void => {\n if (drag === null || event.pointerId !== drag.pointer) return;\n props.onMove(drag.left + event.clientX - drag.x, drag.top + event.clientY - drag.y);\n };\n const end = (event: PointerEvent & { currentTarget: HTMLElement }): void => {\n if (drag === null || event.pointerId !== drag.pointer) return;\n drag = null;\n event.currentTarget.classList.remove(\"dragging\");\n if (\n typeof event.currentTarget.releasePointerCapture === \"function\" &&\n event.currentTarget.hasPointerCapture?.(event.pointerId) === true\n ) {\n event.currentTarget.releasePointerCapture(event.pointerId);\n }\n };\n\n const clear = (): void => {\n void props.state.backend.request({ type: \"clear\" }).then(() => props.state.refresh());\n };\n\n return (\n <section\n class=\"panel\"\n role=\"dialog\"\n aria-label=\"ShadowJS devtools\"\n hidden={!props.open}\n style={{ left: `${props.x}px`, top: `${props.y}px` }}\n >\n <header\n class=\"header\"\n on:pointerdown={begin}\n on:pointermove={move}\n on:pointerup={end}\n on:pointercancel={end}\n >\n <span class=\"title\">ShadowJS devtools</span>\n <span class=\"spacer\" />\n <button\n type=\"button\"\n class=\"btn\"\n aria-pressed={props.state.paused()}\n title=\"Pause or resume recording\"\n on:click={() => props.state.setPaused(!props.state.paused())}\n >\n {props.state.paused() ? \"Resume\" : \"Pause\"}\n </button>\n <button\n type=\"button\"\n class=\"btn\"\n aria-pressed={props.state.flash()}\n title=\"Flash components when they update\"\n on:click={() => props.state.setFlash(!props.state.flash())}\n >\n Flash\n </button>\n <button type=\"button\" class=\"btn\" title=\"Forget everything recorded\" on:click={clear}>\n Clear\n </button>\n <button type=\"button\" class=\"btn\" aria-label=\"Close\" on:click={() => props.onClose()}>\n ×\n </button>\n </header>\n <div class=\"tabs\" role=\"tablist\">\n <For each={TABS}>\n {(tab) => (\n <button\n type=\"button\"\n class=\"tab\"\n role=\"tab\"\n aria-selected={props.state.tab() === tab.id}\n data-tab={tab.id}\n on:click={() => props.state.setTab(tab.id)}\n >\n {tab.label}\n <Show\n when={\n tab.id === \"issues\" &&\n props.state.snapshot().counts.errors + props.state.snapshot().counts.hydration > 0\n }\n >\n <span class=\"badge bad\">\n {props.state.snapshot().counts.errors + props.state.snapshot().counts.hydration}\n </span>\n </Show>\n </button>\n )}\n </For>\n </div>\n <div class=\"body\">\n <Switch>\n <Match when={props.state.tab() === \"tree\"}>\n <TreeTab state={props.state} />\n </Match>\n <Match when={props.state.tab() === \"timeline\"}>\n <TimelineTab state={props.state} />\n </Match>\n <Match when={props.state.tab() === \"state\"}>\n <StateTab state={props.state} />\n </Match>\n <Match when={props.state.tab() === \"issues\"}>\n <IssuesTab state={props.state} />\n </Match>\n </Switch>\n </div>\n <footer class=\"footer\">\n <span>{props.state.snapshot().counts.components} components</span>\n <span>{props.state.snapshot().counts.signals} signals</span>\n <span>\n {props.state.snapshot().counts.memos + props.state.snapshot().counts.effects} computations\n </span>\n <span>{props.state.snapshot().counts.runs} runs</span>\n </footer>\n </section>\n );\n};\n"],"mappings":"wsCAkEaK,EAAgC,GAAU,CACrD,IAAIC,EAAyB,KAEvBC,EAAS,GAA+D,CAE1EC,EAAM,kBAAkB,aACxBA,EAAM,OAAO,QAAQ,sBAAsB,IAAM,OAGnD,EAAO,CACL,QAASA,EAAM,UACf,EAAGA,EAAM,QACT,EAAGA,EAAM,QACT,KAAMC,EAAM,EACZ,IAAKA,EAAM,CACb,EACAD,EAAM,cAAc,UAAU,IAAI,UAAU,EACxC,OAAOA,EAAM,cAAc,mBAAsB,YACnDA,EAAM,cAAc,kBAAkBA,EAAM,SAAS,EAEzD,EACME,EAAQ,GAA8B,CACtCJ,IAAS,MAAQE,EAAM,YAAcF,EAAK,SAC9CG,EAAM,OAAOH,EAAK,KAAOE,EAAM,QAAUF,EAAK,EAAGA,EAAK,IAAME,EAAM,QAAUF,EAAK,CAAC,CACpF,EACMK,EAAO,GAA+D,CACtEL,IAAS,MAAQE,EAAM,YAAcF,EAAK,UAC9C,EAAO,KACPE,EAAM,cAAc,UAAU,OAAO,UAAU,EAE7C,OAAOA,EAAM,cAAc,uBAA0B,YACrDA,EAAM,cAAc,oBAAoBA,EAAM,SAAS,IAAM,IAE7DA,EAAM,cAAc,sBAAsBA,EAAM,SAAS,EAE7D,EAEMI,MAAoB,CACxBH,EAAW,MAAM,QAAQ,QAAQ,CAAE,KAAM,OAAQ,CAAC,CAAC,CAAC,SAAWA,EAAM,MAAM,QAAQ,CAAC,CACtF,EAEA,CAAA,IAAAI,EAAAT,EAAA,EAAAU,EAAAD,EAAA,WAAAE,EAAAD,EAAA,WAAA,YAAA,YAAAE,EAAAD,EAAA,YAAAE,EAAAD,EAAA,YAAAE,EAAAD,EAAA,YAAAE,EAAAL,EAAA,YAAAM,EAAAD,EAAA,YAAAE,EAAAD,EAAA,YAAA,WAAAE,EAAAD,EAAA,WAAAE,EAAAF,EAAA,YAAAG,EAAAD,EAAA,WAAAE,EAAAF,EAAA,YAAAG,EAAAD,EAAA,WAAAE,EAAAF,EAAA,YAAAG,EAAAD,EAAA,WAAA,OAAAE,MAAA,CAAA,EAAA,OAAA,CAAApB,EAAA,IAAA,CAAA,EAAAoB,OAAA,CAAA,KAAA,GAAApB,EAAA,EAAA,IAAA,IAAA,GAAAA,EAAA,EAAA,GAAA,IAAA,EAAA,IAAA,CAAAqB,EAAAjB,EAAAkB,EAAAC,CAAA,CAAA,CAAA,EAAAC,EAAAnB,EAAA,cAAAP,CAAA,EAAA0B,EAAAnB,EAAA,cAAAJ,CAAA,EAAAuB,EAAAnB,EAAA,YAAAH,CAAA,EAAAsB,EAAAnB,EAAA,gBAAAH,CAAA,EAAAkB,MAAA,CAAAK,EAAAnB,EAAA,eAAAN,EAAA,MAAA,OAAA,CAAA,CAAA,CAAA,EAAAwB,EAAAlB,EAAA,YAAAN,EAAA,MAAA,UAAA,CAAAA,EAAA,MAAA,OAAA,CAAA,CAAA,EAAA0B,EAAApB,MAAAN,EAAA,MAAA,OAAA,EAAA,SAAA,OAAA,EAAAoB,MAAA,CAAAK,EAAAlB,EAAA,eAAAP,EAAA,MAAA,MAAA,CAAA,CAAA,CAAA,EAAAwB,EAAAjB,EAAA,YAAAP,EAAA,MAAA,SAAA,CAAAA,EAAA,MAAA,MAAA,CAAA,CAAA,EAAAwB,EAAAhB,EAAA,QAAAL,CAAA,EAAAqB,EAAAf,EAAA,YAAAT,EAAA,QAAA,CAAA,EAAA0B,EAAAhB,EAAAnB,EAAAqC,EAAA,CAAA,KAAAC,EAAA,SAAA,GAAA,CAAA,IAAAzB,EAAAV,EAAA,EAAAW,EAAAb,EAAAY,EAAA,IAAA,EAAA,OAAAgB,MAAA,CAAAK,EAAArB,EAAA,gBAAAJ,EAAA,MAAA,IAAA,IAAA+B,EAAA,EAAA,CAAA,CAAA,EAAAX,MAAA,CAAAK,EAAArB,EAAA,WAAA2B,EAAA,EAAA,CAAA,CAAA,EAAAP,EAAApB,EAAA,YAAAJ,EAAA,MAAA,OAAA+B,EAAA,EAAA,CAAA,EAAAL,EAAAtB,MAAA2B,EAAA,MAAA1B,CAAA,EAAAqB,EAAAtB,EAAAb,EAAAyC,EAAA,CAAA,IAAA,MAAA,CAAA,OAAAD,EAAA,KAAA,UAAA/B,EAAA,MAAA,SAAA,CAAA,CAAA,OAAA,OAAAA,EAAA,MAAA,SAAA,CAAA,CAAA,OAAA,UAAA,CAAA,EAAA,IAAA,UAAA,CAAA,IAAAI,EAAAX,EAAA,EAAA,OAAAiC,EAAAtB,MAAAJ,EAAA,MAAA,SAAA,CAAA,CAAA,OAAA,OAAAA,EAAA,MAAA,SAAA,CAAA,CAAA,OAAA,SAAA,EAAAI,CAAA,CAAA,CAAA,CAAA,EAAAA,CAAA,CAAA,CAAA,CAAA,EAAAsB,EAAAf,EAAApB,EAAA0C,EAAA,CAAA,IAAA,UAAA,CAAA,MAAA,CAAA1C,EAAA2C,EAAA,CAAA,IAAA,MAAA,CAAA,OAAAlC,EAAA,MAAA,IAAA,IAAA,MAAA,EAAA,IAAA,UAAA,CAAA,OAAAT,EAAA4C,EAAA,CAAA,IAAA,OAAA,CAAA,OAAAnC,EAAA,KAAA,CAAA,CAAA,CAAA,CAAA,CAAA,EAAAT,EAAA2C,EAAA,CAAA,IAAA,MAAA,CAAA,OAAAlC,EAAA,MAAA,IAAA,IAAA,UAAA,EAAA,IAAA,UAAA,CAAA,OAAAT,EAAA6C,EAAA,CAAA,IAAA,OAAA,CAAA,OAAApC,EAAA,KAAA,CAAA,CAAA,CAAA,CAAA,CAAA,EAAAT,EAAA2C,EAAA,CAAA,IAAA,MAAA,CAAA,OAAAlC,EAAA,MAAA,IAAA,IAAA,OAAA,EAAA,IAAA,UAAA,CAAA,OAAAT,EAAA8C,EAAA,CAAA,IAAA,OAAA,CAAA,OAAArC,EAAA,KAAA,CAAA,CAAA,CAAA,CAAA,CAAA,EAAAT,EAAA2C,EAAA,CAAA,IAAA,MAAA,CAAA,OAAAlC,EAAA,MAAA,IAAA,IAAA,QAAA,EAAA,IAAA,UAAA,CAAA,OAAAT,EAAA+C,EAAA,CAAA,IAAA,OAAA,CAAA,OAAAtC,EAAA,KAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,EAAA0B,EAAAd,MAAAZ,EAAA,MAAA,SAAA,CAAA,CAAA,OAAA,WAAAa,CAAA,EAAAa,EAAAZ,MAAAd,EAAA,MAAA,SAAA,CAAA,CAAA,OAAA,QAAAe,CAAA,EAAAW,EAAAV,MAAAhB,EAAA,MAAA,SAAA,CAAA,CAAA,OAAA,MAAAA,EAAA,MAAA,SAAA,CAAA,CAAA,OAAA,QAAAiB,CAAA,EAAAS,EAAAR,MAAAlB,EAAA,MAAA,SAAA,CAAA,CAAA,OAAA,KAAAmB,CAAA,EAAAf,CAAA,CA8FF"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import"../protocol.js";import{nodeLabel as e}from"../inspector.js";function t(t){return t.isComponent?`<${t.name??`component#${t.id}`}>`:e(t)}function n(e){switch(e.type){case`signal`:return e.preview;case`resource`:return e.state;case`memo`:case`effect`:return e.runs===0?``:`${e.runs} run${e.runs===1?``:`s`}`;default:return``}}function r(e,r){let i=/* @__PURE__ */ new Map;for(let t of e.nodes)i.set(t.id,t);let a=r.filter.trim().toLowerCase(),o=a!==``,s=/* @__PURE__ */ new Map;for(let t of e.nodes){if(t.updates===0)continue;let n=t,r=0;for(;n!==void 0&&r<e.nodes.length+1;)s.set(n.id,(s.get(n.id)??0)+t.updates),n=n.parent===0?void 0:i.get(n.parent),r+=1}let c=null;if(o){let n=/* @__PURE__ */ new Set;for(let r of e.nodes){if(!`${t(r)} ${r.type} ${r.preview} ${r.state}`.toLowerCase().includes(a))continue;let e=r;for(;e!==void 0&&!n.has(e.id);)n.add(e.id),e=e.parent===0?void 0:i.get(e.parent)}c=n}let l=r.limit??2e3,u=[],d=0,f=[];for(let t=e.roots.length-1;t>=0;--t){let n=e.roots[t];n!==void 0&&f.push({id:n,depth:0})}for(;f.length>0;){let e=f.pop();if(e===void 0)break;let a=i.get(e.id);if(a===void 0||c!==null&&!c.has(a.id))continue;let p=!r.componentsOnly||a.isComponent,m=a.children.filter(e=>c===null||c.has(e)),h=!o&&r.collapsed.has(a.id);if(p&&(u.length<l?u.push({id:a.id,depth:e.depth,type:a.type,isComponent:a.isComponent,label:t(a),detail:n(a),hasChildren:m.length>0,collapsed:h,updates:s.get(a.id)??0,component:a.component}):d+=1),h)continue;let g=p?e.depth+1:e.depth;for(let e=m.length-1;e>=0;--e){let t=m[e];t!==void 0&&f.push({id:t,depth:g})}}return{rows:u,hidden:d}}function i(e){if(e.isComponent)return`C`;switch(e.type){case`owner`:return`O`;case`signal`:return`S`;case`memo`:return`M`;case`effect`:return`E`;case`resource`:return`R`;case`store`:return`ST`}}export{r as buildRows,i as kindLetter};
|
|
2
|
+
//# sourceMappingURL=rows.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rows.js","names":["labelOf","node","nodeLabel","detailOf","buildRows","byId","snapshot","filter","options","filtering","rolled","current","guard","included","matches","limit","rows","hidden","stack","i","id","entry","shown","children","collapsed","childDepth","child","kindLetter","row"],"sources":["../../../src/ui/rows.ts"],"sourcesContent":["import { type GraphSnapshot, nodeLabel, type SnapshotNode } from \"../inspector\";\nimport type { NodeType } from \"../model\";\nimport { NO_ID } from \"../protocol\";\n\n/** One line of the tree view. @since 1.0.0 */\nexport interface TreeRow {\n /**\n * Id of the node.\n */\n id: number;\n /**\n * Nesting level of the row: `0` for a root. With components only, a row sits one level below its\n * nearest shown ancestor.\n */\n depth: number;\n /**\n * The kind of the node.\n */\n type: NodeType;\n /**\n * True for a component node.\n */\n isComponent: boolean;\n /** Display name: `<Counter>`, `count`, `effect#12`. */\n label: string;\n /**\n * Secondary text: a signal's value, a resource's state, or the run count of a memo or effect.\n */\n detail: string;\n /**\n * True when the node has children in the list. While a filter is set, only matching children count.\n */\n hasChildren: boolean;\n /**\n * True when the node's children are hidden. Always false while a filter is set.\n */\n collapsed: boolean;\n /**\n * Re-runs after the first, summed over the node and everything below it in the owners tree.\n */\n updates: number;\n /** Id of the component that owns the node ({@link NO_ID} for none). */\n component: number;\n}\n\n/** What to show in the tree. @since 1.0.0 */\nexport interface RowOptions {\n /** Case-insensitive text matched against names, kinds and values. Matches are shown with their ancestors. */\n filter: string;\n /** Show only components (their descendants are attached to the nearest shown ancestor). */\n componentsOnly: boolean;\n /**\n * Ids the user collapsed. Everything else is expanded. Ignored while `filter` is set.\n */\n collapsed: ReadonlySet<number>;\n /**\n * Most rows returned.\n *\n * @default 2000\n */\n limit?: number;\n}\n\n/**\n * Result of {@link buildRows}.\n *\n * @since 1.0.0\n */\nexport interface RowsResult {\n /**\n * The visible rows, depth first.\n */\n rows: TreeRow[];\n /** Rows that matched but were cut by `limit`. */\n hidden: number;\n}\n\nfunction labelOf(node: SnapshotNode): string {\n if (node.isComponent) return `<${node.name ?? `component#${node.id}`}>`;\n return nodeLabel(node);\n}\n\nfunction detailOf(node: SnapshotNode): string {\n switch (node.type) {\n case \"signal\":\n return node.preview;\n case \"resource\":\n return node.state;\n case \"memo\":\n case \"effect\":\n return node.runs === 0 ? \"\" : `${node.runs} run${node.runs === 1 ? \"\" : \"s\"}`;\n default:\n return \"\";\n }\n}\n\n/**\n * Flattens the owners tree into the rows the tree view shows: depth first, children in creation order,\n * honoring the collapse state, the text filter and the components-only switch. Iterative, so a deep or\n * large tree does not overflow the call stack.\n *\n * Internal to the panel: the package index does not export this function.\n *\n * @param snapshot A graph snapshot.\n * @param options Filter, switches and the row limit.\n * @returns The rows and how many matches were cut.\n * @internal\n */\nexport function buildRows(snapshot: GraphSnapshot, options: RowOptions): RowsResult {\n const byId = new Map<number, SnapshotNode>();\n for (const node of snapshot.nodes) byId.set(node.id, node);\n const filter = options.filter.trim().toLowerCase();\n const filtering = filter !== \"\";\n\n // Updates roll up: a component shows the re-runs of everything below it.\n const rolled = new Map<number, number>();\n for (const node of snapshot.nodes) {\n if (node.updates === 0) continue;\n let current: SnapshotNode | undefined = node;\n let guard = 0;\n while (current !== undefined && guard < snapshot.nodes.length + 1) {\n rolled.set(current.id, (rolled.get(current.id) ?? 0) + node.updates);\n current = current.parent === NO_ID ? undefined : byId.get(current.parent);\n guard += 1;\n }\n }\n\n // With a filter, only matches and their ancestors take part.\n let included: Set<number> | null = null;\n if (filtering) {\n const matches = new Set<number>();\n for (const node of snapshot.nodes) {\n const haystack = `${labelOf(node)} ${node.type} ${node.preview} ${node.state}`.toLowerCase();\n if (!haystack.includes(filter)) continue;\n let current: SnapshotNode | undefined = node;\n while (current !== undefined && !matches.has(current.id)) {\n matches.add(current.id);\n current = current.parent === NO_ID ? undefined : byId.get(current.parent);\n }\n }\n included = matches;\n }\n\n const limit = options.limit ?? 2000;\n const rows: TreeRow[] = [];\n let hidden = 0;\n const stack: Array<{ id: number; depth: number }> = [];\n for (let i = snapshot.roots.length - 1; i >= 0; i -= 1) {\n const id = snapshot.roots[i];\n if (id !== undefined) stack.push({ id, depth: 0 });\n }\n while (stack.length > 0) {\n const entry = stack.pop();\n if (entry === undefined) break;\n const node = byId.get(entry.id);\n if (node === undefined) continue;\n if (included !== null && !included.has(node.id)) continue;\n const shown = !options.componentsOnly || node.isComponent;\n const children = node.children.filter((id) => included === null || included.has(id));\n const collapsed = !filtering && options.collapsed.has(node.id);\n if (shown) {\n if (rows.length < limit) {\n rows.push({\n id: node.id,\n depth: entry.depth,\n type: node.type,\n isComponent: node.isComponent,\n label: labelOf(node),\n detail: detailOf(node),\n hasChildren: children.length > 0,\n collapsed,\n updates: rolled.get(node.id) ?? 0,\n component: node.component,\n });\n } else {\n hidden += 1;\n }\n }\n if (collapsed) continue;\n const childDepth = shown ? entry.depth + 1 : entry.depth;\n for (let i = children.length - 1; i >= 0; i -= 1) {\n const child = children[i];\n if (child !== undefined) stack.push({ id: child, depth: childDepth });\n }\n }\n return { rows, hidden };\n}\n\n/**\n * One or two letters that name a row's kind in the tree: `C` for a component, `O` owner, `S` signal,\n * `M` memo, `E` effect, `R` resource and `ST` store.\n *\n * Internal to the panel: the package index does not export this function.\n *\n * @param row The row.\n * @returns The letters.\n * @internal\n */\nexport function kindLetter(row: { type: NodeType; isComponent: boolean }): string {\n if (row.isComponent) return \"C\";\n switch (row.type) {\n case \"owner\":\n return \"O\";\n case \"signal\":\n return \"S\";\n case \"memo\":\n return \"M\";\n case \"effect\":\n return \"E\";\n case \"resource\":\n return \"R\";\n case \"store\":\n return \"ST\";\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{Details as e}from"./details.js";import{For as t,Show as n,addEventListener as r,className as i,createComponent as a,effect as o,insert as s,setAttribute as c,template as l}from"@shadow-js/web";const u=/*#__PURE__*/ l(`<div class="empty">No signals or stores yet.</div>`),d=/*#__PURE__*/ l(`<span class="updates" title="Writes">×</span>`),f=/*#__PURE__*/ l(`<div class="row" role="option" tabindex="0"><span></span><span class="name"></span><span class="value"></span></div>`),p=/*#__PURE__*/ l(`<div class="split"><div class="list" role="listbox" aria-label="State"></div></div>`),m=l=>{let m=()=>l.state.snapshot().nodes.filter(e=>e.type===`signal`||e.type===`store`||e.type===`resource`);{let h=p(),g=h.firstChild;return s(g,a(n,{get when(){return m().length>0},get fallback(){return u()},get children(){return a(t,{get each(){return m()},key:e=>e.id,children:e=>{let t=f(),u=t.firstChild,p=u.nextSibling,m=p.nextSibling;return o(()=>{c(t,`data-id`,e().id)}),o(()=>{c(t,`aria-selected`,l.state.selected()===e().id)}),r(t,`click`,()=>l.state.select(e().id)),r(t,`keydown`,t=>{(t.key===`Enter`||t.key===` `)&&(t.preventDefault(),l.state.select(e().id))}),o(()=>{i(u,`kind ${e().type}`)}),s(u,()=>e().type),s(p,()=>l.state.nameOf(e().id)),s(m,()=>e().type===`resource`?e().state:e().preview),s(t,a(n,{get when(){return e().writes>0},get children(){let t=d();return s(t,()=>e().writes),t}})),t}})}})),s(h,a(n,{get when(){return l.state.selectedNode()!==void 0},get children(){return a(e,{get state(){return l.state}})}})),h}};export{m as StateTab};
|
|
2
|
+
//# sourceMappingURL=state-tab.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"state-tab.js","names":["_$createComponent","_tmpl$","_tmpl$2","_tmpl$3","_tmpl$4","StateTab","items","props","node","_el$","_el$2","insert","createComponent","Show","For","_el$3","_el$4","effect","setAttribute","addEventListener","event","className","Details"],"sources":["../../../src/ui/state-tab.tsx"],"sourcesContent":["import { For, Show } from \"@shadow-js/web\";\nimport type { Component } from \"@shadow-js/web/jsx-runtime\";\nimport { Details } from \"./details\";\nimport type { TabProps } from \"./tree\";\n\n/**\n * The State tab. Props: `state` is the panel's {@link PanelState}. It lists every signal, store and\n * resource with its current value (and its write count when it is above zero). Selecting a row (click, Enter or Space) shows its\n * details; for signals and stores the details include a value editor, where a change is applied like any\n * other write.\n *\n * Internal to the panel: the package index does not export this component. Used in development builds\n * only.\n *\n * @internal\n */\nexport const StateTab: Component<TabProps> = (props) => {\n const items = () =>\n props.state\n .snapshot()\n .nodes.filter(\n (node) => node.type === \"signal\" || node.type === \"store\" || node.type === \"resource\",\n );\n return (\n <div class=\"split\">\n <div class=\"list\" role=\"listbox\" aria-label=\"State\">\n <Show\n when={items().length > 0}\n fallback={<div class=\"empty\">No signals or stores yet.</div>}\n >\n <For each={items()} key={(node) => node.id}>\n {(node) => (\n <div\n class=\"row\"\n role=\"option\"\n data-id={node().id}\n tabIndex={0}\n aria-selected={props.state.selected() === node().id}\n on:click={() => props.state.select(node().id)}\n on:keydown={(event) => {\n if (event.key === \"Enter\" || event.key === \" \") {\n event.preventDefault();\n props.state.select(node().id);\n }\n }}\n >\n <span class={`kind ${node().type}`}>{node().type}</span>\n <span class=\"name\">{props.state.nameOf(node().id)}</span>\n <span class=\"value\">\n {node().type === \"resource\" ? node().state : node().preview}\n </span>\n <Show when={node().writes > 0}>\n <span class=\"updates\" title=\"Writes\">\n ×{node().writes}\n </span>\n </Show>\n </div>\n )}\n </For>\n </Show>\n </div>\n <Show when={props.state.selectedNode() !== undefined}>\n <Details state={props.state} />\n </Show>\n </div>\n );\n};\n"],"mappings":"4kBAgBaK,EAAiC,GAAU,CACtD,IAAMC,MACJC,EAAM,MACH,SAAS,CAAA,CACT,MAAM,OACJ,GAASC,EAAK,OAAS,UAAYA,EAAK,OAAS,SAAWA,EAAK,OAAS,UAC7E,EACJ,CAAA,IAAAC,EAAAL,EAAA,EAAAM,EAAAD,EAAA,WAAA,OAAAE,EAAAD,EAAAV,EAAAa,EAAA,CAAA,IAAA,MAAA,CAAA,OAAAP,EAAA,CAAA,CAAA,OAAA,CAAA,EAAA,IAAA,UAAA,CAAA,OAAAL,EAAA,CAAA,EAAA,IAAA,UAAA,CAAA,OAAAD,EAAAc,EAAA,CAAA,IAAA,MAAA,CAAA,OAAAR,EAAA,CAAA,EAAA,IAAA,GAAAE,EAAA,GAAA,SAAA,GAAA,CAAA,IAAAC,EAAAN,EAAA,EAAAO,EAAAD,EAAA,WAAAM,EAAAL,EAAA,YAAAM,EAAAD,EAAA,YAAA,OAAAE,MAAA,CAAAC,EAAAT,EAAA,UAAAD,EAAA,CAAA,CAAA,EAAA,CAAA,CAAA,EAAAS,MAAA,CAAAC,EAAAT,EAAA,gBAAAF,EAAA,MAAA,SAAA,IAAAC,EAAA,CAAA,CAAA,EAAA,CAAA,CAAA,EAAAW,EAAAV,EAAA,YAAAF,EAAA,MAAA,OAAAC,EAAA,CAAA,CAAA,EAAA,CAAA,EAAAW,EAAAV,EAAA,UAAA,GAAA,EAAAW,EAAA,MAAA,SAAAA,EAAA,MAAA,OAAAA,EAAA,eAAA,EAAAb,EAAA,MAAA,OAAAC,EAAA,CAAA,CAAA,EAAA,EAAA,CAAA,EAAAS,MAAA,CAAAI,EAAAX,EAAA,QAAAF,EAAA,CAAA,CAAA,MAAA,CAAA,CAAA,EAAAG,EAAAD,MAAAF,EAAA,CAAA,CAAA,IAAA,EAAAG,EAAAI,MAAAR,EAAA,MAAA,OAAAC,EAAA,CAAA,CAAA,EAAA,CAAA,EAAAG,EAAAK,MAAAR,EAAA,CAAA,CAAA,OAAA,WAAAA,EAAA,CAAA,CAAA,MAAAA,EAAA,CAAA,CAAA,OAAA,EAAAG,EAAAF,EAAAT,EAAAa,EAAA,CAAA,IAAA,MAAA,CAAA,OAAAL,EAAA,CAAA,CAAA,OAAA,CAAA,EAAA,IAAA,UAAA,CAAA,IAAAC,EAAAP,EAAA,EAAA,OAAAS,EAAAF,MAAAD,EAAA,CAAA,CAAA,MAAA,EAAAC,CAAA,CAAA,CAAA,CAAA,EAAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,EAAAE,EAAAF,EAAAT,EAAAa,EAAA,CAAA,IAAA,MAAA,CAAA,OAAAN,EAAA,MAAA,aAAA,IAAA,IAAA,EAAA,EAAA,IAAA,UAAA,CAAA,OAAAP,EAAAsB,EAAA,CAAA,IAAA,OAAA,CAAA,OAAAf,EAAA,KAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,EAAAE,CAAA,CA2CF"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{componentStats as e,explainRun as t,flameTimeline as n,nodeLabel as r,snapshotOf as i}from"../inspector.js";import{buildRows as a}from"./rows.js";import{useMemo as o,useSignal as s}from"@shadow-js/core";const c=[{id:`tree`,label:`Components`},{id:`timeline`,label:`Timeline`},{id:`state`,label:`State`},{id:`issues`,label:`Issues`}];function l(c,l={}){let[u,d]=s(l.tab??`tree`),[f,p]=s(0),[m,h]=s(``),[g,_]=s(!1),[v,y]=s(/* @__PURE__ */ new Set),[b,x]=s(l.rowLimit??300),[S,C]=s(l.flash!==!1),[w,T]=s(!1),[E,D]=s(0),O=o(()=>(E(),i(c.model))),k=o(()=>new Map(O().nodes.map(e=>[e.id,e]))),A=o(()=>a(O(),{filter:m(),componentsOnly:g(),collapsed:v(),limit:b()})),j=o(()=>k().get(f()));return{backend:c,tab:u,setTab:d,selected:f,select:p,filter:m,setFilter:h,componentsOnly:g,setComponentsOnly:_,collapsed:v,toggleCollapsed(e){y(t=>{let n=new Set(t);return n.delete(e)||n.add(e),n})},rowLimit:b,showMoreRows(){x(e=>e*2)},flash:S,setFlash(e){C(e),l.onFlash?.(e)},paused:w,setPaused(e){T(e),c.request({type:e?`pause`:`resume`}),l.onPause?.(e)},version:E,refresh(){D(e=>e+1)},snapshot:O,nodes:k,rows:A,selectedNode:j,explanation:o(()=>{E();let e=j();return e===void 0||e.type!==`memo`&&e.type!==`effect`?null:t(c.model,e.id)}),flames:o(()=>(E(),n(c.model,{last:12}))),hot:o(()=>(E(),e(c.model).filter(e=>e.updates>0).slice(0,10))),errors:o(()=>(E(),c.model.errors.toArray().reverse())),hydration:o(()=>(E(),c.model.hydration.toArray().reverse())),highlight(e){c.request({type:`highlight`,id:e})},nameOf(e){let t=k().get(e)??c.model.node(e);return t===void 0?`#${e}`:r(t)}}}export{c as TABS,l as createPanelState};
|
|
2
|
+
//# sourceMappingURL=state.js.map
|