├── .eslintrc.js
├── .gitignore
├── .prettierrc
├── LICENSE
├── README.md
├── extension
├── .DS_Store
├── devtools
│ ├── 32709ce1000d77ae3c14fa968595e573.png
│ ├── background.js
│ ├── index.html
│ ├── index.js
│ ├── listener.js
│ ├── panel.html
│ └── public
│ │ └── images
│ │ ├── icon128.png
│ │ ├── icon16.png
│ │ └── svelte_slicer_logo_64x64.png
└── manifest.json
├── package-lock.json
├── package.json
├── src
├── App.svelte
├── CollapsibleSection.svelte
├── Component.svelte
├── Diffs.svelte
├── FileStructure.svelte
├── StateChart.svelte
├── StateTree.svelte
├── Variable.svelte
├── global.css
├── main.js
└── stores.js
└── webpack.config.js
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parserOptions: {
3 | ecmaVersion: 2019,
4 | sourceType: 'module'
5 | },
6 | env: {
7 | es6: true,
8 | browser: true
9 | },
10 | plugins: [
11 | 'svelte3'
12 | ],
13 | overrides: [
14 | {
15 | files: ['*.svelte'],
16 | processor: 'svelte3/svelte3'
17 | }
18 | ],
19 | rules: {
20 | // ...
21 | },
22 | settings: {
23 | // ...
24 | }
25 | };
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | extension/devtools/build/
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "tabWidth": 2,
3 | "useTabs": false
4 | }
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 OSLabs Beta
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SvelteSlicer
2 |
3 | 
4 |
5 | # About The Project
6 | Svelte Slicer is an open-source Chrome Developer Tool for visualizing component and state changes in Svelte applications. Svelte Slicer allows users to capture, store and traverse detailed snapshots of application state to aid in debugging.
7 |
8 | # Key features include:
9 | - Visualization of component relationships
10 | - Moment-by-moment tracking of state variables
11 | - Snapshot diffing to identify specific state changes
12 | - Dynamic time travel through past state snapshots
13 |
14 | # Built With
15 | - [Svelte](https://svelte.dev/)
16 | - [D3.js](https://d3js.org/)
17 | - [Chrome Extension API’s](https://developer.chrome.com/docs/extensions/reference/)
18 | - [Webpack](https://webpack.js.org/)
19 |
20 | # Getting Started
21 | - Install Svelte Slicer from the Chrome Web Store
22 | - Run your Svelte application in development mode.
23 | - Open Chrome Developer Tools (Cmd + Option + I) & navigate to the “Slicer” panel
24 |
25 | # Using Svelte Slicer
26 | After opening the tool, you will see two panels. With each DOM update in your application, the panel on the left will populate a new snapshot with a *Data* and a *Jump* button. Clicking on the Data button for a particular snapshot will display in the right hand panel data about the application’s state at the moment the selected snapshot was captured.
27 |
28 | Snapshots that result from a specific user interaction are labeled with the component, event and event handler that triggered its state changes. Users can also use the *Filter* feature to identify specific snapshots based on their labels.
29 |
30 | Each snapshot can be explored in several ways. While in the *State* view, selecting the *Tree* button will display a list of all components with stateful variables that were part of the DOM when the snapshot was captured. Clicking on the name of a component will show it’s stateful variables and their values at the moment of snapshot capture. Clicking the Chart button will display a graphical visualization of the component relationships on the DOM for the selected snapshot. Clicking the *Diff* button will present a list of the specific components and variables that changed from the previous snapshot.
31 |
32 | The *Component* view displays the relationship between user-defined components in the file structure of the application. The *Tree*button displays this information as a collapsible tree, while the *Chart* button shows a hierarchical graphical representation.
33 |
34 | Using the *Jump* buttons allows the user to not only see the data for a chosen snapshot, but also to actually re-render their application as it was at the moment the snapshot was captured. After jumping, the user can choose to start a new timeline by continuing to interact with their application. This will result in new snapshots that build off application state at the last jump.
35 |
36 | Snapshots outside the timeline of the currently rendered snapshot are retained and can still be viewed and jumped to, but are washed out in the snapshot panel to indicate that they are not related to the current snapshot. Using the *Path* button will clear out these washed out snapshots, leaving only the current timeline in the panel.
37 |
38 | The *Previous* and *Forward* buttons also clear out unwanted snapshots, removing all snapshots respectively before or after the currently rendered one.
39 |
40 | # Contributing
41 | Found a bug or have suggestions for improvement? We would love to hear from you!
42 |
43 | Please open an issue to submit feedback or problems you come across.
44 |
45 | # Authors
46 | - Heather Barney - [LinkedIn](https://www.linkedin.com/in/heather-barney-81ab2834/)
47 | - Rachel Collins - [LinkedIn](https://www.linkedin.com/in/rachel-c-bb5b0346/)
48 | - Lynda Labranche - [LinkedIn](https://www.linkedin.com/in/lynda-labranche-854184146/)
49 | - Anchi Teng - [LinkedIn](https://www.linkedin.com/in/anchiteng/)
50 |
51 | # License
52 | This project is licensed under the MIT [License](https://github.com/oslabs-beta/svelte-sight/blob/master/LICENSE) - see the LICENSE file for details
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/extension/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/SvelteSlicer/fe8bcd53e83551cc27667ab7a3fa06b4610d4e0e/extension/.DS_Store
--------------------------------------------------------------------------------
/extension/devtools/32709ce1000d77ae3c14fa968595e573.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/SvelteSlicer/fe8bcd53e83551cc27667ab7a3fa06b4610d4e0e/extension/devtools/32709ce1000d77ae3c14fa968595e573.png
--------------------------------------------------------------------------------
/extension/devtools/background.js:
--------------------------------------------------------------------------------
1 | // background.js
2 | var connections = {};
3 |
4 | chrome.runtime.onConnect.addListener(function (port) {
5 |
6 | var extensionListener = function (message, sender, sendResponse) {
7 |
8 | // The original connection event doesn't include the tab ID of the
9 | // DevTools page, so we need to send it explicitly.
10 | if (message.name == "init") {
11 | connections[message.tabId] = port;
12 | return;
13 | }
14 |
15 | if (message.name === "jumpState" || message.name === "clearSnapshots") {
16 | chrome.tabs.sendMessage(message.tabId, message);
17 | }
18 | }
19 |
20 | // Listen to messages sent from the DevTools page
21 | port.onMessage.addListener(extensionListener);
22 |
23 | port.onDisconnect.addListener(function(port) {
24 | port.onMessage.removeListener(extensionListener);
25 |
26 | var tabs = Object.keys(connections);
27 | for (var i=0, len=tabs.length; i < len; i++) {
28 | if (connections[tabs[i]] == port) {
29 | delete connections[tabs[i]]
30 | break;
31 | }
32 | }
33 | });
34 | });
35 |
36 | // Receive message from content script and relay to the devTools page for the
37 | // current tab
38 | chrome.runtime.onMessage.addListener(function(message, sender) {
39 | // Messages from content scripts should have sender.tab set
40 | if (sender.tab) {
41 | var tabId = sender.tab.id;
42 | if (tabId in connections) {
43 | connections[tabId].postMessage(message);
44 | } else {
45 | console.log("Tab not found in connection list.");
46 | }
47 | } else {
48 | console.log("sender.tab not defined.");
49 | }
50 | return true;
51 | });
--------------------------------------------------------------------------------
/extension/devtools/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/extension/devtools/index.js:
--------------------------------------------------------------------------------
1 | chrome.devtools.panels.create(
2 | "Slicer",
3 | "svelte_logo.png",
4 | "devtools/panel.html",
5 | function (panel) {
6 | panel.onShown.addListener(() => {
7 | chrome.devtools.inspectedWindow.reload(
8 | {injectedScript:
9 | `
10 | const components = [];
11 | const deletedNodes = [];
12 | const insertedNodes = [];
13 | const listeners = {};
14 | const nodes = new Map();
15 | const componentCounts = {};
16 | const componentObject = {};
17 | let node_id = 0;
18 | let firstLoadSent = false;
19 | let stateHistory = [];
20 | const storeVariables = {};
21 | let rebuildingDom = false;
22 | let snapshotLabel = "Init";
23 | let jumpIndex;
24 |
25 | function setup(root) {
26 | root.addEventListener('SvelteRegisterComponent', svelteRegisterComponent);
27 | root.addEventListener('SvelteDOMInsert', svelteDOMInsert);
28 | root.addEventListener('SvelteDOMRemove', svelteDOMRemove);
29 | root.addEventListener('SvelteDOMAddEventListener', svelteDOMAddEventListener);
30 | }
31 |
32 | function svelteRegisterComponent (e) {
33 | const { component, tagName, options } = e.detail;
34 | // assign sequential instance value
35 | let instance = 0;
36 | if (componentCounts.hasOwnProperty(tagName)) {
37 | instance = ++componentCounts[tagName];
38 | }
39 | componentCounts[tagName] = instance;
40 | const id = tagName + instance;
41 |
42 | componentObject[id] = {component, tagName};
43 |
44 | data = {
45 | id,
46 | state: captureComponentState(component),
47 | tagName,
48 | instance,
49 | target: (options.target) ? options.target.nodeName + options.target.id : null
50 | }
51 | components.push(data);
52 | }
53 |
54 | function parseState(element, name = null) {
55 | if (element === null) {
56 | return {
57 | value: element,
58 | name
59 | };
60 | }
61 | else if (typeof element === "function") {
62 | return {
63 | name,
64 | value: element.toString()
65 | };
66 | }
67 | else if (typeof element === "object") {
68 | if (element.constructor) {
69 | if (element.constructor.name === "Object" || element.constructor.name === "Array") {
70 | const value = {};
71 | for (let i in element) {
72 | value[i] = parseState(element[i], i);
73 | }
74 | return {value, name};
75 | }
76 | else {
77 | return {
78 | name,
79 | value: '<' + element.constructor.name + '>'
80 | }
81 | }
82 | }
83 | else {
84 | return {
85 | name,
86 | value: "Unknown Object"
87 | }
88 | }
89 | }
90 | else {
91 | return {
92 | value: element,
93 | name
94 | };
95 | }
96 | }
97 |
98 | function captureComponentState(component) {
99 | const captureStateFunc = component.$capture_state;
100 | let state = captureStateFunc ? captureStateFunc() : {};
101 | // if capture_state produces an empty object, may need to use ctx instead (older version of Svelte)
102 | if (state && !Object.keys(state).length) {
103 | if (component.$$.ctx.constructor.name === "Object") {
104 | state = deepClone(component.$$.ctx);
105 | }
106 | }
107 |
108 | const parsedState = {};
109 | for (let variable in state) {
110 | if (typeof state[variable] === "function") {
111 | delete state[variable];
112 | }
113 | else if (state[variable] === null) {
114 | parsedState[variable] = parseState(state[variable], variable);
115 | }
116 | else if (typeof state[variable] === "object") {
117 | if (state[variable].constructor) {
118 | if (state[variable].constructor.name === "Object" || state[variable].constructor.name === "Array") {
119 | // check if variable is a store variable
120 | if (state[variable].hasOwnProperty('subscribe')) {
121 | // if a writable store, we need to store the instance
122 | if (state[variable].hasOwnProperty('set') && state[variable].hasOwnProperty('update')) {
123 | storeVariables[variable] = state[variable];
124 | }
125 | delete state[variable];
126 | }
127 | else {
128 | parsedState[variable] = parseState(state[variable], variable);
129 | }
130 | }
131 | else {
132 | parsedState[variable] = parseState(state[variable], variable)
133 | }
134 | }
135 | else {
136 | delete state[variable];
137 | }
138 | }
139 | else {
140 | parsedState[variable] = parseState(state[variable], variable);
141 | }
142 | }
143 | return parsedState;
144 | }
145 |
146 | function svelteDOMRemove(e) {
147 |
148 | const { node } = e.detail;
149 | const nodeData = nodes.get(node);
150 | if (nodeData) {
151 | deletedNodes.push({
152 | id: nodeData.id,
153 | component: nodeData.component
154 | })
155 | }
156 | }
157 |
158 | function svelteDOMInsert(e) {
159 |
160 | const { node, target } = e.detail;
161 | if (node.__svelte_meta) {
162 | let id = nodes.get(node);
163 | if (!id) {
164 | id = node_id++;
165 | componentName = getComponentName(node.__svelte_meta.loc.file)
166 | nodes.set(node, {id, componentName});
167 | }
168 | insertedNodes.push({
169 | target: ((nodes.get(target)) ? nodes.get(target).id : target.nodeName + target.id),
170 | id,
171 | component: componentName,
172 | loc: node.__svelte_meta.loc.char
173 | });
174 | }
175 | }
176 |
177 | function svelteDOMAddEventListener(e) {
178 | const { node, event } = e.detail;
179 | if (node.__svelte_meta) {
180 | if (!nodes.has(node)) {
181 | const nodeId = node_id++;
182 | const componentName = getComponentName(node.__svelte_meta.loc.file)
183 | nodes.set(node, {nodeId, componentName});
184 | }
185 | const nodeData = nodes.get(node);
186 | const listenerId = nodeData.id + event;
187 | node.addEventListener(event, () => updateLabel(nodeData.id, event));
188 |
189 | listeners[listenerId] = ({
190 | node: nodeData.id,
191 | event,
192 | handlerName: e.detail.handler.name,
193 | component: nodeData.componentName,
194 | })
195 | }
196 | }
197 |
198 | function getComponentName(file) {
199 | if (file.indexOf('/') === -1) {
200 | tagName = file.slice((file.lastIndexOf('\\\\') + 1), -7);
201 | }
202 | else {
203 | tagName = file.slice((file.lastIndexOf('/') + 1), -7);
204 | }
205 | return tagName;
206 | }
207 |
208 | const deepClone = (inObject) => {
209 | let outObject, value, key
210 |
211 | if (typeof inObject !== "object" || inObject === null) {
212 | return inObject // Return the value if inObject is not an object
213 | }
214 |
215 | if (inObject.constructor.name !== "Object" && inObject.constructor.name !== "Array") {
216 | return inObject // Return the value if inObject is not an object
217 | }
218 |
219 | // Create an array or object to hold the values
220 | outObject = Array.isArray(inObject) ? [] : {}
221 |
222 | for (key in inObject) {
223 | value = inObject[key]
224 |
225 | // Recursively (deep) copy for nested objects, including arrays
226 | outObject[key] = deepClone(value)
227 | }
228 |
229 | return outObject
230 | }
231 |
232 | function updateLabel(nodeId, event) {
233 | const listener = listeners[nodeId + event];
234 | const { component, handlerName } = listener;
235 | snapshotLabel = component + ' - ' + event + " -> " + handlerName;
236 | rebuildingDom = false;
237 | }
238 |
239 | function rebuildDom(tree) {
240 | rebuildingDom = true;
241 |
242 | tree.forEach(componentFile => {
243 | for (let componentInstance in componentObject) {
244 | if (componentObject[componentInstance].tagName === componentFile) {
245 | if (stateHistory[jumpIndex].hasOwnProperty(componentInstance)) {
246 | const variables = stateHistory[jumpIndex][componentInstance];
247 | for (let variable in variables) {
248 | if (variable[0] === '$') {
249 | updateStore(componentInstance, variable, variables[variable]);
250 | }
251 | else {
252 | injectState(componentInstance, variable, variables[variable]);
253 | }
254 | }
255 | }
256 | }
257 | }
258 | })
259 | }
260 |
261 | function injectState(componentId, key, value) {
262 | const component = componentObject[componentId].component;
263 | component.$inject_state({ [key]: value })
264 | }
265 |
266 | function updateStore(componentId, name, value) {
267 | const component = componentObject[componentId].component;
268 | const store = storeVariables[name.slice(1)];
269 | store.set(value);
270 | }
271 |
272 | function clearSnapshots(index, path, clearType) {
273 | if (clearType === 'forward') {
274 | stateHistory = stateHistory.slice(0, index + 1);
275 | }
276 | else if (clearType === 'previous') {
277 | stateHistory = stateHistory.slice(index);
278 | }
279 | else if (clearType === 'path') {
280 | for (let i = stateHistory.length -1; i > 0 ; i--){
281 | if (!path.includes(i)){
282 | stateHistory.splice(i,1);
283 | }
284 | }
285 | }
286 | }
287 |
288 | setup(window.document);
289 |
290 | for (let i = 0; i < window.frames.length; i++) {
291 | const frame = window.frames[i]
292 | const root = frame.document
293 | setup(root)
294 | }
295 |
296 | // observe for changes to the DOM
297 | const observer = new MutationObserver(() => {
298 | if (!rebuildingDom){
299 | const domChange = new CustomEvent('dom-changed');
300 | window.document.dispatchEvent(domChange)
301 | }
302 | else {
303 | const rebuild = new CustomEvent('rebuild');
304 | window.document.dispatchEvent(rebuild);
305 | }
306 | });
307 |
308 | // capture initial DOM load as one snapshot
309 | window.onload = () => {
310 | // make sure that data is being sent
311 | if (components.length || insertedNodes.length || deletedNodes.length) {
312 | stateHistory.push(deepClone(captureRawAppState()));
313 | firstLoadSent = true;
314 |
315 | window.postMessage({
316 | source: 'panel.js',
317 | type: 'firstLoad',
318 | data: {
319 | stateObject: captureParsedAppState(),
320 | components,
321 | insertedNodes,
322 | deletedNodes,
323 | snapshotLabel
324 | }
325 | })
326 |
327 | // reset arrays
328 | components.splice(0, components.length);
329 | insertedNodes.splice(0, insertedNodes.length);
330 | deletedNodes.splice(0, deletedNodes.length);
331 | snapshotLabel = undefined;
332 | }
333 |
334 | // start MutationObserver
335 | observer.observe(window.document, {attributes: true, childList: true, subtree: true});
336 | }
337 |
338 | function captureRawAppState() {
339 | const appState = {};
340 | for (let component in componentObject) {
341 | const captureStateFunc = componentObject[component].component.$capture_state;
342 | let state = captureStateFunc ? captureStateFunc() : {};
343 | // if state object is empty, may need to use ctx instead (older version of Svelte)
344 | if (state && !Object.keys(state).length) {
345 | if (componentObject[component].component.$$.ctx.constructor.name === "Object") {
346 | state = componentObject[component].component.$$.ctx;
347 | }
348 | }
349 | appState[component] = state;
350 | }
351 | return appState;
352 | }
353 |
354 | function captureParsedAppState() {
355 | const appState = {};
356 | for (let component in componentObject) {
357 | appState[component] = captureComponentState(componentObject[component].component);
358 | }
359 | return appState;
360 | }
361 |
362 | // capture subsequent DOM changes to update snapshots
363 | window.document.addEventListener('dom-changed', (e) => {
364 | // only send message if something changed in SvelteDOM or stateObject
365 | if (components.length || insertedNodes.length || deletedNodes.length) {
366 | // check for deleted components
367 | for (let component in componentObject) {
368 | if (componentObject[component].component.$$.fragment === null) {
369 | delete componentObject[component];
370 | }
371 | }
372 | const currentState = captureRawAppState();
373 | stateHistory.push(deepClone(currentState));
374 | let type;
375 | // make sure the first load has already been sent; if not, this is the first load
376 | if (!firstLoadSent) {
377 | type = "firstLoad";
378 | firstLoadSent = true;
379 | }
380 | else type = "update";
381 |
382 | window.postMessage({
383 | source: 'panel.js',
384 | type,
385 | data: {
386 | stateObject: captureParsedAppState(),
387 | components,
388 | insertedNodes,
389 | deletedNodes,
390 | snapshotLabel
391 | }
392 | });
393 |
394 | // reset arrays
395 | components.splice(0, components.length);
396 | insertedNodes.splice(0, insertedNodes.length);
397 | deletedNodes.splice(0, deletedNodes.length);
398 | snapshotLabel = undefined;
399 | }
400 | });
401 |
402 | // clean up after jumps
403 | window.document.addEventListener('rebuild', (e) => {
404 | deletedComponents = [];
405 | for (let component in componentObject) {
406 | if (componentObject[component].component.$$.fragment === null) {
407 | delete componentObject[component];
408 | deletedComponents.push(component);
409 | }
410 | }
411 |
412 | components.forEach(newComponent => {
413 | const { tagName, id } = newComponent;
414 | const component = componentObject[id].component;
415 | const captureStateFunc = component.$capture_state;
416 | let componentState = captureStateFunc ? captureStateFunc() : {};
417 | if (componentState && !Object.keys(componentState).length) {
418 | if (component.$$.ctx.constructor.name === "Object") {
419 | componentState = deepClone(component.$$.ctx);
420 | }
421 | }
422 |
423 | const previousState = stateHistory[jumpIndex];
424 | for (let componentId in previousState) {
425 | if (JSON.stringify(previousState[componentId]) === JSON.stringify(componentState) && !componentObject.hasOwnProperty(componentId)) {
426 | componentObject[componentId] = {
427 | component,
428 | tagName
429 | }
430 | newComponent.id = componentId;
431 | delete componentObject[id];
432 | componentCounts[tagName]--;
433 | }
434 | }
435 | })
436 |
437 | window.postMessage({
438 | source: 'panel.js',
439 | type: 'rebuild',
440 | data: {
441 | stateObject: captureParsedAppState(),
442 | components,
443 | insertedNodes,
444 | deletedNodes,
445 | deletedComponents,
446 | snapshotLabel
447 | }
448 | });
449 |
450 | components.splice(0, components.length);
451 | insertedNodes.splice(0, insertedNodes.length);
452 | deletedNodes.splice(0, deletedNodes.length);
453 | snapshotLabel = undefined;
454 | jumpIndex = undefined;
455 | })
456 |
457 | // listen for devTool messages
458 | window.addEventListener('message', function () {
459 | // Only accept messages from the same frame
460 | if (event.source !== window) {
461 | return;
462 | }
463 |
464 | // Only accept messages that we know are ours
465 | if (typeof event.data !== 'object' || event.data === null ||
466 | !event.data.source === 'panel.js') {
467 | return;
468 | }
469 |
470 | if (event.data.type === 'jumpState') {
471 | const { index, tree} = event.data;
472 | jumpIndex = index;
473 | rebuildDom(tree);
474 | }
475 |
476 | if (event.data.type === 'clearSnapshots') {
477 | const { index, path, clearType } = event.data;
478 | clearSnapshots(index, path, clearType);
479 | }
480 | })
481 | `
482 | }
483 | );
484 | })
485 | }
486 | )
--------------------------------------------------------------------------------
/extension/devtools/listener.js:
--------------------------------------------------------------------------------
1 | window.addEventListener('message', function(event) {
2 | // Only accept messages from the same frame
3 | if (event.source !== window) {
4 | return;
5 | }
6 |
7 | // Only accept messages that we know are ours
8 | if (typeof event.data !== 'object' || event.data === null ||
9 | !event.data.source === 'panel.js') {
10 | return;
11 | }
12 |
13 | const data = event.data;
14 |
15 | chrome.runtime.sendMessage(JSON.stringify(data));
16 | });
17 |
18 | chrome.runtime.onMessage.addListener(message => {
19 | if (message.name === "jumpState") {
20 | window.postMessage({
21 | source: 'listener.js',
22 | type: 'jumpState',
23 | index: message.index,
24 | state: message.state,
25 | tree: message.tree
26 | });
27 | }
28 |
29 | else if (message.name === "clearSnapshots") {
30 | window.postMessage({
31 | source: 'listeneter.js',
32 | type: 'clearSnapshots',
33 | index: message.index,
34 | clearType: message.clearType,
35 | path: message.path
36 | })
37 | }
38 | });
39 |
40 |
--------------------------------------------------------------------------------
/extension/devtools/panel.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Analysis
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/extension/devtools/public/images/icon128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/SvelteSlicer/fe8bcd53e83551cc27667ab7a3fa06b4610d4e0e/extension/devtools/public/images/icon128.png
--------------------------------------------------------------------------------
/extension/devtools/public/images/icon16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/SvelteSlicer/fe8bcd53e83551cc27667ab7a3fa06b4610d4e0e/extension/devtools/public/images/icon16.png
--------------------------------------------------------------------------------
/extension/devtools/public/images/svelte_slicer_logo_64x64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/SvelteSlicer/fe8bcd53e83551cc27667ab7a3fa06b4610d4e0e/extension/devtools/public/images/svelte_slicer_logo_64x64.png
--------------------------------------------------------------------------------
/extension/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 3,
3 | "name": "Svelte Slicer",
4 | "version": "1.0",
5 | "minimum_chrome_version": "10.0",
6 | "description": "Browser devtools extension for time traveling and visualizing Svelte applications.",
7 |
8 | "devtools_page": "devtools/index.html",
9 | "permissions": [
10 | "activeTab",
11 | "scripting"
12 | ],
13 | "host_permissions": [
14 | "http://*/*",
15 | "https://*/*"
16 | ],
17 | "background": {
18 | "service_worker": "devtools/background.js"
19 | },
20 | "content_scripts": [
21 | {
22 | "matches": [
23 | "http://*/*",
24 | "https://*/*"
25 | ],
26 | "js": ["devtools/listener.js"],
27 | "run_at": "document_start"
28 | }
29 | ],
30 | "action": {
31 | "default_icon": {
32 | "16": "devtools/public/images/icon16.png",
33 | "128": "devtools/public/images/icon128.png"
34 | }
35 | },
36 | "icons": {
37 | "16": "devtools/public/images/icon16.png",
38 | "128": "devtools/public/images/icon128.png"
39 | }
40 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "svelte-app",
3 | "version": "1.0.0",
4 | "devDependencies": {
5 | "cross-env": "^7.0.3",
6 | "css-loader": "^5.0.1",
7 | "eslint": "^8.8.0",
8 | "eslint-plugin-import": "^2.25.4",
9 | "eslint-plugin-svelte3": "^3.4.0",
10 | "file-loader": "^6.2.0",
11 | "mini-css-extract-plugin": "^1.3.4",
12 | "prettier": "^2.5.1",
13 | "prettier-plugin-svelte": "^2.6.0",
14 | "svelte": "^3.31.2",
15 | "svelte-loader": "^3.0.0",
16 | "webpack": "^5.16.0",
17 | "webpack-cli": "^4.4.0",
18 | "webpack-dev-server": "^3.11.2"
19 | },
20 | "scripts": {
21 | "build": "cross-env NODE_ENV=production webpack",
22 | "dev": "webpack serve --content-base public",
23 | "format": "prettier --write '{public,src}/**/*.{css,html,js,svelte}'",
24 | "lint": "eslint . --ext .js,.svelte --fix"
25 | },
26 | "dependencies": {
27 | "d3": "^7.2.1",
28 | "lodash": "^4.17.21"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/App.svelte:
--------------------------------------------------------------------------------
1 |
184 |
185 |
186 |
187 |
188 |
189 |
Svelte Slicer
190 |
191 |
192 |
207 | Reset
208 |
209 |
210 | {#if !filtered.length}
211 | {#each $snapshots as snapshot, i}
212 |
217 |
Snapshot {i}
219 | {snapshot.label ? " : " + snapshot.label : ""}
221 |
222 | selectState(i)}
225 | class:selectedButton={i === CurrentI}>Data
227 | jumpState(i)}
228 | >Jump
230 |
231 |
232 | {/each}
233 | {:else if filtered.length}
234 | {#each filtered as snapshot}
235 |
242 |
Snapshot {snapshot.index}
244 | {snapshot.snapshot.label
245 | ? " : " + snapshot.snapshot.label
246 | : ""}
248 |
249 | selectState(snapshot.index)}
252 | class:selectedButton={snapshot.index === CurrentI}>Data
254 | jumpState(snapshot.index)}>Jump
258 |
259 |
260 | {/each}
261 | {/if}
262 |
263 |
264 | selectView("files")}
266 | class:activeButton={view === "files"}>Components
268 | selectView("state")}
270 | class:activeButton={view === "state"}>State
272 |
273 |
274 | {#if View === "files"}
275 | selectVis("tree")}
277 | class:activeButton={vis === "tree"}>Tree
279 | selectVis("chart")}
281 | class:activeButton={vis === "chart"}>Chart
283 | {:else if View === "state"}
284 | selectVis("tree")}
286 | class:activeButton={vis === "tree"}>Tree
288 | selectVis("chart")}
290 | class:activeButton={vis === "chart"}>Chart
292 | selectVis("diff")}
294 | class:activeButton={vis === "diff"}>Diff
296 | {/if}
297 |
298 |
299 | {#if $snapshots.length}
300 | {#if View === "files" && Vis === "tree"}
301 | {#if Object.keys($fileTree).length}
302 |
303 | {:else}
304 |
File structure data unavailable
305 | {/if}
306 | {:else if View === "files" && Vis === "chart"}
307 | {#if Object.keys($fileTree).length}
308 |
309 | {:else}
310 |
File structure data unavailable
311 | {/if}
312 | {:else if View === "state"}
313 | {#if Vis === "tree"}
314 |
315 | {:else if Vis === "chart"}
316 |
317 | {:else if Vis === "diff"}
318 |
319 | {/if}
320 | {/if}
321 | {/if}
322 |
323 |
353 |
354 |
355 |
356 |
622 |
--------------------------------------------------------------------------------
/src/CollapsibleSection.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 | (expanded = !expanded)}
12 | class="variableVal"
13 | >{headerText}
14 |
15 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
56 |
--------------------------------------------------------------------------------
/src/Component.svelte:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
18 |
19 |
20 | {#if children.length}
21 |
22 | ▶ {id}
24 | {#if expanded}
25 | {#each children as child}
26 |
27 | {/each}
28 | {/if}
29 | {:else}
30 | {id}
31 | {/if}
32 |
33 |
34 |
35 |
36 |
37 |
63 |
--------------------------------------------------------------------------------
/src/Diffs.svelte:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
Snapshot {I}: {snapshot.label}
13 | {#if newComponents.length}
14 |
New Components:
15 | {#each newComponents as component}
16 |
{component.component}
17 | {/each}
18 | {/if}
19 | {#if deletedComponents.length}
20 |
Deleted Components:
21 | {#each deletedComponents as component}
22 |
{component.component}
23 | {/each}
24 | {/if}
25 | {#if Object.keys(changedVariables).length}
26 |
Changed Variables:
27 | {#each Object.entries(changedVariables) as [componentName, component]}
28 |
{componentName}
29 |
30 | {#each Object.entries(component) as [variableName, variable]}
31 | {#if variable.oldValue !== "" && variable.newValue !== ""}
32 |
33 | {variableName}:
34 | {variable.oldValue}
35 | → {variable.newValue}
36 |
37 | {:else if variable.oldValue === ""}
38 |
39 | {variableName}: ' '
40 | → {variable.newValue}
41 |
42 | {:else if variable.newValue === ""}
43 |
44 | {variableName}:
45 | {variable.oldValue}
46 | → ' '
47 |
48 | {/if}
49 | {/each}
50 |
51 | {/each}
52 | {/if}
53 |
54 |
55 |
56 |
111 |
--------------------------------------------------------------------------------
/src/FileStructure.svelte:
--------------------------------------------------------------------------------
1 |
169 |
170 |
171 |
--------------------------------------------------------------------------------
/src/StateChart.svelte:
--------------------------------------------------------------------------------
1 |
248 |
249 |
250 | Snapshot {I}: {label}
251 |
252 |
253 |
254 |
--------------------------------------------------------------------------------
/src/StateTree.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 | {#if data}
17 | Snapshot {I}: {label}
18 | {#each componentList as componentName}
19 | {#if Object.keys(data[componentName].variables).length}
20 |
21 |
22 | {#each Object.keys(data[componentName].variables) as variable}
23 | {#if data[componentName].variables[variable].value !== undefined && data[componentName].variables[variable].value !== null}
24 |
25 | {/if}
26 | {/each}
27 |
28 |
29 | {/if}
30 | {/each}
31 | {/if}
32 |
33 |
34 |
36 |
--------------------------------------------------------------------------------
/src/Variable.svelte:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 | {#if typeof variable.value !== "object" || variable.value === null}
8 | {#if variable.value === ""}
9 | {variable.name}: ' '
10 | {:else}
11 | {variable.name}: {variable.value}
12 | {/if}
13 | {:else}
14 |
15 |
{variable.name}:
16 |
17 | {#each Object.keys(variable.value) as nestedValue}
18 |
19 |
20 |
21 | {/each}
22 |
23 |
24 | {/if}
25 |
26 |
--------------------------------------------------------------------------------
/src/global.css:
--------------------------------------------------------------------------------
1 | @import url("https://fonts.googleapis.com/css2?family=Overpass:wght@100&display=swap");
2 |
3 | body {
4 | margin: 0;
5 | background: rgb(83, 81, 81);
6 | margin: 0;
7 | height: 100%;
8 | font-size: 16px;
9 | color: whitesmoke;
10 | font-family: "Overpass", sans-serif;
11 | }
12 |
13 | h2 {
14 | font-weight: 700;
15 | font-size: 19px;
16 | margin: 2px;
17 | padding: 10px;
18 | }
19 |
20 | div {
21 | padding: 1em;
22 | }
23 |
24 | .panelDiv {
25 | /* border-top: 1px solid whitesmoke; */
26 | text-align: center;
27 | }
28 |
29 | a {
30 | color: rgb(0, 100, 200);
31 | text-decoration: none;
32 | }
33 |
34 | ol li::marker {
35 | content: "▼";
36 | }
37 |
38 | a:hover {
39 | text-decoration: underline;
40 | }
41 |
42 | a:visited {
43 | color: rgb(0, 80, 160);
44 | }
45 |
46 | label {
47 | display: block;
48 | }
49 |
50 | input,
51 | select,
52 | textarea {
53 | color: whitesmoke;
54 | background: #333;
55 | outline: none;
56 | margin: 2px;
57 | font-size: 16px;
58 | font-family: "Overpass", sans-serif;
59 | padding: 0.4em;
60 | border: none;
61 | border-radius: 2px;
62 | }
63 |
64 | button {
65 | color: whitesmoke;
66 | background: #333;
67 | outline: none;
68 | margin: 2px;
69 | font-size: 16px;
70 | font-weight: bold;
71 | font-family: "Overpass", sans-serif;
72 | padding: 5px;
73 | border: none;
74 | border-radius: 2px;
75 | }
76 |
77 | input {
78 | width: 150px;
79 | }
80 |
81 | input:disabled {
82 | color: #ccc;
83 | }
84 |
85 | button:disabled {
86 | color: #999;
87 | }
88 |
89 | button:not(:disabled):active {
90 | background-color: #ddd;
91 | }
92 |
93 | button:focus {
94 | border-color: #666;
95 | }
96 |
97 | main {
98 | margin: 0px;
99 | align-items: center;
100 | color: whitesmoke;
101 | /* background: rgb(83, 81, 81); */
102 | }
103 |
104 | span {
105 | /* padding: 1em; */
106 | font-size: 16px;
107 | }
108 |
109 | .xAxis path,
110 | .xAxis line {
111 | stroke: teal;
112 | shape-rendering: crispEdges;
113 | }
114 |
115 | .xAxis text {
116 | font-weight: bold;
117 | font-size: 14px;
118 | fill: teal;
119 | }
120 |
121 | .node circle {
122 | fill: #fff;
123 | stroke: steelblue;
124 | stroke-width: 3px;
125 | }
126 |
127 | .node text {
128 | font: 12px Overpass;
129 | }
130 |
131 | .link {
132 | fill: none;
133 | stroke: #ccc;
134 | stroke-width: 2px;
135 | }
136 |
137 | .test {
138 | content: "▼";
139 | }
140 |
141 | .no-arrow {
142 | font-size: 16px;
143 | }
144 |
145 | ul.ulArrows {
146 | list-style-type: none;
147 | }
148 |
149 | ul.variableVal {
150 | font-size: 16px;
151 | list-style-type: none;
152 | }
153 |
154 | #variableName {
155 | /* font-size: 16px; */
156 | list-style-type: none;
157 | }
158 | .arrow {
159 | cursor: pointer;
160 | font-weight: bold;
161 | display: inline-block;
162 | transition: transform 200ms;
163 | text-align: center;
164 | }
165 |
166 | .arrowDown {
167 | transform: rotate(90deg);
168 | }
169 |
170 | .search-button {
171 | color: rgb(162, 159, 159);
172 | background: transparent;
173 | border: none;
174 | outline: none;
175 | margin-right: -25px;
176 | }
177 | form.form button:hover {
178 | background: rgb(238, 137, 5);
179 | }
180 |
181 | .content {
182 | padding: 2px;
183 | font-size: 16px;
184 | font-weight: bold;
185 | border: none;
186 | /* transition: max-height 0.2s ease-out; */
187 | }
188 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import "./global.css";
2 |
3 | import App from "./App.svelte";
4 |
5 | const app = new App({
6 | target: document.body,
7 | });
8 |
9 | export default app;
10 |
--------------------------------------------------------------------------------
/src/stores.js:
--------------------------------------------------------------------------------
1 | import { writable, get } from "svelte/store";
2 | import { compile } from "svelte/compiler";
3 | import _ from "lodash";
4 |
5 | export const snapshots = writable([]);
6 | export const fileTree = writable({});
7 | export const flatFileTree = writable([]);
8 | export const backgroundPageConnection = writable(
9 | chrome.runtime.connect({ name: "panel" })
10 | );
11 | export const sharedAppView = writable();
12 |
13 | // store updateable objects for current component state
14 | let componentData = {};
15 | // store ALL nodes and listeners
16 | const nodes = {};
17 |
18 | // store AST info for each file
19 | const astInfo = {};
20 | const componentTree = {};
21 | let parentComponent;
22 | let domParent;
23 | let rebuild = false;
24 |
25 | // set up background page Connection
26 | const connection = get(backgroundPageConnection);
27 |
28 | connection.postMessage({
29 | name: "init",
30 | tabId: chrome.devtools.inspectedWindow.tabId,
31 | });
32 |
33 | // Listen for SvelteDOM messages from content script
34 | chrome.runtime.onMessage.addListener((msg) => {
35 | const parsedMessage = JSON.parse(msg);
36 |
37 | const { data, type } = parsedMessage;
38 |
39 | if (type === "firstLoad") {
40 | snapshots.set([]);
41 | const snapshot = buildSnapshot(data);
42 | snapshots.update((array) => [...array, snapshot]);
43 | } else if (type === "update") {
44 | const newSnapshot = buildSnapshot(data);
45 | snapshots.update((array) => [...array, newSnapshot]);
46 | } else if (type === "rebuild") {
47 | rebuild = true;
48 | const newSnapshot = buildSnapshot(data);
49 | }
50 | });
51 |
52 | // get and parse through the AST for additional variable info
53 | chrome.devtools.inspectedWindow.getResources((resources) => {
54 | const arrSvelteFiles = resources.filter((file) =>
55 | file.url.includes(".svelte")
56 | );
57 | arrSvelteFiles.forEach((svelteFile) => {
58 | svelteFile.getContent((source) => {
59 | if (source) {
60 | const { ast } = compile(source);
61 | const componentName = svelteFile.url.slice(
62 | svelteFile.url.lastIndexOf("/") + 1,
63 | svelteFile.url.lastIndexOf(".svelte")
64 | );
65 | const components = {};
66 | if (ast.instance) {
67 | const astVariables = ast.instance.content.body;
68 | astVariables.forEach((variable) => {
69 | const data = {};
70 | if (
71 | variable.type === "ImportDeclaration" &&
72 | variable.source.value.includes(".svelte")
73 | ) {
74 | data.name = variable.specifiers[0].local.name;
75 | data.parent = componentName;
76 | components[data.name] = data;
77 | }
78 | });
79 | }
80 |
81 | astInfo[componentName] = components;
82 | componentTree[componentName] = {
83 | id: componentName,
84 | children: [],
85 | };
86 | }
87 | });
88 | });
89 | });
90 |
91 | function buildSnapshot(data) {
92 | const {
93 | components,
94 | insertedNodes,
95 | deletedNodes,
96 | stateObject,
97 | snapshotLabel,
98 | } = data;
99 | const diff = {
100 | newComponents: [],
101 | deletedComponents: [],
102 | changedVariables: {},
103 | };
104 |
105 | // build nodes object
106 | insertedNodes.forEach((node) => {
107 | nodes[node.id] = {
108 | children: [],
109 | id: node.id,
110 | component: node.component,
111 | target: node.target,
112 | loc: node.loc,
113 | };
114 | // add as a child to target node
115 | if (typeof node.target === "number") {
116 | nodes[node.target].children.push({ id: node.id });
117 | }
118 | });
119 |
120 | // build components and assign nodes and variables
121 | components.forEach((component) => {
122 | const { tagName, id, instance } = component;
123 |
124 | const data = {
125 | tagName,
126 | id,
127 | nodes: {},
128 | variables: {},
129 | active: true,
130 | instance,
131 | };
132 |
133 | const targets = {};
134 | // create object with all associated nodes
135 | const nodeLocations = {}; // store nodes by code location and parent to ensure they get assigned to correct component
136 | insertedNodes.forEach((node, i) => {
137 | // make sure node belongs to this component - component name should match, should be only node in component from that location OR if same location, must share a target
138 | if (
139 | node.component === tagName &&
140 | (!nodeLocations.hasOwnProperty(node.loc) ||
141 | nodeLocations[node.loc] === node.target)
142 | ) {
143 | // update node component to include full component id with instance number
144 | nodes[node.id].component = id;
145 | // assign node by reference to component data
146 | data.nodes[node.id] = nodes[node.id];
147 | // mark the node location as taken for this component and store target node
148 | nodeLocations[node.loc] = node.target;
149 | // remove node from insertedNodes array so it can't be assigned to another component
150 | delete insertedNodes[i];
151 | // push node target and node to targetArray for later reference
152 | targets[node.target] = true;
153 | targets[node.id] = true;
154 | }
155 | });
156 |
157 | // identify the top-level parent node for the component
158 | let parentNode;
159 | if (component.target) {
160 | parentNode = component.target;
161 | domParent = component.id;
162 | }
163 | parentNode = Math.min(...Object.keys(targets));
164 | data.parentNode = parentNode;
165 | data.targets = targets;
166 |
167 | // assign variables to components using state object
168 | const variables = stateObject[id];
169 | for (let variable in variables) {
170 | const varData = {
171 | name: variable,
172 | component: id,
173 | value: variables[variable].value,
174 | };
175 |
176 | data.variables[varData.name] = varData;
177 | }
178 |
179 | diff.newComponents.push({ component: id, variables: data.variables });
180 |
181 | componentData[id] = data;
182 | });
183 |
184 | // assign any remaining inserted Nodes that didn't go to new components
185 | insertedNodes.forEach((node) => {
186 | // loop through components in case there are multiple instances
187 | for (let component in componentData) {
188 | if (
189 | componentData[component].tagName === node.component &&
190 | componentData[component].targets.hasOwnProperty(node.target)
191 | ) {
192 | // update node component to include full component id with instance number
193 | nodes[node.id].component = componentData[component].id;
194 | // assign node by reference to component data
195 | componentData[component].nodes[node.id] = nodes[node.id];
196 | // store node target and node id for later reference
197 | componentData[component].targets[node.target] = true;
198 | componentData[component].targets[node.id] = true;
199 | }
200 | }
201 | });
202 |
203 | // delete nodes and descendents
204 | deletedNodes.forEach((node) => {
205 | deleteNode(node.id);
206 | });
207 |
208 | // if DOM was rebuilt by jumping, need to explicitly remove components
209 | if (rebuild) {
210 | data.deletedComponents.forEach((component) => {
211 | delete componentData[component];
212 | });
213 | }
214 |
215 | // determine and assign the DOM parent (can't happen until all components are built and have nodes assigned)
216 | for (let component in componentData) {
217 | const { parentNode } = componentData[component];
218 | componentData[component].parent = nodes.hasOwnProperty(parentNode)
219 | ? nodes[parentNode].component
220 | : component === domParent
221 | ? null
222 | : domParent;
223 | componentData[component].children = [];
224 | }
225 |
226 | // assign children to components and determine if component is active in the DOM
227 | for (let i in componentData) {
228 | const component = componentData[i];
229 | const parent = component.parent;
230 | if (parent && componentData.hasOwnProperty(parent)) {
231 | componentData[parent].children.push(component);
232 | }
233 | // if no current nodes, mark component as not active
234 | if (!Object.keys(component.nodes).length && component.id !== domParent) {
235 | if (component.active === true) {
236 | component.active = false;
237 | diff.deletedComponents.push({
238 | component: component.id,
239 | variables: component.variables,
240 | });
241 | }
242 | } else {
243 | component.active = true;
244 | }
245 | }
246 |
247 | // update state variables
248 | const currentIndex = get(sharedAppView);
249 | if (currentIndex >= 0) {
250 | const allStates = get(snapshots);
251 | const stateHistory = allStates[currentIndex].data;
252 | const storeDiff = {};
253 |
254 | for (let [componentId, component] of Object.entries(componentData)) {
255 | const componentDiff = {};
256 |
257 | if (stateObject.hasOwnProperty(componentId)) {
258 | for (let [varName, variable] of Object.entries(
259 | stateObject[componentId]
260 | )) {
261 | const { value } = variable;
262 | let data = {};
263 | // if variable is in stateObject but not in componentData, set value in componentData to null
264 | if (!component.variables.hasOwnProperty(varName)) {
265 | component.variables[varName] = variable;
266 | }
267 | // if values are different, set value in componentData to value from stateObject
268 | else if (!_.isEqual(value, component.variables[varName].value)) {
269 | component.variables[varName].value = value;
270 | }
271 |
272 | if (stateHistory.hasOwnProperty(componentId)) {
273 | // if variable is in stateObject but not in stateHistory, old value is null
274 | if (!stateHistory[componentId].variables.hasOwnProperty(varName)) {
275 | data = {
276 | name: varName,
277 | oldValue: null,
278 | newValue: getDiffValue(value),
279 | };
280 | }
281 | // if values are different in stateObject and stateHistory, set old and new values respetively
282 | else if (
283 | !_.isEqual(
284 | stateHistory[componentId].variables[varName].value,
285 | value
286 | )
287 | ) {
288 | data = {
289 | name: varName,
290 | oldValue:
291 | stateHistory[componentId].variables[varName].value !== undefined
292 | ? getDiffValue(
293 | stateHistory[componentId].variables[varName].value
294 | )
295 | : "undefined",
296 | newValue: getDiffValue(value),
297 | };
298 | }
299 | }
300 |
301 | // if there are diffs, add to component diff or store diff
302 | if (!_.isEmpty(data)) {
303 | if (varName[0] === "$") {
304 | storeDiff[varName] = data;
305 | } else {
306 | componentDiff[varName] = data;
307 | }
308 | }
309 | }
310 | }
311 |
312 | if (stateObject.hasOwnProperty(componentId)) {
313 | for (let [varName, variable] of Object.entries(component.variables)) {
314 | // if variable is in componentData but not in stateObject, new value in componentData is null
315 | if (!stateObject[componentId].hasOwnProperty(varName)) {
316 | variable.value = null;
317 | }
318 | }
319 |
320 | if (stateHistory.hasOwnProperty(componentId)) {
321 | for (let [varName, variable] of Object.entries(
322 | stateHistory[componentId].variables
323 | )) {
324 | // if variable is in stateHistory but not in stateObject, new value is null
325 | if (!stateObject[componentId].hasOwnProperty(varName)) {
326 | const data = {
327 | name: varName,
328 | oldValue:
329 | variable.value !== undefined
330 | ? getDiffValue(variable.value)
331 | : "undefined",
332 | newValue: null,
333 | };
334 | if (varName[0] === "$") {
335 | storeDiff[varName] = data;
336 | } else {
337 | componentDiff[varName] = data;
338 | }
339 | }
340 | }
341 | }
342 | }
343 |
344 | if (!_.isEmpty(componentDiff)) {
345 | diff.changedVariables[componentId] = componentDiff;
346 | }
347 | }
348 |
349 | if (!_.isEmpty(storeDiff)) {
350 | diff.changedVariables["Store"] = storeDiff;
351 | }
352 | }
353 |
354 | let currentTree = get(fileTree);
355 | if (_.isEmpty(currentTree)) {
356 | // assign component children
357 | for (let file in astInfo) {
358 | for (let childFile in astInfo[file]) {
359 | componentTree[file].children.push(componentTree[childFile]);
360 | componentTree[childFile].parent = file;
361 | }
362 | }
363 |
364 | // determine top-level parent component
365 | for (let file in astInfo) {
366 | if (!componentTree[file].parent) {
367 | parentComponent = file;
368 | }
369 | }
370 |
371 | if (!_.isEmpty(componentTree)) {
372 | fileTree.set(componentTree[parentComponent]);
373 | }
374 |
375 | //create depth-first ordering of tree for state injections
376 | const flatTreeArray = [];
377 |
378 | // if AST came through, make flat file tree based on that; otherwise use componentData
379 | if (!_.isEmpty(componentTree)) {
380 | depthFirstTraverse(componentTree[parentComponent]);
381 | } else {
382 | depthFirstTraverse(componentData[domParent]);
383 | }
384 |
385 | function depthFirstTraverse(tree) {
386 | flatTreeArray.push(tree.tagName || tree.id);
387 | if (tree.children.length) {
388 | tree.children.forEach((child) => {
389 | depthFirstTraverse(child);
390 | });
391 | }
392 | }
393 |
394 | flatFileTree.set(flatTreeArray);
395 | }
396 |
397 | const snapshot = {
398 | data: componentData,
399 | parent: domParent,
400 | label: snapshotLabel ? snapshotLabel : "Unlabeled Snapshot",
401 | diff,
402 | };
403 |
404 | rebuild = false;
405 |
406 | const deepCloneSnapshot = JSON.parse(JSON.stringify(snapshot));
407 |
408 | return deepCloneSnapshot; // deep clone to "freeze" state
409 | }
410 |
411 | // recursively delete node and all descendents
412 | function deleteNode(nodeId) {
413 | const { children, component, id } = nodes[nodeId];
414 | if (children.length) {
415 | children.forEach((child) => {
416 | deleteNode(child.id);
417 | });
418 | }
419 | delete componentData[component].nodes[id];
420 | }
421 |
422 | function getDiffValue(value) {
423 | let text = "";
424 |
425 | if (value === null || typeof value !== "object") {
426 | text += value;
427 | } else {
428 | text += "\n";
429 | for (let i in value) {
430 | nested(value[i], 1);
431 | }
432 | }
433 |
434 | return text;
435 |
436 | function nested(obj, tabCount) {
437 | if (obj.value) {
438 | // add tabs based on the level in the recursion
439 | for (let i = 1; i <= tabCount; i++) {
440 | text = text + "\t";
441 | }
442 | text = text + obj.name + ": ";
443 | if (typeof obj.value !== "object") {
444 | text = text + obj.value + "\n";
445 | } else {
446 | tabCount++;
447 | text = text + "\n";
448 | for (let val in obj.value) {
449 | nested(obj.value[val], tabCount);
450 | }
451 | }
452 | }
453 | }
454 | }
455 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
2 | const path = require('path');
3 |
4 | const mode = process.env.NODE_ENV || 'development';
5 | const prod = mode === 'production';
6 |
7 | module.exports = {
8 | entry: {
9 | 'build/bundle': ['./src/main.js']
10 | },
11 | resolve: {
12 | alias: {
13 | svelte: path.dirname(require.resolve('svelte/package.json'))
14 | },
15 | extensions: ['.mjs', '.js', '.svelte'],
16 | mainFields: ['svelte', 'browser', 'module', 'main']
17 | },
18 | output: {
19 | path: path.join(__dirname, '/extension/devtools'),
20 | filename: '[name].js',
21 | chunkFilename: '[name].[id].js'
22 | },
23 | module: {
24 | rules: [
25 | {
26 | test: /\.svelte$/,
27 | use: {
28 | loader: 'svelte-loader',
29 | options: {
30 | compilerOptions: {
31 | dev: !prod
32 | },
33 | emitCss: prod,
34 | hotReload: !prod
35 | }
36 | }
37 | },
38 | {
39 | test: /\.css$/,
40 | use: [
41 | MiniCssExtractPlugin.loader,
42 | 'css-loader'
43 | ]
44 | },
45 | {
46 | // required to prevent errors from Svelte on Webpack 5+
47 | test: /node_modules\/svelte\/.*\.mjs$/,
48 | resolve: {
49 | fullySpecified: false
50 | }
51 | },
52 | {
53 | test: /\.(png|jpe?g|gif)$/i,
54 | use: [
55 | {
56 | loader: 'file-loader',
57 | },
58 | ],
59 | },
60 | ]
61 | },
62 | mode,
63 | plugins: [
64 | new MiniCssExtractPlugin({
65 | filename: '[name].css'
66 | })
67 | ],
68 | devtool: prod ? false : 'source-map',
69 | devServer: {
70 | hot: true
71 | }
72 | };
73 |
--------------------------------------------------------------------------------