(isOpen = false)} />
-
-
- - ($visibility.component = !$visibility.component)}>
- Components
-
- - ($visibility.element = !$visibility.element)}>
- Elements
-
- - ($visibility.block = !$visibility.block)}>
- Blocks
-
- - ($visibility.slot = !$visibility.slot)}>
- Slots
-
- - ($visibility.anchor = !$visibility.anchor)}>
- Anchors
-
- - ($visibility.text = !$visibility.text)}>
- Text
-
-
- {/if}
-
diff --git a/test/public/index.html b/test/public/index.html
deleted file mode 100644
index df93b4e..0000000
--- a/test/public/index.html
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
Svelte Devtools test
-
-
-
-
-
-
diff --git a/test/src/App.svelte b/test/src/App.svelte
deleted file mode 100644
index 0f8e72e..0000000
--- a/test/src/App.svelte
+++ /dev/null
@@ -1,42 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/test/src/BasicTree/BasicTree.svelte b/test/src/BasicTree/BasicTree.svelte
deleted file mode 100644
index 778502c..0000000
--- a/test/src/BasicTree/BasicTree.svelte
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
-
- - Basic tree rendering
- - Element attributes, component properties and state
- - Text nodes / anchors
-
-
-
-
diff --git a/test/src/BasicTree/Component.svelte b/test/src/BasicTree/Component.svelte
deleted file mode 100644
index 969d4cc..0000000
--- a/test/src/BasicTree/Component.svelte
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
- A component with string, number, array, and object attributes. The value is
- {value}
-
diff --git a/test/src/Bind/Bind.svelte b/test/src/Bind/Bind.svelte
deleted file mode 100644
index af67498..0000000
--- a/test/src/Bind/Bind.svelte
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
- Prepend 'bind' for bound Component binding. Note: element binds are simple
- implicit event handlers
-
-
-
console.log(e)} />
-
diff --git a/test/src/Bind/BindComponent.svelte b/test/src/Bind/BindComponent.svelte
deleted file mode 100644
index 5e44a4b..0000000
--- a/test/src/Bind/BindComponent.svelte
+++ /dev/null
@@ -1,3 +0,0 @@
-
diff --git a/test/src/Blocks.svelte b/test/src/Blocks.svelte
deleted file mode 100644
index 108696b..0000000
--- a/test/src/Blocks.svelte
+++ /dev/null
@@ -1,71 +0,0 @@
-
-
-
-
- Renders {#each} and {#if} blocks with original
- source line
-
-
- {#each valueList as value}
{value}{/each}
-
-
- {#if value > 10}
- Value is over 10
- {:else if value > 5}Value is over 5{:else}Value is under 5{/if}
-
-
-
- {#await promise}
- waiting for the promise to resolve...
- {:then value}
- Promise resolved to
- {value}
- {:catch error}
- Something went wrong
- {error.message}
- {/await}
-
-
- {#await new Promise(() => {})}
- Pending forever
- {:then value}
- Something went wrong
- {value}
- {:catch error}
- Something went wrong
- {error.message}
- {/await}
-
-
-
- {#await Promise.resolve(5)}
- Something went wrong
- {:then value}
- Promise resolved to
- {value}
- {:catch error}
- Something went wrong
- {error.message}
- {/await}
-
-
- {#await Promise.reject('rejected')}
- Something went wrong
- {:then value}
- Something went wrong
- {value}
- {:catch error}
- Should reject
- {error}
- {/await}
-
-
diff --git a/test/src/Detach/Detach.svelte b/test/src/Detach/Detach.svelte
deleted file mode 100644
index 4068a76..0000000
--- a/test/src/Detach/Detach.svelte
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
-
-
Component / element nodes are
-
- - positioned correctly when mounted after first render
- - removed when detached
-
-
- {#if isShown}
-
-
Element renders below component and above button
- {/if}
-
-
-
diff --git a/test/src/Detach/DetachComponent.svelte b/test/src/Detach/DetachComponent.svelte
deleted file mode 100644
index 337dc72..0000000
--- a/test/src/Detach/DetachComponent.svelte
+++ /dev/null
@@ -1 +0,0 @@
-Element renders above both element and button
diff --git a/test/src/Events/Events.svelte b/test/src/Events/Events.svelte
deleted file mode 100644
index ee035d6..0000000
--- a/test/src/Events/Events.svelte
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
console.log('Captured a key', e)}>
-
Render event listeners on elements and components.
-
-
console.log(e.detail)} />
-
diff --git a/test/src/Events/EventsComponent.svelte b/test/src/Events/EventsComponent.svelte
deleted file mode 100644
index db96e18..0000000
--- a/test/src/Events/EventsComponent.svelte
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
diff --git a/test/src/Slots/SlotComponent.svelte b/test/src/Slots/SlotComponent.svelte
deleted file mode 100644
index 4fa864c..0000000
--- a/test/src/Slots/SlotComponent.svelte
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/test/src/Slots/Slots.svelte b/test/src/Slots/Slots.svelte
deleted file mode 100644
index 43f6d61..0000000
--- a/test/src/Slots/Slots.svelte
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
Render slots.
-
Slot content
-
diff --git a/test/src/index.js b/test/src/index.js
deleted file mode 100644
index c0c67e9..0000000
--- a/test/src/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import App from './App.svelte'
-
-new App({ target: document.body })
diff --git a/workspace/extension/.gitignore b/workspace/extension/.gitignore
new file mode 100644
index 0000000..ef49293
--- /dev/null
+++ b/workspace/extension/.gitignore
@@ -0,0 +1,4 @@
+/build
+
+# generated files
+static/courier.js
diff --git a/workspace/extension/index.html b/workspace/extension/index.html
new file mode 100644
index 0000000..396be6e
--- /dev/null
+++ b/workspace/extension/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/workspace/extension/package.json b/workspace/extension/package.json
new file mode 100644
index 0000000..971a2fa
--- /dev/null
+++ b/workspace/extension/package.json
@@ -0,0 +1,20 @@
+{
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "pnpm run --parallel \"/^dev:.*/\"",
+ "dev:app": "vite build -wd --minify=false --sourcemap=inline",
+ "dev:scripts": "rollup -cw",
+ "build": "rollup -c && vite build",
+ "bundle:zip": "cd build && zip -r svelte-devtools.zip *",
+ "bundle:tar": "cd build && tar -czf svelte-devtools.tar.gz *",
+ "format": "prettier -w .",
+ "check": "pnpm run --parallel \"/^check:.*/\"",
+ "check:style": "prettier -c .",
+ "check:svelte": "svelte-check --tsconfig ./tsconfig.json"
+ },
+ "devDependencies": {
+ "@types/chrome": "^0.0.266",
+ "rollup": "^4.22.4"
+ }
+}
diff --git a/workspace/extension/rollup.config.js b/workspace/extension/rollup.config.js
new file mode 100644
index 0000000..3368c80
--- /dev/null
+++ b/workspace/extension/rollup.config.js
@@ -0,0 +1,17 @@
+import { defineConfig } from 'rollup';
+
+export default defineConfig([
+ {
+ input: 'static/background.js',
+ output: {
+ file: 'build/background.js',
+ },
+ },
+ {
+ input: 'src/client/core.js',
+ output: [
+ { file: 'static/courier.js', format: 'iife' },
+ { file: 'build/courier.js', format: 'iife' },
+ ],
+ },
+]);
diff --git a/workspace/extension/src/App.svelte b/workspace/extension/src/App.svelte
new file mode 100644
index 0000000..eb9d75f
--- /dev/null
+++ b/workspace/extension/src/App.svelte
@@ -0,0 +1,194 @@
+
+
+
{
+ const { target, key } = event;
+ if (target !== document.body || !app.selected) return;
+
+ if (key === 'Enter') {
+ app.selected.expanded = !app.selected.expanded;
+ } else if (key === 'ArrowRight') {
+ event.preventDefault();
+ if (!app.selected) app.selected = app.root[0];
+ app.selected.expanded = true;
+ } else if (key === 'ArrowLeft') {
+ event.preventDefault();
+ if (app.selected.expanded) {
+ app.selected.expanded = false;
+ return;
+ }
+ do app.selected = app.selected.parent ?? app.selected;
+ while (!visibility[app.selected.type]);
+ } else if (key === 'ArrowUp') {
+ event.preventDefault();
+ let nodes = (app.selected.parent?.children || app.root).filter((n) => visibility[n.type]);
+ let sibling = nodes[nodes.findIndex((o) => o.id === app.selected?.id) - 1];
+ while (sibling?.expanded) {
+ nodes = sibling.children.filter((n) => visibility[n.type]);
+ sibling = nodes[nodes.length - 1];
+ }
+ app.selected = sibling ?? app.selected.parent ?? app.selected;
+ } else if (key === 'ArrowDown') {
+ event.preventDefault();
+ const children = app.selected.children.filter((n) => visibility[n.type]);
+
+ if (!app.selected.expanded || children.length === 0) {
+ let next = app.selected;
+ let current = app.selected;
+ do {
+ const nodes = current.parent ? current.parent.children : app.root;
+ const siblings = nodes.filter((n) => visibility[n.type]);
+ const index = siblings.findIndex((o) => o.id === current.id);
+ next = siblings[index + 1];
+ current = current.parent;
+ } while (!next && current);
+
+ app.selected = next ?? app.selected;
+ } else {
+ app.selected = children[0];
+ }
+ }
+ }}
+/>
+
+{#if app.root.length}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ event.currentTarget === event.target && reset()}
+ onmouseleave={reset}
+ >
+ {#each app.root as node (node.id)}
+
+ {/each}
+
+
+
+
+
+
+
+ {@const events = app.selected?.detail.listeners?.map((l) => {
+ const suffix = l.modifiers?.length ? `|${l.modifiers.join('|')}` : '';
+ const value = { __is: 'function', source: l.handler };
+ return { key: l.event + suffix, value };
+ })}
+
+ {#if app.selected?.type === 'component'}
+ Props
+
+
+
+
+ Events
+
+
+
+
+ State
+
+ {:else if app.selected?.type === 'block' || app.selected?.type === 'iteration'}
+ State
+
+ {:else if app.selected?.type === 'element'}
+ Attributes
+
+
+
+
+ Events
+
+ {/if}
+
+{:else}
+
+{/if}
+
+
diff --git a/workspace/extension/src/app.css b/workspace/extension/src/app.css
new file mode 100644
index 0000000..6472580
--- /dev/null
+++ b/workspace/extension/src/app.css
@@ -0,0 +1,108 @@
+:root {
+ tab-size: 2;
+
+ --background: rgb(255, 255, 255);
+ --color: rgb(74, 74, 79);
+
+ --t-duration: 240ms;
+}
+
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+html {
+ height: 100%;
+}
+
+body {
+ display: flex;
+ margin: 0;
+ height: 100%;
+ background: var(--background);
+ color: var(--color);
+ font-family: monospace;
+}
+body.dark {
+ --color: rgb(177, 177, 179);
+ --background: rgb(36, 36, 36);
+ scrollbar-color: rgb(115, 115, 115) rgb(60, 60, 61);
+}
+
+/* dark mode scrollbar */
+body.dark ::-webkit-scrollbar {
+ width: 0.75rem;
+ height: 0.5rem;
+ background-color: transparent;
+}
+body.dark ::-webkit-scrollbar-thumb {
+ background-color: rgb(51, 51, 51);
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ border-radius: 0.25rem;
+}
+
+/* basic resets */
+ul {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+
+/* expandable arrows */
+.expandable::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: calc(var(--indent, 6px) - 6px);
+
+ border-top: 0.375rem solid rgba(135, 135, 137, 0.9);
+ border-right: 0.25rem solid transparent;
+ border-left: 0.25rem solid transparent;
+ transition-duration: var(--t-duration);
+ transform: translate3d(0%, calc(50% + 0.25rem + var(--y-pad, 0px)), 0) rotate(-90deg);
+ /* transform: translate3d(-150%, calc(50% + 0.25rem + var(--y-pad, 0px)), 0) rotate(-90deg); */
+}
+.expandable.expanded::before {
+ transform: translate3d(0%, calc(50% + 0.25rem + var(--y-pad, 0px)), 0) rotate(0deg);
+ /* transform: translate3d(-150%, calc(50% + 0.25rem + var(--y-pad, 0px)), 0) rotate(0deg); */
+}
+
+/* tooltip pseudo-elements */
+[data-tooltip]:hover::after,
+[data-tooltip]:hover::before {
+ opacity: 1;
+ pointer-events: auto;
+}
+[data-tooltip]::before {
+ content: '';
+ opacity: 0;
+ pointer-events: none;
+
+ position: absolute;
+ bottom: -0.2rem;
+ left: 2.5rem;
+ border-right: 0.5rem solid transparent;
+ border-bottom: 0.5rem solid rgb(48, 64, 81);
+ border-left: 0.5rem solid transparent;
+ transition: opacity 0.2s;
+}
+[data-tooltip]::after {
+ content: attr(data-tooltip);
+ opacity: 0;
+ pointer-events: none;
+ z-index: 1;
+ position: absolute;
+ bottom: 0;
+ left: 0;
+
+ display: flex;
+ padding: 0.25rem 0.375rem;
+ border-radius: 0.25rem;
+ transition: opacity 0.2s;
+ transform: translateY(100%);
+
+ background-color: rgb(48, 64, 81);
+ color: white;
+}
diff --git a/workspace/extension/src/app.d.ts b/workspace/extension/src/app.d.ts
new file mode 100644
index 0000000..11f5926
--- /dev/null
+++ b/workspace/extension/src/app.d.ts
@@ -0,0 +1,129 @@
+///
+///
+
+interface SvelteDevInternal {
+ version: string;
+}
+
+declare global {
+ type SvelteComponentDetail = {
+ id: string;
+ options: {
+ $$inline?: boolean;
+ hydrate?: boolean;
+ target?: Element;
+ props?: Record;
+ };
+ tagName: string;
+ component: {
+ $$: {
+ fragment: {
+ c(): void;
+ d(detaching: boolean): void;
+ h(): void;
+ l(nodes: any[]): void;
+ m(target: Node, anchor: Node): void;
+ p(changed: boolean, ctx: any): void;
+ };
+ };
+ $$events_def?: {};
+ $$prop_def?: {};
+ $$slot_def?: {};
+ $capture_state(): any;
+ };
+ };
+
+ type SvelteBlockDetail = {
+ id: string; // crypto.randomUUID();
+ source: string;
+ type:
+ | 'anchor'
+ | 'block'
+ | 'catch'
+ | 'component'
+ | 'each'
+ | 'element'
+ | 'else'
+ | 'if'
+ | 'iteration'
+ | 'key'
+ | 'pending'
+ | 'slot'
+ | 'text'
+ | 'then';
+
+ detail?: any;
+ tagName?: string;
+
+ children: SvelteBlockDetail[];
+ /** `type: 'element' | 'component'` */
+ parent?: SvelteBlockDetail;
+ /** like `parent` but `type: 'component'` */
+ container?: SvelteBlockDetail;
+
+ block: SvelteComponentDetail['component']['$$']['fragment'];
+ ctx: Array; // TODO: do we need this typed?
+ };
+
+ type SvelteListenerDetail = {
+ node: Node & {
+ __listeners?: Omit[];
+ };
+ event: string;
+ handler: EventListenerOrEventListenerObject;
+ modifiers: Array<'capture' | 'preventDefault' | 'stopPropagation' | 'stopImmediatePropagation'>;
+ };
+
+ interface DocumentEventMap {
+ SvelteRegisterComponent: CustomEvent;
+
+ SvelteRegisterBlock: CustomEvent;
+
+ SvelteDOMInsert: CustomEvent;
+ SvelteDOMRemove: CustomEvent;
+
+ SvelteDOMAddEventListener: CustomEvent;
+ SvelteDOMRemoveEventListener: CustomEvent;
+
+ SvelteDOMSetAttribute: CustomEvent<
+ SvelteDevInternal & {
+ node: Element;
+ attribute: string;
+ value?: string;
+ }
+ >;
+ SvelteDOMRemoveAttribute: CustomEvent<
+ SvelteDevInternal & {
+ node: Element;
+ attribute: string;
+ }
+ >;
+ SvelteDOMSetProperty: CustomEvent<
+ SvelteDevInternal & {
+ node: Element;
+ property: string;
+ value?: any;
+ }
+ >;
+ SvelteDOMSetDataset: CustomEvent<
+ SvelteDevInternal & {
+ node: HTMLElement;
+ property: string;
+ value?: any;
+ }
+ >;
+ SvelteDOMSetData: CustomEvent<
+ SvelteDevInternal & {
+ node: Text;
+ data: unknown;
+ }
+ >;
+
+ SvelteDevTools: CustomEvent<{
+ type: string;
+ payload: string;
+ }>;
+ }
+}
+
+export {};
diff --git a/workspace/extension/src/client/core.js b/workspace/extension/src/client/core.js
new file mode 100644
index 0000000..6b874c1
--- /dev/null
+++ b/workspace/extension/src/client/core.js
@@ -0,0 +1,108 @@
+import { highlight } from './highlight.js';
+import { send } from './runtime.js';
+import { index as v4 } from './svelte-4.js';
+import { serialize } from './utils.js';
+
+// @ts-ignore - https://developer.chrome.com/docs/extensions/how-to/devtools/extend-devtools#selected-element
+window['#SvelteDevTools'] = {
+ /**
+ * @param {string} id
+ * @param {string[]} keys
+ * @param {any} value
+ */
+ inject(id, keys, value) {
+ const { detail: component } = v4.map.get(id) || {};
+ if (component) {
+ const [prop, ...rest] = keys;
+ const original = component.$capture_state()[prop];
+ if (typeof original === 'object') {
+ let ref = original;
+ for (let i = 0; i < rest.length - 1; i += 1) {
+ ref = ref[rest[i]];
+ }
+ ref[rest[rest.length - 1]] = value;
+ component.$inject_state({ [prop]: original });
+ } else {
+ component.$inject_state({ [prop]: value });
+ }
+ }
+ },
+};
+
+const previous = {
+ /** @type {HTMLElement | null} */
+ target: null,
+ style: {
+ cursor: '',
+ background: '',
+ outline: '',
+ },
+
+ clear() {
+ if (this.target) {
+ for (const key in this.style) {
+ // @ts-expect-error - trust me TS
+ this.target.style[key] = this.style[key];
+ }
+ }
+ this.target = null;
+ },
+};
+
+const inspect = {
+ /** @param {MouseEvent} event */
+ handle({ target }) {
+ const same = previous.target && previous.target === target;
+ const html = target instanceof HTMLElement;
+ if (same || !html) return;
+
+ if (previous.target) previous.clear();
+ previous.target = target;
+ previous.style = {
+ cursor: target.style.cursor,
+ background: target.style.background,
+ outline: target.style.outline,
+ };
+ target.style.cursor = 'pointer';
+ target.style.background = 'rgba(0, 136, 204, 0.2)';
+ target.style.outline = '1px dashed rgb(0, 136, 204)';
+ },
+ /** @param {MouseEvent} event */
+ click(event) {
+ event.preventDefault();
+ document.removeEventListener('mousemove', inspect.handle, true);
+ const node = v4.map.get(/** @type {Node} */ (event.target));
+ if (node) send('bridge::ext/inspect', { node: serialize(node) });
+ previous.clear();
+ },
+};
+
+window.addEventListener('message', ({ data, source }) => {
+ // only accept messages from our application or script
+ if (source !== window || data?.source !== 'svelte-devtools') return;
+
+ if (data.type === 'bridge::ext/select') {
+ const node = v4.map.get(data.payload);
+ // @ts-expect-error - saved for `devtools.inspect()`
+ if (node) window.$n = node.detail;
+ } else if (data.type === 'bridge::ext/highlight') {
+ const node = v4.map.get(data.payload);
+ return highlight(node);
+ } else if (data.type === 'bridge::ext/inspect') {
+ switch (data.payload) {
+ case 'start': {
+ document.addEventListener('mousemove', inspect.handle, true);
+ document.addEventListener('click', inspect.click, {
+ capture: true,
+ once: true,
+ });
+ break;
+ }
+ default: {
+ document.removeEventListener('mousemove', inspect.handle, true);
+ document.removeEventListener('click', inspect.click, true);
+ previous.clear();
+ }
+ }
+ }
+});
diff --git a/workspace/extension/src/client/highlight.js b/workspace/extension/src/client/highlight.js
new file mode 100644
index 0000000..fde12ef
--- /dev/null
+++ b/workspace/extension/src/client/highlight.js
@@ -0,0 +1,51 @@
+const dom = {
+ area: document.createElement('div'),
+ x: document.createElement('div'),
+ y: document.createElement('div'),
+};
+
+/** @param {Pick} [node] */
+export function highlight(node) {
+ if (!node || node.type !== 'element' || !node.detail) {
+ dom.area.remove();
+ dom.x.remove();
+ dom.y.remove();
+ return;
+ }
+
+ const { clientWidth, scrollHeight } = document.documentElement;
+ const style = window.getComputedStyle(node.detail);
+ const rect = node.detail.getBoundingClientRect();
+
+ // TODO: handle sticky position
+ const position = style.position === 'fixed' ? 'fixed' : 'absolute';
+ const offset = style.position !== 'fixed' ? window.scrollY : 0;
+
+ dom.area.style.setProperty('z-index', '65536');
+ dom.area.style.setProperty('background-color', 'rgba(0, 136, 204, 0.2)');
+ dom.area.style.setProperty('position', position);
+ dom.area.style.setProperty('top', `${offset + rect.top}px`);
+ dom.area.style.setProperty('left', `${rect.left}px`);
+ dom.area.style.setProperty('width', `${rect.width}px`);
+ dom.area.style.setProperty('height', `${rect.height}px`);
+
+ dom.x.style.setProperty('z-index', '65536');
+ dom.x.style.setProperty('border', '0px dashed rgb(0, 136, 204)');
+ dom.x.style.setProperty('border-width', '1px 0px');
+ dom.x.style.setProperty('position', position);
+ dom.x.style.setProperty('top', `${offset + rect.top}px`);
+ dom.x.style.setProperty('width', `${clientWidth}px`);
+ dom.x.style.setProperty('height', `${rect.height}px`);
+
+ dom.y.style.setProperty('z-index', '65536');
+ dom.y.style.setProperty('border', '0px dashed rgb(0, 136, 204)');
+ dom.y.style.setProperty('border-width', '0px 1px');
+ dom.y.style.setProperty('position', 'absolute');
+ dom.y.style.setProperty('left', `${rect.left}px`);
+ dom.y.style.setProperty('width', `${rect.width}px`);
+ dom.y.style.setProperty('height', `${scrollHeight}px`);
+
+ document.body.appendChild(dom.area);
+ document.body.appendChild(dom.x);
+ document.body.appendChild(dom.y);
+}
diff --git a/workspace/extension/src/client/runtime.js b/workspace/extension/src/client/runtime.js
new file mode 100644
index 0000000..fd5a24e
--- /dev/null
+++ b/workspace/extension/src/client/runtime.js
@@ -0,0 +1,7 @@
+/**
+ * @param {string} type
+ * @param {Record} [payload]
+ */
+export function send(type, payload) {
+ window.postMessage({ source: 'svelte-devtools', type, payload });
+}
diff --git a/workspace/extension/src/client/svelte-4.js b/workspace/extension/src/client/svelte-4.js
new file mode 100644
index 0000000..7f85143
--- /dev/null
+++ b/workspace/extension/src/client/svelte-4.js
@@ -0,0 +1,276 @@
+import { send } from './runtime.js';
+import { serialize } from './utils.js';
+
+/** @type {undefined | SvelteBlockDetail} */
+let current_block;
+
+export const index = {
+ /** @type {Map} */
+ map: new Map(),
+
+ /** @param {{ node: SvelteBlockDetail; target?: Node; anchor?: Node }} opts */
+ add({ node, target: source, anchor }) {
+ this.map.set(node.id, node);
+ this.map.set(node.detail, node);
+
+ let target = source && this.map.get(source);
+ if (!target || target.container != node.container) {
+ target = node.container;
+ }
+ node.parent = target;
+
+ const sibling = anchor && this.map.get(anchor);
+ if (target) {
+ const idx = target.children.findIndex((n) => n === sibling);
+ if (idx === -1) target.children.push(node);
+ else target.children.splice(idx, 0, node);
+ }
+
+ send('bridge::courier/node->add', {
+ node: serialize(node),
+ target: node.parent?.id,
+ anchor: sibling?.id,
+ });
+ },
+
+ /** @param {{ node: SvelteBlockDetail; target?: Node; anchor?: Node }} opts */
+ update({ node }) {
+ send('bridge::courier/node->update', {
+ node: serialize(node),
+ });
+ },
+
+ /** @param {SvelteBlockDetail} node */
+ remove(node) {
+ if (!node) return;
+
+ this.map.delete(node.id);
+ this.map.delete(node.detail);
+
+ if (node.parent) {
+ node.parent.children = node.parent.children.filter((n) => n !== node);
+ node.parent = undefined;
+ }
+
+ send('bridge::courier/node->remove', {
+ node: serialize(node),
+ });
+ },
+};
+
+document.addEventListener('SvelteRegisterComponent', ({ detail }) => {
+ const { component, tagName } = detail;
+
+ const node = index.map.get(component.$$.fragment);
+ if (node) {
+ index.map.delete(component.$$.fragment);
+
+ node.detail = component;
+ node.tagName = tagName;
+
+ index.update({ node });
+ } else {
+ // @ts-expect-error - component special case
+ index.map.set(component.$$.fragment, {
+ type: 'component',
+ detail: component,
+ tagName,
+ });
+ }
+});
+
+/** @type {any} */
+let last_promise;
+document.addEventListener('SvelteRegisterBlock', ({ detail }) => {
+ const { type, id, block, ...rest } = detail;
+ const current_node_id = crypto.randomUUID();
+
+ if (block.m) {
+ const original = block.m;
+ block.m = (target, anchor) => {
+ const parent = current_block;
+
+ // @ts-expect-error - only the necessities
+ const node = /** @type {SvelteBlockDetail} */ ({
+ id: current_node_id,
+ type: 'block',
+ detail: rest,
+ tagName: type === 'pending' ? 'await' : type,
+ container: parent,
+ children: [],
+ });
+
+ switch (type) {
+ case 'then':
+ case 'catch':
+ if (!node.container) node.container = last_promise;
+ break;
+
+ case 'slot':
+ node.type = 'slot';
+ break;
+
+ case 'component': {
+ const component = index.map.get(block);
+ if (component) {
+ index.map.delete(block);
+ Object.assign(node, component);
+ } else {
+ node.type = 'component';
+ node.tagName = 'Unknown';
+ node.detail = {};
+ index.map.set(block, node);
+ }
+
+ Promise.resolve().then(() => {
+ const invalidate = node.detail.$$?.bound || {};
+ Object.keys(invalidate).length && index.update({ node });
+ });
+ break;
+ }
+ }
+
+ if (type === 'each') {
+ let group = parent && index.map.get(parent.id + id);
+ if (!group) {
+ // @ts-expect-error - each block fallback
+ group = /** @type {SvelteBlockDetail} */ ({
+ version: '',
+ id: crypto.randomUUID(),
+ type: 'block',
+ tagName: 'each',
+ container: parent,
+ children: [],
+ detail: {
+ ctx: {},
+ source: detail.source,
+ },
+ });
+ parent && index.map.set(parent.id + id, group);
+ index.add({ node: group, target, anchor });
+ }
+
+ node.container = group;
+ node.type = 'iteration';
+
+ // @ts-expect-error - overloaded nodes
+ index.add({ node, target: group, anchor });
+ } else {
+ index.add({ node, target, anchor });
+ }
+
+ current_block = node;
+
+ original(target, anchor);
+
+ current_block = parent;
+ };
+ }
+
+ if (block.p) {
+ const original = block.p;
+ block.p = (changed, ctx) => {
+ const parent = current_block;
+ current_block = index.map.get(current_node_id);
+ current_block && index.update({ node: current_block });
+
+ original(changed, ctx);
+
+ current_block = parent;
+ };
+ }
+
+ if (block.d) {
+ const original = block.d;
+ block.d = (detaching) => {
+ const node = index.map.get(current_node_id);
+ if (node) {
+ if (node.tagName === 'await') {
+ last_promise = node.container;
+ }
+ index.remove(node);
+ }
+
+ original(detaching);
+ };
+ }
+});
+
+document.addEventListener('SvelteDOMInsert', ({ detail }) => {
+ deep_insert(detail); // { node, target, anchor }
+
+ /** @param {Omit} opts */
+ function deep_insert({ node: element, target, anchor }) {
+ const type =
+ element.nodeType === Node.ELEMENT_NODE
+ ? 'element'
+ : element.nodeValue && element.nodeValue !== ' '
+ ? 'text'
+ : 'anchor';
+
+ index.add({
+ anchor,
+ target,
+ // @ts-expect-error - missing properties are irrelevant
+ node: {
+ id: crypto.randomUUID(),
+ type,
+ detail: element,
+ tagName: element.nodeName.toLowerCase(),
+ container: current_block,
+ children: [],
+ },
+ });
+
+ element.childNodes.forEach((child) => {
+ !index.map.has(child) && deep_insert({ node: child, target: element });
+ });
+ }
+});
+
+document.addEventListener('SvelteDOMRemove', ({ detail }) => {
+ const node = index.map.get(detail.node);
+ if (node) index.remove(node);
+});
+
+document.addEventListener('SvelteDOMAddEventListener', ({ detail }) => {
+ const { node, ...rest } = detail;
+ node.__listeners = node.__listeners || [];
+ node.__listeners.push(rest);
+});
+
+document.addEventListener('SvelteDOMRemoveEventListener', ({ detail }) => {
+ const { node, event, handler, modifiers } = detail;
+ if (!node.__listeners || node.__listeners.length) return;
+ node.__listeners = node.__listeners.filter(
+ (l) => l.event !== event || l.handler !== handler || l.modifiers !== modifiers,
+ );
+});
+
+document.addEventListener('SvelteDOMSetData', ({ detail }) => {
+ const node = index.map.get(detail.node);
+ if (!node) return;
+ if (node.type === 'anchor') node.type = 'text';
+ index.update({ node });
+});
+
+document.addEventListener('SvelteDOMSetProperty', ({ detail }) => {
+ const node = index.map.get(detail.node);
+ if (!node) return;
+ if (node.type === 'anchor') node.type = 'text';
+ index.update({ node });
+});
+
+document.addEventListener('SvelteDOMSetAttribute', ({ detail }) => {
+ const node = index.map.get(detail.node);
+ if (!node) return;
+ if (node.type === 'anchor') node.type = 'text';
+ index.update({ node });
+});
+
+document.addEventListener('SvelteDOMRemoveAttribute', ({ detail }) => {
+ const node = index.map.get(detail.node);
+ if (!node) return;
+ if (node.type === 'anchor') node.type = 'text';
+ index.update({ node });
+});
diff --git a/workspace/extension/src/client/utils.js b/workspace/extension/src/client/utils.js
new file mode 100644
index 0000000..6137ab2
--- /dev/null
+++ b/workspace/extension/src/client/utils.js
@@ -0,0 +1,101 @@
+/**
+ * @param {unknown} value
+ * @returns {any}
+ */
+function clone(value, seen = new Map()) {
+ switch (typeof value) {
+ case 'function':
+ return { __is: 'function', source: value.toString(), name: value.name };
+ case 'symbol':
+ return { __is: 'symbol', name: value.toString() };
+ case 'object': {
+ if (value === window || value === null) return null;
+ if (Array.isArray(value)) return value.map((o) => clone(o, seen));
+ if (seen.has(value)) return {};
+
+ /** @type {Record} */
+ const o = {};
+ seen.set(value, o);
+
+ const descriptors = Object.getOwnPropertyDescriptors(value);
+ for (const [key, v] of Object.entries(value)) {
+ const { get, writable } = descriptors[key];
+ const readonly = !writable || get !== undefined;
+ o[key] = { key, value: clone(v, seen), readonly };
+ }
+ return o;
+ }
+ default:
+ return value;
+ }
+}
+
+/** @param {SvelteBlockDetail} node */
+export function serialize(node) {
+ const res = /** @type {SvelteBlockDetail} */ ({
+ id: node.id,
+ type: node.type,
+ tagName: node.tagName,
+ detail: {},
+ });
+ switch (node.type) {
+ case 'component': {
+ const { $$: internal = {} } = node.detail;
+ const captured = node.detail.$capture_state?.() || {};
+ const bindings = Object.values(internal.bound || {}).map(
+ /** @param {Function} f */ (f) => f.name,
+ );
+ const props = Object.keys(internal.props || {}).flatMap((key) => {
+ const value = clone(captured[key]);
+ delete captured[key]; // deduplicate for ctx
+ if (value === undefined) return [];
+
+ const bounded = bindings.some((f) => f.includes(key));
+ return { key, value, bounded };
+ });
+
+ res.detail = {
+ attributes: props,
+ listeners: Object.entries(internal.callbacks || {}).flatMap(([event, value]) =>
+ value.map(/** @param {Function} f */ (f) => ({ event, handler: f.toString() })),
+ ),
+ ctx: Object.entries(captured).map(([key, v]) => ({ key, value: clone(v) })),
+ };
+ break;
+ }
+
+ case 'element': {
+ /** @type {Attr[]} from {NamedNodeMap} */
+ const attributes = Array.from(node.detail.attributes || []);
+
+ /** @type {NonNullable} */
+ const listeners = node.detail.__listeners || [];
+
+ res.detail = {
+ attributes: attributes.map(({ name: key, value }) => ({ key, value, readonly: true })),
+ listeners: listeners.map((o) => ({ ...o, handler: o.handler.toString() })),
+ };
+ break;
+ }
+
+ case 'text': {
+ res.detail = {
+ nodeValue: node.detail.nodeValue,
+ };
+ break;
+ }
+
+ case 'iteration':
+ case 'block': {
+ const { ctx, source } = node.detail;
+ const cloned = Object.entries(clone(ctx));
+ res.detail = {
+ ctx: cloned.map(([key, value]) => ({ key, value, readonly: true })),
+ source: source.slice(source.indexOf('{'), source.indexOf('}') + 1),
+ };
+ break;
+ }
+ }
+
+ return res;
+}
diff --git a/workspace/extension/src/entry.ts b/workspace/extension/src/entry.ts
new file mode 100644
index 0000000..54dd677
--- /dev/null
+++ b/workspace/extension/src/entry.ts
@@ -0,0 +1,13 @@
+import './app.css';
+import App from './App.svelte';
+import { mount } from 'svelte';
+
+if (chrome.devtools.panels.themeName === 'dark') {
+ document.body.classList.add('dark');
+} else {
+ document.body.classList.remove('dark');
+}
+
+export default mount(App, {
+ target: document.querySelector('#app')!,
+});
diff --git a/workspace/extension/src/lib/components/Button.svelte b/workspace/extension/src/lib/components/Button.svelte
new file mode 100644
index 0000000..1883286
--- /dev/null
+++ b/workspace/extension/src/lib/components/Button.svelte
@@ -0,0 +1,83 @@
+
+
+
+
+
diff --git a/workspace/extension/src/lib/components/Divider.svelte b/workspace/extension/src/lib/components/Divider.svelte
new file mode 100644
index 0000000..a44166d
--- /dev/null
+++ b/workspace/extension/src/lib/components/Divider.svelte
@@ -0,0 +1,30 @@
+
+
+
+
+
diff --git a/workspace/extension/src/lib/components/Indexer.svelte b/workspace/extension/src/lib/components/Indexer.svelte
new file mode 100644
index 0000000..d966b46
--- /dev/null
+++ b/workspace/extension/src/lib/components/Indexer.svelte
@@ -0,0 +1,31 @@
+
+
+
+ {#if i === -1 || app.query.length < 2}
+ {text}
+ {:else}
+ {#if i !== 0}{text.slice(0, i)}{/if}
+ {text.slice(i, i + app.query.length)}
+ {#if i + app.query.length < text.length}
+ {text.slice(i + app.query.length)}
+ {/if}
+ {/if}
+
+
+
diff --git a/workspace/extension/src/lib/components/Relative.svelte b/workspace/extension/src/lib/components/Relative.svelte
new file mode 100644
index 0000000..1e63383
--- /dev/null
+++ b/workspace/extension/src/lib/components/Relative.svelte
@@ -0,0 +1,20 @@
+
+
+
+ {@render children()}
+
+
+
diff --git a/workspace/extension/src/lib/components/Resizable.svelte b/workspace/extension/src/lib/components/Resizable.svelte
new file mode 100644
index 0000000..8439766
--- /dev/null
+++ b/workspace/extension/src/lib/components/Resizable.svelte
@@ -0,0 +1,93 @@
+
+
+ (resizing = false)}
+ onmousemove={({ pageX: x, pageY: y }) => {
+ if (!resizing) return;
+ size = axis === 'x' ? window.innerWidth - x : window.innerHeight - y;
+ }}
+/>
+
+
+
+
diff --git a/workspace/extension/src/lib/components/Toolbar.svelte b/workspace/extension/src/lib/components/Toolbar.svelte
new file mode 100644
index 0000000..8f73a8e
--- /dev/null
+++ b/workspace/extension/src/lib/components/Toolbar.svelte
@@ -0,0 +1,26 @@
+
+
+
+
+
diff --git a/workspace/extension/src/lib/nodes/Block.svelte b/workspace/extension/src/lib/nodes/Block.svelte
new file mode 100644
index 0000000..564f8f6
--- /dev/null
+++ b/workspace/extension/src/lib/nodes/Block.svelte
@@ -0,0 +1,43 @@
+
+
+
+ (expanded = !expanded)}>
+ {#if source}
+ {source}
+ {:else}
+
+ {/if}
+ {#if !expanded}
+ (expanded = true)} />
+
+ {/if}
+
+{#if expanded}
+ {@render children()}
+
+
+
+
+{/if}
+
+
diff --git a/workspace/extension/src/lib/nodes/Element.svelte b/workspace/extension/src/lib/nodes/Element.svelte
new file mode 100644
index 0000000..a6e6171
--- /dev/null
+++ b/workspace/extension/src/lib/nodes/Element.svelte
@@ -0,0 +1,96 @@
+
+
+{#snippet close()}
+ </
+
+
+
+ >
+{/snippet}
+
+ (expanded = !empty && !expanded)}
+>
+ <
+
+
+
+
+
+ {#if empty}
+ />
+ {:else}
+ >
+ {#if !expanded}
+ (expanded = true)} />
+
+ {@render close()}
+ {/if}
+ {/if}
+
+
+{#if expanded}
+ {@render children()}
+
+
+ {@render close()}
+
+{/if}
+
+
diff --git a/workspace/extension/src/lib/nodes/ElementAttributes.svelte b/workspace/extension/src/lib/nodes/ElementAttributes.svelte
new file mode 100644
index 0000000..d989fbc
--- /dev/null
+++ b/workspace/extension/src/lib/nodes/ElementAttributes.svelte
@@ -0,0 +1,67 @@
+
+
+{#each attributes as { key, value, bounded } (key)}
+ {@const prefix = bounded ? 'bind:' : ''}
+
+
+
+
+
+
+ =
+
+
+
+
+{/each}
+
+{#each listeners as { event, handler, modifiers }}
+ {@const suffix = modifiers?.length ? `|${modifiers.join('|')}` : ''}
+
+
+
+
+
+{/each}
+
+
diff --git a/workspace/extension/src/lib/nodes/Ellipsis.svelte b/workspace/extension/src/lib/nodes/Ellipsis.svelte
new file mode 100644
index 0000000..466e591
--- /dev/null
+++ b/workspace/extension/src/lib/nodes/Ellipsis.svelte
@@ -0,0 +1,26 @@
+
+
+
+
+
diff --git a/workspace/extension/src/lib/nodes/Iteration.svelte b/workspace/extension/src/lib/nodes/Iteration.svelte
new file mode 100644
index 0000000..dff58c2
--- /dev/null
+++ b/workspace/extension/src/lib/nodes/Iteration.svelte
@@ -0,0 +1,22 @@
+
+
+
+ (expanded = !expanded)}>
+ ↪
+ {#if !expanded}
+ (expanded = true)} />
+ {/if}
+
+
+{#if expanded}
+ {@render children()}
+{/if}
diff --git a/workspace/extension/src/lib/nodes/Node.svelte b/workspace/extension/src/lib/nodes/Node.svelte
new file mode 100644
index 0000000..aac0b82
--- /dev/null
+++ b/workspace/extension/src/lib/nodes/Node.svelte
@@ -0,0 +1,161 @@
+
+
+{#snippet expand(children: (typeof node)['children'], level: number)}
+ {#each children as child (child.id)}
+
+ {/each}
+{/snippet}
+
+{#if visibility[node.type]}
+
+ Object.assign(prev, node)}
+ onclick={(event) => {
+ event.stopPropagation();
+ app.selected = node;
+ }}
+ onmousemove={(event) => {
+ event.stopPropagation();
+ if (app.hovered?.id === node.id) return;
+ background.send('bridge::ext/highlight', node.id);
+ app.hovered = node;
+ }}
+ >
+ {#if node.type === 'component' || node.type === 'element'}
+
+ {@render expand(node.children, depth + 1)}
+
+ {:else if node.type === 'block'}
+
+ {@render expand(node.children, depth + 1)}
+
+ {:else if node.type === 'iteration'}
+
+ {@render expand(node.children, depth + 1)}
+
+ {:else if node.type === 'slot'}
+
+ {@render expand(node.children, depth + 1)}
+
+ {:else if node.type === 'text'}
+
+
+
+ {:else if node.type === 'anchor'}
+ #anchor
+ {/if}
+
+{:else}
+ {@render expand(node.children, depth)}
+{/if}
+
+
diff --git a/workspace/extension/src/lib/nodes/Slot.svelte b/workspace/extension/src/lib/nodes/Slot.svelte
new file mode 100644
index 0000000..efa65e7
--- /dev/null
+++ b/workspace/extension/src/lib/nodes/Slot.svelte
@@ -0,0 +1,29 @@
+
+
+
+ (expanded = !expanded)}>
+
+
+ {#if !expanded}
+ (expanded = true)} />
+
+ {/if}
+
+{#if expanded}
+ {@render children()}
+
+
+
+
+{/if}
diff --git a/workspace/extension/src/lib/panel/Editable.svelte b/workspace/extension/src/lib/panel/Editable.svelte
new file mode 100644
index 0000000..8a08bc0
--- /dev/null
+++ b/workspace/extension/src/lib/panel/Editable.svelte
@@ -0,0 +1,105 @@
+
+
+{#if editing}
+
+ {
+ // @ts-expect-error - target and value exists
+ update(target.value);
+ }}
+ onkeydown={(event) => {
+ const { key, target } = event;
+ if (key === 'Escape') {
+ event.preventDefault();
+ editing = false;
+ return;
+ }
+ if (key !== 'Enter') return;
+ // @ts-expect-error - target and value exists
+ update(target.value);
+ }}
+ />
+{:else}
+
+ (editing = !readonly)}>
+ {type === 'string' ? `"${value}"` : `${value}`}
+
+{/if}
+
+
diff --git a/workspace/extension/src/lib/panel/PropertyList.svelte b/workspace/extension/src/lib/panel/PropertyList.svelte
new file mode 100644
index 0000000..f6188aa
--- /dev/null
+++ b/workspace/extension/src/lib/panel/PropertyList.svelte
@@ -0,0 +1,140 @@
+
+
+{#if entries.length}
+
+ {#each entries as { key, value, readonly = false } (key)}
+ {@const keys = [...parents, key]}
+ {@const type = typeof value}
+
+
+ - {
+ event.stopPropagation();
+ expanded[key] = !expanded[key];
+ }}
+ >
+ {key}:
+
+
+ {#if type === 'string'}
+ inject(keys, updated)}
+ />
+ {:else if value == null || value !== value}
+ inject(keys, updated)}
+ />
+ {:else if type === 'number' || type === 'boolean'}
+ inject(keys, updated)}
+ />
+ {:else if Array.isArray(value)}
+ Array [{value.length || ''}]
+
+ {#if value.length && expanded[key]}
+ {@const entries = value.map((v, i) => ({ key: `${i}`, value: v, readonly }))}
+
+
+ {/if}
+ {:else if type === 'object'}
+ {#if value.__is === 'function'}
+ function {value.name || ''}()
+ {#if expanded[key]}
{value.source}
{/if}
+ {:else if value.__is === 'symbol'}
+ {value.name || 'Symbol()'}
+ {:else if Object.keys(value).length}
+ Object {…}
+
+ {#if expanded[key]}
+
+ {/if}
+ {:else}
+ Object { }
+ {/if}
+ {/if}
+
+ {/each}
+
+{:else}
+ None
+{/if}
+
+
diff --git a/workspace/extension/src/lib/panel/core.svelte.ts b/workspace/extension/src/lib/panel/core.svelte.ts
new file mode 100644
index 0000000..3865e1a
--- /dev/null
+++ b/workspace/extension/src/lib/panel/core.svelte.ts
@@ -0,0 +1,17 @@
+import { app } from '$lib/state.svelte';
+
+export const errors = $state<{ [keys: string]: string | false }>({});
+
+export function inject(keys: string[], value: any) {
+ const uuid = app.selected?.id;
+ if (!uuid) return;
+
+ const accessors = `[${keys.map((k) => `'${k}'`).join(', ')}]`;
+ chrome.devtools.inspectedWindow.eval(
+ `window['#SvelteDevTools'].inject('${uuid}', ${accessors}, ${value})`,
+ (_, error) => {
+ const id = `${uuid}+${keys.join('.')}`;
+ errors[id] = error?.isException && error.value.slice(0, error.value.indexOf('\n'));
+ },
+ );
+}
diff --git a/workspace/extension/src/lib/runtime.svelte.ts b/workspace/extension/src/lib/runtime.svelte.ts
new file mode 100644
index 0000000..d8cd54c
--- /dev/null
+++ b/workspace/extension/src/lib/runtime.svelte.ts
@@ -0,0 +1,121 @@
+import { type DebugNode, app } from './state.svelte';
+
+const tabId = chrome.devtools.inspectedWindow.tabId;
+let port = chrome.runtime.connect({ name: `${tabId}` });
+
+port.postMessage({ source: 'svelte-devtools', tabId, type: 'bypass::ext/init' });
+
+export const background = {
+ send(type: `bridge::${'ext' | 'page'}/${string}` | 'bypass::ext/page->refresh', payload?: any) {
+ try {
+ port.postMessage({ source: 'svelte-devtools', tabId, type, payload });
+ } catch {
+ // https://developer.chrome.com/docs/extensions/develop/concepts/messaging#port-lifetime
+ // chrome aggressively disconnects the port, not much we can do other than to reconnect
+ port = chrome.runtime.connect({ name: `${tabId}` });
+ background.send(type, payload); // retry immediately
+ }
+ },
+};
+
+function resolveEventBubble(node: any) {
+ if (!node.detail || !node.detail.listeners) return;
+
+ for (const listener of node.detail.listeners) {
+ if (!listener.handler.includes('bubble($$self, event)')) continue;
+
+ listener.handler = () => {
+ let target = node;
+ while ((target = target.parent)) if (target.type === 'component') break;
+
+ const listeners = target.detail.listeners;
+ if (!listeners) return null;
+
+ const parentListener = listeners.find((o: any) => o.event === listener.event);
+ if (!parentListener) return null;
+
+ const handler = parentListener.handler;
+ if (!handler) return null;
+
+ return `// From parent\n${handler}`;
+ };
+ }
+}
+
+port.onMessage.addListener(({ type, payload }) => {
+ switch (type) {
+ case 'bridge::ext/clear': {
+ app.nodes = {};
+ app.selected = undefined;
+ app.hovered = undefined;
+ break;
+ }
+
+ case 'bridge::ext/inspect': {
+ if (typeof payload === 'string') break;
+ app.selected = app.nodes[payload.node.id];
+ app.inspecting = false;
+ break;
+ }
+
+ case 'bridge::courier/node->add': {
+ const { node, target, anchor } = payload as {
+ node: DebugNode;
+ target: string;
+ anchor: string;
+ };
+
+ node.parent = app.nodes[target];
+ node.children = [];
+ node.expanded = false;
+ resolveEventBubble(node);
+
+ app.nodes[node.id] = node;
+ if (!node.parent) break;
+
+ const siblings = node.parent.children;
+ const index = siblings.findIndex((n) => n.id === anchor);
+ if (index === -1) siblings.push(node);
+ else siblings.splice(index, 0, node);
+
+ break;
+ }
+
+ case 'bridge::courier/node->remove': {
+ const node = payload.node as SvelteBlockDetail;
+ const current = app.nodes[node.id];
+ if (current) delete app.nodes[current.id];
+ if (!current?.parent) break;
+
+ const index = current.parent.children.findIndex((o) => o.id === current.id);
+ current.parent.children.splice(index, 1);
+ break;
+ }
+
+ case 'bridge::courier/node->update': {
+ const node = payload.node as SvelteBlockDetail;
+ const current = app.nodes[node.id];
+ if (!current) break;
+ Object.assign(current, node);
+ resolveEventBubble(current);
+ break;
+ }
+
+ // case 'bridge::courier/profile->update': {
+ // resolveFrame(frame);
+ // profileFrame.set(frame);
+ // break;
+
+ // function resolveFrame(frame) {
+ // frame.children.forEach(resolveFrame);
+
+ // if (!frame.node) return;
+
+ // frame.node = app.nodes.get(frame.node) || {
+ // type: 'Unknown',
+ // tagName: 'Unknown',
+ // };
+ // }
+ // }
+ }
+});
diff --git a/workspace/extension/src/lib/state.svelte.ts b/workspace/extension/src/lib/state.svelte.ts
new file mode 100644
index 0000000..f192277
--- /dev/null
+++ b/workspace/extension/src/lib/state.svelte.ts
@@ -0,0 +1,53 @@
+type Overwrite = Omit & B;
+
+export type DebugNode = Overwrite<
+ SvelteBlockDetail,
+ {
+ expanded: boolean;
+ detail: {
+ attributes?: Array<{
+ key: string;
+ value: string;
+ bounded?: boolean;
+ flash?: boolean;
+ }>;
+ listeners?: Array<{
+ event: any;
+ handler: any;
+ modifiers: any;
+ }>;
+ ctx: any;
+ source: string;
+ nodeValue: string;
+ };
+
+ tagName: string;
+ parent: DebugNode;
+ children: DebugNode[];
+ dom?: HTMLLIElement;
+ }
+>;
+
+export const app = $state({
+ nodes: {} as { [key: string]: DebugNode },
+ get root() {
+ const nodes = Object.values(this.nodes);
+ return nodes.filter((node) => !node.parent);
+ },
+
+ selected: undefined as undefined | DebugNode,
+ hovered: undefined as undefined | DebugNode,
+
+ inspecting: false,
+ query: '',
+});
+
+export const visibility = $state<{ [key: string]: boolean }>({
+ component: true,
+ element: true,
+ block: true,
+ iteration: true,
+ slot: true,
+ text: true,
+ anchor: false,
+});
diff --git a/workspace/extension/src/routes/Breadcrumbs.svelte b/workspace/extension/src/routes/Breadcrumbs.svelte
new file mode 100644
index 0000000..3cf1c56
--- /dev/null
+++ b/workspace/extension/src/routes/Breadcrumbs.svelte
@@ -0,0 +1,65 @@
+
+
+{#if breadcrumbs.length}
+
+ {#each breadcrumbs as node}
+ {#if visibility[node.type]}
+
+ {/if}
+ {/each}
+
+{/if}
+
+
diff --git a/workspace/extension/src/routes/ConnectMessage.svelte b/workspace/extension/src/routes/ConnectMessage.svelte
new file mode 100644
index 0000000..4815049
--- /dev/null
+++ b/workspace/extension/src/routes/ConnectMessage.svelte
@@ -0,0 +1,66 @@
+
+
+
+ Svelte DevTools
+
+ No Svelte app detected
+
+
+
+
+
+
+
+
diff --git a/workspace/extension/src/routes/Inspector.svelte b/workspace/extension/src/routes/Inspector.svelte
new file mode 100644
index 0000000..ff7ca08
--- /dev/null
+++ b/workspace/extension/src/routes/Inspector.svelte
@@ -0,0 +1,22 @@
+
+
+
diff --git a/workspace/extension/src/routes/ProfileButton.svelte b/workspace/extension/src/routes/ProfileButton.svelte
new file mode 100644
index 0000000..75951f1
--- /dev/null
+++ b/workspace/extension/src/routes/ProfileButton.svelte
@@ -0,0 +1,18 @@
+
+
+
diff --git a/workspace/extension/src/routes/Profiler.svelte b/workspace/extension/src/routes/Profiler.svelte
new file mode 100644
index 0000000..5b99c4b
--- /dev/null
+++ b/workspace/extension/src/routes/Profiler.svelte
@@ -0,0 +1,133 @@
+
+
+
+ {#if top}
+
+ {:else}
+
+ {/if}
+
+
+
+ {#if children.length}
+
{
+ if (selected === frame) top = frame;
+ else selected = frame;
+ }}
+ />
+ {:else}
+ Nothing to display. Perform an action or refresh the page.
+ {/if}
+
+{#if selected}
+
+
+
+ Tag name
+ {selected.node.tagName}
+ (#{selected.node.id})
+
+
+ Start
+ {round(selected.start)}ms
+
+
+ Operation
+ {selected.type}
+
+
+ Block type
+ {selected.node.type}
+
+
+ End
+ {round(selected.end)}ms
+
+
+ Duration
+
+ {round(selected.children.reduce((acc, o) => acc - o.duration, selected.duration))}ms
+
+ of
+ {round(selected.duration)}ms
+
+
+
+{/if}
+
+
diff --git a/workspace/extension/src/routes/ProfilerFrame.svelte b/workspace/extension/src/routes/ProfilerFrame.svelte
new file mode 100644
index 0000000..c102163
--- /dev/null
+++ b/workspace/extension/src/routes/ProfilerFrame.svelte
@@ -0,0 +1,72 @@
+
+
+{#if children?.length}
+
+ {#each children as frame}
+ -
+
+
+ onclick(frame)} />
+
+ {/each}
+
+{/if}
+
+
diff --git a/workspace/extension/src/routes/SearchBox.svelte b/workspace/extension/src/routes/SearchBox.svelte
new file mode 100644
index 0000000..e305db3
--- /dev/null
+++ b/workspace/extension/src/routes/SearchBox.svelte
@@ -0,0 +1,98 @@
+
+
+
+
+
diff --git a/workspace/extension/src/routes/VisibilitySelection.svelte b/workspace/extension/src/routes/VisibilitySelection.svelte
new file mode 100644
index 0000000..d126ff9
--- /dev/null
+++ b/workspace/extension/src/routes/VisibilitySelection.svelte
@@ -0,0 +1,80 @@
+
+
+
+
+
+ {#if opened}
+
+ {/if}
+
+
+
diff --git a/workspace/extension/static/background.js b/workspace/extension/static/background.js
new file mode 100644
index 0000000..ce2d67a
--- /dev/null
+++ b/workspace/extension/static/background.js
@@ -0,0 +1,131 @@
+/** @type {Map} */
+const ports = new Map();
+
+chrome.runtime.onConnect.addListener((port) => {
+ if (port.sender?.url !== chrome.runtime.getURL('/index.html')) {
+ console.error(`Unexpected connection from ${port.sender?.url || ''}`);
+ return port.disconnect();
+ }
+
+ // messages are from the devtools page and not content script (courier.js)
+ port.onMessage.addListener((message, sender) => {
+ switch (message.type) {
+ case 'bypass::ext/init': {
+ ports.set(message.tabId, sender);
+ if (!chrome.tabs.onUpdated.hasListener(courier)) {
+ chrome.tabs.onUpdated.addListener(courier);
+ }
+ break;
+ }
+ case 'bypass::ext/page->refresh': {
+ chrome.tabs.reload(message.tabId, { bypassCache: true });
+ break;
+ }
+
+ default: // relay messages from devtools to tab
+ chrome.tabs.sendMessage(message.tabId, message);
+ }
+ });
+
+ port.onDisconnect.addListener((disconnected) => {
+ ports.delete(+disconnected.name);
+
+ if (ports.size === 0) {
+ chrome.tabs.onUpdated.removeListener(courier);
+ }
+ });
+});
+
+// relay messages from `chrome.scripting` to devtools page
+chrome.runtime.onMessage.addListener((message, sender) => {
+ if (sender.id !== chrome.runtime.id) return; // unexpected sender
+
+ if (message.type === 'bypass::ext/icon:set') {
+ const selected = message.payload ? 'default' : 'disabled';
+ const icons = [16, 24, 48, 96, 128].map((s) => [s, `icons/${selected}-${s}.png`]);
+ return chrome.action.setIcon({ path: Object.fromEntries(icons) });
+ }
+
+ const port = sender.tab?.id && ports.get(sender.tab.id);
+ if (port) return port.postMessage(message);
+});
+
+/** @type {Parameters[0]} */
+function courier(tabId, changed) {
+ if (!ports.has(tabId) || changed.status !== 'loading') return;
+
+ chrome.scripting.executeScript({
+ target: { tabId },
+
+ // ensures we're listening to the events before they're dispatched
+ injectImmediately: true,
+
+ // no lexical context, `func` is serialized and deserialized.
+ // a limbo world where both `chrome` and `window` are defined
+ // with many unexpected and out of the ordinary behaviors, do
+ // minimal work here and delegate to `courier.js` in the page.
+ // only a subset of APIs are available in this `chrome` limbo
+ // - chrome.csi->f()
+ // - chrome.dom.{openOrClosedShadowRoot->f()}
+ // - chrome.extension.{ViewType, inIncognitoContext}
+ // - chrome.i18n
+ // - chrome.runtime
+ func: () => {
+ chrome.runtime.onMessage.addListener((message, sender) => {
+ if (sender.id !== chrome.runtime.id) return; // unexpected sender
+ window.postMessage(message); // relay to content script (courier.js)
+ });
+
+ window.addEventListener('message', ({ source, data }) => {
+ // only accept messages from our application or script
+ if (source === window && data?.source === 'svelte-devtools') {
+ chrome.runtime.sendMessage(data);
+ }
+ });
+
+ window.addEventListener('unload', () => {
+ chrome.runtime.sendMessage({ type: 'bridge::ext/clear' });
+ });
+ },
+ });
+}
+
+chrome.tabs.onActivated.addListener(({ tabId }) => sensor(tabId));
+chrome.tabs.onUpdated.addListener(
+ (tabId, changed) => changed.status === 'complete' && sensor(tabId),
+);
+
+/** @param {number} tabId */
+async function sensor(tabId) {
+ try {
+ // add SvelteDevTools event listener
+ await chrome.scripting.executeScript({
+ target: { tabId },
+ func: () => {
+ document.addEventListener('SvelteDevTools', ({ detail }) => {
+ chrome.runtime.sendMessage(detail);
+ });
+ },
+ });
+ // capture data to send to listener
+ await chrome.scripting.executeScript({
+ target: { tabId },
+ world: 'MAIN',
+ func: () => {
+ // @ts-ignore - injected if the website is using svelte
+ const [major] = [...(window.__svelte?.v ?? [])];
+
+ document.dispatchEvent(
+ new CustomEvent('SvelteDevTools', {
+ detail: { type: 'bypass::ext/icon:set', payload: major },
+ }),
+ );
+ },
+ });
+ } catch {
+ // for internal URLs like `chrome://` or `edge://` and extension gallery
+ // https://chromium.googlesource.com/chromium/src/+/ee77a52baa1f8a98d15f9749996f90e9d3200f2d/chrome/common/extensions/chrome_extensions_client.cc#131
+ const icons = [16, 24, 48, 96, 128].map((s) => [s, `icons/disabled-${s}.png`]);
+ chrome.action.setIcon({ path: Object.fromEntries(icons) });
+ }
+}
diff --git a/workspace/extension/static/icons/default-128.png b/workspace/extension/static/icons/default-128.png
new file mode 100644
index 0000000..e6ee090
Binary files /dev/null and b/workspace/extension/static/icons/default-128.png differ
diff --git a/workspace/extension/static/icons/default-16.png b/workspace/extension/static/icons/default-16.png
new file mode 100644
index 0000000..9b7bb76
Binary files /dev/null and b/workspace/extension/static/icons/default-16.png differ
diff --git a/workspace/extension/static/icons/default-24.png b/workspace/extension/static/icons/default-24.png
new file mode 100644
index 0000000..b5eb83c
Binary files /dev/null and b/workspace/extension/static/icons/default-24.png differ
diff --git a/workspace/extension/static/icons/default-48.png b/workspace/extension/static/icons/default-48.png
new file mode 100644
index 0000000..543a150
Binary files /dev/null and b/workspace/extension/static/icons/default-48.png differ
diff --git a/workspace/extension/static/icons/default-96.png b/workspace/extension/static/icons/default-96.png
new file mode 100644
index 0000000..903efd5
Binary files /dev/null and b/workspace/extension/static/icons/default-96.png differ
diff --git a/workspace/extension/static/icons/disabled-128.png b/workspace/extension/static/icons/disabled-128.png
new file mode 100644
index 0000000..8df184a
Binary files /dev/null and b/workspace/extension/static/icons/disabled-128.png differ
diff --git a/workspace/extension/static/icons/disabled-16.png b/workspace/extension/static/icons/disabled-16.png
new file mode 100644
index 0000000..a3b737f
Binary files /dev/null and b/workspace/extension/static/icons/disabled-16.png differ
diff --git a/workspace/extension/static/icons/disabled-24.png b/workspace/extension/static/icons/disabled-24.png
new file mode 100644
index 0000000..d75f5a2
Binary files /dev/null and b/workspace/extension/static/icons/disabled-24.png differ
diff --git a/workspace/extension/static/icons/disabled-48.png b/workspace/extension/static/icons/disabled-48.png
new file mode 100644
index 0000000..602426f
Binary files /dev/null and b/workspace/extension/static/icons/disabled-48.png differ
diff --git a/workspace/extension/static/icons/disabled-96.png b/workspace/extension/static/icons/disabled-96.png
new file mode 100644
index 0000000..906eccb
Binary files /dev/null and b/workspace/extension/static/icons/disabled-96.png differ
diff --git a/dest/devtools/svelte-logo-dark.svg b/workspace/extension/static/icons/svelte-dark.svg
similarity index 100%
rename from dest/devtools/svelte-logo-dark.svg
rename to workspace/extension/static/icons/svelte-dark.svg
diff --git a/dest/devtools/svelte-logo-light.svg b/workspace/extension/static/icons/svelte-default.svg
similarity index 100%
rename from dest/devtools/svelte-logo-light.svg
rename to workspace/extension/static/icons/svelte-default.svg
diff --git a/workspace/extension/static/icons/svelte-disabled.svg b/workspace/extension/static/icons/svelte-disabled.svg
new file mode 100644
index 0000000..43dc3fb
--- /dev/null
+++ b/workspace/extension/static/icons/svelte-disabled.svg
@@ -0,0 +1,14 @@
+
+
diff --git a/src/svelte-logo.svg b/workspace/extension/static/icons/svelte.svg
similarity index 100%
rename from src/svelte-logo.svg
rename to workspace/extension/static/icons/svelte.svg
diff --git a/workspace/extension/static/manifest.json b/workspace/extension/static/manifest.json
new file mode 100644
index 0000000..55e14ed
--- /dev/null
+++ b/workspace/extension/static/manifest.json
@@ -0,0 +1,48 @@
+{
+ "manifest_version": 3,
+ "name": "Svelte DevTools",
+ "version": "2.2.2",
+ "description": "Browser DevTools extension for debugging Svelte applications.",
+ "icons": {
+ "16": "icons/default-16.png",
+ "24": "icons/default-24.png",
+ "48": "icons/default-48.png",
+ "96": "icons/default-96.png",
+ "128": "icons/default-128.png"
+ },
+
+ "action": {
+ "default_icon": {
+ "16": "icons/disabled-16.png",
+ "24": "icons/disabled-24.png",
+ "48": "icons/disabled-48.png",
+ "96": "icons/disabled-96.png",
+ "128": "icons/disabled-128.png"
+ }
+ },
+ "background": {
+ "scripts": ["background.js"],
+ "service_worker": "background.js"
+ },
+ "content_scripts": [
+ {
+ "matches": [""],
+ "js": ["courier.js"],
+ "run_at": "document_start",
+ "world": "MAIN"
+ }
+ ],
+ "devtools_page": "register.html",
+ "host_permissions": ["*://*/*"],
+ "permissions": ["activeTab", "scripting"],
+ "web_accessible_resources": [{ "matches": ["*://*/*"], "resources": ["courier.js"] }],
+
+ "minimum_chrome_version": "121",
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "firefox-devtools@svelte.dev",
+ "strict_min_version": "121.0",
+ "update_url": "https://svelte.dev/devtools/updates.json"
+ }
+ }
+}
diff --git a/workspace/extension/static/register.html b/workspace/extension/static/register.html
new file mode 100644
index 0000000..1b02225
--- /dev/null
+++ b/workspace/extension/static/register.html
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/workspace/extension/static/register.js b/workspace/extension/static/register.js
new file mode 100644
index 0000000..a33bdbc
--- /dev/null
+++ b/workspace/extension/static/register.js
@@ -0,0 +1,12 @@
+chrome.devtools.panels.create(
+ 'Svelte',
+ `icons/svelte-${chrome.devtools.panels.themeName}.svg`,
+ 'index.html',
+ // (panel) => {
+ // panel.onShown.addListener((win) =>
+ // chrome.devtools.inspectedWindow.eval('$0', (payload) =>
+ // win.postMessage({ source: 'svelte-devtools', type: 'bridge::ext/inspect', payload }),
+ // ),
+ // );
+ // },
+);
diff --git a/workspace/extension/svelte.config.js b/workspace/extension/svelte.config.js
new file mode 100644
index 0000000..58e94ac
--- /dev/null
+++ b/workspace/extension/svelte.config.js
@@ -0,0 +1,14 @@
+import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
+
+export default {
+ compilerOptions: {
+ runes: true,
+ },
+
+ preprocess: vitePreprocess(),
+
+ onwarn(warning, handler) {
+ if (warning.message.includes('A11y')) return;
+ !warning.message.includes('chrome') && handler(warning);
+ },
+};
diff --git a/workspace/extension/tsconfig.json b/workspace/extension/tsconfig.json
new file mode 100644
index 0000000..10ae081
--- /dev/null
+++ b/workspace/extension/tsconfig.json
@@ -0,0 +1,35 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+
+ "checkJs": true,
+ "strict": true,
+ "composite": true,
+ "noEmit": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noImplicitReturns": true,
+
+ "skipLibCheck": true,
+ "isolatedModules": true,
+ "useDefineForClassFields": true,
+ "forceConsistentCasingInFileNames": true,
+
+ "paths": {
+ "$lib": ["./src/lib"],
+ "$lib/*": ["./src/lib/*"]
+ }
+ },
+ "include": [
+ "vite.config.ts",
+ "src/**/*.d.ts",
+ "src/**/*.ts",
+ "src/**/*.js",
+ "src/**/*.svelte",
+ "static/**/*.js"
+ ],
+ "exclude": ["static/courier.js"]
+}
diff --git a/workspace/extension/vite.config.ts b/workspace/extension/vite.config.ts
new file mode 100644
index 0000000..fde691d
--- /dev/null
+++ b/workspace/extension/vite.config.ts
@@ -0,0 +1,22 @@
+import { resolve } from 'node:path';
+import { svelte } from '@sveltejs/vite-plugin-svelte';
+import { defineConfig } from 'vite';
+
+export default defineConfig(() => {
+ return {
+ plugins: [svelte()],
+
+ build: {
+ cssTarget: 'chrome111',
+ outDir: 'build',
+ },
+
+ publicDir: 'static',
+
+ resolve: {
+ alias: {
+ $lib: resolve(__dirname, 'src/lib'),
+ },
+ },
+ };
+});
diff --git a/zen b/zen
deleted file mode 100644
index 3c6d0dc..0000000
--- a/zen
+++ /dev/null
@@ -1,404 +0,0 @@
-position
-top
-right
-bottom
-left
-z-index
-display
-visibility
-float
-clear
-overflow
--ms-overflow-x
--ms-overflow-y
-overflow-x
-overflow-y
--webkit-overflow-scrolling
-clip
--webkit-align-content
--ms-flex-line-pack
-align-content
--webkit-box-align
--moz-box-align
--webkit-align-items
-align-items
--ms-flex-align
--webkit-align-self
--ms-flex-item-align
--ms-grid-row-align
-align-self
--webkit-box-flex
--webkit-flex
--moz-box-flex
--ms-flex
-flex
--webkit-flex-flow
--ms-flex-flow
-flex-flow
--webkit-flex-basis
--ms-flex-preferred-size
-flex-basis
--webkit-box-orient
--webkit-box-direction
--webkit-flex-direction
--moz-box-orient
--moz-box-direction
--ms-flex-direction
-flex-direction
--webkit-flex-grow
--ms-flex-positive
-flex-grow
--webkit-flex-shrink
--ms-flex-negative
-flex-shrink
--webkit-flex-wrap
--ms-flex-wrap
-flex-wrap
--webkit-box-pack
--moz-box-pack
--ms-flex-pack
--webkit-justify-content
-justify-content
--webkit-box-ordinal-group
--webkit-order
--moz-box-ordinal-group
--ms-flex-order
-order
--webkit-box-sizing
--moz-box-sizing
-box-sizing
-margin
-margin-top
-margin-right
-margin-bottom
-margin-left
-padding
-padding-top
-padding-right
-padding-bottom
-padding-left
-min-width
-min-height
-max-width
-max-height
-width
-height
-outline
-outline-width
-outline-style
-outline-color
-outline-offset
-border
-border-spacing
-border-collapse
-border-width
-border-style
-border-color
-border-top
-border-top-width
-border-top-style
-border-top-color
-border-right
-border-right-width
-border-right-style
-border-right-color
-border-bottom
-border-bottom-width
-border-bottom-style
-border-bottom-color
-border-left
-border-left-width
-border-left-style
-border-left-color
--webkit-border-radius
--moz-border-radius
-border-radius
--webkit-border-top-left-radius
--moz-border-radius-topleft
-border-top-left-radius
--webkit-border-top-right-radius
--moz-border-radius-topright
-border-top-right-radius
--webkit-border-bottom-right-radius
--moz-border-radius-bottomright
-border-bottom-right-radius
--webkit-border-bottom-left-radius
--moz-border-radius-bottomleft
-border-bottom-left-radius
--webkit-border-image
--moz-border-image
--o-border-image
-border-image
--webkit-border-image-source
--moz-border-image-source
--o-border-image-source
-border-image-source
--webkit-border-image-slice
--moz-border-image-slice
--o-border-image-slice
-border-image-slice
--webkit-border-image-width
--moz-border-image-width
--o-border-image-width
-border-image-width
--webkit-border-image-outset
--moz-border-image-outset
--o-border-image-outset
-border-image-outset
--webkit-border-image-repeat
--moz-border-image-repeat
--o-border-image-repeat
-border-image-repeat
--webkit-border-top-image
--moz-border-top-image
--o-border-top-image
-border-top-image
--webkit-border-right-image
--moz-border-right-image
--o-border-right-image
-border-right-image
--webkit-border-bottom-image
--moz-border-bottom-image
--o-border-bottom-image
-border-bottom-image
--webkit-border-left-image
--moz-border-left-image
--o-border-left-image
-border-left-image
--webkit-border-corner-image
--moz-border-corner-image
--o-border-corner-image
-border-corner-image
--webkit-border-top-left-image
--moz-border-top-left-image
--o-border-top-left-image
-border-top-left-image
--webkit-border-top-right-image
--moz-border-top-right-image
--o-border-top-right-image
-border-top-right-image
--webkit-border-bottom-right-image
--moz-border-bottom-right-image
--o-border-bottom-right-image
-border-bottom-right-image
--webkit-border-bottom-left-image
--moz-border-bottom-left-image
--o-border-bottom-left-image
-border-bottom-left-image
-background
-filter:progid:DXImageTransform.Microsoft.AlphaImageLoader
-background-color
-background-image
-background-attachment
-background-position
--ms-background-position-x
--ms-background-position-y
-background-position-x
-background-position-y
--webkit-background-clip
--moz-background-clip
-background-clip
-background-origin
--webkit-background-size
--moz-background-size
--o-background-size
-background-size
-background-repeat
-box-decoration-break
--webkit-box-shadow
--moz-box-shadow
-box-shadow
-color
-table-layout
-caption-side
-empty-cells
-list-style
-list-style-position
-list-style-type
-list-style-image
-quotes
-content
-counter-increment
-counter-reset
--ms-writing-mode
-vertical-align
-text-align
--webkit-text-align-last
--moz-text-align-last
--ms-text-align-last
-text-align-last
-text-decoration
-text-emphasis
-text-emphasis-position
-text-emphasis-style
-text-emphasis-color
-text-indent
--ms-text-justify
-text-justify
-text-outline
-text-transform
-text-wrap
--ms-text-overflow
-text-overflow
-text-overflow-ellipsis
-text-overflow-mode
-text-shadow
-white-space
-word-spacing
--ms-word-wrap
-word-wrap
--ms-word-break
-word-break
--moz-tab-size
--o-tab-size
-tab-size
--webkit-hyphens
--moz-hyphens
-hyphens
-letter-spacing
-font
-font-weight
-font-style
-font-variant
-font-size-adjust
-font-stretch
-font-size
-font-family
-src
-line-height
-opacity
--ms-filter:\\'progid:DXImageTransform.Microsoft.Alpha
-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity
--ms-interpolation-mode
--webkit-filter
--ms-filter
-filter
-resize
-cursor
-nav-index
-nav-up
-nav-right
-nav-down
-nav-left
--webkit-transition
--moz-transition
--ms-transition
--o-transition
-transition
--webkit-transition-delay
--moz-transition-delay
--ms-transition-delay
--o-transition-delay
-transition-delay
--webkit-transition-timing-function
--moz-transition-timing-function
--ms-transition-timing-function
--o-transition-timing-function
-transition-timing-function
--webkit-transition-duration
--moz-transition-duration
--ms-transition-duration
--o-transition-duration
-transition-duration
--webkit-transition-property
--moz-transition-property
--ms-transition-property
--o-transition-property
-transition-property
--webkit-transform
--moz-transform
--ms-transform
--o-transform
-transform
--webkit-transform-origin
--moz-transform-origin
--ms-transform-origin
--o-transform-origin
-transform-origin
--webkit-animation
--moz-animation
--ms-animation
--o-animation
-animation
--webkit-animation-name
--moz-animation-name
--ms-animation-name
--o-animation-name
-animation-name
--webkit-animation-duration
--moz-animation-duration
--ms-animation-duration
--o-animation-duration
-animation-duration
--webkit-animation-play-state
--moz-animation-play-state
--ms-animation-play-state
--o-animation-play-state
-animation-play-state
--webkit-animation-timing-function
--moz-animation-timing-function
--ms-animation-timing-function
--o-animation-timing-function
-animation-timing-function
--webkit-animation-delay
--moz-animation-delay
--ms-animation-delay
--o-animation-delay
-animation-delay
--webkit-animation-iteration-count
--moz-animation-iteration-count
--ms-animation-iteration-count
--o-animation-iteration-count
-animation-iteration-count
--webkit-animation-direction
--moz-animation-direction
--ms-animation-direction
--o-animation-direction
-animation-direction
-pointer-events
-unicode-bidi
-direction
--webkit-columns
--moz-columns
-columns
--webkit-column-span
--moz-column-span
-column-span
--webkit-column-width
--moz-column-width
-column-width
--webkit-column-count
--moz-column-count
-column-count
--webkit-column-fill
--moz-column-fill
-column-fill
--webkit-column-gap
--moz-column-gap
-column-gap
--webkit-column-rule
--moz-column-rule
-column-rule
--webkit-column-rule-width
--moz-column-rule-width
-column-rule-width
--webkit-column-rule-style
--moz-column-rule-style
-column-rule-style
--webkit-column-rule-color
--moz-column-rule-color
-column-rule-color
-break-before
-break-inside
-break-after
-page-break-before
-page-break-inside
-page-break-after
-orphans
-widows
--ms-zoom
-zoom
-max-zoom
-min-zoom
-user-zoom
-orientation