├── .gitignore ├── Readme.md ├── app ├── .gitignore ├── .vscode │ └── extensions.json ├── helper │ ├── helper.ts │ ├── stringify.ts │ └── tsconfig.json ├── package-lock.json ├── package.json ├── rollup.config.js ├── src │ ├── App.svelte │ ├── Components │ │ ├── Container.svelte │ │ ├── Details.svelte │ │ ├── Details │ │ │ ├── CollapsableValue.svelte │ │ │ ├── Editable.svelte │ │ │ ├── PropertyList.svelte │ │ │ └── nodes │ │ │ │ └── Collapse.svelte │ │ ├── LeftPane.svelte │ │ ├── List.svelte │ │ ├── Resize.svelte │ │ ├── RightPane.svelte │ │ ├── Statement.svelte │ │ ├── Summary.svelte │ │ └── Toolbar.svelte │ ├── main.ts │ ├── scss │ │ ├── _icons.scss │ │ ├── _json-diff.scss │ │ ├── _tablesort.scss │ │ ├── _theme.scss │ │ └── global.scss │ ├── store.ts │ ├── types.ts │ └── utils.ts └── tsconfig.json ├── extension ├── devtools │ ├── background.js │ ├── content.js │ ├── devtools.html │ ├── devtools.js │ ├── panel.html │ ├── svelte-logo-dark.svg │ └── svelte-logo-light.svg ├── dist │ ├── .gitignore │ └── icons │ │ ├── largeIcons-on.svg │ │ ├── largeIcons.svg │ │ └── mediumIcons.svg └── manifest.json ├── images ├── chrome_32x32.png ├── firefox_32x32.png ├── list.png └── summary.png ├── logo └── logo.svg ├── package.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | web-ext-artifacts 3 | *.map 4 | extension/devtools/helper.js 5 | extension/icons 6 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Svelte Reactive Debugger 2 | Easily monitor svelte reactive statements 3 | 4 | This extension needs this custom preprocessor to work 5 | https://github.com/unlocomqx/svelte-reactive-preprocessor 6 | 7 | ## Install (Chrome/Firefox) 8 | [![](images/chrome_32x32.png)](https://chrome.google.com/webstore/detail/svelte-reactive-debugger/mieppkcamgfhpjedhnfdlbndijhohmjf) 9 | [![](images/firefox_32x32.png)](https://addons.mozilla.org/en-US/firefox/addon/svelte-reactive-debugger/) 10 | 11 | ## List 12 | The extension displays the statements as they are executed 13 | 14 | ![panel preview](images/list.png) 15 | 16 | ## Summary 17 | The statements are grouped to show the total count and duration of each statement 18 | 19 | ![panel preview](images/summary.png) 20 | 21 | # How to build 22 | ```shell 23 | npm i && npm i --prefix app 24 | npm run build 25 | ``` 26 | _Built using node 15.4.0 and npm 7.0.15_ 27 | 28 | # How to test (requires Firefox) 29 | ```shell 30 | npm run start 31 | ``` 32 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /public/build/ 3 | 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /app/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["svelte.svelte-vscode"] 3 | } 4 | -------------------------------------------------------------------------------- /app/helper/helper.ts: -------------------------------------------------------------------------------- 1 | import { stringify } from "./stringify"; 2 | 3 | (window as any).rpDsp = function (type, detail, start_time, exec_id, start_state, end_state) { 4 | detail = detail || {}; 5 | detail.start_time = start_time; 6 | detail.exec_id = exec_id; 7 | detail.start_state = stringify(start_state); 8 | detail.end_state = stringify(end_state); 9 | const ev = new CustomEvent(type, { detail }); 10 | document.dispatchEvent(ev); 11 | }; 12 | -------------------------------------------------------------------------------- /app/helper/stringify.ts: -------------------------------------------------------------------------------- 1 | export function stringify(obj, replacer?, spaces?, cycleReplacer?) { 2 | 3 | function serializer(replacer, cycleReplacer) { 4 | var stack = [], keys = []; 5 | 6 | if (cycleReplacer == null) { 7 | cycleReplacer = function (key, value) { 8 | if (stack[0] === value) { 9 | return "[Circular ~]"; 10 | } 11 | return "[Circular ~." + keys.slice(0, stack.indexOf(value)).join(".") + "]"; 12 | }; 13 | } 14 | 15 | return function (key, value) { 16 | if (stack.length > 0) { 17 | var thisPos = stack.indexOf(this); 18 | ~thisPos ? stack.splice(thisPos + 1) : stack.push(this); 19 | ~thisPos ? keys.splice(thisPos, Infinity, key) : keys.push(key); 20 | if (~stack.indexOf(value)) { 21 | value = cycleReplacer.call(this, key, value); 22 | } 23 | } else { 24 | stack.push(value); 25 | } 26 | 27 | // filter rpGlobal from dispatched state (being equal to window, we want to avoid seriliazing it) 28 | if (key === "rpGlobal") { 29 | return undefined; 30 | } 31 | // filter functions 32 | if (typeof value === "function") { 33 | return undefined; 34 | } 35 | // filter svelte stores 36 | if (value && typeof value.subscribe === "function") { 37 | return undefined; 38 | } 39 | 40 | if (value instanceof Element) { 41 | return undefined; 42 | } 43 | 44 | return replacer == null ? value : replacer.call(this, key, value); 45 | }; 46 | } 47 | 48 | return JSON.stringify(obj, serializer(replacer, cycleReplacer), spaces); 49 | } 50 | -------------------------------------------------------------------------------- /app/helper/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "moduleResolution": "node", 5 | "allowSyntheticDefaultImports": true 6 | }, 7 | "include": [ 8 | "**/*" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-app", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "build": "rollup -c", 6 | "dev": "rollup -c -w", 7 | "start": "sirv ../extension/dist", 8 | "validate": "svelte-check" 9 | }, 10 | "devDependencies": { 11 | "@rollup/plugin-commonjs": "^17.0.0", 12 | "@rollup/plugin-node-resolve": "^11.0.1", 13 | "@rollup/plugin-typescript": "^8.1.0", 14 | "@tsconfig/svelte": "^1.0.10", 15 | "@types/chrome": "^0.0.128", 16 | "node-sass": "^7.0.1", 17 | "rollup": "^2.36.1", 18 | "rollup-plugin-css-only": "^3.1.0", 19 | "rollup-plugin-livereload": "^2.0.0", 20 | "rollup-plugin-scss": "^2.6.1", 21 | "rollup-plugin-svelte": "^7.0.0", 22 | "rollup-plugin-terser": "^7.0.2", 23 | "svelte": "^3.31.2", 24 | "svelte-check": "^1.1.26", 25 | "svelte-preprocess": "^4.6.1", 26 | "tslib": "^2.1.0", 27 | "typescript": "^4.1.3" 28 | }, 29 | "dependencies": { 30 | "copy-to-clipboard": "^3.3.1", 31 | "deep-object-diff": "^1.1.0", 32 | "jsondiffpatch": "^0.4.1", 33 | "sirv-cli": "^1.0.10", 34 | "svelte-local-storage-store": "^0.1.4", 35 | "svelte-tablesort": "github:unlocomqx/svelte-tablesort" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/rollup.config.js: -------------------------------------------------------------------------------- 1 | import svelte from "rollup-plugin-svelte"; 2 | import commonjs from "@rollup/plugin-commonjs"; 3 | import resolve from "@rollup/plugin-node-resolve"; 4 | import css from "rollup-plugin-css-only"; 5 | import sveltePreprocess from "svelte-preprocess"; 6 | import typescript from "@rollup/plugin-typescript"; 7 | import scss from "rollup-plugin-scss"; 8 | import livereload from "rollup-plugin-livereload"; 9 | 10 | const production = !process.env.ROLLUP_WATCH; 11 | 12 | function serve() { 13 | let server; 14 | 15 | function toExit() { 16 | if (server) { 17 | server.kill(0); 18 | } 19 | } 20 | 21 | return { 22 | writeBundle() { 23 | if (server) { 24 | return; 25 | } 26 | server = require("child_process").spawn("npm", ["run", "start", "--", "--dev"], { 27 | stdio: ["ignore", "inherit", "inherit"], 28 | shell: true 29 | }); 30 | 31 | process.on("SIGTERM", toExit); 32 | process.on("exit", toExit); 33 | } 34 | }; 35 | } 36 | 37 | const svelteCompile = { 38 | input: "src/main.ts", 39 | output: { 40 | sourcemap: true, 41 | format: "iife", 42 | name: "app", 43 | file: "../extension/dist/build/bundle.js", 44 | globals: {chrome: "chrome"}, 45 | }, 46 | external: ["chrome"], 47 | plugins: [ 48 | svelte({ 49 | preprocess: sveltePreprocess(), 50 | compilerOptions: { 51 | // enable run-time checks when not in production 52 | dev: !production 53 | } 54 | }), 55 | // we'll extract any component CSS out into 56 | // a separate file - better for performance 57 | css({output: "bundle.css"}), 58 | 59 | // If you have external dependencies installed from 60 | // npm, you'll most likely need these plugins. In 61 | // some cases you'll need additional configuration - 62 | // consult the documentation for details: 63 | // https://github.com/rollup/plugins/tree/master/packages/commonjs 64 | resolve({ 65 | browser: true, 66 | dedupe: importee => importee === "svelte" || importee.startsWith("svelte/") 67 | }), 68 | commonjs(), 69 | typescript({ 70 | sourceMap: true, 71 | inlineSources: !production 72 | }), 73 | scss({ 74 | output: '../extension/dist/build/global.css', 75 | }), 76 | 77 | // Watch the `public` directory and refresh the 78 | // browser on changes when not in production 79 | !production && livereload("../dist"), 80 | 81 | // In dev mode, call `npm run start` once 82 | // the bundle has been generated 83 | !production && serve(), 84 | ], 85 | watch: { 86 | clearScreen: false 87 | } 88 | }; 89 | 90 | const helperCompile = { 91 | input: "helper/helper.ts", 92 | output: { 93 | sourcemap: true, 94 | format: "iife", 95 | file: "../extension/devtools/helper.js", 96 | }, 97 | plugins: [ 98 | resolve({ 99 | extensions: [".js", ".jsx", ".ts", ".tsx"], 100 | browser: true, 101 | }), 102 | commonjs(), 103 | typescript({ 104 | tsconfig: "helper/tsconfig.json", 105 | sourceMap: true, 106 | inlineSources: !production 107 | }), 108 | ] 109 | }; 110 | 111 | export default [svelteCompile, helperCompile]; 112 | -------------------------------------------------------------------------------- /app/src/App.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 |
18 | {#if $dbg_store.debugger_enabled} 19 | 20 | {#if !$dbg_store.tab_connected} 21 |

22 | Reload the page to connect to the svelte reactive debugger 23 |

24 | {:else} 25 | 26 | 27 | {#if $pref_store.group_statements} 28 | 29 | {:else} 30 | 31 | {/if} 32 | 33 | 34 | {#if $ui_store.show_details} 35 |
36 | {/if} 37 | 38 | 39 | {/if} 40 | {:else} 41 |
42 |
43 | Svelte RD not detected, follow the steps described here to enable it 44 |
45 | 50 |
51 | {/if} 52 |
53 | 54 | 60 | -------------------------------------------------------------------------------- /app/src/Components/Container.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 | 7 |
8 | 9 | -------------------------------------------------------------------------------- /app/src/Components/Details.svelte: -------------------------------------------------------------------------------- 1 | 35 | 36 |
37 |
38 | State 39 |
40 |
41 | {#if !start_state && !end_state} 42 | No state was detected, here are the checks to make 43 | 47 | {/if} 48 | {#if diffHtml} 49 |
50 | Changes 51 |
52 | {@html diffHtml} 53 |
54 |
55 | {/if} 56 | {#if endStateObj} 57 |
58 | 62 |
63 | {/if} 64 | 65 | {#if startEntries} 66 |
67 | 71 |
72 | {/if} 73 |
74 | 75 |
76 | 77 | 104 | -------------------------------------------------------------------------------- /app/src/Components/Details/CollapsableValue.svelte: -------------------------------------------------------------------------------- 1 | 35 | 36 |
  • (collapsed = !collapsed)}> 38 | {#if type == 'string'} 39 | {key}:  40 | 41 | {:else if value == null || value == undefined || value != value} 42 | {key}:  43 | 44 | {:else if type == 'number' || type == 'boolean'} 45 | {key}:  46 | 47 | {:else if Array.isArray(value)} 48 | {#if value.length} 49 | 50 | {key}:  51 | Array [{value.length}] 52 | {#if !collapsed} 53 |
      54 | {#each value as v, key} 55 | dispatch('change', stringify(value, key, e.detail))} /> 60 | {/each} 61 |
    62 | {/if} 63 | {:else} 64 | {key}:  65 | Array [] 66 | {/if} 67 | {:else if type == 'object'} 68 | {#if value.__isFunction} 69 | 70 | {key}:  71 | function {value.name || ''} () 72 | {#if !collapsed} 73 |
    {value.source}
    74 | {/if} 75 | {:else if value.__isSymbol} 76 | {key}:  77 | {value.name || 'Symbol()'} 78 | {:else if Object.keys(value).length} 79 | 80 | {key}:  81 | Object {…} 82 | {#if !collapsed} 83 |
      84 | {#each Object.entries(value) as [key, v] (key)} 85 | dispatch('change', stringify(value, key, e.detail))} /> 90 | {/each} 91 |
    92 | {/if} 93 | {:else} 94 | {key}:  95 | Object { } 96 | {/if} 97 | {/if} 98 |
  • 99 | 100 | 147 | -------------------------------------------------------------------------------- /app/src/Components/Details/Editable.svelte: -------------------------------------------------------------------------------- 1 | 20 | 21 | 36 | 37 | {#if isEditing} 38 | e.key == 'Enter' && commit(e)} 42 | on:blur={commit} /> 43 | {:else} 44 | (isEditing = !readOnly)}> 48 | {JSON.stringify(value)} 49 | 50 | {/if} 51 | -------------------------------------------------------------------------------- /app/src/Components/Details/PropertyList.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 25 | 26 | {#if header} 27 | {header} 28 | {/if} 29 | 30 | {#if entries.length} 31 | 39 | {:else} 40 |
    None
    41 | {/if} 42 | -------------------------------------------------------------------------------- /app/src/Components/Details/nodes/Collapse.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 48 | 49 | (collapsed = !collapsed)} /> 54 | -------------------------------------------------------------------------------- /app/src/Components/LeftPane.svelte: -------------------------------------------------------------------------------- 1 |
    2 | 3 |
    -------------------------------------------------------------------------------- /app/src/Components/List.svelte: -------------------------------------------------------------------------------- 1 | 57 | 58 | {#if $ev_store.length > 0} 59 |
    60 | 61 | 62 | 63 | Statements ({items.length}) 64 | 65 | Duration 66 | Start time 67 | 68 | 69 | showDetails(ev, item)}> 73 | 74 | 76 | 77 | {item.duration} 78 | {time(item.start_time)} 79 | 80 | 81 |
    82 | {/if} 83 | 84 | 102 | -------------------------------------------------------------------------------- /app/src/Components/Resize.svelte: -------------------------------------------------------------------------------- 1 | 30 | 31 | 32 | 33 |
    34 | 35 | -------------------------------------------------------------------------------- /app/src/Components/RightPane.svelte: -------------------------------------------------------------------------------- 1 |
    2 | 3 |
    -------------------------------------------------------------------------------- /app/src/Components/Statement.svelte: -------------------------------------------------------------------------------- 1 | 25 | 26 | 28 | {has_changes ? '*' : ''} {statement} 29 | 30 | 31 | 41 | -------------------------------------------------------------------------------- /app/src/Components/Summary.svelte: -------------------------------------------------------------------------------- 1 | 49 | 50 | {#if statements.size > 0} 51 |
    52 | 53 | 54 | Statements ({items.length}) 55 | Count 56 | Duration 57 | 58 | 59 | 60 | 61 | 62 | 63 | {#key item.count} 64 |
    {item.count}
    65 | {/key} 66 | 67 | {item.duration} 68 | 69 |
    70 |
    71 | {/if} 72 | -------------------------------------------------------------------------------- /app/src/Components/Toolbar.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 |
    20 |
    43 | 44 |
    45 | 46 | {#if !$pref_store.group_statements} 47 | 48 | 49 | 50 | 51 | {/if} 52 | 53 | 54 | 91 | -------------------------------------------------------------------------------- /app/src/main.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import { devtools, runtime } from "chrome" 3 | import { stringify } from "javascript-stringify" 4 | import { get } from "svelte/store" 5 | import App from "./App.svelte" 6 | import { dbg_store, ev_store, pref_store, ui_store } from "./store" 7 | import type { ReactiveEvent } from "./types" 8 | 9 | const prefs = get(pref_store) 10 | 11 | function setDarkMode (theme) { 12 | if (theme == "dark") { 13 | document.body.classList.add("dark") 14 | } else { 15 | document.body.classList.remove("dark") 16 | } 17 | } 18 | 19 | setDarkMode(chrome.devtools.panels.themeName) 20 | if (devtools.panels.onThemeChanged) { 21 | devtools.panels.onThemeChanged.addListener(setDarkMode) 22 | } 23 | 24 | // Create a connection to the background page 25 | var backgroundPort = runtime.connect() 26 | 27 | setTimeout(() => { 28 | backgroundPort.postMessage({ 29 | name : "init", 30 | tabId: devtools.inspectedWindow.tabId 31 | }) 32 | }, 1000) 33 | 34 | backgroundPort.onMessage.addListener(function (request) { 35 | if (request.type === "SvelteReactiveEnable" || request.type === "SvelteReactiveStart") { 36 | dbg_store.setProp("tab_connected", true) 37 | dbg_store.setProp("debugger_enabled", true) 38 | } 39 | 40 | if (request.type === "SvelteReactiveEnd") { 41 | const now = Date.now() 42 | let reactiveEvent: ReactiveEvent = request.detail 43 | const duration = now - reactiveEvent.start_time 44 | 45 | let hasDiff = stringify(reactiveEvent.start_state) !== stringify(reactiveEvent.end_state) 46 | 47 | if (!dbg_store.getProp("paused")) { 48 | ev_store.insertEvent({ 49 | ...reactiveEvent, 50 | duration, 51 | has_changes: hasDiff, 52 | }) 53 | } 54 | } 55 | 56 | if (request.type === "Reload") { 57 | if (!prefs.preserve_log) { 58 | ui_store.setProp("show_details", false) 59 | ui_store.setProp("inspected_item", null) 60 | ev_store.clear() 61 | } 62 | } 63 | }) 64 | 65 | new App({ 66 | target: document.body 67 | }) 68 | -------------------------------------------------------------------------------- /app/src/scss/_icons.scss: -------------------------------------------------------------------------------- 1 | .icon { 2 | display: inline-block; 3 | width: 24px; 4 | height: 24px; 5 | border: none; 6 | background-color: transparent; 7 | background-image: url(/dist/icons/largeIcons.svg); 8 | cursor: pointer; 9 | outline: none; 10 | } 11 | 12 | .icon.medium { 13 | width: 16px; 14 | height: 16px; 15 | background-image: url(/dist/icons/mediumIcons.svg); 16 | } 17 | 18 | .icon.on { 19 | background-image: url(/dist/icons/largeIcons-on.svg); 20 | } 21 | 22 | .icon.delete { 23 | background-position: 0px 144px; 24 | width: 28px; 25 | height: 24px; 26 | } 27 | 28 | .icon.pause { 29 | background-position: -56px 73px; 30 | width: 28px; 31 | height: 24px; 32 | } 33 | 34 | .icon.resume { 35 | background-position: -28px 49px; 36 | width: 28px; 37 | height: 24px; 38 | } 39 | 40 | .icon.medium.clear { 41 | background-position: -32px 32px; 42 | } 43 | -------------------------------------------------------------------------------- /app/src/scss/_json-diff.scss: -------------------------------------------------------------------------------- 1 | .jsondiffpatch-unchanged { 2 | display: none; 3 | } 4 | 5 | .jsondiffpatch-delta { 6 | font-family: inherit !important; 7 | font-size: inherit !important; 8 | padding: 0 !important; 9 | } 10 | 11 | .jsondiffpatch-delta ul { 12 | white-space: nowrap; 13 | } 14 | 15 | .jsondiffpatch-delta pre { 16 | font-family: inherit !important; 17 | font-size: inherit !important; 18 | } 19 | 20 | .jsondiffpatch-added .jsondiffpatch-property-name, 21 | .jsondiffpatch-added .jsondiffpatch-value pre, 22 | .jsondiffpatch-modified .jsondiffpatch-right-value pre, 23 | .jsondiffpatch-textdiff-added { 24 | background: #8BC34A !important; 25 | color: #000; 26 | } 27 | 28 | .jsondiffpatch-deleted .jsondiffpatch-property-name, 29 | .jsondiffpatch-deleted pre, 30 | .jsondiffpatch-modified .jsondiffpatch-left-value pre, 31 | .jsondiffpatch-textdiff-deleted { 32 | background: #f44336 !important; 33 | color: #fff; 34 | } 35 | -------------------------------------------------------------------------------- /app/src/scss/_tablesort.scss: -------------------------------------------------------------------------------- 1 | table.tablesort { 2 | -table-layout: fixed; 3 | width: 100%; 4 | border-collapse: collapse; 5 | border-spacing: 0; 6 | border: 1px solid var(--border-color); 7 | } 8 | 9 | table.tablesort th, table.tablesort td { 10 | padding: 3px 5px; 11 | } 12 | 13 | table.tablesort th { 14 | text-align: left; 15 | white-space: pre; 16 | border-bottom: 1px solid var(--border-color); 17 | } 18 | 19 | table.tablesort th::after { 20 | content: ' '; 21 | white-space: pre; 22 | } 23 | 24 | table.tablesort th.ascending::after { 25 | content: ' \2191'; 26 | } 27 | 28 | table.tablesort th.descending::after { 29 | content: ' \2193'; 30 | } 31 | 32 | table.tablesort tbody tr:nth-child(even) { 33 | background-color: var(--bg-alt-color); 34 | } 35 | -------------------------------------------------------------------------------- /app/src/scss/_theme.scss: -------------------------------------------------------------------------------- 1 | $text-color-light: #3A3A3A; 2 | $bg-color-light: #FFF; 3 | $bg-alt-color-light: #f5f5f5; 4 | $border-color-light: #CDCDCD; 5 | 6 | $text-color-dark: #B1B1B3; 7 | $bg-color-dark: #2A2A2E; 8 | $bg-alt-color-dark: #242424; 9 | $border-color-dark: #3A3A3A; 10 | 11 | :root { 12 | --text-color: #{$text-color-light}; 13 | --bg-color: #{$bg-color-light}; 14 | --bg-alt-color: #{$bg-alt-color-light}; 15 | --border-color: #{$border-color-light}; 16 | } 17 | 18 | .dark { 19 | --text-color: #{$text-color-dark}; 20 | --bg-color: #{$bg-color-dark}; 21 | --bg-alt-color: #{$bg-alt-color-dark}; 22 | --border-color: #{$border-color-dark}; 23 | } 24 | 25 | $highlight-color: #FF3E00; 26 | -------------------------------------------------------------------------------- /app/src/scss/global.scss: -------------------------------------------------------------------------------- 1 | @import "theme"; 2 | @import "json-diff"; 3 | @import "tablesort"; 4 | @import "icons"; 5 | 6 | html, body { 7 | margin: 0; 8 | padding: 0; 9 | height: 100%; 10 | } 11 | 12 | body { 13 | background: var(--bg-color); 14 | color: var(--text-color); 15 | } 16 | 17 | .noselect { 18 | user-select: none; 19 | } 20 | 21 | a { 22 | color: #2196f3; 23 | } 24 | -------------------------------------------------------------------------------- /app/src/store.ts: -------------------------------------------------------------------------------- 1 | import { writable as localStorageWritable } from "svelte-local-storage-store"; 2 | import { get, writable } from "svelte/store"; 3 | import type { DbgStore, EventStore, PrefStore, UiStore } from "./types"; 4 | 5 | function createStore () { 6 | const { subscribe, update, set } = writable([]); 7 | 8 | return { 9 | subscribe, 10 | 11 | insertEvent (data) { 12 | return update(state => { 13 | state.push(data); 14 | return state; 15 | }); 16 | }, 17 | 18 | clear () { 19 | set([]); 20 | } 21 | }; 22 | } 23 | export const ev_store: EventStore = createStore(); 24 | 25 | 26 | function createPrefStore (): PrefStore { 27 | const { subscribe, update, set } = localStorageWritable("svrd_prefs", { 28 | group_statements: true, 29 | preserve_log : false, 30 | changes_only : false, 31 | filter_text : null, 32 | sort : { 33 | list : { 34 | name: "start_time", 35 | dir : "descending", 36 | }, 37 | summary: { 38 | name: "count", 39 | dir : "descending", 40 | }, 41 | }, 42 | details_width : null, 43 | }); 44 | 45 | return { 46 | subscribe, 47 | update, 48 | set, 49 | 50 | setPref (name, value) { 51 | return update(state => { 52 | state[name] = value; 53 | return state; 54 | }); 55 | }, 56 | }; 57 | } 58 | export const pref_store: PrefStore = createPrefStore(); 59 | 60 | 61 | function createDbgStore () { 62 | const { subscribe, set, update } = writable({ 63 | tab_connected : false, 64 | debugger_enabled: false, 65 | paused : false, 66 | }); 67 | 68 | let store = { 69 | subscribe, 70 | update, 71 | set, 72 | 73 | setProp (name, value) { 74 | return update(state => { 75 | state[name] = value; 76 | return state; 77 | }); 78 | }, 79 | 80 | getProp (name) { 81 | const store_data = get(store); 82 | return store_data[name]; 83 | }, 84 | }; 85 | return store; 86 | } 87 | export const dbg_store: DbgStore = createDbgStore(); 88 | 89 | 90 | function createUiStore (): UiStore { 91 | const { subscribe, update, set } = writable({ 92 | show_details : false, 93 | inspected_item: null, 94 | }); 95 | 96 | return { 97 | subscribe, 98 | set, 99 | update, 100 | 101 | setProp (name, value) { 102 | return update(state => { 103 | state[name] = value; 104 | return state; 105 | }); 106 | }, 107 | }; 108 | } 109 | export const ui_store: UiStore = createUiStore(); 110 | -------------------------------------------------------------------------------- /app/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { Readable, Writable } from "svelte/store"; 2 | 3 | export interface ReactiveEvent { 4 | id: string; 5 | statement: string; 6 | filename: string; 7 | line: number; 8 | start_time: number; 9 | start_state: string; 10 | end_state: string; 11 | exec_id: string; 12 | duration: number; 13 | has_changes: boolean; 14 | } 15 | 16 | export interface EventStore extends Readable { 17 | insertEvent (ev: ReactiveEvent); 18 | clear (); 19 | } 20 | 21 | export type Sort = { 22 | name: "statement" | "count" | "duration" | "start_time"; 23 | direction: "ascending" | "descending"; 24 | }; 25 | 26 | export interface PrefStoreData { 27 | group_statements: boolean; 28 | preserve_log: boolean; 29 | changes_only: boolean; 30 | filter_text: string; 31 | sort: { 32 | [name in "list" | "summary"]: Sort 33 | } 34 | details_width: number; 35 | } 36 | 37 | export interface PrefStore extends Writable { 38 | setPref (name: string, value: boolean); 39 | } 40 | 41 | export interface DbgStoreData { 42 | tab_connected: boolean; 43 | debugger_enabled: boolean; 44 | paused: boolean; 45 | } 46 | 47 | export interface DbgStore extends Writable { 48 | setProp (name: string, value: boolean); 49 | getProp (name: string); 50 | } 51 | 52 | export interface UiStoreData { 53 | show_details: boolean; 54 | inspected_item: ReactiveEvent; 55 | } 56 | 57 | export interface UiStore extends Writable { 58 | setProp (name: string, value: boolean); 59 | } 60 | -------------------------------------------------------------------------------- /app/src/utils.ts: -------------------------------------------------------------------------------- 1 | export function trigger (node: HTMLElement, event: string, data = null): boolean { 2 | const customEvent = new CustomEvent(event, { detail: data }); 3 | return node.dispatchEvent(customEvent); 4 | } 5 | -------------------------------------------------------------------------------- /app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/svelte/tsconfig.json", 3 | 4 | "include": ["src/**/*"], 5 | "exclude": ["node_modules/*", "__sapper__/*", "public/*"] 6 | } 7 | -------------------------------------------------------------------------------- /extension/devtools/background.js: -------------------------------------------------------------------------------- 1 | const isChrome = typeof browser == "undefined"; 2 | 3 | // background.js 4 | var connections = new Map(); 5 | 6 | chrome.runtime.onConnect.addListener(function (port) { 7 | 8 | var extensionListener = function (message) { 9 | 10 | // The original connection event doesn't include the tab ID of the 11 | // DevTools page, so we need to send it explicitly. 12 | if (message.name == "init") { 13 | connections.set(message.tabId, port); 14 | return; 15 | } 16 | 17 | }; 18 | 19 | // Listen to messages sent from the DevTools page 20 | port.onMessage.addListener(extensionListener); 21 | 22 | port.onDisconnect.addListener(function (port) { 23 | port.onMessage.removeListener(extensionListener); 24 | 25 | var tabs = Array.from(connections.keys()); 26 | for (var i = 0, len = tabs.length; i < len; i++) { 27 | if (connections.get(tabs[i]) == port) { 28 | connections.delete(tabs[i]); 29 | break; 30 | } 31 | } 32 | }); 33 | }); 34 | 35 | // Receive message from content script and relay to the devTools page for the 36 | // current tab 37 | chrome.runtime.onMessage.addListener(function (request, sender) { 38 | console.log({request}) 39 | // Messages from content scripts should have sender.tab set 40 | if (sender.tab) { 41 | var tabId = sender.tab.id; 42 | if (connections.has(tabId)) { 43 | console.log('post message', tabId, request); 44 | connections.get(tabId).postMessage(request); 45 | } else { 46 | console.log("Tab not found in connection list."); 47 | } 48 | } else { 49 | console.log("sender.tab not defined."); 50 | } 51 | return true; 52 | }); 53 | 54 | chrome.tabs.onUpdated.addListener((tabId, changed) => { 55 | if (!connections.has(tabId) || changed.status != "loading" || (isChrome && changed.url)) { 56 | return; 57 | } 58 | 59 | chrome.scripting.executeScript({ 60 | target: {tabId}, 61 | files: ["/devtools/content.js"], 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /extension/devtools/content.js: -------------------------------------------------------------------------------- 1 | if (!window.svrdAttached) { 2 | document.addEventListener("SvelteReactiveStart", function (ev) { 3 | post("SvelteReactiveStart", ev.detail); 4 | }); 5 | 6 | document.addEventListener("SvelteReactiveEnd", function (ev) { 7 | post("SvelteReactiveEnd", ev.detail); 8 | }); 9 | 10 | document.addEventListener("SvelteReactiveEnable", function () { 11 | post("SvelteReactiveEnable"); 12 | }); 13 | } 14 | 15 | window.svrdAttached = true; 16 | 17 | var connected = true; 18 | 19 | function post(type, detail) { 20 | if (!connected) { 21 | // avoid extension context invaluidated errors 22 | return; 23 | } 24 | chrome.runtime.sendMessage({ 25 | type, 26 | detail, 27 | }); 28 | } 29 | 30 | post("Reload"); 31 | 32 | var runtime_port = chrome.runtime.connect(); 33 | 34 | runtime_port.onDisconnect.addListener(function () { 35 | connected = false; 36 | }); 37 | 38 | function injectScript(file_path, tag) { 39 | var node = document.getElementsByTagName(tag)[0]; 40 | if (!node) { 41 | return; 42 | } 43 | var script = document.createElement('script'); 44 | script.setAttribute('type', 'text/javascript'); 45 | script.setAttribute('src', file_path); 46 | node.appendChild(script); 47 | } 48 | injectScript(chrome.runtime.getURL('devtools/helper.js'), 'head'); 49 | -------------------------------------------------------------------------------- /extension/devtools/devtools.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /extension/devtools/devtools.js: -------------------------------------------------------------------------------- 1 | // Create a tab in the devtools area 2 | chrome.devtools.panels.create( 3 | "Svelte RD", 4 | chrome.devtools.panels.themeName == "dark" 5 | ? "/devtools/svelte-logo-dark.svg" 6 | : "/devtools/svelte-logo-light.svg", 7 | "/devtools/panel.html", 8 | function (panel) { 9 | } 10 | ); 11 | -------------------------------------------------------------------------------- /extension/devtools/panel.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /extension/devtools/svelte-logo-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /extension/devtools/svelte-logo-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /extension/dist/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /extension/dist/icons/largeIcons-on.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 16 | 17 | 18 | 21 | 24 | 25 | 26 | 27 | 28 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 38 | 39 | 40 | 42 | 43 | 45 | 46 | 48 | 49 | 50 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 61 | 62 | 64 | 65 | 66 | 68 | 69 | 70 | 71 | 73 | 74 | 75 | 78 | 79 | 81 | 82 | 85 | 88 | 89 | 90 | 91 | 94 | 95 | 98 | 100 | 101 | 103 | 105 | 106 | 108 | 109 | 111 | 112 | 114 | 115 | 116 | 117 | 118 | 119 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 154 | 155 | 156 | 157 | 159 | 160 | 161 | 162 | 163 | 166 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 179 | 180 | 181 | 182 | 183 | 184 | 186 | a 187 | 188 | 190 | b 191 | 192 | 194 | c 195 | 196 | 198 | d 199 | 200 | 202 | e 203 | 204 | 206 | f 207 | 208 | 210 | g 211 | 212 | 214 | h 215 | 216 | 218 | 1 219 | 220 | 222 | 2 223 | 224 | 226 | 3 227 | 228 | 230 | 4 231 | 232 | 234 | 5 235 | 236 | 238 | 6 239 | 240 | 242 | 7 243 | 244 | 246 | 8 247 | 248 | 250 | 9 251 | 252 | 253 | 254 | 256 | 257 | 258 | 259 | 260 | 261 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 273 | 274 | 277 | 278 | 279 | 280 | 281 | 282 | 284 | i 285 | 286 | 288 | 9 289 | 290 | 292 | 295 | 296 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 306 | 307 | Sprites are deprecated, do not modify this file. 308 | 309 | 310 | See readme in front_end/Images for the new 311 | 312 | 313 | workflow. 314 | 315 | 316 | 317 | 318 | 320 | 321 | 322 | 324 | 326 | 327 | 329 | 330 | 331 | -------------------------------------------------------------------------------- /extension/dist/icons/largeIcons.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 16 | 17 | 18 | 21 | 24 | 25 | 26 | 27 | 28 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 38 | 39 | 40 | 42 | 43 | 45 | 46 | 48 | 49 | 50 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 61 | 62 | 64 | 65 | 66 | 68 | 69 | 70 | 71 | 73 | 74 | 75 | 78 | 79 | 81 | 82 | 85 | 88 | 89 | 90 | 91 | 94 | 95 | 98 | 100 | 101 | 103 | 105 | 106 | 108 | 109 | 111 | 112 | 114 | 115 | 116 | 117 | 118 | 119 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 154 | 155 | 156 | 157 | 159 | 160 | 161 | 162 | 163 | 166 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 179 | 180 | 181 | 182 | 183 | 184 | 186 | a 187 | 188 | 190 | b 191 | 192 | 194 | c 195 | 196 | 198 | d 199 | 200 | 202 | e 203 | 204 | 206 | f 207 | 208 | 210 | g 211 | 212 | 214 | h 215 | 216 | 218 | 1 219 | 220 | 222 | 2 223 | 224 | 226 | 3 227 | 228 | 230 | 4 231 | 232 | 234 | 5 235 | 236 | 238 | 6 239 | 240 | 242 | 7 243 | 244 | 246 | 8 247 | 248 | 250 | 9 251 | 252 | 253 | 254 | 256 | 257 | 258 | 259 | 260 | 261 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 273 | 274 | 277 | 278 | 279 | 280 | 281 | 282 | 284 | i 285 | 286 | 288 | 9 289 | 290 | 292 | 295 | 296 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 306 | 307 | Sprites are deprecated, do not modify this file. 308 | 309 | 310 | See readme in front_end/Images for the new 311 | 312 | 313 | workflow. 314 | 315 | 316 | 317 | 318 | 320 | 321 | 322 | 324 | 326 | 327 | 329 | 330 | -------------------------------------------------------------------------------- /extension/dist/icons/mediumIcons.svg: -------------------------------------------------------------------------------- 1 | 1234abcd5eAB6fgSprites are deprecated, do not modify this file.See readme in front_end/Images for the new workflow. -------------------------------------------------------------------------------- /extension/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Svelte Reactive Debugger", 3 | "version": "0.8.3", 4 | "description": "Monitor svelte reactive statements", 5 | "icons": { 6 | "16": "icons/icon-16.png", 7 | "24": "icons/icon-24.png", 8 | "32": "icons/icon-32.png", 9 | "48": "icons/icon-48.png", 10 | "96": "icons/icon-96.png", 11 | "128": "icons/icon-128.png" 12 | }, 13 | "devtools_page": "devtools/devtools.html", 14 | "web_accessible_resources": [ 15 | { 16 | "resources": [ 17 | "devtools/content.js", 18 | "devtools/helper.js" 19 | ], 20 | "matches": [ 21 | "*://*/*" 22 | ] 23 | } 24 | ], 25 | "background": { 26 | "service_worker": "devtools/background.js" 27 | }, 28 | "permissions": [ 29 | "tabs", 30 | "scripting" 31 | ], 32 | "optional_host_permissions": [ 33 | "" 34 | ], 35 | "manifest_version": 3 36 | } 37 | -------------------------------------------------------------------------------- /images/chrome_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unlocomqx/svelte-reactive-debugger/a0d530f89b96a2c2f9d82d68f44cf79cecc88ff5/images/chrome_32x32.png -------------------------------------------------------------------------------- /images/firefox_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unlocomqx/svelte-reactive-debugger/a0d530f89b96a2c2f9d82d68f44cf79cecc88ff5/images/firefox_32x32.png -------------------------------------------------------------------------------- /images/list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unlocomqx/svelte-reactive-debugger/a0d530f89b96a2c2f9d82d68f44cf79cecc88ff5/images/list.png -------------------------------------------------------------------------------- /images/summary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unlocomqx/svelte-reactive-debugger/a0d530f89b96a2c2f9d82d68f44cf79cecc88ff5/images/summary.png -------------------------------------------------------------------------------- /logo/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 8 | 11 | 14 | 15 | 16 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-reactive-debugger", 3 | "version": "0.0.0", 4 | "description": "Easily monitor svelte reactive statements", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "npm run dev --prefix app", 8 | "start": "npm run dev & web-ext run -s extension -u http://localhost:8888 -u about:debugging#/runtime/this-firefox && kill $!", 9 | "build:icons": "for size in 16 24 32 48 96 128; do if [ ! -e extension/icons/icon-$size.png -o logo/logo.svg -nt extension/icons/icon-$size.png ]; then svgexport logo/logo.svg extension/icons/icon-$size.png $size:; fi; done", 10 | "build": "npm run build --prefix app && npm run build:icons && web-ext build -s extension --overwrite-dest && git archive -o web-ext-artifacts/code.zip HEAD" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/unlocomqx/svelte-reactive-debugger.git" 15 | }, 16 | "keywords": [ 17 | "svelte", 18 | "extension" 19 | ], 20 | "author": "unlocomqx", 21 | "license": "ISC", 22 | "bugs": { 23 | "url": "https://github.com/unlocomqx/svelte-reactive-debugger/issues" 24 | }, 25 | "homepage": "https://github.com/unlocomqx/svelte-reactive-debugger#readme", 26 | "devDependencies": { 27 | "@types/chrome": "^0.0.204", 28 | "sass": "^1.56.2", 29 | "svgexport": "^0.4.1", 30 | "typescript": "^4.1.3", 31 | "web-ext": "^5.4.1" 32 | }, 33 | "dependencies": { 34 | "copy-to-clipboard": "^3.3.1", 35 | "javascript-stringify": "^2.0.1" 36 | } 37 | } 38 | --------------------------------------------------------------------------------