vite-devtools-svelte 0.3.0 → 0.4.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/README.md +159 -56
- package/dist/analyzers/api.d.ts +16 -0
- package/dist/analyzers/assets.d.ts +6 -1
- package/dist/analyzers/build.d.ts +3 -0
- package/dist/analyzers/module-graph.d.ts +13 -0
- package/dist/analyzers/og.d.ts +7 -0
- package/dist/analyzers/source.d.ts +6 -0
- package/dist/client/assets/ApiPlayground-BMx5j-7t.js +3 -0
- package/dist/client/assets/ApiPlayground-D6P2iQCp.css +1 -0
- package/dist/client/assets/Assets-BHhg5Smm.js +1 -0
- package/dist/client/assets/Assets-D-EORnuB.css +1 -0
- package/dist/client/assets/Badge-BBLKgCiU.css +1 -0
- package/dist/client/assets/Badge-CBVv8IFU.js +1 -0
- package/dist/client/assets/BuildAnalysis-BdVCnWpb.js +1 -0
- package/dist/client/assets/BuildAnalysis-ClES_VN-.css +1 -0
- package/dist/client/assets/CaptureNotice-B2odNyXw.css +1 -0
- package/dist/client/assets/CaptureNotice-BeXCVhO0.js +1 -0
- package/dist/client/assets/Components-Co50xTrv.css +1 -0
- package/dist/client/assets/Components-D_qDgEUt.js +1 -0
- package/dist/client/assets/DataTable-C6-tmcTQ.js +1 -0
- package/dist/client/assets/DataTable-CdVIFm8T.css +1 -0
- package/dist/client/assets/ErrorDashboard-0CGkszcN.css +1 -0
- package/dist/client/assets/ErrorDashboard-CfUhPzue.js +1 -0
- package/dist/client/assets/FpsMonitor-B_AuHgyu.js +1 -0
- package/dist/client/assets/FpsMonitor-BmS11Tn7.css +1 -0
- package/dist/client/assets/Inspect-BOgmKm6W.js +3 -0
- package/dist/client/assets/Inspect-Ci9HWo78.css +1 -0
- package/dist/client/assets/Inspector-CDwlmfEg.css +1 -0
- package/dist/client/assets/Inspector-DGmAzhki.js +1 -0
- package/dist/client/assets/LoadProfiler-5xhnDLjR.js +1 -0
- package/dist/client/assets/LoadProfiler-cg28l5zW.css +1 -0
- package/dist/client/assets/ModuleGraph-B6XWZJQb.css +1 -0
- package/dist/client/assets/ModuleGraph-Cp7f0Nz0.js +1 -0
- package/dist/client/assets/OGPreview-CSIAKIGn.css +1 -0
- package/dist/client/assets/OGPreview-CjBhXSgn.js +1 -0
- package/dist/client/assets/Overview-B8hvIsIM.css +1 -0
- package/dist/client/assets/Overview-Kbpkgsas.js +1 -0
- package/dist/client/assets/ReactiveGraph-Bq_aP4uo.css +1 -0
- package/dist/client/assets/ReactiveGraph-w2TK82c8.js +5 -0
- package/dist/client/assets/RenderProfiler-CS1FAvX6.css +1 -0
- package/dist/client/assets/RenderProfiler-CeF8_j3b.js +1 -0
- package/dist/client/assets/Routes-DArWZtzC.js +1 -0
- package/dist/client/assets/Routes-eFPp9T07.css +1 -0
- package/dist/client/assets/SearchField-DWMQuleP.css +1 -0
- package/dist/client/assets/SearchField-eocQu1eB.js +1 -0
- package/dist/client/assets/Segmented-JGNuI5EZ.js +1 -0
- package/dist/client/assets/Segmented-OaYMdNpv.css +1 -0
- package/dist/client/assets/SplitView-B8lYXXEm.js +1 -0
- package/dist/client/assets/SplitView-CbR-hDtW.css +1 -0
- package/dist/client/assets/StateTimeline-B4FdF3L3.css +1 -0
- package/dist/client/assets/StateTimeline-HpyvgoK1.js +1 -0
- package/dist/client/assets/TreeView-BL5XbGhe.css +1 -0
- package/dist/client/assets/TreeView-fty5aCC5.js +1 -0
- package/dist/client/assets/VirtualList-BvOBTnYJ.js +1 -0
- package/dist/client/assets/VirtualList-CRDBwjCp.css +1 -0
- package/dist/client/assets/browser-agent-rpc-BXhoSh1z-D9g7mT9G.js +1 -0
- package/dist/client/assets/capture.svelte-CUhwdJwX.js +1 -0
- package/dist/client/assets/format-CW_NXiBg.js +1 -0
- package/dist/client/assets/index-BzMw8R8l.js +7 -0
- package/dist/client/assets/index-eZHqMk0z.css +1 -0
- package/dist/client/assets/reactive-C2hv2QLH.js +1 -0
- package/dist/client/assets/reactive-selection.svelte-Dfd9zWsh.js +1 -0
- package/dist/client/assets/versions-CZjRj40E.js +1 -0
- package/dist/client/assets/versions-Co596WUJ.css +1 -0
- package/dist/client/index.html +3 -14
- package/dist/collector.d.ts +284 -0
- package/dist/devframe.d.ts +90 -0
- package/dist/index.mjs +1839 -808
- package/dist/index.mjs.map +1 -1
- package/dist/mcp/server.d.ts +21 -1
- package/dist/mcp/sessions.d.ts +9 -0
- package/dist/mount.d.ts +18 -0
- package/dist/runtime.d.ts +2 -2
- package/dist/security.d.ts +27 -0
- package/dist/template-injector.d.ts +14 -2
- package/dist/types.d.ts +176 -0
- package/package.json +8 -7
- package/dist/client/assets/client-D_AN-igc.js +0 -2
- package/dist/client/assets/index-CJElsHas.js +0 -6
- package/dist/client/assets/index-D-ChuYqP.css +0 -1
package/dist/mcp/server.d.ts
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
3
|
-
import { RenderProfile, ReactiveGraph, LoadProfile, FpsSample, ComponentInstance, ProjectInfo, RouteInfo, ComponentRelation } from '../types.js';
|
|
3
|
+
import { RenderProfile, ReactiveGraph, LoadProfile, FpsSample, ComponentInstance, ProjectInfo, RouteInfo, ComponentRelation, CaptureInfoMap, ReactiveGraphRequest, ReactiveGraphResult, ReactiveSummary, ReactiveSummaryRequest, StateTimelineDelta } from '../types.js';
|
|
4
4
|
import { SessionStore } from './sessions.js';
|
|
5
5
|
export interface McpDeps {
|
|
6
6
|
getProject: () => ProjectInfo;
|
|
7
7
|
getRoutes: () => RouteInfo[];
|
|
8
8
|
getLiveComponents: () => ComponentInstance[];
|
|
9
|
+
/**
|
|
10
|
+
* The served page load's components with its epoch, read together
|
|
11
|
+
* (`get_live_components` with `includeMeta`).
|
|
12
|
+
*/
|
|
13
|
+
getLiveSnapshot?: () => {
|
|
14
|
+
epoch: string | null;
|
|
15
|
+
total: number;
|
|
16
|
+
components: ComponentInstance[];
|
|
17
|
+
};
|
|
9
18
|
getComponentRelations: () => ComponentRelation[];
|
|
10
19
|
getRenderProfiles: () => RenderProfile[];
|
|
11
20
|
/** Resolves with the current reactive graph after refreshing from the browser. */
|
|
@@ -13,6 +22,17 @@ export interface McpDeps {
|
|
|
13
22
|
getLoadProfiles: () => LoadProfile[];
|
|
14
23
|
getFpsSamples: () => FpsSample[];
|
|
15
24
|
sessions: SessionStore;
|
|
25
|
+
getReactiveSummary?: (req: ReactiveSummaryRequest) => Promise<ReactiveSummary>;
|
|
26
|
+
getReactiveScope?: (req: ReactiveGraphRequest) => Promise<ReactiveGraphResult>;
|
|
27
|
+
getStateTimelineDelta?: (since?: number) => StateTimelineDelta;
|
|
28
|
+
getCaptureInfo?: () => CaptureInfoMap;
|
|
16
29
|
}
|
|
30
|
+
/** Most timeline entries one `get_state_timeline` call returns. */
|
|
31
|
+
export declare const MCP_TIMELINE_LIMIT = 500;
|
|
32
|
+
/** Default / largest JSON size of one old/new value in `get_state_timeline` output. */
|
|
33
|
+
export declare const MCP_VALUE_CHARS: {
|
|
34
|
+
readonly default: 2048;
|
|
35
|
+
readonly max: 32768;
|
|
36
|
+
};
|
|
17
37
|
export declare function buildMcpServer(deps: McpDeps): McpServer;
|
|
18
38
|
export { StreamableHTTPServerTransport };
|
package/dist/mcp/sessions.d.ts
CHANGED
|
@@ -80,6 +80,14 @@ export interface MetricGetters {
|
|
|
80
80
|
getLoadProfiles: () => LoadProfile[];
|
|
81
81
|
getFpsSamples: () => FpsSample[];
|
|
82
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* The only id shape `start()` issues: `s_<Date.now() base36>_<6 hex>`. Ids
|
|
85
|
+
* reach the store from MCP clients and become file names, so anything else
|
|
86
|
+
* (path separators, `..`, absolute paths) is rejected before touching disk
|
|
87
|
+
* (review C-7).
|
|
88
|
+
*/
|
|
89
|
+
export declare const SESSION_ID_PATTERN: RegExp;
|
|
90
|
+
export declare function isSessionId(id: unknown): id is string;
|
|
83
91
|
export interface SessionStoreOptions {
|
|
84
92
|
persistDir: string;
|
|
85
93
|
getters: MetricGetters;
|
|
@@ -108,6 +116,7 @@ export declare class SessionStore {
|
|
|
108
116
|
delete(id: string): boolean;
|
|
109
117
|
delta(id: string): SessionDelta;
|
|
110
118
|
compare(idA: string, idB: string): SessionDiff;
|
|
119
|
+
/** The session's file, guaranteed to be a direct child of `persistDir`. */
|
|
111
120
|
private pathFor;
|
|
112
121
|
private persistToDisk;
|
|
113
122
|
private loadFromDisk;
|
package/dist/mount.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { AddressInfo } from 'node:net';
|
|
2
|
+
import { ViteDevServer } from 'vite';
|
|
3
|
+
import { DevframeDefinition } from 'devframe';
|
|
4
|
+
/**
|
|
5
|
+
* Origins a browser may load the DevTools from when the dev server is
|
|
6
|
+
* reachable beyond loopback (`server.host`). devframe accepts loopback
|
|
7
|
+
* origins by default and rejects every other WS `Origin` unless listed.
|
|
8
|
+
*/
|
|
9
|
+
export declare function networkOrigins(address: AddressInfo, proto: string): string[];
|
|
10
|
+
/**
|
|
11
|
+
* Serve the devframe standalone on this dev server: SPA, discovery,
|
|
12
|
+
* WebSocket RPC on Vite's own HTTP server at `<base>__ws`, auth gate.
|
|
13
|
+
*
|
|
14
|
+
* The instance is created per server and closed by *that* server's `close`
|
|
15
|
+
* event, so a restart (which builds the new server before closing the old
|
|
16
|
+
* one) can never tear down the new instance.
|
|
17
|
+
*/
|
|
18
|
+
export declare function mountStandalone(server: ViteDevServer, def: DevframeDefinition): (() => Promise<void>) | undefined;
|
package/dist/runtime.d.ts
CHANGED
|
@@ -15,5 +15,5 @@ export declare const WRAPPER_MODULE_ID = "\0svelte-devtools:wrapped-client";
|
|
|
15
15
|
* This single module replaces all post-compilation regex transforms
|
|
16
16
|
* for reactive tracking, making the approach Svelte-compiler-output agnostic.
|
|
17
17
|
*/
|
|
18
|
-
export declare const wrapperCode = "\nimport * as __svelte_original from 'svelte/internal/client';\nexport * from 'svelte/internal/client';\n\nfunction __dt() {\n return typeof window !== 'undefined' ? window.__SVELTE_DEVTOOLS__ : null;\n}\n\n// Component ID stack (parallel to runtime._stack but local to wrapper)\nconst __idStack = [];\nfunction __currentId() {\n return __idStack.length > 0 ? __idStack[__idStack.length - 1] : null;\n}\n\n// Tracks the most recently created signal for type determination in tag()\nconst __pendingSignal = { ref: null, type: null };\n\n// --- Component Lifecycle ---\n//\n// Observer must not break the observed: every devtools side-effect below is\n// wrapped in try/catch so a bug or an out-of-shape runtime cannot tear down\n// the user's app. The original Svelte function is always called, and its\n// return value always returned, regardless of devtools failures.\n\nexport function push() {\n const result = __svelte_original.push.apply(null, arguments);\n try {\n const dt = __dt();\n if (dt) {\n const file = dt._pendingFile || 'Unknown';\n dt._pendingFile = null;\n const id = dt.register(file);\n __idStack.push(id);\n dt.startInit(id);\n try {\n __svelte_original.user_effect(() => {\n return () => { try { dt.unmount(id); } catch {} };\n });\n } catch {}\n }\n } catch {}\n return result;\n}\n\nexport function pop() {\n // Capture devtools errors but ALWAYS forward to the original pop so the\n // Svelte component stack stays balanced.\n try {\n const dt = __dt();\n if (dt && __idStack.length > 0) {\n const id = __idStack.pop();\n try { dt.endInit(id); } catch {}\n try { dt.registered(id); } catch {}\n }\n } catch {}\n return __svelte_original.pop.apply(null, arguments);\n}\n\n// --- Signal Creation (type markers) ---\n\nexport function state() {\n const signal = __svelte_original.state.apply(null, arguments);\n try {\n __pendingSignal.ref = signal;\n __pendingSignal.type = 'state';\n } catch {}\n return signal;\n}\n\nexport function derived() {\n const signal = __svelte_original.derived.apply(null, arguments);\n try {\n __pendingSignal.ref = signal;\n __pendingSignal.type = 'derived';\n } catch {}\n return signal;\n}\n\nexport function proxy() {\n const p = __svelte_original.proxy.apply(null, arguments);\n try {\n __pendingSignal.ref = p;\n __pendingSignal.type = 'proxy';\n } catch {}\n return p;\n}\n\n// --- Signal Tagging (Svelte dev mode) ---\n\nexport function tag(signal, name) {\n const result = __svelte_original.tag.apply(null, arguments);\n try {\n const dt = __dt();\n const cid = __currentId();\n if (dt && cid !== null) {\n const type = (__pendingSignal.ref === signal) ? __pendingSignal.type : 'state';\n if (type === 'derived') {\n dt.trackDerived(signal, name, cid);\n } else {\n dt.trackState(signal, name, cid);\n }\n }\n __pendingSignal.ref = null;\n __pendingSignal.type = null;\n } catch {}\n return result;\n}\n\nexport function tag_proxy(proxy, name) {\n const result = __svelte_original.tag_proxy.apply(null, arguments);\n try {\n const dt = __dt();\n const cid = __currentId();\n if (dt && cid !== null) {\n dt.trackProxy(proxy, name, cid);\n }\n __pendingSignal.ref = null;\n __pendingSignal.type = null;\n } catch {}\n return result;\n}\n\n// --- Effect Tracking ---\n\nexport function user_effect() {\n const result = __svelte_original.user_effect.apply(null, arguments);\n try {\n const dt = __dt();\n const cid = __currentId();\n if (dt && cid !== null) {\n dt._effectCounter = (dt._effectCounter || 0) + 1;\n // Track the effect OBJECT (not the callback). The Svelte runtime\n // populates result.deps with the signals this effect depends on,\n // which is what getReactiveGraph() reads to build edges.\n dt.trackEffect(result, 'effect_' + dt._effectCounter, cid);\n }\n } catch {}\n return result;\n}\n\nexport function user_pre_effect() {\n const result = __svelte_original.user_pre_effect.apply(null, arguments);\n try {\n const dt = __dt();\n const cid = __currentId();\n if (dt && cid !== null) {\n dt._effectCounter = (dt._effectCounter || 0) + 1;\n dt.trackEffect(result, 'effect_pre_' + dt._effectCounter, cid);\n }\n } catch {}\n return result;\n}\n\n// --- Render Profiling ---\n//\n// Svelte 5 has no virtual DOM: the compiler wraps every dynamic part of a\n// template in a template_effect (deferred_template_effect for <svelte:head>\n// <title>). A re-run of such an effect IS the component updating the DOM, so\n// these two functions are the measurement points for renders / render time.\n//\n// The first run of an effect is the mount-time paint, already covered by\n// initTime, so it is skipped (initialRun). One state change re-runs several\n// effects of the same component, so durations are pooled per microtask and\n// recorded once per flush: recordRender() +1, recordRenderTime() the sum.\n\nconst __pendingRenderDurations = new Map();\nlet __renderFlushScheduled = false;\n\nfunction __flushRenderDurations() {\n __renderFlushScheduled = false;\n const dt = __dt();\n for (const [cid, duration] of __pendingRenderDurations) {\n if (dt) {\n try { dt.recordRender(cid); } catch {}\n try { dt.recordRenderTime(cid, duration); } catch {}\n }\n }\n __pendingRenderDurations.clear();\n}\n\nfunction __recordRenderDuration(cid, duration) {\n __pendingRenderDurations.set(cid, (__pendingRenderDurations.get(cid) || 0) + duration);\n if (!__renderFlushScheduled) {\n __renderFlushScheduled = true;\n queueMicrotask(__flushRenderDurations);\n }\n}\n\n// Returns a (possibly new) args array whose first element times each re-run\n// of the effect closure. The owning component is whatever id is on top of\n// __idStack at effect-creation time. User exceptions propagate untouched;\n// only the args are built inside try so the original is called exactly once.\nfunction __wrapTemplateEffect(args) {\n const cid = __currentId();\n const fn = args[0];\n if (cid === null || typeof fn !== 'function') return args;\n const wrapped = Array.prototype.slice.call(args);\n let initialRun = true;\n wrapped[0] = function () {\n if (initialRun) {\n initialRun = false;\n return fn.apply(this, arguments);\n }\n const start = performance.now();\n try {\n return fn.apply(this, arguments);\n } finally {\n try { __recordRenderDuration(cid, performance.now() - start); } catch {}\n }\n };\n return wrapped;\n}\n\nexport function template_effect() {\n let args = arguments;\n try { args = __wrapTemplateEffect(arguments); } catch {}\n return __svelte_original.template_effect.apply(null, args);\n}\n\nexport function deferred_template_effect() {\n let args = arguments;\n try { args = __wrapTemplateEffect(arguments); } catch {}\n return __svelte_original.deferred_template_effect.apply(null, args);\n}\n\n// --- Block Attribution ---\n//\n// Svelte creates render effects synchronously at creation time. At mount this\n// happens during the component function (between push/pop), so __idStack has\n// the owner. But effects for {#each} items added later or re-created {#if}\n// branches are created during a batch flush, when the stack is empty \u2014 they\n// would never be attributed. Block helpers themselves ARE always called\n// during their owner's init (or inside an outer block's callback), so the\n// owner id is on the stack at block-creation time. __wrapBlock captures it\n// and re-pushes it around every function argument the block invokes later,\n// so effects created inside land on the innermost owner. Child components\n// push their own id on top, keeping nested attribution correct.\n\nfunction __wrapBlockFn(fn, cid) {\n return function () {\n let pushed = false;\n try { __idStack.push(cid); pushed = true; } catch {}\n try {\n return fn.apply(this, arguments);\n } finally {\n if (pushed) { try { __idStack.pop(); } catch {} }\n }\n };\n}\n\nfunction __wrapBlock(args) {\n const cid = __currentId();\n if (cid === null) return args;\n const wrapped = Array.prototype.slice.call(args);\n for (let i = 0; i < wrapped.length; i++) {\n if (typeof wrapped[i] === 'function') wrapped[i] = __wrapBlockFn(wrapped[i], cid);\n }\n return wrapped;\n}\n\nexport function each() {\n let args = arguments;\n try { args = __wrapBlock(arguments); } catch {}\n return __svelte_original.each.apply(null, args);\n}\n\nexport function key() {\n let args = arguments;\n try { args = __wrapBlock(arguments); } catch {}\n return __svelte_original.key.apply(null, args);\n}\n\nexport function component() {\n let args = arguments;\n try { args = __wrapBlock(arguments); } catch {}\n return __svelte_original.component.apply(null, args);\n}\n\nexport function boundary() {\n let args = arguments;\n try { args = __wrapBlock(arguments); } catch {}\n return __svelte_original.boundary.apply(null, args);\n}\n\n// 'if' and 'await' are reserved words, so they cannot be declared as\n// function names and are exported via aliases instead.\nfunction __if_block() {\n let args = arguments;\n try { args = __wrapBlock(arguments); } catch {}\n return __svelte_original['if'].apply(null, args);\n}\n\nfunction __await_block() {\n let args = arguments;\n try { args = __wrapBlock(arguments); } catch {}\n return __svelte_original['await'].apply(null, args);\n}\n\nexport { __if_block as if, __await_block as await };\n";
|
|
19
|
-
export declare const runtimeCode = "\nif (typeof window !== 'undefined' && !window.__SVELTE_DEVTOOLS__) {\n const __SVELTE_DT = {\n _nextId: 0,\n _instances: new Map(),\n _stack: [],\n _pendingFile: null,\n _effectCounter: 0,\n _debounceTimer: null,\n _listeners: new Set(),\n\n // Phase 2: Profiling data\n _profiles: new Map(),\n _initStartTimes: new Map(),\n _profileDebounceTimer: null,\n\n // Phase 2: Reactive graph data\n _reactiveNodes: new Map(),\n _reactiveProxies: new Map(),\n\n // Phase 3: State timeline\n _stateTimeline: [],\n _stateSnapshots: new Map(),\n _timelineDebounceTimer: null,\n\n register(file) {\n const id = this._nextId++;\n const parentId = this._stack.length > 0 ? this._stack[this._stack.length - 1] : null;\n const name = file.split('/').pop()?.replace('.svelte', '') || 'Unknown';\n this._instances.set(id, { id, file, name, parentId, mounted: true, children: [] });\n if (parentId !== null) {\n const parent = this._instances.get(parentId);\n if (parent) parent.children.push(id);\n }\n this._stack.push(id);\n this._scheduleUpdate();\n return id;\n },\n\n registered(id) {\n const idx = this._stack.indexOf(id);\n if (idx !== -1) this._stack.splice(idx, 1);\n },\n\n mount(id) {\n const instance = this._instances.get(id);\n if (instance && !instance.mounted) {\n instance.mounted = true;\n this._scheduleUpdate();\n }\n },\n\n unmount(id) {\n const instance = this._instances.get(id);\n if (instance) {\n if (instance.parentId !== null) {\n const parent = this._instances.get(instance.parentId);\n if (parent) {\n parent.children = parent.children.filter(cid => cid !== id);\n }\n }\n this._removeChildren(id);\n this._cleanupComponent(id);\n }\n this._scheduleUpdate();\n this._scheduleProfileUpdate();\n },\n\n // Remounts get a fresh id, so entries keyed by a dead id would otherwise\n // survive forever \u2014 every per-component map must be purged here.\n _cleanupComponent(id) {\n this._cleanupReactiveNodes(id);\n this._instances.delete(id);\n this._profiles.delete(id);\n this._initStartTimes.delete(id);\n },\n\n _cleanupReactiveNodes(componentId) {\n for (const [nodeId, entry] of this._reactiveNodes) {\n if (entry.meta.componentId === componentId) {\n this._reactiveNodes.delete(nodeId);\n this._reactiveProxies.delete(nodeId);\n this._stateSnapshots.delete(nodeId);\n if (this._stateSnapshotStrs) this._stateSnapshotStrs.delete(nodeId);\n }\n }\n },\n\n _removeChildren(parentId) {\n const parent = this._instances.get(parentId);\n if (!parent) return;\n for (const childId of [...parent.children]) {\n this._removeChildren(childId);\n this._cleanupComponent(childId);\n }\n },\n\n _scheduleUpdate() {\n if (this._debounceTimer) clearTimeout(this._debounceTimer);\n this._debounceTimer = setTimeout(() => {\n this._sendUpdate();\n }, 100);\n },\n\n _sendUpdate() {\n const components = [];\n for (const [, instance] of this._instances) {\n if (instance.mounted) {\n components.push({\n id: instance.id,\n file: instance.file,\n name: instance.name,\n parentId: instance.parentId,\n mounted: instance.mounted,\n });\n }\n }\n if (import.meta.hot) {\n import.meta.hot.send('svelte-devtools:components', { components });\n }\n },\n\n getTree() {\n const components = [];\n for (const [, instance] of this._instances) {\n if (instance.mounted) {\n components.push({\n id: instance.id,\n file: instance.file,\n name: instance.name,\n parentId: instance.parentId,\n mounted: instance.mounted,\n });\n }\n }\n return components;\n },\n\n // --- Phase 2: Render Profiling ---\n\n startInit(id) {\n this._initStartTimes.set(id, performance.now());\n },\n\n endInit(id) {\n const start = this._initStartTimes.get(id);\n if (start === undefined) return;\n const initTime = performance.now() - start;\n this._initStartTimes.delete(id);\n const instance = this._instances.get(id);\n if (!instance) return;\n this._profiles.set(id, {\n componentId: id,\n file: instance.file,\n name: instance.name,\n initTime,\n renderCount: 0,\n totalRenderTime: 0,\n lastRenderTime: 0,\n lastRenderAt: Date.now(),\n });\n this._scheduleProfileUpdate();\n },\n\n recordRender(id) {\n const profile = this._profiles.get(id);\n if (!profile) {\n const instance = this._instances.get(id);\n if (!instance) return;\n this._profiles.set(id, {\n componentId: id,\n file: instance.file,\n name: instance.name,\n initTime: 0,\n renderCount: 1,\n totalRenderTime: 0,\n lastRenderTime: 0,\n lastRenderAt: Date.now(),\n });\n } else {\n profile.renderCount++;\n profile.lastRenderAt = Date.now();\n }\n this._scheduleProfileUpdate();\n },\n\n recordRenderTime(id, duration) {\n const profile = this._profiles.get(id);\n if (profile) {\n profile.totalRenderTime += duration;\n profile.lastRenderTime = duration;\n }\n },\n\n _scheduleProfileUpdate() {\n if (this._profileDebounceTimer) clearTimeout(this._profileDebounceTimer);\n this._profileDebounceTimer = setTimeout(() => {\n this._sendProfileUpdate();\n }, 500);\n },\n\n _sendProfileUpdate() {\n const profiles = Array.from(this._profiles.values());\n if (import.meta.hot) {\n import.meta.hot.send('svelte-devtools:profiles', { profiles });\n }\n },\n\n getProfiles() {\n return Array.from(this._profiles.values());\n },\n\n resetProfiles() {\n this._profiles.clear();\n this._scheduleProfileUpdate();\n },\n\n // --- Phase 2: Reactive Graph Tracking ---\n\n trackState(signal, name, componentId) {\n const instance = this._instances.get(componentId);\n const nodeId = componentId + ':' + name;\n this._reactiveNodes.set(nodeId, {\n signal: new WeakRef(signal),\n meta: { id: nodeId, type: 'state', name, componentId, componentFile: instance ? instance.file : '' }\n });\n },\n\n trackProxy(proxy, name, componentId) {\n const instance = this._instances.get(componentId);\n const nodeId = componentId + ':' + name;\n this._reactiveProxies.set(nodeId, new WeakRef(proxy));\n this._reactiveNodes.set(nodeId, {\n signal: new WeakRef({ v: '(proxy)', _isProxy: true }),\n meta: { id: nodeId, type: 'state', name, componentId, componentFile: instance ? instance.file : '' }\n });\n },\n\n trackDerived(signal, name, componentId) {\n const instance = this._instances.get(componentId);\n const nodeId = componentId + ':' + name;\n this._reactiveNodes.set(nodeId, {\n signal: new WeakRef(signal),\n meta: { id: nodeId, type: 'derived', name, componentId, componentFile: instance ? instance.file : '' }\n });\n },\n\n trackEffect(effect, name, componentId) {\n const instance = this._instances.get(componentId);\n const nodeId = componentId + ':' + name;\n this._reactiveNodes.set(nodeId, {\n signal: new WeakRef(effect || { v: undefined, _isEffect: true }),\n meta: { id: nodeId, type: 'effect', name, componentId, componentFile: instance ? instance.file : '' }\n });\n return effect;\n },\n\n getReactiveGraph() {\n const nodes = [];\n const edges = [];\n const signalToId = new Map();\n\n for (const [nodeId, entry] of this._reactiveNodes) {\n const signal = entry.signal.deref();\n if (!signal || !this._instances.has(entry.meta.componentId)) {\n this._reactiveNodes.delete(nodeId);\n continue;\n }\n signalToId.set(signal, nodeId);\n const node = { ...entry.meta };\n if (signal._isProxy) {\n const proxyRef = this._reactiveProxies.get(nodeId);\n const proxy = proxyRef?.deref();\n if (proxy) {\n try {\n node.value = Array.isArray(proxy) ? '[' + proxy.length + ']' : '{' + Object.keys(proxy).length + '}';\n } catch { node.value = '(proxy)'; }\n }\n } else if (node.type !== 'effect' && signal.v !== undefined && typeof signal.v !== 'symbol') {\n try {\n const v = signal.v;\n if (typeof v === 'number' || typeof v === 'string' || typeof v === 'boolean' || v === null) {\n node.value = v;\n } else {\n node.value = '(object)';\n }\n } catch { /* ignore */ }\n }\n nodes.push(node);\n }\n\n // Map proxy internal signals to their proxy node ID\n for (const [nodeId, ref] of this._reactiveProxies) {\n const proxy = ref.deref();\n if (!proxy) { this._reactiveProxies.delete(nodeId); continue; }\n const meta = this._reactiveNodes.get(nodeId)?.meta;\n if (!meta || !this._instances.has(meta.componentId)) { this._reactiveProxies.delete(nodeId); continue; }\n }\n\n // Build edges from deps\n const edgeSet = new Set();\n for (const [nodeId, entry] of this._reactiveNodes) {\n const signal = entry.signal.deref();\n if (!signal) continue;\n if (!signal.deps) continue;\n\n const visited = new Set();\n const queue = [...signal.deps];\n while (queue.length > 0) {\n const dep = queue.shift();\n if (!dep || visited.has(dep)) continue;\n visited.add(dep);\n const depId = signalToId.get(dep);\n if (depId) {\n const key = depId + '>' + nodeId;\n if (depId !== nodeId && !edgeSet.has(key)) {\n edgeSet.add(key);\n edges.push({ from: depId, to: nodeId });\n }\n } else if (dep.deps) {\n for (const d of dep.deps) queue.push(d);\n }\n }\n }\n\n return { nodes, edges };\n },\n\n sendReactiveGraph() {\n const graph = this.getReactiveGraph();\n if (import.meta.hot) {\n import.meta.hot.send('svelte-devtools:reactive-graph', graph);\n }\n },\n\n // --- Phase 3: State Timeline ---\n\n _pollStateValues() {\n for (const [nodeId, entry] of this._reactiveNodes) {\n if (entry.meta.type !== 'state') continue;\n if (!this._instances.has(entry.meta.componentId)) {\n this._reactiveNodes.delete(nodeId);\n continue;\n }\n const signal = entry.signal.deref();\n if (!signal || signal.v === undefined) continue;\n try {\n const currentVal = signal.v;\n const prev = this._stateSnapshots.get(nodeId);\n if (prev !== undefined && currentVal === prev) continue;\n const isPrimitive = currentVal === null || typeof currentVal !== 'object';\n let newSnapshot;\n if (isPrimitive) {\n if (prev !== undefined && prev === currentVal) continue;\n newSnapshot = currentVal;\n } else {\n const currStr = JSON.stringify(currentVal);\n const prevStr = this._stateSnapshotStrs?.get(nodeId);\n if (prevStr === currStr) continue;\n newSnapshot = JSON.parse(currStr);\n if (!this._stateSnapshotStrs) this._stateSnapshotStrs = new Map();\n this._stateSnapshotStrs.set(nodeId, currStr);\n }\n if (this._stateTimeline.length >= 500) this._stateTimeline.splice(0, this._stateTimeline.length - 499);\n this._stateTimeline.push({\n id: nodeId,\n name: entry.meta.name,\n componentFile: entry.meta.componentFile,\n oldValue: prev !== undefined ? prev : null,\n newValue: newSnapshot,\n timestamp: Date.now(),\n });\n this._stateSnapshots.set(nodeId, newSnapshot);\n this._scheduleTimelineUpdate();\n } catch { /* ignore non-serializable */ }\n }\n },\n\n _scheduleTimelineUpdate() {\n if (this._timelineDebounceTimer) clearTimeout(this._timelineDebounceTimer);\n this._timelineDebounceTimer = setTimeout(() => {\n if (import.meta.hot) {\n import.meta.hot.send('svelte-devtools:state-timeline', { changes: this._stateTimeline });\n }\n }, 300);\n },\n\n getStateTimeline() {\n return this._stateTimeline;\n },\n\n clearStateTimeline() {\n this._stateTimeline = [];\n this._scheduleTimelineUpdate();\n },\n\n // --- FPS Monitoring ---\n // Uses requestAnimationFrame to measure frame rate with minimal overhead.\n // Only stores timestamps - no allocations per frame beyond a single array push.\n _fpsFrameTimes: [],\n\n _fpsLoop() {\n this._fpsFrameTimes.push(performance.now());\n requestAnimationFrame(() => this._fpsLoop());\n },\n\n _sampleFps() {\n const now = performance.now();\n // Remove frame timestamps older than 1 second\n const cutoff = now - 1000;\n let i = 0;\n while (i < this._fpsFrameTimes.length && this._fpsFrameTimes[i] < cutoff) i++;\n if (i > 0) this._fpsFrameTimes.splice(0, i);\n const fps = this._fpsFrameTimes.length;\n if (import.meta.hot) {\n import.meta.hot.send('svelte-devtools:fps', { timestamp: Date.now(), fps });\n }\n }\n };\n\n window.__SVELTE_DEVTOOLS__ = __SVELTE_DT;\n\n // Poll state values for timeline\n setInterval(() => { __SVELTE_DT._pollStateValues(); }, 200);\n\n // FPS monitoring\n requestAnimationFrame(() => __SVELTE_DT._fpsLoop());\n setInterval(() => __SVELTE_DT._sampleFps(), 500);\n\n // Phase 3: Capture runtime errors\n window.addEventListener('error', (event) => {\n if (import.meta.hot) {\n import.meta.hot.send('svelte-devtools:runtime-error', {\n message: event.message,\n file: event.filename,\n line: event.lineno,\n column: event.colno,\n stack: event.error?.stack || '',\n timestamp: Date.now(),\n });\n }\n });\n window.addEventListener('unhandledrejection', (event) => {\n if (import.meta.hot) {\n const reason = event.reason;\n import.meta.hot.send('svelte-devtools:runtime-error', {\n message: reason?.message || String(reason),\n stack: reason?.stack || '',\n timestamp: Date.now(),\n });\n }\n });\n\n // Listen for reactive graph requests from server\n if (import.meta.hot) {\n import.meta.hot.on('svelte-devtools:request-reactive-graph', () => {\n __SVELTE_DT.sendReactiveGraph();\n });\n import.meta.hot.on('svelte-devtools:request-state-timeline', () => {\n import.meta.hot.send('svelte-devtools:state-timeline', { changes: __SVELTE_DT._stateTimeline });\n });\n import.meta.hot.on('svelte-devtools:clear-state-timeline', () => {\n __SVELTE_DT.clearStateTimeline();\n });\n }\n}\n";
|
|
18
|
+
export declare const wrapperCode = "\nimport * as __svelte_original from 'svelte/internal/client';\nexport * from 'svelte/internal/client';\n\nfunction __dt() {\n return typeof window !== 'undefined' ? window.__SVELTE_DEVTOOLS__ : null;\n}\n\n// Component ID stack (parallel to runtime._stack but local to wrapper)\nconst __idStack = [];\nfunction __currentId() {\n return __idStack.length > 0 ? __idStack[__idStack.length - 1] : null;\n}\n\n// Tracks the most recently created signal for type determination in tag()\nconst __pendingSignal = { ref: null, type: null };\n\n// Component context -> devtools id (review B1b). A component created later\n// by a block update ({#each}/{#if} after mount) runs outside its parent's\n// push..pop, where the runtime's init stack is empty (the wrapper stack\n// below covers the wrapped block helpers; this covers the rest). Svelte (5.56.8,\n// internal/client) exports active_effect; every effect records the\n// component context it was created in (effect.ctx) and runs with that\n// context restored. The block effect that creates the child belongs to the\n// parent's template, so active_effect.ctx at push time IS the parent's\n// context. Each context is mapped to its id once the component mounted\n// (inside its own deferred effect, where active_effect.ctx is the\n// component's own context). Weak map: no leak, no Svelte state touched.\nconst __idByContext = new WeakMap();\nfunction __parentFromContext() {\n const effect = __svelte_original.active_effect;\n const ctx = effect ? effect.ctx : null;\n return ctx ? __idByContext.get(ctx) : undefined;\n}\n\n// --- Component Lifecycle ---\n//\n// Observer must not break the observed: every devtools side-effect below is\n// wrapped in try/catch so a bug or an out-of-shape runtime cannot tear down\n// the user's app. The original Svelte function is always called, and its\n// return value always returned, regardless of devtools failures.\n\nexport function push() {\n // Parent hint, read before this component's own id exists:\n // 1. the wrapper stack: the component whose init is running (children\n // rendered directly by a template, where active_effect still belongs to\n // an outer block), or the block owner re-pushed by __wrapBlockFn for a\n // later {#each}/{#if}/{#key} update (see Block Attribution below);\n // 2. else the context-derived owner, for re-creation paths the wrapper\n // does not wrap (snippet, svelte:element, ...).\n let parentHint = null;\n try { parentHint = __currentId(); } catch {}\n if (parentHint === null) {\n try { parentHint = __parentFromContext(); } catch {}\n }\n const result = __svelte_original.push.apply(null, arguments);\n try {\n const dt = __dt();\n if (dt) {\n const file = dt._pendingFile || 'Unknown';\n dt._pendingFile = null;\n dt._pendingParent = parentHint;\n const id = dt.register(file);\n __idStack.push(id);\n dt.startInit(id);\n try {\n __svelte_original.user_effect(() => {\n try {\n const own = __svelte_original.active_effect;\n if (own && own.ctx) __idByContext.set(own.ctx, id);\n } catch {}\n return () => { try { dt.unmount(id); } catch {} };\n });\n } catch {}\n }\n } catch {}\n return result;\n}\n\nexport function pop() {\n // Capture devtools errors but ALWAYS forward to the original pop so the\n // Svelte component stack stays balanced.\n try {\n const dt = __dt();\n if (dt && __idStack.length > 0) {\n const id = __idStack.pop();\n try { dt.endInit(id); } catch {}\n try { dt.registered(id); } catch {}\n }\n } catch {}\n return __svelte_original.pop.apply(null, arguments);\n}\n\n// --- Signal Creation (type markers) ---\n\nexport function state() {\n const signal = __svelte_original.state.apply(null, arguments);\n try {\n __pendingSignal.ref = signal;\n __pendingSignal.type = 'state';\n } catch {}\n return signal;\n}\n\nexport function derived() {\n const signal = __svelte_original.derived.apply(null, arguments);\n try {\n __pendingSignal.ref = signal;\n __pendingSignal.type = 'derived';\n } catch {}\n return signal;\n}\n\nexport function proxy() {\n const p = __svelte_original.proxy.apply(null, arguments);\n try {\n __pendingSignal.ref = p;\n __pendingSignal.type = 'proxy';\n } catch {}\n return p;\n}\n\n// --- Signal Tagging (Svelte dev mode) ---\n\nexport function tag(signal, name) {\n const result = __svelte_original.tag.apply(null, arguments);\n try {\n const dt = __dt();\n const cid = __currentId();\n if (dt && cid !== null) {\n const type = (__pendingSignal.ref === signal) ? __pendingSignal.type : 'state';\n if (type === 'derived') {\n dt.trackDerived(signal, name, cid);\n } else {\n dt.trackState(signal, name, cid);\n }\n }\n __pendingSignal.ref = null;\n __pendingSignal.type = null;\n } catch {}\n return result;\n}\n\nexport function tag_proxy(proxy, name) {\n const result = __svelte_original.tag_proxy.apply(null, arguments);\n try {\n const dt = __dt();\n const cid = __currentId();\n if (dt && cid !== null) {\n dt.trackProxy(proxy, name, cid);\n }\n __pendingSignal.ref = null;\n __pendingSignal.type = null;\n } catch {}\n return result;\n}\n\n// --- Effect Tracking ---\n\nexport function user_effect() {\n const result = __svelte_original.user_effect.apply(null, arguments);\n try {\n const dt = __dt();\n const cid = __currentId();\n if (dt && cid !== null) {\n dt._effectCounter = (dt._effectCounter || 0) + 1;\n // Track the effect OBJECT (not the callback). The Svelte runtime\n // populates result.deps with the signals this effect depends on,\n // which is what getReactiveGraph() reads to build edges.\n dt.trackEffect(result, 'effect_' + dt._effectCounter, cid);\n }\n } catch {}\n return result;\n}\n\nexport function user_pre_effect() {\n const result = __svelte_original.user_pre_effect.apply(null, arguments);\n try {\n const dt = __dt();\n const cid = __currentId();\n if (dt && cid !== null) {\n dt._effectCounter = (dt._effectCounter || 0) + 1;\n dt.trackEffect(result, 'effect_pre_' + dt._effectCounter, cid);\n }\n } catch {}\n return result;\n}\n\n// --- Render Profiling ---\n//\n// Svelte 5 has no virtual DOM: the compiler wraps every dynamic part of a\n// template in a template_effect (deferred_template_effect for <svelte:head>\n// <title>). A re-run of such an effect IS the component updating the DOM, so\n// these two functions are the measurement points for renders / render time.\n//\n// The first run of an effect is the mount-time paint, already covered by\n// initTime, so it is skipped (initialRun). One state change re-runs several\n// effects of the same component, so durations are pooled per microtask and\n// recorded once per flush: recordRender() +1, recordRenderTime() the sum.\n\nconst __pendingRenderDurations = new Map();\nlet __renderFlushScheduled = false;\n\nfunction __flushRenderDurations() {\n __renderFlushScheduled = false;\n const dt = __dt();\n for (const [cid, duration] of __pendingRenderDurations) {\n if (dt) {\n try { dt.recordRender(cid); } catch {}\n try { dt.recordRenderTime(cid, duration); } catch {}\n }\n }\n __pendingRenderDurations.clear();\n}\n\nfunction __recordRenderDuration(cid, duration) {\n __pendingRenderDurations.set(cid, (__pendingRenderDurations.get(cid) || 0) + duration);\n if (!__renderFlushScheduled) {\n __renderFlushScheduled = true;\n queueMicrotask(__flushRenderDurations);\n }\n}\n\n// Returns a (possibly new) args array whose first element times each re-run\n// of the effect closure. The owning component is whatever id is on top of\n// __idStack at effect-creation time. User exceptions propagate untouched;\n// only the args are built inside try so the original is called exactly once.\nfunction __wrapTemplateEffect(args) {\n const cid = __currentId();\n const fn = args[0];\n if (cid === null || typeof fn !== 'function') return args;\n const wrapped = Array.prototype.slice.call(args);\n let initialRun = true;\n wrapped[0] = function () {\n if (initialRun) {\n initialRun = false;\n return fn.apply(this, arguments);\n }\n const start = performance.now();\n try {\n return fn.apply(this, arguments);\n } finally {\n try { __recordRenderDuration(cid, performance.now() - start); } catch {}\n }\n };\n return wrapped;\n}\n\nexport function template_effect() {\n let args = arguments;\n try { args = __wrapTemplateEffect(arguments); } catch {}\n return __svelte_original.template_effect.apply(null, args);\n}\n\nexport function deferred_template_effect() {\n let args = arguments;\n try { args = __wrapTemplateEffect(arguments); } catch {}\n return __svelte_original.deferred_template_effect.apply(null, args);\n}\n\n// --- Block Attribution ---\n//\n// Svelte creates render effects synchronously at creation time. At mount this\n// happens during the component function (between push/pop), so __idStack has\n// the owner. But effects for {#each} items added later or re-created {#if}\n// branches are created during a batch flush, when the stack is empty \u2014 they\n// would never be attributed. Block helpers themselves ARE always called\n// during their owner's init (or inside an outer block's callback), so the\n// owner id is on the stack at block-creation time. __wrapBlock captures it\n// and re-pushes it around every function argument the block invokes later,\n// so effects created inside land on the innermost owner. Child components\n// push their own id on top, keeping nested attribution correct.\n\nfunction __wrapBlockFn(fn, cid) {\n return function () {\n let pushed = false;\n try { __idStack.push(cid); pushed = true; } catch {}\n try {\n return fn.apply(this, arguments);\n } finally {\n if (pushed) { try { __idStack.pop(); } catch {} }\n }\n };\n}\n\nfunction __wrapBlock(args) {\n const cid = __currentId();\n if (cid === null) return args;\n const wrapped = Array.prototype.slice.call(args);\n for (let i = 0; i < wrapped.length; i++) {\n if (typeof wrapped[i] === 'function') wrapped[i] = __wrapBlockFn(wrapped[i], cid);\n }\n return wrapped;\n}\n\nexport function each() {\n let args = arguments;\n try { args = __wrapBlock(arguments); } catch {}\n return __svelte_original.each.apply(null, args);\n}\n\nexport function key() {\n let args = arguments;\n try { args = __wrapBlock(arguments); } catch {}\n return __svelte_original.key.apply(null, args);\n}\n\nexport function component() {\n let args = arguments;\n try { args = __wrapBlock(arguments); } catch {}\n return __svelte_original.component.apply(null, args);\n}\n\nexport function boundary() {\n let args = arguments;\n try { args = __wrapBlock(arguments); } catch {}\n return __svelte_original.boundary.apply(null, args);\n}\n\n// 'if' and 'await' are reserved words, so they cannot be declared as\n// function names and are exported via aliases instead.\nfunction __if_block() {\n let args = arguments;\n try { args = __wrapBlock(arguments); } catch {}\n return __svelte_original['if'].apply(null, args);\n}\n\nfunction __await_block() {\n let args = arguments;\n try { args = __wrapBlock(arguments); } catch {}\n return __svelte_original['await'].apply(null, args);\n}\n\nexport { __if_block as if, __await_block as await };\n";
|
|
19
|
+
export declare const runtimeCode = "\nif (typeof window !== 'undefined' && !window.__SVELTE_DEVTOOLS__) {\n const __NO_VALUE = Symbol('no value');\n // State timeline ring (\u00A76.7 C): every read sees at most 500 entries and\n // __TIMELINE_BYTES (also the server's buffer budget). The raw array may\n // hold up to __TIMELINE_TRIM_AT (2 x the 500 cap) between bulk trims.\n const __TIMELINE_BYTES = 4 * 1024 * 1024;\n const __TIMELINE_TRIM_AT = 1000;\n // FNV-1a over UTF-16 code units: cheap change detection for large snapshots.\n const __hash = (str) => {\n let h = 0x811c9dc5;\n for (let i = 0; i < str.length; i++) {\n h ^= str.charCodeAt(i);\n h = Math.imul(h, 0x01000193);\n }\n return (h >>> 0).toString(36);\n };\n const __SVELTE_DT = {\n _nextId: 0,\n _instances: new Map(),\n _stack: [],\n _pendingParent: undefined,\n _pendingFile: null,\n _effectCounter: 0,\n _debounceTimer: null,\n _listeners: new Set(),\n\n // Phase 2: Profiling data\n _profiles: new Map(),\n _initStartTimes: new Map(),\n _profileDebounceTimer: null,\n\n // Phase 2: Reactive graph data\n _reactiveNodes: new Map(),\n _reactiveProxies: new Map(),\n\n // componentId -> Set<nodeId>. Lets unmount purge a component's reactive\n // nodes without scanning every node of the app (was O(components x nodes)).\n _nodesByComponent: new Map(),\n\n // Phase 3: State timeline\n _stateTimeline: [],\n _stateSnapshots: new Map(),\n _timelineDebounceTimer: null,\n // signal (or proxy marker) -> nodeId, set at track time and removed by\n // _forgetNode: a scoped graph resolves dependencies without scanning\n // every node (\u00A76.7 D).\n _idBySignal: new WeakMap(),\n // Polling bookkeeping (\u00A76.7 D): $state node ids in a dense array kept up\n // to date incrementally (_pollIndex: nodeId -> position; removal is\n // swap-with-last), a cursor carried across ticks, nodes found changed but\n // not yet serialized (_pollDirty), and per-node change hints\n // (nodeId -> { ref, wv, nextCheckAt }).\n _pollIds: [],\n _pollIndex: new Map(),\n _pollCursor: 0,\n _pollDirty: new Set(),\n // Full-sweep bookkeeping: how long the last complete pass over all $state\n // nodes took (ms), for disclosure (= the worst-case detection delay).\n _pollSweep: { startedAt: 0, lastMs: null, nodes: 0 },\n _pollMeta: new Map(),\n _deepCredit: 0,\n _lastSnapshotSize: new Map(),\n _timelineSeq: 0,\n _pushedSeq: 0,\n _timelineReset: false,\n _entryInfo: new WeakMap(),\n // Sum of _entryInfo.bytes over _stateTimeline (runtime byte budget).\n _timelineBytes: 0,\n // Disclosure since the last push (\u00A76.7 C): unsent entries the ring removed\n // (by count / by bytes) and changes recorded only as a size summary.\n _timelineDropped: { 'runtime-count': 0, 'runtime-bytes': 0 },\n _valueTooLarge: 0,\n\n // Overview aggregate (\u00A76.7 I/J), O(1) per event: registered nodes per\n // component and type (+ app totals), and per-component activity in a\n // ring of 60 one-second buckets (sampled changes, renders, render ms,\n // plus how long sampling ran in that second).\n _nodeCounts: new Map(),\n _nodeTotals: { state: 0, derived: 0, effect: 0 },\n _activity: [],\n _epoch: Math.random().toString(36).slice(2) + Date.now().toString(36),\n\n // --- Activity subscription (docs/devframe-migration.md \u00A76.3) ---\n // Inactive until the server says a DevTools client (UI tab or MCP lease)\n // is watching: no state polling, FPS rAF loop, or periodic HMR sends.\n _active: false,\n _componentsCost: { ms: 0, bytes: 0 },\n _componentDeltas: false,\n _pendingAdded: new Set(),\n _pendingRemoved: new Set(),\n _lastFullAt: 0,\n _resync: false,\n _profilesCost: { ms: 0, bytes: 0 },\n // = collector LIMITS.renderProfiles (pinned by a unit test): the server\n // keeps only the newest entries, so sending more is wasted bytes.\n _profileCap: 5000,\n _sampling: null,\n _baselining: false,\n // Polled state nodes not observed yet (no seed value). Their first\n // observation is a seed, not a change: no timeline entry, no activity\n // count, no drops \u2014 the old value is unknown, not null (review B-1/B-2).\n _unseeded: new Set(),\n // pendingNodes last sent on a state-timeline message (null = none yet)\n _sentPendingNodes: null,\n _fpsGen: 0,\n\n register(file) {\n const id = this._nextId++;\n // Parent: the wrapper's context-derived hint (components created by a\n // later block update, review B1b) when it names a live instance, else\n // the synchronous init stack (children created during the parent's own\n // init, before the parent is mapped).\n const hint = this._pendingParent;\n this._pendingParent = undefined;\n const parentId =\n hint !== undefined && hint !== null && this._instances.has(hint)\n ? hint\n : this._stack.length > 0 ? this._stack[this._stack.length - 1] : null;\n const name = file.split('/').pop()?.replace('.svelte', '') || 'Unknown';\n // children is a Set so unmounting one row of a 5 000-row list is O(1).\n this._instances.set(id, { id, file, name, parentId, mounted: true, children: new Set() });\n if (parentId !== null) {\n const parent = this._instances.get(parentId);\n if (parent) parent.children.add(id);\n }\n this._stack.push(id);\n if (this._active) this._pendingAdded.add(id);\n this._scheduleUpdate();\n return id;\n },\n\n registered(id) {\n const idx = this._stack.indexOf(id);\n if (idx !== -1) this._stack.splice(idx, 1);\n },\n\n mount(id) {\n const instance = this._instances.get(id);\n if (instance && !instance.mounted) {\n instance.mounted = true;\n if (this._active) this._pendingAdded.add(id);\n this._scheduleUpdate();\n }\n },\n\n unmount(id) {\n const instance = this._instances.get(id);\n if (instance) {\n if (instance.parentId !== null) {\n const parent = this._instances.get(instance.parentId);\n if (parent) parent.children.delete(id);\n }\n this._removeChildren(id);\n this._cleanupComponent(id);\n }\n this._scheduleUpdate();\n this._scheduleProfileUpdate();\n },\n\n // Remounts get a fresh id, so entries keyed by a dead id would otherwise\n // survive forever \u2014 every per-component map must be purged here.\n _cleanupComponent(id) {\n // Delta bookkeeping: an id added and removed between two pushes was\n // never sent, so it cancels out.\n if (this._active && !this._pendingAdded.delete(id)) this._pendingRemoved.add(id);\n this._cleanupReactiveNodes(id);\n this._instances.delete(id);\n this._profiles.delete(id);\n this._initStartTimes.delete(id);\n },\n\n _cleanupReactiveNodes(componentId) {\n const nodeIds = this._nodesByComponent.get(componentId);\n if (!nodeIds) return;\n this._nodesByComponent.delete(componentId);\n for (const nodeId of nodeIds) this._forgetNode(nodeId);\n },\n\n _forgetNode(nodeId) {\n const entry = this._reactiveNodes.get(nodeId);\n if (entry) this._countNode(entry.meta.componentId, entry.meta.type, -1);\n const signal = entry && entry.signal.deref();\n if (signal && this._idBySignal.get(signal) === nodeId) this._idBySignal.delete(signal);\n this._pollRemove(nodeId);\n this._pollDirty.delete(nodeId);\n this._reactiveNodes.delete(nodeId);\n this._reactiveProxies.delete(nodeId);\n this._stateSnapshots.delete(nodeId);\n if (this._stateSnapshotStrs) this._stateSnapshotStrs.delete(nodeId);\n this._pollMeta.delete(nodeId);\n this._lastSnapshotSize.delete(nodeId);\n },\n\n _indexNode(nodeId, componentId) {\n let set = this._nodesByComponent.get(componentId);\n if (!set) {\n set = new Set();\n this._nodesByComponent.set(componentId, set);\n }\n set.add(nodeId);\n },\n\n _pollAdd(nodeId) {\n if (this._pollIndex.has(nodeId)) return;\n if (!this._stateSnapshots.has(nodeId)) this._unseeded.add(nodeId);\n this._pollIndex.set(nodeId, this._pollIds.length);\n this._pollIds.push(nodeId);\n },\n\n // O(1): the last id takes the removed slot. If that slot is behind the\n // cursor, the moved id is visited in the next sweep instead of this one\n // (still visited within a bounded number of ticks).\n // Before a track* call replaces/creates nodeId: move the count from the\n // previous registration (same name re-tracked) to the new one.\n _trackNodeCount(nodeId, componentId, type) {\n const prev = this._reactiveNodes.get(nodeId);\n if (prev) this._countNode(prev.meta.componentId, prev.meta.type, -1);\n this._countNode(componentId, type, 1);\n },\n\n _countNode(componentId, type, d) {\n const kind = type === 'derived' || type === 'effect' ? type : 'state';\n let c = this._nodeCounts.get(componentId);\n if (!c) {\n if (d < 0) return;\n c = { state: 0, derived: 0, effect: 0 };\n this._nodeCounts.set(componentId, c);\n }\n c[kind] += d;\n this._nodeTotals[kind] += d;\n if (c.state + c.derived + c.effect <= 0) this._nodeCounts.delete(componentId);\n },\n\n _bucket() {\n const sec = Math.floor(performance.now() / 1000);\n const slot = ((sec % 60) + 60) % 60;\n let b = this._activity[slot];\n if (!b || b.second !== sec) {\n b = { second: sec, sampledMs: 0, rows: new Map() };\n this._activity[slot] = b;\n }\n return b;\n },\n\n _activityRow(componentId) {\n const rows = this._bucket().rows;\n let r = rows.get(componentId);\n if (!r) {\n r = { changes: 0, renders: 0, renderMs: 0 };\n rows.set(componentId, r);\n }\n return r;\n },\n\n _pollRemove(nodeId) {\n const i = this._pollIndex.get(nodeId);\n if (i === undefined) return;\n this._unseeded.delete(nodeId);\n const ids = this._pollIds;\n const last = ids.pop();\n this._pollIndex.delete(nodeId);\n if (i < ids.length) {\n ids[i] = last;\n this._pollIndex.set(last, i);\n }\n },\n\n _removeChildren(parentId) {\n const parent = this._instances.get(parentId);\n if (!parent) return;\n for (const childId of [...parent.children]) {\n this._removeChildren(childId);\n this._cleanupComponent(childId);\n }\n },\n\n // Full-list pushes are throttled, not debounced: a pending timer is not\n // reset, so continuous churn still produces updates (a debounce would\n // starve), and the interval adapts to the last push's cost so a 10 000+\n // instance app can't spend its main thread serializing (review P3):\n // max(min, 20 x last send ms, 1 s when the last payload exceeded ~1 MB).\n _sendInterval(cost, min) {\n return Math.max(min, 20 * cost.ms, cost.bytes > 1000000 ? 1000 : 0);\n },\n\n _scheduleUpdate() {\n if (!this._active || this._debounceTimer) return;\n this._debounceTimer = setTimeout(() => {\n this._debounceTimer = null;\n this._sendUpdate();\n }, this._sendInterval(this._componentsCost, 100));\n },\n\n // components push (\u00A76.5). Full form { epoch, components } replaces the\n // server's tree for this epoch: sent on activation, when the server does\n // not advertise componentDeltas, as a periodic checkpoint, or when the\n // delta would be larger than the tree. Otherwise the delta form\n // { epoch, added, removed } (removed ids first, \u2264 2 000 entries per\n // message, added in registration order = parents first).\n _sendUpdate(forceFull) {\n // Nobody watching: keep the registry, skip the O(N) payload. A full\n // snapshot is sent on activation (\u00A76.3).\n if (!this._active) return;\n const t0 = performance.now();\n const CHUNK = 2000;\n const CHECKPOINT_MS = 30000;\n const pending = this._pendingAdded.size + this._pendingRemoved.size;\n const full =\n forceFull ||\n !this._componentDeltas ||\n t0 - this._lastFullAt >= CHECKPOINT_MS ||\n pending > this._instances.size;\n const toEntry = (instance) => ({\n id: instance.id,\n file: instance.file,\n name: instance.name,\n parentId: instance.parentId,\n mounted: instance.mounted,\n });\n let bytes = 0;\n const msgs = [];\n if (full) {\n const components = [];\n for (const [, instance] of this._instances) {\n if (instance.mounted) {\n components.push(toEntry(instance));\n bytes += instance.file.length + instance.name.length + 72;\n }\n }\n msgs.push({ epoch: this._epoch, reset: true, components });\n this._lastFullAt = t0;\n } else if (pending > 0) {\n const removed = [...this._pendingRemoved];\n const added = [];\n for (const id of this._pendingAdded) {\n const instance = this._instances.get(id);\n if (instance && instance.mounted) {\n added.push(toEntry(instance));\n bytes += instance.file.length + instance.name.length + 72;\n }\n }\n bytes += removed.length * 8;\n for (let r = 0; r < removed.length; r += CHUNK) {\n msgs.push({ epoch: this._epoch, added: [], removed: removed.slice(r, r + CHUNK) });\n }\n for (let a = 0; a < added.length; a += CHUNK) {\n msgs.push({ epoch: this._epoch, added: added.slice(a, a + CHUNK), removed: [] });\n }\n }\n this._pendingAdded.clear();\n this._pendingRemoved.clear();\n if (import.meta.hot) {\n for (const msg of msgs) import.meta.hot.send('svelte-devtools:components', msg);\n }\n this._componentsCost = { ms: performance.now() - t0, bytes };\n },\n\n getTree() {\n const components = [];\n for (const [, instance] of this._instances) {\n if (instance.mounted) {\n components.push({\n id: instance.id,\n file: instance.file,\n name: instance.name,\n parentId: instance.parentId,\n mounted: instance.mounted,\n });\n }\n }\n return components;\n },\n\n // --- Phase 2: Render Profiling ---\n\n startInit(id) {\n this._initStartTimes.set(id, performance.now());\n },\n\n endInit(id) {\n const start = this._initStartTimes.get(id);\n if (start === undefined) return;\n const initTime = performance.now() - start;\n this._initStartTimes.delete(id);\n const instance = this._instances.get(id);\n if (!instance) return;\n this._profiles.set(id, {\n componentId: id,\n file: instance.file,\n name: instance.name,\n initTime,\n renderCount: 0,\n totalRenderTime: 0,\n lastRenderTime: 0,\n lastRenderAt: Date.now(),\n });\n this._scheduleProfileUpdate();\n },\n\n recordRender(id) {\n const profile = this._profiles.get(id);\n if (!profile) {\n const instance = this._instances.get(id);\n if (!instance) return;\n this._profiles.set(id, {\n componentId: id,\n file: instance.file,\n name: instance.name,\n initTime: 0,\n renderCount: 1,\n totalRenderTime: 0,\n lastRenderTime: 0,\n lastRenderAt: Date.now(),\n });\n } else {\n profile.renderCount++;\n profile.lastRenderAt = Date.now();\n }\n this._activityRow(id).renders++;\n this._scheduleProfileUpdate();\n },\n\n recordRenderTime(id, duration) {\n const profile = this._profiles.get(id);\n if (profile) {\n profile.totalRenderTime += duration;\n profile.lastRenderTime = duration;\n this._activityRow(id).renderMs += duration;\n }\n },\n\n // Profiles push (review P-1 Option A; contract in\n // docs/performance-status.md). Throttled like components, but the floor\n // grows with the payload: every push carries the whole retained map, so\n // a large app coalesces bursts instead of resending ~1 MB twice a second.\n // Hard cap 2 s (contract R6): a slow serialization must not stretch the\n // latency beyond it.\n _profileInterval() {\n const cost = this._profilesCost;\n return Math.min(\n 2000,\n Math.max(500, 20 * cost.ms, cost.bytes > 1000000 ? 2000 : cost.bytes > 250000 ? 1000 : 0),\n );\n },\n\n _scheduleProfileUpdate() {\n if (!this._active || this._profileDebounceTimer) return;\n this._profileDebounceTimer = setTimeout(() => {\n this._profileDebounceTimer = null;\n this._sendProfileUpdate();\n }, this._profileInterval());\n },\n\n // Also the forced path (activation / resync): a pending throttled push is\n // cancelled so the same state is not sent twice.\n _sendProfileUpdate() {\n if (this._profileDebounceTimer) {\n clearTimeout(this._profileDebounceTimer);\n this._profileDebounceTimer = null;\n }\n if (!this._active) return;\n const t0 = performance.now();\n const all = Array.from(this._profiles.values());\n // Exactly what the collector retains: its tail, in the same order.\n const profiles = all.length > this._profileCap ? all.slice(all.length - this._profileCap) : all;\n if (import.meta.hot) {\n // total = all retained profiles; the message carries the newest-mounted\n // tail (\u00A76.7 J IA1 capture policy 'newest-mounted').\n import.meta.hot.send('svelte-devtools:profiles', { epoch: this._epoch, profiles, total: all.length });\n }\n // Payload size from a small sample (entries differ mainly by file path).\n const k = Math.min(8, profiles.length);\n let sampled = 0;\n for (let i = 0; i < k; i++) sampled += JSON.stringify(profiles[i]).length;\n const bytes = k > 0 ? Math.round((sampled / k) * profiles.length) : 0;\n this._profilesCost = { ms: performance.now() - t0, bytes };\n },\n\n getProfiles() {\n return Array.from(this._profiles.values());\n },\n\n resetProfiles() {\n this._profiles.clear();\n this._scheduleProfileUpdate();\n },\n\n // --- Phase 2: Reactive Graph Tracking ---\n\n trackState(signal, name, componentId) {\n const instance = this._instances.get(componentId);\n const nodeId = componentId + ':' + name;\n this._trackNodeCount(nodeId, componentId, 'state');\n this._reactiveNodes.set(nodeId, {\n signal: new WeakRef(signal),\n meta: { id: nodeId, type: 'state', name, componentId, componentFile: instance ? instance.file : '' }\n });\n this._idBySignal.set(signal, nodeId);\n this._indexNode(nodeId, componentId);\n this._pollAdd(nodeId);\n },\n\n trackProxy(proxy, name, componentId) {\n const instance = this._instances.get(componentId);\n const nodeId = componentId + ':' + name;\n this._reactiveProxies.set(nodeId, new WeakRef(proxy));\n // The proxy itself stays weakly held (_reactiveProxies). The marker is\n // held strongly so the node is not GC-dropped while the component lives;\n // _cleanupComponent removes it on unmount.\n const marker = { v: '(proxy)', _isProxy: true };\n this._trackNodeCount(nodeId, componentId, 'state');\n this._reactiveNodes.set(nodeId, {\n signal: { deref: () => marker },\n meta: { id: nodeId, type: 'state', name, componentId, componentFile: instance ? instance.file : '' }\n });\n this._idBySignal.set(marker, nodeId);\n this._indexNode(nodeId, componentId);\n this._pollAdd(nodeId);\n },\n\n trackDerived(signal, name, componentId) {\n const instance = this._instances.get(componentId);\n const nodeId = componentId + ':' + name;\n this._trackNodeCount(nodeId, componentId, 'derived');\n this._reactiveNodes.set(nodeId, {\n signal: new WeakRef(signal),\n meta: { id: nodeId, type: 'derived', name, componentId, componentFile: instance ? instance.file : '' }\n });\n this._idBySignal.set(signal, nodeId);\n this._indexNode(nodeId, componentId);\n },\n\n trackEffect(effect, name, componentId) {\n const instance = this._instances.get(componentId);\n const nodeId = componentId + ':' + name;\n const target = effect || { v: undefined, _isEffect: true };\n this._trackNodeCount(nodeId, componentId, 'effect');\n this._reactiveNodes.set(nodeId, {\n signal: new WeakRef(target),\n meta: { id: nodeId, type: 'effect', name, componentId, componentFile: instance ? instance.file : '' }\n });\n this._idBySignal.set(target, nodeId);\n this._indexNode(nodeId, componentId);\n return effect;\n },\n\n _graphNode(nodeId, entry, signal) {\n const node = { ...entry.meta };\n if (signal._isProxy) {\n const proxy = this._reactiveProxies.get(nodeId)?.deref();\n if (proxy) {\n try {\n node.value = Array.isArray(proxy) ? '[' + proxy.length + ']' : '{' + Object.keys(proxy).length + '}';\n } catch { node.value = '(proxy)'; }\n }\n } else if (node.type !== 'effect' && signal.v !== undefined && typeof signal.v !== 'symbol') {\n try {\n const v = signal.v;\n if (typeof v === 'number' || typeof v === 'string' || typeof v === 'boolean' || v === null) {\n node.value = v;\n } else {\n node.value = '(object)';\n }\n } catch { /* ignore */ }\n }\n return node;\n },\n\n // Reactive graph (\u00A76.7 A/D). Caps are applied while building: past\n // maxNodes no node objects are created and only edges whose endpoints\n // are both included are emitted (the rest are counted in edgesOmitted).\n // - scoped (componentId): that component's nodes plus the tracked nodes\n // directly connected to them (via deps / reactions through untracked\n // intermediates). Dependencies are resolved through _idBySignal, so\n // the cost is O(scope + neighbours), not O(all nodes). The walk is\n // completed even past the cap, so total.nodes/edges are exact.\n // - global: nodes in registration order up to maxNodes ('global-head'),\n // with their incoming dependency edges. Nodes past the cap are not\n // dereferenced; total.nodes is the registered count (may include\n // nodes whose signal was already collected, nodesKind 'registered')\n // and total.edges is null when the walk stopped early (not guessed).\n getReactiveGraph(componentId, caps) {\n const scoped = componentId !== undefined && componentId !== null;\n const maxNodes = this._graphCap(caps && caps.maxNodes, 5000);\n const maxEdges = this._graphCap(caps && caps.maxEdges, 20000);\n const live = new Map();\n // Live signal of a tracked node, or null (forgets dead / unmounted nodes).\n const liveSignal = (nodeId) => {\n if (live.has(nodeId)) return live.get(nodeId);\n const entry = this._reactiveNodes.get(nodeId);\n let signal = entry ? entry.signal.deref() : null;\n if (entry && (!signal || !this._instances.has(entry.meta.componentId))) {\n this._forgetNode(nodeId);\n signal = null;\n }\n live.set(nodeId, signal || null);\n return signal || null;\n };\n const idOf = (dep) => {\n const id = this._idBySignal.get(dep);\n return id !== undefined && liveSignal(id) === dep ? id : null;\n };\n\n const included = new Set();\n const seen = new Set();\n const edges = [];\n const edgeSet = new Set();\n let edgesOmitted = 0;\n let truncated = false;\n const see = (id) => {\n seen.add(id);\n if (!included.has(id) && included.size < maxNodes) included.add(id);\n return included.has(id);\n };\n const addEdge = (from, to) => {\n const key = from + '>' + to;\n if (from === to || edgeSet.has(key)) return;\n edgeSet.add(key);\n const inFrom = see(from);\n const inTo = see(to);\n if (inFrom && inTo && edges.length < maxEdges) edges.push({ from, to });\n else edgesOmitted++;\n };\n // Walk a signal's neighbours (deps or reactions) through untracked\n // intermediates until tracked nodes are reached.\n const walk = (start, field, onHit) => {\n if (!start) return;\n const visited = new Set();\n const queue = [...start];\n // Index cursor instead of queue.shift(): shift is O(queue) per pop.\n for (let qi = 0; qi < queue.length; qi++) {\n const dep = queue[qi];\n if (!dep || visited.has(dep)) continue;\n visited.add(dep);\n const id = idOf(dep);\n if (id) onHit(id);\n else if (dep[field]) for (const d of dep[field]) queue.push(d);\n }\n };\n\n let totalNodes = 0;\n let totalEdges = null;\n if (scoped) {\n for (const nodeId of [...(this._nodesByComponent.get(componentId) || [])]) {\n const signal = liveSignal(nodeId);\n if (!signal) continue;\n see(nodeId);\n walk(signal.deps, 'deps', (depId) => addEdge(depId, nodeId));\n walk(signal.reactions, 'reactions', (rId) => addEdge(nodeId, rId));\n }\n totalNodes = seen.size;\n totalEdges = edgeSet.size;\n truncated = included.size < seen.size || edgesOmitted > 0;\n } else {\n let complete = true;\n for (const nodeId of [...this._reactiveNodes.keys()]) {\n if (included.size >= maxNodes && !included.has(nodeId)) {\n complete = false;\n break;\n }\n const signal = liveSignal(nodeId);\n if (!signal) continue;\n see(nodeId);\n walk(signal.deps, 'deps', (depId) => addEdge(depId, nodeId));\n }\n // registered count, minus nodes found dead while building\n totalNodes = this._reactiveNodes.size;\n totalEdges = complete ? edgeSet.size : null;\n truncated = !complete || edgesOmitted > 0;\n }\n\n const nodes = [];\n for (const nodeId of included) {\n const signal = liveSignal(nodeId);\n if (signal) nodes.push(this._graphNode(nodeId, this._reactiveNodes.get(nodeId), signal));\n }\n return {\n scope: scoped ? componentId : null,\n nodes,\n edges,\n total: { nodes: totalNodes, nodesKind: 'registered', edges: totalEdges },\n truncated,\n edgesOmitted,\n policy: scoped ? 'scoped' : 'global-head',\n };\n },\n\n _graphCap(value, max) {\n const n = Math.floor(Number(value));\n return Number.isFinite(n) && n >= 1 ? Math.min(n, max) : max;\n },\n\n // Request (\u00A76.7 A + review M1): { requestId?, epoch?, componentId?,\n // maxNodes?, maxEdges? }. A request for another page load (epoch) is not\n // answered by this one; the reply echoes requestId and epoch. An old\n // server sends {} \u2192 global graph with the default caps (= its LIMITS).\n sendReactiveGraph(request) {\n const req = request || {};\n if (req.epoch !== undefined && req.epoch !== null && req.epoch !== this._epoch) return;\n const cid = typeof req.componentId === 'number' ? req.componentId : null;\n const graph = this.getReactiveGraph(cid, { maxNodes: req.maxNodes, maxEdges: req.maxEdges });\n graph.epoch = this._epoch;\n // freshness (review): when this answer was built; caches keep it\n graph.computedAt = Date.now();\n if (req.requestId !== undefined) graph.requestId = req.requestId;\n if (import.meta.hot) {\n import.meta.hot.send('svelte-devtools:reactive-graph', graph);\n }\n },\n\n // Overview aggregate (\u00A76.7 I/J). Built from the activity buckets of the\n // window (whole seconds: the current partial second plus the previous\n // ones, ceil(windowMs / 1000) buckets) \u2014 cost O(components active in the\n // window), no graph, no node scan. Rows rank by sampled changes, then\n // renders, then render ms; rows + other = total (components and nodes).\n getReactiveSummary(opts) {\n const o = opts || {};\n const clampInt = (v, lo, hi, d) => {\n const n = Math.floor(Number(v));\n return Number.isFinite(n) ? Math.min(hi, Math.max(lo, n)) : d;\n };\n const topK = clampInt(o.topK, 1, 200, 50);\n const windowMs = clampInt(o.windowMs, 1000, 60000, 10000);\n const nowSec = Math.floor(performance.now() / 1000);\n const agg = new Map();\n let sampledMs = 0;\n for (let k = 0; k < Math.ceil(windowMs / 1000); k++) {\n const sec = nowSec - k;\n const b = this._activity[((sec % 60) + 60) % 60];\n if (!b || b.second !== sec) continue;\n sampledMs += b.sampledMs;\n for (const [cid, r] of b.rows) {\n if (!this._instances.has(cid)) continue;\n let a = agg.get(cid);\n if (!a) {\n a = { changes: 0, renders: 0, renderMs: 0 };\n agg.set(cid, a);\n }\n a.changes += r.changes;\n a.renders += r.renders;\n a.renderMs += r.renderMs;\n }\n }\n const ranked = [...agg].sort(\n (x, y) =>\n y[1].changes - x[1].changes || y[1].renders - x[1].renders || y[1].renderMs - x[1].renderMs || x[0] - y[0],\n );\n let rowNodes = 0;\n const rows = ranked.slice(0, topK).map(([cid, a]) => {\n const n = this._nodeCounts.get(cid) || { state: 0, derived: 0, effect: 0 };\n rowNodes += n.state + n.derived + n.effect;\n return {\n componentId: cid,\n file: this._instances.get(cid).file,\n nodes: { state: n.state, derived: n.derived, effect: n.effect },\n changes: a.changes,\n renders: a.renders,\n renderMs: Math.round(a.renderMs * 1000) / 1000,\n };\n });\n const t = this._nodeTotals;\n const until = Date.now();\n return {\n epoch: this._epoch,\n window: { ms: windowMs, since: until - windowMs, until, sampledActiveMs: Math.min(windowMs, sampledMs) },\n policy: 'sampled-200ms',\n coverage: 'component-init',\n components: { total: this._instances.size, withActivity: agg.size },\n rows,\n other: { components: this._instances.size - rows.length, nodes: t.state + t.derived + t.effect - rowNodes },\n truncated: agg.size > topK,\n capabilities: { valueInspection: false, signalHistory: false, writeCause: false },\n // Seed coverage: changes of pending nodes are not observable yet.\n baseline: this._baselineInfo(),\n };\n },\n\n // Request { requestId?, epoch?, topK?, windowMs? } (same correlation as\n // the graph, review M1).\n sendReactiveSummary(request) {\n const req = request || {};\n if (req.epoch !== undefined && req.epoch !== null && req.epoch !== this._epoch) return;\n const summary = this.getReactiveSummary(req);\n if (req.requestId !== undefined) summary.requestId = req.requestId;\n if (import.meta.hot) {\n import.meta.hot.send('svelte-devtools:reactive-summary', summary);\n }\n },\n\n // --- Phase 3: State Timeline ---\n\n // Current value of a $state node: the signal value, or for tag_proxy\n // nodes (non-reassigned object/array state) the live proxy itself.\n // Returns a sentinel when the value can't be read.\n _readStateValue(nodeId, signal) {\n if (signal._isProxy) {\n const proxy = this._reactiveProxies.get(nodeId)?.deref();\n return proxy === undefined ? __NO_VALUE : proxy;\n }\n const v = signal.v;\n // Svelte's UNINITIALIZED marker is a symbol; functions aren't state data.\n return typeof v === 'symbol' || typeof v === 'function' ? __NO_VALUE : v;\n },\n\n // Runs every 200 ms in the user's app, so it must stay cheap regardless of\n // how much state the app holds (\u00A76.7 D).\n // 1. Hint pass, time-sliced: at most HINT_NODES $state nodes per tick,\n // continuing from a cursor carried across ticks, so a full sweep over\n // n nodes takes ceil(n / HINT_NODES) ticks (_pollSweep.lastMs is the\n // measured sweep time = worst-case detection delay). Per node O(1):\n // primitives are compared directly and recorded at once; objects\n // whose reference or Svelte write version (wv) changed are added to\n // _pollDirty, which persists across ticks until serialized.\n // 2. Serialization work is limited to BUDGET_MS per tick (dirty nodes\n // carry over, nothing is dropped).\n // 3. In-place deep mutations (proxies don't bump an outer wv) are found by\n // time-based re-checks: each object is re-verified no earlier than\n // nextCheckAt = now + max(RECHECK_MS, 50 x its serialization cost),\n // paid from a DEEP_MS-per-tick credit, so the amortized cost is bounded\n // however many large states the app holds.\n // 4. Values serializing above MAX_SNAPSHOT_CHARS are kept as length+hash\n // (not the string) and appear in the timeline as a summary.\n // Writes between two visits of a node are observed as one change (the\n // timeline is a sample, policy 'sampled-200ms'; not counted as dropped).\n _pollStateValues(baseline) {\n const HINT_NODES = 4096;\n const BUDGET_MS = 2;\n const DEEP_MS = 0.5;\n const now = performance.now();\n if (!baseline) {\n this._deepCredit = Math.min(10 * DEEP_MS, (this._deepCredit || 0) + DEEP_MS);\n // this tick samples 200 ms of wall time (window.sampledActiveMs)\n const bucket = this._bucket();\n bucket.sampledMs = Math.min(1000, bucket.sampledMs + 200);\n }\n const ids = this._pollIds;\n const due = [];\n const limit = Math.min(HINT_NODES, ids.length);\n for (let visited = 0; visited < limit && ids.length > 0; visited++) {\n if (this._pollCursor >= ids.length) {\n this._pollCursor = 0;\n if (!baseline) {\n if (this._pollSweep.startedAt) this._pollSweep.lastMs = now - this._pollSweep.startedAt;\n this._pollSweep.startedAt = now;\n this._pollSweep.nodes = ids.length;\n }\n }\n if (!baseline && !this._pollSweep.startedAt) this._pollSweep.startedAt = now;\n const nodeId = ids[this._pollCursor];\n const entry = this._reactiveNodes.get(nodeId);\n if (!entry || entry.meta.type !== 'state') {\n // re-tracked as another type: drop from the poll list (slot reused)\n this._pollRemove(nodeId);\n continue;\n }\n if (!this._instances.has(entry.meta.componentId)) {\n this._forgetNode(nodeId); // swaps the last id into this slot\n continue;\n }\n this._pollCursor++;\n const signal = entry.signal.deref();\n const value = signal ? this._readStateValue(nodeId, signal) : __NO_VALUE;\n if (value === __NO_VALUE) {\n // nothing readable (collected, uninitialized, function): not pending\n this._unseeded.delete(nodeId);\n continue;\n }\n if (value === null || typeof value !== 'object') {\n const had = this._stateSnapshots.has(nodeId);\n const prev = this._stateSnapshots.get(nodeId);\n if (had && Object.is(prev, value)) continue;\n // Forget the object key so object -> primitive -> same object again\n // is still recorded as a change.\n if (this._stateSnapshotStrs) this._stateSnapshotStrs.delete(nodeId);\n this._pollDirty.delete(nodeId);\n if (!had) {\n // first observation: seed only\n this._stateSnapshots.set(nodeId, value);\n this._unseeded.delete(nodeId);\n continue;\n }\n this._recordChange(nodeId, entry, prev, value, typeof value === 'string' ? 2 * value.length : 64);\n continue;\n }\n let meta = this._pollMeta.get(nodeId);\n if (!meta) {\n meta = { ref: undefined, wv: undefined, nextCheckAt: 0 };\n this._pollMeta.set(nodeId, meta);\n }\n if (meta.ref !== value || meta.wv !== signal.wv) this._pollDirty.add(nodeId);\n else if (now >= meta.nextCheckAt) due.push(nodeId);\n }\n const start = performance.now();\n for (const nodeId of this._pollDirty) {\n if (performance.now() - start > BUDGET_MS) return;\n this._pollDirty.delete(nodeId);\n this._serializeNode(nodeId, false);\n }\n for (const nodeId of due) {\n if (this._deepCredit <= 0 || performance.now() - start > BUDGET_MS) return;\n this._serializeNode(nodeId, true);\n }\n },\n\n // Activation (late consumer): nodes registered while inactive have no\n // seed yet, so a write before their first visit could not be seen as a\n // change. The first slice is seeded at activation instead of 200 ms later:\n // ONE normal pass (<= HINT_NODES nodes, the per-tick serialization budget;\n // the rest follow on the usual ticks). Seeds record nothing; readiness is\n // disclosed in the summary as baseline { complete, pendingNodes }.\n _pollBaseline() {\n this._baselining = true;\n try {\n this._pollStateValues(true);\n } catch { /* never break activation */ } finally {\n this._baselining = false;\n }\n },\n\n _serializeNode(nodeId, deep) {\n const MAX_SNAPSHOT_CHARS = 32768;\n const RECHECK_MS = 1000;\n const entry = this._reactiveNodes.get(nodeId);\n const meta = this._pollMeta.get(nodeId);\n if (!entry || !meta) return;\n const signal = entry.signal.deref();\n if (!signal) return;\n const value = this._readStateValue(nodeId, signal);\n if (value === __NO_VALUE || value === null || typeof value !== 'object') return;\n try {\n const t0 = performance.now();\n const str = JSON.stringify(value);\n const cost = performance.now() - t0;\n if (deep) this._deepCredit -= cost;\n meta.ref = value;\n meta.wv = signal.wv;\n meta.nextCheckAt = t0 + Math.max(RECHECK_MS, 50 * cost);\n // Small values keep the string (exact compare); large ones only a\n // length+hash so a 5 MB array doesn't pin 5 MB per node.\n const key = str === undefined ? '' : str.length > MAX_SNAPSHOT_CHARS ? str.length + ':' + __hash(str) : str;\n if (!this._stateSnapshotStrs) this._stateSnapshotStrs = new Map();\n const hadKey = this._stateSnapshotStrs.has(nodeId);\n if (hadKey && this._stateSnapshotStrs.get(nodeId) === key) return;\n this._stateSnapshotStrs.set(nodeId, key);\n const tooLarge = str !== undefined && str.length > MAX_SNAPSHOT_CHARS;\n const snapshot =\n str === undefined\n ? null\n : tooLarge\n ? '(object: ' + str.length + ' chars, too large to snapshot)'\n : JSON.parse(str);\n const size = typeof snapshot === 'string' ? 64 : str.length;\n if (!this._stateSnapshots.has(nodeId)) {\n // first observation: seed only (no entry, no counters)\n this._stateSnapshots.set(nodeId, snapshot);\n this._lastSnapshotSize.set(nodeId, size);\n this._unseeded.delete(nodeId);\n return;\n }\n if (tooLarge) this._valueTooLarge++;\n const prev = this._stateSnapshots.get(nodeId);\n this._recordChange(nodeId, entry, prev, snapshot, size + (this._lastSnapshotSize.get(nodeId) || 0));\n this._lastSnapshotSize.set(nodeId, size);\n } catch {\n // non-serializable (cycles, BigInt): never observable, so not pending\n this._unseeded.delete(nodeId);\n }\n },\n\n _recordChange(nodeId, entry, oldValue, newValue, approxBytes) {\n const change = {\n id: nodeId,\n name: entry.meta.name,\n componentFile: entry.meta.componentFile,\n oldValue,\n newValue,\n timestamp: Date.now(),\n };\n const bytes = (approxBytes || 64) + 192;\n // seq stays runtime-internal: the server assigns its own seq (\u00A76.4).\n this._entryInfo.set(change, { seq: ++this._timelineSeq, bytes });\n this._stateTimeline.push(change);\n this._timelineBytes += bytes;\n // sampled change count (\u00A76.7 J IA2): independent of the ring\n this._activityRow(entry.meta.componentId).changes++;\n this._stateSnapshots.set(nodeId, newValue);\n // The ring (_trimTimeline) is applied in bulk, not with one splice per\n // change (a poll tick can record thousands): before every read (each\n // push \u2014 with a hot channel and an active consumer, at least every 200\n // changes \u2014 the full snapshot, getStateTimeline() and clear), and here\n // once more than __TIMELINE_TRIM_AT entries are held or the byte budget\n // is exceeded, which bounds memory when nothing reads or pushes.\n if (this._stateTimeline.length > __TIMELINE_TRIM_AT || this._timelineBytes > __TIMELINE_BYTES) {\n this._trimTimeline();\n }\n this._scheduleTimelineUpdate();\n },\n\n // Ring: at most 500 entries and __TIMELINE_BYTES (the newest entry is\n // always kept). Removed entries that were never pushed are disclosed\n // as dropped (\u00A76.7 C); pushed ones are already on the server.\n _trimTimeline() {\n const all = this._stateTimeline;\n let cut = 0;\n let bytesLeft = this._timelineBytes;\n while (cut < all.length - 1 && (all.length - cut > 500 || bytesLeft > __TIMELINE_BYTES)) {\n const info = this._entryInfo.get(all[cut]);\n const reason = all.length - cut > 500 ? 'runtime-count' : 'runtime-bytes';\n if (info.seq > this._pushedSeq) this._timelineDropped[reason]++;\n bytesLeft -= info.bytes;\n cut++;\n }\n if (cut > 0) {\n all.splice(0, cut);\n this._timelineBytes = bytesLeft;\n }\n },\n\n // Push (\u00A76.7 C): a throttle, not a debounce \u2014 a pending timer is never\n // re-armed, so unsent entries leave within 300 ms of the first one even\n // under continuous change \u2014 plus an immediate flush once 200 entries\n // (one message) are unsent, since one poll tick can record thousands.\n _scheduleTimelineUpdate() {\n if (!this._active || this._baselining) return;\n if (this._timelineSeq - this._pushedSeq >= 200) {\n this._flushTimeline();\n return;\n }\n if (this._timelineDebounceTimer) return;\n this._timelineDebounceTimer = setTimeout(() => {\n this._timelineDebounceTimer = null;\n this._flushTimeline();\n }, 300);\n },\n\n _flushTimeline() {\n if (this._timelineDebounceTimer) {\n clearTimeout(this._timelineDebounceTimer);\n this._timelineDebounceTimer = null;\n }\n if (!import.meta.hot) return;\n for (const msg of this._timelineDeltas()) {\n import.meta.hot.send('svelte-devtools:state-timeline', msg);\n }\n },\n\n // Disclosure counters since the previous push, attached to (and reset\n // by) the next message: dropped [{ reason, count }] and valueTooLarge.\n _takeTimelineDisclosure(msg) {\n msg.baseline = this._baselineInfo();\n this._sentPendingNodes = msg.baseline.pendingNodes;\n const dropped = [];\n for (const reason of ['runtime-count', 'runtime-bytes']) {\n const count = this._timelineDropped[reason];\n if (count > 0) dropped.push({ reason, count });\n this._timelineDropped[reason] = 0;\n }\n if (dropped.length) msg.dropped = dropped;\n if (this._valueTooLarge > 0) msg.valueTooLarge = this._valueTooLarge;\n this._valueTooLarge = 0;\n return msg;\n },\n\n // Seed coverage (review B-2), on every state-timeline message and the\n // summary: pendingNodes = polled $state nodes without a first sample;\n // their writes are not observable yet.\n _baselineInfo() {\n return { complete: this._unseeded.size === 0, pendingNodes: this._unseeded.size };\n },\n\n _hasTimelineDisclosure() {\n return (\n this._sentPendingNodes !== this._unseeded.size ||\n this._timelineDropped['runtime-count'] > 0 ||\n this._timelineDropped['runtime-bytes'] > 0 ||\n this._valueTooLarge > 0\n );\n },\n\n // Delta push (docs/devframe-migration.md \u00A76.4): only entries recorded\n // since the previous push, \u2264 200 per message, tagged with a per-page-load\n // epoch; reset after clearStateTimeline(). A message with no changes is\n // still sent when there is something to disclose (all unsent dropped).\n _timelineDeltas() {\n this._trimTimeline();\n const all = this._stateTimeline;\n let start = all.length;\n while (start > 0 && this._entryInfo.get(all[start - 1]).seq > this._pushedSeq) start--;\n const pending = all.slice(start);\n const msgs = [];\n const first = this._timelineReset || this._hasTimelineDisclosure();\n for (let i = 0; i < pending.length || (first && i === 0); i += 200) {\n const msg = { epoch: this._epoch, changes: pending.slice(i, i + 200) };\n if (i === 0 && this._timelineReset) msg.reset = true;\n if (i === 0) this._takeTimelineDisclosure(msg);\n else msg.baseline = this._baselineInfo();\n msgs.push(msg);\n }\n this._timelineReset = false;\n this._pushedSeq = this._timelineSeq;\n return msgs;\n },\n\n // Full buffer (reset: true) \u2014 reply to request-state-timeline and the\n // activation snapshot. Newest entries first up to __TIMELINE_BYTES, the\n // server's own buffer budget, so nothing is sent that it would drop.\n _timelineFull() {\n this._trimTimeline();\n const all = this._stateTimeline;\n let start = all.length;\n let bytes = 0;\n while (start > 0) {\n const size = this._entryInfo.get(all[start - 1]).bytes;\n if (bytes + size > __TIMELINE_BYTES && start < all.length) break;\n bytes += size;\n start--;\n }\n this._timelineReset = false;\n this._pushedSeq = this._timelineSeq;\n return this._takeTimelineDisclosure({ epoch: this._epoch, changes: all.slice(start), reset: true });\n },\n\n getStateTimeline() {\n this._trimTimeline();\n return this._stateTimeline;\n },\n\n clearStateTimeline() {\n // Ring first: unsent entries past the cap are disclosed as dropped,\n // exactly as if they had been removed when recorded.\n this._trimTimeline();\n this._stateTimeline = [];\n this._timelineBytes = 0;\n this._pushedSeq = this._timelineSeq;\n this._timelineReset = true;\n this._scheduleTimelineUpdate();\n },\n\n // --- FPS Monitoring ---\n // Uses requestAnimationFrame to measure frame rate with minimal overhead.\n // Only stores timestamps - no allocations per frame beyond a single array push.\n _fpsFrameTimes: [],\n\n _fpsLoop(gen) {\n if (gen !== this._fpsGen) return;\n this._fpsFrameTimes.push(performance.now());\n requestAnimationFrame(() => this._fpsLoop(gen));\n },\n\n _isVisible() {\n return typeof document === 'undefined' || document.visibilityState !== 'hidden';\n },\n\n _setSubscription(data) {\n const wasActive = this._active;\n this._active = !!(data && data.active);\n this._componentDeltas = !!(data && data.componentDeltas);\n if (!this._active) {\n this._pendingAdded.clear();\n this._pendingRemoved.clear();\n }\n // resync: the server holds no full base for this epoch (\u00A76.5, review\n // C1), or we reconnected to a possibly restarted server.\n const resync = this._resync || !!(data && data.resync);\n this._resync = false;\n if (this._active && !wasActive) this._pollBaseline();\n if (this._active && (!wasActive || resync)) {\n // Collector state may be stale or empty (first activation, resync, or a\n // reconnect to a possibly restarted server): resend everything once, in\n // the order components -> profiles -> timeline(reset: true); the\n // reset push marks the activation snapshot complete (D4).\n this._sendUpdate(true);\n this._sendProfileUpdate();\n if (import.meta.hot) {\n import.meta.hot.send('svelte-devtools:state-timeline', this._timelineFull());\n }\n }\n this._syncSampling();\n },\n\n // Sampling (state poll + FPS) runs only while active AND the app tab is\n // visible.\n _syncSampling() {\n const want = this._active && this._isVisible();\n if (want && !this._sampling) {\n this._fpsFrameTimes = [];\n const gen = ++this._fpsGen;\n this._sampling = {\n poll: setInterval(() => {\n this._pollStateValues();\n // seed progress changed: push it even without changes (B-2)\n if (this._sentPendingNodes !== this._unseeded.size) this._scheduleTimelineUpdate();\n }, 200),\n fps: setInterval(() => { this._sampleFps(); }, 500),\n };\n requestAnimationFrame(() => this._fpsLoop(gen));\n } else if (!want && this._sampling) {\n clearInterval(this._sampling.poll);\n clearInterval(this._sampling.fps);\n this._sampling = null;\n this._fpsGen++;\n this._fpsFrameTimes = [];\n }\n },\n\n _sampleFps() {\n const now = performance.now();\n // Remove frame timestamps older than 1 second\n const cutoff = now - 1000;\n let i = 0;\n while (i < this._fpsFrameTimes.length && this._fpsFrameTimes[i] < cutoff) i++;\n if (i > 0) this._fpsFrameTimes.splice(0, i);\n const fps = this._fpsFrameTimes.length;\n if (import.meta.hot) {\n import.meta.hot.send('svelte-devtools:fps', { timestamp: Date.now(), fps });\n }\n }\n };\n\n window.__SVELTE_DEVTOOLS__ = __SVELTE_DT;\n\n // State polling and FPS sampling start on the server's subscription\n // message (\u00A76.3) and pause while the app tab is hidden.\n if (typeof document !== 'undefined' && document.addEventListener) {\n document.addEventListener('visibilitychange', () => __SVELTE_DT._syncSampling());\n }\n\n // Phase 3: Capture runtime errors\n window.addEventListener('error', (event) => {\n if (import.meta.hot) {\n import.meta.hot.send('svelte-devtools:runtime-error', {\n message: event.message,\n file: event.filename,\n line: event.lineno,\n column: event.colno,\n stack: event.error?.stack || '',\n timestamp: Date.now(),\n });\n }\n });\n window.addEventListener('unhandledrejection', (event) => {\n if (import.meta.hot) {\n const reason = event.reason;\n import.meta.hot.send('svelte-devtools:runtime-error', {\n message: reason?.message || String(reason),\n stack: reason?.stack || '',\n timestamp: Date.now(),\n });\n }\n });\n\n // Listen for reactive graph requests from server\n // Pull requests are answered regardless of the subscription state.\n if (import.meta.hot) {\n import.meta.hot.on('svelte-devtools:subscription', (data) => {\n __SVELTE_DT._setSubscription(data);\n });\n import.meta.hot.on('vite:ws:connect', () => {\n // The server may have restarted with an empty collector: the reply to\n // runtime-ready is treated as a fresh activation (full snapshot).\n __SVELTE_DT._resync = true;\n import.meta.hot.send('svelte-devtools:runtime-ready', {});\n });\n import.meta.hot.send('svelte-devtools:runtime-ready', {});\n import.meta.hot.on('svelte-devtools:request-reactive-graph', (data) => {\n __SVELTE_DT.sendReactiveGraph(data);\n });\n import.meta.hot.on('svelte-devtools:request-reactive-summary', (data) => {\n __SVELTE_DT.sendReactiveSummary(data);\n });\n import.meta.hot.on('svelte-devtools:request-state-timeline', () => {\n import.meta.hot.send('svelte-devtools:state-timeline', __SVELTE_DT._timelineFull());\n });\n import.meta.hot.on('svelte-devtools:clear-state-timeline', () => {\n __SVELTE_DT.clearStateTimeline();\n });\n }\n}\n";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate that a URL does not target private/internal network addresses (SSRF prevention).
|
|
3
|
+
* Allows only http/https schemes and blocks private IP ranges.
|
|
4
|
+
*/
|
|
5
|
+
export declare function validateExternalUrl(urlStr: string): void;
|
|
6
|
+
export interface OutboundUrlOptions {
|
|
7
|
+
/**
|
|
8
|
+
* Origins that may be fetched even though they are local — the dev
|
|
9
|
+
* server's own resolved URLs, so the API playground can call the app.
|
|
10
|
+
*/
|
|
11
|
+
allowedOrigins?: readonly string[];
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Like {@link validateExternalUrl}, but also resolves the hostname and
|
|
15
|
+
* rejects names that point at private / loopback addresses (DNS-based SSRF).
|
|
16
|
+
* Exact dev-server origins in `allowedOrigins` bypass the private checks.
|
|
17
|
+
* Callers must also fetch with `redirect: 'manual'` so a public URL cannot
|
|
18
|
+
* bounce the request to a private one.
|
|
19
|
+
*/
|
|
20
|
+
export declare function assertOutboundUrl(urlStr: string, options?: OutboundUrlOptions): Promise<URL>;
|
|
21
|
+
export declare function isPrivateIP(ip: string): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Resolve a user-supplied file path strictly under `root`. Symlinks are
|
|
24
|
+
* followed via realpathSync so that symlinks inside the project cannot be
|
|
25
|
+
* used to escape the root sandbox. Throws if the real path is outside.
|
|
26
|
+
*/
|
|
27
|
+
export declare function resolveWithinRoot(root: string, input: string): string;
|
|
@@ -24,6 +24,12 @@ import { Plugin } from 'vite';
|
|
|
24
24
|
* up in CI instead of as a missing dock in production.
|
|
25
25
|
*/
|
|
26
26
|
export declare const SVELTEKIT_INTERNAL_SUFFIX = ".svelte-kit/generated/server/internal.js";
|
|
27
|
+
/**
|
|
28
|
+
* SvelteKit 3 writes the same `templates: { app: … }` server module to
|
|
29
|
+
* `generated/dev/server.js` while serving (and `generated/build/server.js`
|
|
30
|
+
* for builds, which this dev-only plugin never sees).
|
|
31
|
+
*/
|
|
32
|
+
export declare const SVELTEKIT3_DEV_SERVER_SUFFIX = ".svelte-kit/generated/dev/server.js";
|
|
27
33
|
/** Marker that the `templates.app` literal in SvelteKit's generated server. */
|
|
28
34
|
export declare const TEMPLATE_APP_MARKER = "templates: {";
|
|
29
35
|
/**
|
|
@@ -36,7 +42,13 @@ export declare const TEMPLATE_APP_MARKER = "templates: {";
|
|
|
36
42
|
*/
|
|
37
43
|
export declare function injectIntoSvelteKitInternal(code: string): string | null;
|
|
38
44
|
/**
|
|
39
|
-
* Vite plugin: inject
|
|
45
|
+
* Vite plugin: inject the Vite DevTools dock bootstrap into SvelteKit's
|
|
40
46
|
* dev-only generated server template. Dev-only by construction.
|
|
47
|
+
*
|
|
48
|
+
* Only when the Vite DevTools hub is actually present: standalone has no
|
|
49
|
+
* hub serving `/__devtools/embedded.js`, so the tag would fail to load.
|
|
50
|
+
* `isHosted` is read in `transform` rather than `apply` because `apply` runs
|
|
51
|
+
* while the plugin list is still being resolved; every `configResolved` hook
|
|
52
|
+
* has finished before the server transforms its first module.
|
|
41
53
|
*/
|
|
42
|
-
export declare function sveltekitTemplateInjector(): Plugin;
|
|
54
|
+
export declare function sveltekitTemplateInjector(isHosted: () => boolean): Plugin;
|
package/dist/types.d.ts
CHANGED
|
@@ -27,6 +27,8 @@ export interface AssetInfo {
|
|
|
27
27
|
name: string;
|
|
28
28
|
path: string;
|
|
29
29
|
relativePath: string;
|
|
30
|
+
/** Public dev-server URL of the file (Vite `base` + path under the static dir). */
|
|
31
|
+
url: string;
|
|
30
32
|
size: number;
|
|
31
33
|
type: string;
|
|
32
34
|
mtime: number;
|
|
@@ -80,6 +82,147 @@ export interface ReactiveGraph {
|
|
|
80
82
|
nodes: ReactiveNode[];
|
|
81
83
|
edges: ReactiveEdge[];
|
|
82
84
|
}
|
|
85
|
+
/** `get-reactive-graph` arguments (docs/devframe-migration.md §6.7 A). Omitted = whole app. */
|
|
86
|
+
export interface ReactiveGraphRequest {
|
|
87
|
+
/** Component instance id within the served epoch (page load). */
|
|
88
|
+
componentId?: number;
|
|
89
|
+
/**
|
|
90
|
+
* The page load `componentId` came from. When it is no longer the served
|
|
91
|
+
* one, the answer is empty with `staleReason: 'epoch-changed'` instead of
|
|
92
|
+
* scoping a different instance that reuses the id.
|
|
93
|
+
*/
|
|
94
|
+
epoch?: string;
|
|
95
|
+
maxNodes?: number;
|
|
96
|
+
maxEdges?: number;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Sizes behind a (possibly capped) graph. `nodesKind: 'registered'` counts
|
|
100
|
+
* every tracked node the runtime holds (an exact live count would need a full
|
|
101
|
+
* scan); `'sent'` is what an older runtime sent before the server cut it.
|
|
102
|
+
* `edges` is `null` when unknown (e.g. the runtime stopped at the cap).
|
|
103
|
+
*/
|
|
104
|
+
export interface ReactiveGraphTotal {
|
|
105
|
+
nodes: number;
|
|
106
|
+
nodesKind: 'registered' | 'sent';
|
|
107
|
+
edges: number | null;
|
|
108
|
+
}
|
|
109
|
+
export interface ReactiveGraphResult extends ReactiveGraph {
|
|
110
|
+
/** Component instance the graph is scoped to; `null` = whole app. */
|
|
111
|
+
scope: number | null;
|
|
112
|
+
/** Page load the graph came from; `null` when no page load is known. */
|
|
113
|
+
epoch: string | null;
|
|
114
|
+
/** `null` = unknown, never a guessed number. */
|
|
115
|
+
total: ReactiveGraphTotal | null;
|
|
116
|
+
/** Nodes or edges were left out because of a cap. */
|
|
117
|
+
truncated: boolean;
|
|
118
|
+
/** Edges left out because an endpoint was not included or the edge cap was hit. */
|
|
119
|
+
edgesOmitted: number;
|
|
120
|
+
/**
|
|
121
|
+
* When the app built this graph (ms since epoch). Kept as is when the
|
|
122
|
+
* answer is reused from the cache (≤ 1 s) or served as a stale fallback;
|
|
123
|
+
* `null` for an empty fallback or a runtime that doesn't report it.
|
|
124
|
+
*/
|
|
125
|
+
computedAt: number | null;
|
|
126
|
+
/** `server-filter`: an older runtime sent the whole graph and the server scoped it. */
|
|
127
|
+
policy: 'scoped' | 'global-head' | 'server-filter';
|
|
128
|
+
/** Served from an earlier reply (or empty) because the runtime did not answer. */
|
|
129
|
+
stale?: boolean;
|
|
130
|
+
staleReason?: 'timeout' | 'no-runtime' | 'epoch-changed';
|
|
131
|
+
}
|
|
132
|
+
/** `get-reactive-summary` arguments (§6.7 I). */
|
|
133
|
+
export interface ReactiveSummaryRequest {
|
|
134
|
+
topK?: number;
|
|
135
|
+
windowMs?: number;
|
|
136
|
+
}
|
|
137
|
+
/** One component instance in the overview, from runtime counters over all instances. */
|
|
138
|
+
export interface ReactiveSummaryRow {
|
|
139
|
+
componentId: number;
|
|
140
|
+
file: string;
|
|
141
|
+
/** Tracked nodes registered for this instance, by type. */
|
|
142
|
+
nodes: {
|
|
143
|
+
state: number;
|
|
144
|
+
derived: number;
|
|
145
|
+
effect: number;
|
|
146
|
+
};
|
|
147
|
+
/** Sampled state changes in the window (≤ 1 per node per 200 ms tick); not a rate. */
|
|
148
|
+
changes: number;
|
|
149
|
+
/** Renders in the window. */
|
|
150
|
+
renders: number;
|
|
151
|
+
renderMs: number;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Whole-app overview without capturing the graph (§6.7 I/J). `rows` + `other`
|
|
155
|
+
* add up to `components` / the registered node counts; `null` = unknown.
|
|
156
|
+
*/
|
|
157
|
+
export interface ReactiveSummary {
|
|
158
|
+
epoch: string | null;
|
|
159
|
+
window: {
|
|
160
|
+
ms: number;
|
|
161
|
+
since: number;
|
|
162
|
+
until: number;
|
|
163
|
+
sampledActiveMs: number;
|
|
164
|
+
};
|
|
165
|
+
policy: 'sampled-200ms';
|
|
166
|
+
/** Only state created during a component's init is tracked. */
|
|
167
|
+
coverage: 'component-init';
|
|
168
|
+
components: {
|
|
169
|
+
total: number | null;
|
|
170
|
+
withActivity: number;
|
|
171
|
+
};
|
|
172
|
+
rows: ReactiveSummaryRow[];
|
|
173
|
+
other: {
|
|
174
|
+
components: number;
|
|
175
|
+
nodes: number;
|
|
176
|
+
} | null;
|
|
177
|
+
/** More active instances than `topK`. */
|
|
178
|
+
truncated: boolean;
|
|
179
|
+
capabilities: {
|
|
180
|
+
valueInspection: boolean;
|
|
181
|
+
signalHistory: boolean;
|
|
182
|
+
writeCause: boolean;
|
|
183
|
+
};
|
|
184
|
+
/** First samples of the tracked `$state` (activity counts only real changes after them). */
|
|
185
|
+
baseline?: TimelineBaseline;
|
|
186
|
+
stale?: boolean;
|
|
187
|
+
staleReason?: 'timeout' | 'no-runtime';
|
|
188
|
+
}
|
|
189
|
+
/** Datasets with capture metadata (`get-capture-info`). */
|
|
190
|
+
export type CaptureKey = 'liveComponents' | 'renderProfiles' | 'stateTimeline' | 'reactiveNodes' | 'reactiveEdges' | 'fpsSamples' | 'runtimeErrors' | 'loadProfiles' | 'compilerWarnings';
|
|
191
|
+
/** Why entries were dropped: runtime ring/byte budget before sending, or server caps. */
|
|
192
|
+
export type DropReason = 'runtime-count' | 'runtime-bytes' | 'server-count' | 'server-bytes';
|
|
193
|
+
/**
|
|
194
|
+
* What the server holds versus what was reported, per dataset (§6.7 B).
|
|
195
|
+
* Counts are monotonic until the dataset is cleared or its page load is
|
|
196
|
+
* dropped; both bump the dataset version.
|
|
197
|
+
*/
|
|
198
|
+
export interface CaptureInfo {
|
|
199
|
+
/** Items the server holds (what the dataset's RPC returns). */
|
|
200
|
+
captured: number;
|
|
201
|
+
/** Items reported before any cap; `null` = unknown. */
|
|
202
|
+
total: number | null;
|
|
203
|
+
truncated: boolean;
|
|
204
|
+
/** How the subset is chosen, e.g. 'tail', 'roots-first', 'sampled-200ms'. */
|
|
205
|
+
policy?: string;
|
|
206
|
+
dropped?: Array<{
|
|
207
|
+
reason: DropReason;
|
|
208
|
+
count: number;
|
|
209
|
+
}>;
|
|
210
|
+
/** State values too large to snapshot (shown as a size summary). */
|
|
211
|
+
valueTooLarge?: number;
|
|
212
|
+
/** stateTimeline only: first samples taken at activation (never recorded as changes). */
|
|
213
|
+
baseline?: TimelineBaseline;
|
|
214
|
+
epoch?: string;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Whether the runtime has a first sample of every tracked `$state`. Until
|
|
218
|
+
* `complete`, a change to one of the `pendingNodes` is not yet observable as
|
|
219
|
+
* old → new (its old value is unknown).
|
|
220
|
+
*/
|
|
221
|
+
export interface TimelineBaseline {
|
|
222
|
+
complete: boolean;
|
|
223
|
+
pendingNodes: number;
|
|
224
|
+
}
|
|
225
|
+
export type CaptureInfoMap = Partial<Record<CaptureKey, CaptureInfo>>;
|
|
83
226
|
export interface LoadProfile {
|
|
84
227
|
route: string;
|
|
85
228
|
file: string;
|
|
@@ -96,6 +239,39 @@ export interface StateChange {
|
|
|
96
239
|
newValue: unknown;
|
|
97
240
|
timestamp: number;
|
|
98
241
|
}
|
|
242
|
+
/** A timeline entry as served to clients: `seq` is server-assigned and monotonic. */
|
|
243
|
+
export interface StateTimelineEntry extends StateChange {
|
|
244
|
+
seq: number;
|
|
245
|
+
}
|
|
246
|
+
/** Cursor-based timeline read (`getStateTimelineDelta`). */
|
|
247
|
+
export interface StateTimelineDelta {
|
|
248
|
+
/** Pass back as `since` on the next call. */
|
|
249
|
+
cursor: number;
|
|
250
|
+
/** `true`: `changes` is the complete buffer (replace local copy). `false`: append `changes`. */
|
|
251
|
+
reset: boolean;
|
|
252
|
+
changes: StateTimelineEntry[];
|
|
253
|
+
}
|
|
254
|
+
/** Per-dataset change counters; refetch a dataset only when its counter moved. */
|
|
255
|
+
export interface DatasetVersions {
|
|
256
|
+
components: number;
|
|
257
|
+
renderProfiles: number;
|
|
258
|
+
loadProfiles: number;
|
|
259
|
+
stateTimeline: number;
|
|
260
|
+
reactiveGraph: number;
|
|
261
|
+
errors: number;
|
|
262
|
+
fps: number;
|
|
263
|
+
}
|
|
264
|
+
export interface LiveComponentsMeta {
|
|
265
|
+
/** Instances the app currently has mounted. */
|
|
266
|
+
total: number;
|
|
267
|
+
/** Instances returned by `getLiveComponents()` (parents first). */
|
|
268
|
+
kept: number;
|
|
269
|
+
truncated: boolean;
|
|
270
|
+
/** App page load (epoch) being shown; several app tabs → the one that pushed most recently. */
|
|
271
|
+
epoch?: string;
|
|
272
|
+
/** App page loads currently tracked (≤ 4). */
|
|
273
|
+
epochs: number;
|
|
274
|
+
}
|
|
99
275
|
export interface ApiEndpoint {
|
|
100
276
|
route: string;
|
|
101
277
|
path: string;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vite-devtools-svelte",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Svelte DevTools
|
|
3
|
+
"version": "0.4.1",
|
|
4
|
+
"description": "Svelte DevTools for Vite: standalone on your dev server or inside the Vite DevTools dock",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"devtools",
|
|
7
7
|
"svelte",
|
|
@@ -40,18 +40,19 @@
|
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
43
|
-
"@vitejs/devtools-kit": "^0.
|
|
43
|
+
"@vitejs/devtools-kit": "^0.7.6",
|
|
44
|
+
"devframe": "^1.2.0",
|
|
44
45
|
"zod": "^4.4.3"
|
|
45
46
|
},
|
|
46
47
|
"devDependencies": {
|
|
47
48
|
"@types/node": "^25.6.2",
|
|
48
49
|
"typescript": "^6.0.3",
|
|
49
|
-
"vite": "^8.
|
|
50
|
-
"vite-plugin-dts": "^5.
|
|
51
|
-
"vitest": "^
|
|
50
|
+
"vite": "^8.3.2",
|
|
51
|
+
"vite-plugin-dts": "^5.1.2",
|
|
52
|
+
"vitest": "^5.0.3"
|
|
52
53
|
},
|
|
53
54
|
"peerDependencies": {
|
|
54
|
-
"vite": "^8.
|
|
55
|
+
"vite": "^8.3.2"
|
|
55
56
|
},
|
|
56
57
|
"engines": {
|
|
57
58
|
"node": ">=22.0.0"
|