├── .github └── workflows │ └── runTests.yml ├── .gitignore ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── README.md ├── __tests__ ├── connect.test.js ├── contentMain.test.js ├── example.test.ts ├── serviceWorker.test.js └── tree.test.js ├── index.html ├── my-app ├── .gitignore ├── .npmrc ├── README.md ├── jsconfig.json ├── package.json ├── src │ ├── app.d.ts │ ├── app.html │ ├── lib │ │ └── images │ │ │ ├── github.svg │ │ │ ├── svelte-logo.svg │ │ │ ├── svelte-welcome.png │ │ │ └── svelte-welcome.webp │ └── routes │ │ ├── +layout.svelte │ │ ├── +page.js │ │ ├── +page.svelte │ │ ├── Counter.svelte │ │ ├── Header.svelte │ │ ├── about │ │ ├── +page.js │ │ └── +page.svelte │ │ ├── styles.css │ │ └── sverdle │ │ ├── +page.server.js │ │ ├── +page.svelte │ │ ├── game.js │ │ ├── how-to-play │ │ ├── +page.js │ │ └── +page.svelte │ │ ├── reduced-motion.js │ │ └── words.server.js ├── static │ ├── favicon.png │ └── robots.txt ├── svelte.config.js └── vite.config.js ├── package-lock.json ├── package.json ├── public ├── contentIsolated.js ├── contentMain.js ├── devtools.html ├── devtools.js ├── manifest.json └── serviceWorker.js ├── src ├── App.svelte ├── app.css ├── components │ ├── connect │ │ └── connect.svelte │ └── mainPanel │ │ ├── leftPanel │ │ ├── LeftPanel.svelte │ │ └── Tree.svelte │ │ ├── mainPanel.svelte │ │ └── rightPanel │ │ └── RightPanel.svelte ├── main.ts ├── store.ts ├── types.ts └── vite-env.d.ts ├── svelte.config.js ├── test.js ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.ts └── vitest.config.ts /.github/workflows/runTests.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 3 | 4 | name: Vitest Testing 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Use Node.js 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: '18.14.2' 22 | - name: Install Dependencies 23 | run: npm ci 24 | - name: Build Application 25 | run: npm run build --if-present 26 | - name: Run Tests 27 | run: npm test 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .idea 17 | .DS_Store 18 | *.suo 19 | *.ntvs* 20 | *.njsproj 21 | *.sln 22 | *.sw? 23 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["svelte.svelte-vscode"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "editor.formatOnPaste": false, // required 4 | "editor.formatOnType": false, // required 5 | "editor.formatOnSave": true, // optional 6 | "editor.formatOnSaveMode": "file", // required to format on save 7 | "files.autoSave": "onFocusChange", // optional but recommended 8 | "vs-code-prettier-eslint.prettierLast": false, 9 | "[svelte]": { 10 | "editor.defaultFormatter": "svelte.svelte-vscode" 11 | }, 12 | "svelte.enable-ts-plugin": true //enables intellisense for svelte in ts/js files 13 | } 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 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 | # Sveltechron 2 | 3 | ![Banner](https://github.com/oslabs-beta/Sveltechron/assets/123424761/ec9abb2d-67fc-469d-a38c-8b6873db4661) 4 | 5 | ## About me 6 | ![Svelte](https://img.shields.io/badge/svelte-%23f1413d.svg?style=for-the-badge&logo=svelte&logoColor=white) 7 | ![TypeScript](https://img.shields.io/badge/typescript-%23007ACC.svg?style=for-the-badge&logo=typescript&logoColor=white) 8 | ![D3.js](https://img.shields.io/badge/d3.js-F9A03C?style=for-the-badge&logo=d3.js&logoColor=white) 9 | ![Vite](https://img.shields.io/badge/Vite-B73BFE?style=for-the-badge&logo=vite&logoColor=FFD62E) 10 | ![Vitest](https://img.shields.io/badge/Vitest-6E9F18?style=for-the-badge&logo=vitest&logoColor=white) 11 | ![Chrome Dev Tool API](https://img.shields.io/badge/Chrome%20Dev%20Tool%20Api-4285F4?style=for-the-badge&logo=googlechrome&logoColor=white) 12 | [![Medium](https://img.shields.io/badge/Medium-12100E?style=for-the-badge&logo=medium&logoColor=white)](https://medium.com/@willpratt/introducing-sveltechron-b87fc2b5e3a5) 13 | [![LinkedIn](https://img.shields.io/badge/-LinkedIn-black.svg?style=for-the-badge&logo=linkedin&colorB=555)](https://www.linkedin.com/company/sveltechron/) 14 | 15 | 16 | A Chrome DevTool that enables Svelte developers to visualize their Svelte application's structure while quickly understanding the state and prop changes in real time. 17 | 18 | ## Features 19 | * Component hierarchy visualization 20 | * State and Props Display 21 | 22 | ### Component hierarchy visualization 23 | Sveltechron creates a tree structure of the application's component hierarchy, allowing for a greater understanding of the structure of the application. Which is incredibly useful when digging through a larger-scale application containing a large amount of components. 24 | 25 | ![hierarchy](https://github.com/oslabs-beta/Sveltechron/assets/123424761/e674bcfb-370b-4797-a6c7-db4e22e4703f) 26 | 27 | 28 | ### State and Props Display 29 | When inspecting one of the components nodes in Sveltechron, users are also able to view the state and props are held in the selected component. this allows for an easy understanding of the data flow of the users application. 30 | 31 | ![stateandprop](https://github.com/oslabs-beta/Sveltechron/assets/123424761/c5898a49-c2f9-4c0f-8f81-7052b6fafcbe) 32 | 33 | 34 | ## How to use Sveltechron 35 | 1. Fork and clone this repo. 36 | 2. Go to the projects root directory and execute npm install in the CLI. 37 | 3. Execute npm run build in the CLI. 38 | 4. Unload the dist folder in the Chrome extension manager page. 39 | 5. Inspect the browser window when running your Svelte application in development mode and navigate to Sveltechron in the dev panel. 40 | 41 | (Chrome extension coming soon!) 42 | 43 | ## How to use the demo app 44 | 1. Navigate to demo-app folder. 45 | 2. Execute npm install in the CLI. 46 | 3. Execute npm run dev in the CLI. 47 | 48 | 49 | ## Authors 50 | 51 | [jayachandra Kura](https://github.com/jayachankura) 52 | 53 | [Kevin Wong](https://github.com/KW-SWE) 54 | 55 | [Sean Romine](https://github.com/seanRomine) 56 | 57 | [Will Pratt](https://github.com/WillPrattCodes) 58 | 59 | 60 | ## License 61 | Distributed under the MIT License. See LICENSE.txt for more information. 62 | -------------------------------------------------------------------------------- /__tests__/connect.test.js: -------------------------------------------------------------------------------- 1 | import { mount, mock, trigger } from 'vitest'; 2 | import '../src/components/connect/connect.svelte'; // Update the path accordingly 3 | 4 | describe('Svelte Component Tests', () => { 5 | it('should connect to service worker and set connected to true on successful connection', () => { 6 | // Mock the necessary dependencies 7 | const chromeRuntimeMock = mock('chrome.runtime', { 8 | connect: jest.fn(() => ({ 9 | postMessage: jest.fn(), 10 | onMessage: { addListener: jest.fn() }, 11 | })), 12 | }); 13 | 14 | const chromeDevToolsMock = mock('chrome.devtools', { 15 | inspectedWindow: { tabId: 123 }, // Replace with the actual tabId 16 | }); 17 | 18 | const storeMock = mock('../../store', { 19 | rootNodes: { set: jest.fn() }, 20 | connected: { set: jest.fn() }, 21 | }); 22 | 23 | // Mount the component 24 | mount(); 25 | 26 | // Trigger connectToServiceWorker function 27 | trigger('button', 'click'); 28 | 29 | // Assertions 30 | expect(chromeRuntimeMock.connect).toHaveBeenCalled(); 31 | expect(chromeRuntimeMock.postMessage).toHaveBeenCalledWith({ 32 | action: 'connect', 33 | body: 123, // Replace with the actual tabId 34 | }); 35 | expect(storeMock.connected.set).toHaveBeenCalledWith(true); 36 | }); 37 | 38 | it('should add nodes to nodeMap and rootNodes on "addNode" message', () => { 39 | // Mock the necessary dependencies 40 | const chromeRuntimeMock = mock('chrome.runtime', { 41 | connect: () => ({ 42 | postMessage: jest.fn(), 43 | onMessage: { addListener: jest.fn() }, 44 | }), 45 | }); 46 | 47 | const storeMock = mock('../../store', { 48 | rootNodes: { set: jest.fn() }, 49 | connected: { set: jest.fn() }, 50 | }); 51 | 52 | // Mount the component 53 | mount(); 54 | 55 | // Trigger connectToServiceWorker function 56 | trigger('button', 'click'); 57 | 58 | // Trigger addNode message 59 | const messageHandler = 60 | chromeRuntimeMock.onMessage.addListener.mock.calls[0][0]; 61 | messageHandler({ type: 'addNode', node: { id: 1, target: null } }); 62 | 63 | // Assertions 64 | expect(storeMock.rootNodes.set).toHaveBeenCalledWith([ 65 | { id: 1, children: [] }, 66 | ]); 67 | }); 68 | }); 69 | 70 | // Run the tests 71 | mount(); 72 | -------------------------------------------------------------------------------- /__tests__/contentMain.test.js: -------------------------------------------------------------------------------- 1 | import { mount, mock } from 'vitest'; 2 | import { addNodeByMessage, extractNode } from '../public/contentMain'; // Update the path accordingly 3 | 4 | // Mock window.postMessage 5 | const postMessageMock = mock(window, 'postMessage'); 6 | 7 | // Mock processNode function 8 | const processNodeMock = mock( 9 | './your-process-node-file', 10 | 'processNode', 11 | (node) => node 12 | ); 13 | 14 | describe('addNodeByMessage', () => { 15 | it('should call postMessage with the correct parameters', () => { 16 | // Arrange 17 | const node = { 18 | parent: { 19 | id: 1, 20 | }, 21 | // Other properties of your node object 22 | }; 23 | 24 | // Act 25 | addNodeByMessage(node); 26 | 27 | // Assert 28 | postMessageMock.assertCalledWith({ 29 | target: node.parent ? node.parent.id : null, 30 | type: 'addNode', 31 | node: node, // Assuming processNode just returns the input node for simplicity 32 | }); 33 | 34 | // Optionally, you can also assert that the processNode function is called 35 | processNodeMock.assertCalledWith(node); 36 | }); 37 | }); 38 | 39 | // Run the tests 40 | mount(); 41 | 42 | import { mount } from 'vitest'; 43 | import { extractNode } from './your-extract-node-file'; // Update the path accordingly 44 | 45 | describe('extractNode', () => { 46 | it('should extract details for a component node', () => { 47 | // Arrange 48 | const componentNode = { 49 | type: 'component', 50 | detail: { 51 | $$: { 52 | props: { prop1: 'value1', prop2: 'value2' }, 53 | bound: { prop1: true }, 54 | callbacks: { 55 | click: [{ toString: () => 'handleClick' }], 56 | hover: [{ toString: () => 'handleHover' }], 57 | }, 58 | }, 59 | $capture_state: () => ({ capturedState: 'capturedValue' }), 60 | }, 61 | }; 62 | 63 | // Act 64 | const result = extractNode(componentNode); 65 | 66 | // Assert 67 | expect(result).toEqual({ 68 | tagName: undefined, // Assuming tagName is not used for components 69 | type: 'component', 70 | id: undefined, // Assuming id is not used for components 71 | detail: { 72 | attributes: [ 73 | { key: 'prop1', value: 'value1', isBound: true }, 74 | { key: 'prop2', value: 'value2', isBound: false }, 75 | ], 76 | listeners: [ 77 | { event: 'click', handler: 'handleClick' }, 78 | { event: 'hover', handler: 'handleHover' }, 79 | ], 80 | ctx: [{ key: 'capturedState', value: 'capturedValue' }], 81 | }, 82 | }); 83 | }); 84 | 85 | it('should extract details for an element node', () => { 86 | // Arrange 87 | const elementNode = { 88 | type: 'element', 89 | detail: { 90 | attributes: [{ name: 'attr1', value: 'value1' }], 91 | __listeners: [{ event: 'click', handler: 'handleClick' }], 92 | }, 93 | }; 94 | 95 | // Act 96 | const result = extractNode(elementNode); 97 | 98 | // Assert 99 | expect(result).toEqual({ 100 | tagName: undefined, // Assuming tagName is not used for elements 101 | type: 'element', 102 | id: undefined, // Assuming id is not used for elements 103 | detail: { 104 | attributes: [{ key: 'attr1', value: 'value1' }], 105 | listeners: [{ event: 'click', handler: 'handleClick' }], 106 | }, 107 | }); 108 | }); 109 | }); 110 | 111 | // Run the tests 112 | mount(); 113 | -------------------------------------------------------------------------------- /__tests__/example.test.ts: -------------------------------------------------------------------------------- 1 | //these must be imported each time 2 | import { fireEvent, render, screen } from '@testing-library/svelte'; 3 | import '@testing-library/jest-dom'; 4 | import Options from '../src/components/App.svelte'; 5 | 6 | global.chrome = { 7 | runtime: { 8 | connect: jest.fn(() => ({ 9 | onMessage: { 10 | addListener: jest.fn((callback) => { 11 | // You can simulate a successful connection message here 12 | callback('successfully connected'); 13 | }), 14 | }, 15 | postMessage: jest.fn(), 16 | })), 17 | }, 18 | devtools: { 19 | inspectedWindow: { 20 | tabId: '123', // Mock tab ID 21 | }, 22 | }, 23 | }; 24 | 25 | describe('Options.svelte', () => { 26 | /** 27 | * @author Jay Kura 28 | * afterEach(() => cleanup()) is done automatically to clear the dom between tests 29 | */ 30 | test('that shows count when rendered', () => { 31 | render(Options, { count: 2 }); 32 | const count = screen.getByText('Current count:'); 33 | expect(count).toHaveTextContent('Current count: 2'); 34 | }); 35 | test('number increments when plus button is clicked', async () => { 36 | render(Options, { count: 2 }); 37 | const plusButton = screen.getByText('+'); 38 | const count = screen.getByText('Current count:'); 39 | await fireEvent.click(plusButton); 40 | expect(count).toHaveTextContent('Current count: 3'); 41 | }); 42 | test('number increments when minus button is clicked', async () => { 43 | render(Options, { count: 10 }); 44 | console.log(screen.getByText('*')); 45 | const minusButton = screen.getByText('-'); 46 | const count = screen.getByText('Current count:'); 47 | await fireEvent.click(minusButton); 48 | expect(count).toHaveTextContent('Current count: 9'); 49 | }); 50 | test('devtools successfully connect to the app', async () => { 51 | // Render your component 52 | render(Options); 53 | 54 | // Simulate the click on the 'Connect' button 55 | const connectButton = screen.getByText('Connect'); 56 | await fireEvent.click(connectButton); 57 | 58 | // Check if the connect method has been called 59 | expect(chrome.runtime.connect).toHaveBeenCalled(); 60 | 61 | // Check if a message for a successful connection has been sent 62 | expect(chrome.runtime.connect().postMessage).toHaveBeenCalledWith( 63 | expect.objectContaining({ 64 | action: 'connect', 65 | body: chrome.devtools.inspectedWindow.tabId, 66 | }) 67 | ); 68 | }); 69 | 70 | -------------------------------------------------------------------------------- /__tests__/serviceWorker.test.js: -------------------------------------------------------------------------------- 1 | import { mount, mock } from 'vitest'; 2 | import '../public/serviceWorker'; 3 | 4 | // Mock chrome.runtime 5 | const runtimeMock = mock(chrome, 'runtime'); 6 | 7 | describe('Background Script', () => { 8 | it('should handle extension connection and relay messages', () => { 9 | // Arrange 10 | const extensionPort = { 11 | onMessage: { 12 | addListener: jest.fn(), 13 | removeListener: jest.fn(), 14 | }, 15 | onDisconnect: { 16 | addListener: jest.fn(), 17 | }, 18 | postMessage: jest.fn(), 19 | }; 20 | 21 | runtimeMock.onConnect.addListener.mock.calls[0][0](extensionPort); 22 | 23 | // Act 24 | extensionPort.onMessage.addListener.mock.calls[0][0]({ 25 | action: 'connect', 26 | body: 123, // Replace with the desired tabId value 27 | }); 28 | 29 | // Assert 30 | expect(extensionPort.postMessage).toHaveBeenCalledWith( 31 | 'successfully connected' 32 | ); 33 | 34 | // Act 35 | extensionPort.onDisconnect.addListener.mock.calls[0][0](); 36 | 37 | // Assert 38 | expect(extensionPort.onMessage.removeListener).toHaveBeenCalled(); 39 | }); 40 | 41 | it('should relay messages from content script to the extension', () => { 42 | // Arrange 43 | const message = { 44 | // Your message properties 45 | }; 46 | 47 | // Act 48 | runtimeMock.onMessage.addListener.mock.calls[0][0](message); 49 | 50 | // Assert 51 | expect(extensionPort.postMessage).toHaveBeenCalledWith(message); 52 | }); 53 | }); 54 | 55 | // Run the tests 56 | mount(); 57 | -------------------------------------------------------------------------------- /__tests__/tree.test.js: -------------------------------------------------------------------------------- 1 | import { mount, mock, trigger } from 'vitest'; 2 | import '../src/components/mainPanel/leftPanel/Tree.svelte'; // Update the path accordingly 3 | 4 | describe('Svelte Component Tests', () => { 5 | it('should initialize and render the tree on mount', () => { 6 | // Mock the necessary dependencies 7 | const d3Mock = mock('d3', { 8 | tree: () => ({ 9 | size: () => ({ descendants: () => [] }), 10 | hierarchy: () => ({ each: () => {} }), 11 | }), 12 | }); 13 | 14 | // Mount the component 15 | mount(); 16 | 17 | // Assertions 18 | expect(d3Mock.tree).toHaveBeenCalled(); 19 | expect(d3Mock.hierarchy).toHaveBeenCalled(); 20 | // Add more assertions based on your specific logic 21 | }); 22 | 23 | it('should update the tree on data change', () => { 24 | // Mock the necessary dependencies 25 | const d3Mock = mock('d3', { 26 | tree: () => ({ 27 | size: () => ({ descendants: () => [] }), 28 | hierarchy: () => ({ each: () => {} }), 29 | }), 30 | }); 31 | 32 | // Mount the component 33 | mount(); 34 | 35 | // Trigger data change 36 | trigger(afterUpdate); 37 | 38 | // Assertions 39 | expect(d3Mock.tree).toHaveBeenCalled(); 40 | expect(d3Mock.hierarchy).toHaveBeenCalled(); 41 | // Add more assertions based on your specific logic 42 | }); 43 | }); 44 | 45 | // Run the tests 46 | mount(); 47 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | DEV TOOL 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /my-app/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | .vercel 10 | .output 11 | vite.config.js.timestamp-* 12 | vite.config.ts.timestamp-* 13 | -------------------------------------------------------------------------------- /my-app/.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /my-app/README.md: -------------------------------------------------------------------------------- 1 | # create-svelte 2 | 3 | Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte). 4 | 5 | ## Creating a project 6 | 7 | If you're seeing this, you've probably already done this step. Congrats! 8 | 9 | ```bash 10 | # create a new project in the current directory 11 | npm create svelte@latest 12 | 13 | # create a new project in my-app 14 | npm create svelte@latest my-app 15 | ``` 16 | 17 | ## Developing 18 | 19 | Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: 20 | 21 | ```bash 22 | npm run dev 23 | 24 | # or start the server and open the app in a new browser tab 25 | npm run dev -- --open 26 | ``` 27 | 28 | ## Building 29 | 30 | To create a production version of your app: 31 | 32 | ```bash 33 | npm run build 34 | ``` 35 | 36 | You can preview the production build with `npm run preview`. 37 | 38 | > To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment. 39 | -------------------------------------------------------------------------------- /my-app/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true 12 | } 13 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias and https://kit.svelte.dev/docs/configuration#files 14 | // 15 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 16 | // from the referenced tsconfig.json - TypeScript does not merge them in 17 | } 18 | -------------------------------------------------------------------------------- /my-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-app", 3 | "version": "0.0.1", 4 | "scripts": { 5 | "dev": "vite dev", 6 | "build": "vite build", 7 | "preview": "vite preview", 8 | "check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json", 9 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch" 10 | }, 11 | "devDependencies": { 12 | "@fontsource/fira-mono": "^4.5.10", 13 | "@neoconfetti/svelte": "^1.0.0", 14 | "@sveltejs/adapter-auto": "^2.0.0", 15 | "@sveltejs/kit": "^1.20.4", 16 | "@types/cookie": "^0.5.1", 17 | "svelte": "^4.0.5", 18 | "svelte-check": "^3.4.3", 19 | "typescript": "^5.0.0", 20 | "vite": "^4.4.2" 21 | }, 22 | "type": "module" 23 | } 24 | -------------------------------------------------------------------------------- /my-app/src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://kit.svelte.dev/docs/types#app 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface Platform {} 9 | } 10 | } 11 | 12 | export {}; 13 | -------------------------------------------------------------------------------- /my-app/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |
%sveltekit.body%
11 | 12 | 13 | -------------------------------------------------------------------------------- /my-app/src/lib/images/github.svg: -------------------------------------------------------------------------------- 1 | 2 | 9 | 16 | -------------------------------------------------------------------------------- /my-app/src/lib/images/svelte-logo.svg: -------------------------------------------------------------------------------- 1 | svelte-logo -------------------------------------------------------------------------------- /my-app/src/lib/images/svelte-welcome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Sveltechron/1757404cb6a7b7b38d46b606f061e6998a28b48f/my-app/src/lib/images/svelte-welcome.png -------------------------------------------------------------------------------- /my-app/src/lib/images/svelte-welcome.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Sveltechron/1757404cb6a7b7b38d46b606f061e6998a28b48f/my-app/src/lib/images/svelte-welcome.webp -------------------------------------------------------------------------------- /my-app/src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 |
7 |
8 | 9 |
10 | 11 |
12 | 13 | 16 |
17 | 18 | 54 | -------------------------------------------------------------------------------- /my-app/src/routes/+page.js: -------------------------------------------------------------------------------- 1 | // since there's no dynamic data here, we can prerender 2 | // it so that it gets served as a static asset in production 3 | export const prerender = true; 4 | -------------------------------------------------------------------------------- /my-app/src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | Home 9 | 10 | 11 | 12 |
13 |

14 | 15 | 16 | 17 | Welcome 18 | 19 | 20 | 21 | to your new
SvelteKit app 22 |

23 | 24 |

25 | try editing src/routes/+page.svelte 26 |

27 | 28 | 29 |
30 | 31 | 60 | -------------------------------------------------------------------------------- /my-app/src/routes/Counter.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 |
21 | 26 | 27 |
28 |
29 | 30 | {Math.floor($displayed_count)} 31 |
32 |
33 | 34 | 39 |
40 | 41 | 107 | -------------------------------------------------------------------------------- /my-app/src/routes/Header.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 |
8 |
9 | 10 | SvelteKit 11 | 12 |
13 | 14 | 33 | 34 |
35 | 36 | GitHub 37 | 38 |
39 |
40 | 41 | 130 | -------------------------------------------------------------------------------- /my-app/src/routes/about/+page.js: -------------------------------------------------------------------------------- 1 | import { dev } from '$app/environment'; 2 | 3 | // we don't need any JS on this page, though we'll load 4 | // it in dev so that we get hot module replacement 5 | export const csr = dev; 6 | 7 | // since there's no dynamic data here, we can prerender 8 | // it so that it gets served as a static asset in production 9 | export const prerender = true; 10 | -------------------------------------------------------------------------------- /my-app/src/routes/about/+page.svelte: -------------------------------------------------------------------------------- 1 | 2 | About 3 | 4 | 5 | 6 |
7 |

About this app

8 | 9 |

10 | This is a SvelteKit app. You can make your own by typing the 11 | following into your command line and following the prompts: 12 |

13 | 14 |
npm create svelte@latest
15 | 16 |

17 | The page you're looking at is purely static HTML, with no client-side interactivity needed. 18 | Because of that, we don't need to load any JavaScript. Try viewing the page's source, or opening 19 | the devtools network panel and reloading. 20 |

21 | 22 |

23 | The Sverdle page illustrates SvelteKit's data loading and form handling. Try 24 | using it with JavaScript disabled! 25 |

26 |
27 | -------------------------------------------------------------------------------- /my-app/src/routes/styles.css: -------------------------------------------------------------------------------- 1 | @import '@fontsource/fira-mono'; 2 | 3 | :root { 4 | --font-body: Arial, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, 5 | Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 6 | --font-mono: 'Fira Mono', monospace; 7 | --color-bg-0: rgb(202, 216, 228); 8 | --color-bg-1: hsl(209, 36%, 86%); 9 | --color-bg-2: hsl(224, 44%, 95%); 10 | --color-theme-1: #ff3e00; 11 | --color-theme-2: #4075a6; 12 | --color-text: rgba(0, 0, 0, 0.7); 13 | --column-width: 42rem; 14 | --column-margin-top: 4rem; 15 | font-family: var(--font-body); 16 | color: var(--color-text); 17 | } 18 | 19 | body { 20 | min-height: 100vh; 21 | margin: 0; 22 | background-attachment: fixed; 23 | background-color: var(--color-bg-1); 24 | background-size: 100vw 100vh; 25 | background-image: radial-gradient( 26 | 50% 50% at 50% 50%, 27 | rgba(255, 255, 255, 0.75) 0%, 28 | rgba(255, 255, 255, 0) 100% 29 | ), 30 | linear-gradient(180deg, var(--color-bg-0) 0%, var(--color-bg-1) 15%, var(--color-bg-2) 50%); 31 | } 32 | 33 | h1, 34 | h2, 35 | p { 36 | font-weight: 400; 37 | } 38 | 39 | p { 40 | line-height: 1.5; 41 | } 42 | 43 | a { 44 | color: var(--color-theme-1); 45 | text-decoration: none; 46 | } 47 | 48 | a:hover { 49 | text-decoration: underline; 50 | } 51 | 52 | h1 { 53 | font-size: 2rem; 54 | text-align: center; 55 | } 56 | 57 | h2 { 58 | font-size: 1rem; 59 | } 60 | 61 | pre { 62 | font-size: 16px; 63 | font-family: var(--font-mono); 64 | background-color: rgba(255, 255, 255, 0.45); 65 | border-radius: 3px; 66 | box-shadow: 2px 2px 6px rgb(255 255 255 / 25%); 67 | padding: 0.5em; 68 | overflow-x: auto; 69 | color: var(--color-text); 70 | } 71 | 72 | .text-column { 73 | display: flex; 74 | max-width: 48rem; 75 | flex: 0.6; 76 | flex-direction: column; 77 | justify-content: center; 78 | margin: 0 auto; 79 | } 80 | 81 | input, 82 | button { 83 | font-size: inherit; 84 | font-family: inherit; 85 | } 86 | 87 | button:focus:not(:focus-visible) { 88 | outline: none; 89 | } 90 | 91 | @media (min-width: 720px) { 92 | h1 { 93 | font-size: 2.4rem; 94 | } 95 | } 96 | 97 | .visually-hidden { 98 | border: 0; 99 | clip: rect(0 0 0 0); 100 | height: auto; 101 | margin: 0; 102 | overflow: hidden; 103 | padding: 0; 104 | position: absolute; 105 | width: 1px; 106 | white-space: nowrap; 107 | } 108 | -------------------------------------------------------------------------------- /my-app/src/routes/sverdle/+page.server.js: -------------------------------------------------------------------------------- 1 | import { fail } from '@sveltejs/kit'; 2 | import { Game } from './game'; 3 | 4 | /** @satisfies {import('./$types').PageServerLoad} */ 5 | export const load = ({ cookies }) => { 6 | const game = new Game(cookies.get('sverdle')); 7 | 8 | return { 9 | /** 10 | * The player's guessed words so far 11 | */ 12 | guesses: game.guesses, 13 | 14 | /** 15 | * An array of strings like '__x_c' corresponding to the guesses, where 'x' means 16 | * an exact match, and 'c' means a close match (right letter, wrong place) 17 | */ 18 | answers: game.answers, 19 | 20 | /** 21 | * The correct answer, revealed if the game is over 22 | */ 23 | answer: game.answers.length >= 6 ? game.answer : null 24 | }; 25 | }; 26 | 27 | /** @satisfies {import('./$types').Actions} */ 28 | export const actions = { 29 | /** 30 | * Modify game state in reaction to a keypress. If client-side JavaScript 31 | * is available, this will happen in the browser instead of here 32 | */ 33 | update: async ({ request, cookies }) => { 34 | const game = new Game(cookies.get('sverdle')); 35 | 36 | const data = await request.formData(); 37 | const key = data.get('key'); 38 | 39 | const i = game.answers.length; 40 | 41 | if (key === 'backspace') { 42 | game.guesses[i] = game.guesses[i].slice(0, -1); 43 | } else { 44 | game.guesses[i] += key; 45 | } 46 | 47 | cookies.set('sverdle', game.toString()); 48 | }, 49 | 50 | /** 51 | * Modify game state in reaction to a guessed word. This logic always runs on 52 | * the server, so that people can't cheat by peeking at the JavaScript 53 | */ 54 | enter: async ({ request, cookies }) => { 55 | const game = new Game(cookies.get('sverdle')); 56 | 57 | const data = await request.formData(); 58 | const guess = /** @type {string[]} */ (data.getAll('guess')); 59 | 60 | if (!game.enter(guess)) { 61 | return fail(400, { badGuess: true }); 62 | } 63 | 64 | cookies.set('sverdle', game.toString()); 65 | }, 66 | 67 | restart: async ({ cookies }) => { 68 | cookies.delete('sverdle'); 69 | } 70 | }; 71 | -------------------------------------------------------------------------------- /my-app/src/routes/sverdle/+page.svelte: -------------------------------------------------------------------------------- 1 | 91 | 92 | 93 | 94 | 95 | Sverdle 96 | 97 | 98 | 99 |

Sverdle

100 | 101 |
{ 105 | // prevent default callback from resetting the form 106 | return ({ update }) => { 107 | update({ reset: false }); 108 | }; 109 | }} 110 | > 111 | How to play 112 | 113 |
114 | {#each Array.from(Array(6).keys()) as row (row)} 115 | {@const current = row === i} 116 |

Row {row + 1}

117 |
118 | {#each Array.from(Array(5).keys()) as column (column)} 119 | {@const guess = current ? currentGuess : data.guesses[row]} 120 | {@const answer = data.answers[row]?.[column]} 121 | {@const value = guess?.[column] ?? ''} 122 | {@const selected = current && column === guess.length} 123 | {@const exact = answer === 'x'} 124 | {@const close = answer === 'c'} 125 | {@const missing = answer === '_'} 126 |
127 | {value} 128 | 129 | {#if exact} 130 | (correct) 131 | {:else if close} 132 | (present) 133 | {:else if missing} 134 | (absent) 135 | {:else} 136 | empty 137 | {/if} 138 | 139 | 140 |
141 | {/each} 142 |
143 | {/each} 144 |
145 | 146 |
147 | {#if won || data.answers.length >= 6} 148 | {#if !won && data.answer} 149 |

the answer was "{data.answer}"

150 | {/if} 151 | 154 | {:else} 155 |
156 | 157 | 158 | 167 | 168 | {#each ['qwertyuiop', 'asdfghjkl', 'zxcvbnm'] as row} 169 |
170 | {#each row as letter} 171 | 183 | {/each} 184 |
185 | {/each} 186 |
187 | {/if} 188 |
189 |
190 | 191 | {#if won} 192 |
202 | {/if} 203 | 204 | 416 | -------------------------------------------------------------------------------- /my-app/src/routes/sverdle/game.js: -------------------------------------------------------------------------------- 1 | import { words, allowed } from './words.server'; 2 | 3 | export class Game { 4 | /** 5 | * Create a game object from the player's cookie, or initialise a new game 6 | * @param {string | undefined} serialized 7 | */ 8 | constructor(serialized = undefined) { 9 | if (serialized) { 10 | const [index, guesses, answers] = serialized.split('-'); 11 | 12 | this.index = +index; 13 | this.guesses = guesses ? guesses.split(' ') : []; 14 | this.answers = answers ? answers.split(' ') : []; 15 | } else { 16 | this.index = Math.floor(Math.random() * words.length); 17 | this.guesses = ['', '', '', '', '', '']; 18 | this.answers = /** @type {string[]} */ ([]); 19 | } 20 | 21 | this.answer = words[this.index]; 22 | } 23 | 24 | /** 25 | * Update game state based on a guess of a five-letter word. Returns 26 | * true if the guess was valid, false otherwise 27 | * @param {string[]} letters 28 | */ 29 | enter(letters) { 30 | const word = letters.join(''); 31 | const valid = allowed.has(word); 32 | 33 | if (!valid) return false; 34 | 35 | this.guesses[this.answers.length] = word; 36 | 37 | const available = Array.from(this.answer); 38 | const answer = Array(5).fill('_'); 39 | 40 | // first, find exact matches 41 | for (let i = 0; i < 5; i += 1) { 42 | if (letters[i] === available[i]) { 43 | answer[i] = 'x'; 44 | available[i] = ' '; 45 | } 46 | } 47 | 48 | // then find close matches (this has to happen 49 | // in a second step, otherwise an early close 50 | // match can prevent a later exact match) 51 | for (let i = 0; i < 5; i += 1) { 52 | if (answer[i] === '_') { 53 | const index = available.indexOf(letters[i]); 54 | if (index !== -1) { 55 | answer[i] = 'c'; 56 | available[index] = ' '; 57 | } 58 | } 59 | } 60 | 61 | this.answers.push(answer.join('')); 62 | 63 | return true; 64 | } 65 | 66 | /** 67 | * Serialize game state so it can be set as a cookie 68 | */ 69 | toString() { 70 | return `${this.index}-${this.guesses.join(' ')}-${this.answers.join(' ')}`; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /my-app/src/routes/sverdle/how-to-play/+page.js: -------------------------------------------------------------------------------- 1 | import { dev } from '$app/environment'; 2 | 3 | // we don't need any JS on this page, though we'll load 4 | // it in dev so that we get hot module replacement 5 | export const csr = dev; 6 | 7 | // since there's no dynamic data here, we can prerender 8 | // it so that it gets served as a static asset in production 9 | export const prerender = true; 10 | -------------------------------------------------------------------------------- /my-app/src/routes/sverdle/how-to-play/+page.svelte: -------------------------------------------------------------------------------- 1 | 2 | How to play Sverdle 3 | 4 | 5 | 6 |
7 |

How to play Sverdle

8 | 9 |

10 | Sverdle is a clone of Wordle, the 11 | word guessing game. To play, enter a five-letter English word. For example: 12 |

13 | 14 |
15 | r 16 | i 17 | t 18 | z 19 | y 20 |
21 | 22 |

23 | The y is in the right place. r and 24 | t 25 | are the right letters, but in the wrong place. The other letters are wrong, and can be discarded. 26 | Let's make another guess: 27 |

28 | 29 |
30 | p 31 | a 32 | r 33 | t 34 | y 35 |
36 | 37 |

This time we guessed right! You have six guesses to get the word.

38 | 39 |

40 | Unlike the original Wordle, Sverdle runs on the server instead of in the browser, making it 41 | impossible to cheat. It uses <form> and cookies to submit data, meaning you can 42 | even play with JavaScript disabled! 43 |

44 |
45 | 46 | 96 | -------------------------------------------------------------------------------- /my-app/src/routes/sverdle/reduced-motion.js: -------------------------------------------------------------------------------- 1 | import { readable } from 'svelte/store'; 2 | import { browser } from '$app/environment'; 3 | 4 | const reduced_motion_query = '(prefers-reduced-motion: reduce)'; 5 | 6 | const get_initial_motion_preference = () => { 7 | if (!browser) return false; 8 | return window.matchMedia(reduced_motion_query).matches; 9 | }; 10 | 11 | export const reduced_motion = readable(get_initial_motion_preference(), (set) => { 12 | if (browser) { 13 | /** 14 | * @param {MediaQueryListEvent} event 15 | */ 16 | const set_reduced_motion = (event) => { 17 | set(event.matches); 18 | }; 19 | const media_query_list = window.matchMedia(reduced_motion_query); 20 | media_query_list.addEventListener('change', set_reduced_motion); 21 | 22 | return () => { 23 | media_query_list.removeEventListener('change', set_reduced_motion); 24 | }; 25 | } 26 | }); 27 | -------------------------------------------------------------------------------- /my-app/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Sveltechron/1757404cb6a7b7b38d46b606f061e6998a28b48f/my-app/static/favicon.png -------------------------------------------------------------------------------- /my-app/static/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /my-app/svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-auto'; 2 | 3 | /** @type {import('@sveltejs/kit').Config} */ 4 | const config = { 5 | kit: { 6 | // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. 7 | // If your environment is not supported or you settled on a specific environment, switch out the adapter. 8 | // See https://kit.svelte.dev/docs/adapters for more information about adapters. 9 | adapter: adapter() 10 | } 11 | }; 12 | 13 | export default config; 14 | -------------------------------------------------------------------------------- /my-app/vite.config.js: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import { defineConfig } from 'vite'; 3 | 4 | export default defineConfig({ 5 | plugins: [sveltekit()] 6 | }); 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "dependencies": { 4 | "d3": "^7.8.4", 5 | "path": "^0.12.7" 6 | }, 7 | "name": "sveltechron", 8 | "description": "dev tools for svelte (OSP)", 9 | "version": "1.0.0", 10 | "main": "./src/main.ts", 11 | "devDependencies": { 12 | "@sveltejs/vite-plugin-svelte": "^2.0.3", 13 | "@testing-library/svelte": "^4.0.4", 14 | "@tsconfig/svelte": "^4.0.1", 15 | "@types/chrome": "^0.0.250", 16 | "jsdom": "^22.1.0", 17 | "prettier": "2.8.8", 18 | "prettier-plugin-svelte": "^2.10.1", 19 | "svelte": "^3.57.0", 20 | "svelte-check": "^2.10.3", 21 | "svelte-preprocess": "^5.0.4", 22 | "tslib": "^2.5.0", 23 | "typescript": "^5.0.2", 24 | "vite": "^4.3.2", 25 | "vitest": "^0.34.6" 26 | }, 27 | "scripts": { 28 | "dev": "vite", 29 | "build": "vite build", 30 | "test": "vitest" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /public/contentIsolated.js: -------------------------------------------------------------------------------- 1 | // forward messages from contentMain to serviceWorker 2 | window.addEventListener("message", (message) => { 3 | console.log("message in isolated", message); 4 | if (typeof message === "object") chrome.runtime.sendMessage(message.data); 5 | }); 6 | -------------------------------------------------------------------------------- /public/contentMain.js: -------------------------------------------------------------------------------- 1 | // store all the nodes in a map 2 | const nodeMap = new Map(); 3 | 4 | //each node gets a unique id 5 | let _id = 0; 6 | 7 | // **************************** MESSAGING ************************************* 8 | 9 | // add node by sending a message 10 | function addNodeByMessage(node) { 11 | window.postMessage({ 12 | target: node.parent ? node.parent.id : null, 13 | type: "addNode", 14 | node: extractNode(node), 15 | }); 16 | } 17 | 18 | // update node by sending a message 19 | function updateNodeByMessage(node) { 20 | window.postMessage({ 21 | type: "updateNode", 22 | node: extractNode(node), 23 | }); 24 | } 25 | 26 | // delete node by sending message 27 | function deleteNodeByMessage(node) { 28 | window.postMessage({ 29 | type: "removeNode", 30 | node: extractNode(node), 31 | }); 32 | } 33 | 34 | //******************************* PROCESSING NODES ****************************************** 35 | 36 | // gets node and extracts info 37 | function extractNode(node) { 38 | const extractedDetails = { 39 | tagName: node.tagName, 40 | type: node.type, 41 | id: node.id, 42 | }; 43 | 44 | //check for component type node or text type node 45 | switch (node.type) { 46 | case "component": { 47 | if (!node.detail.$$) { 48 | extractedDetails.detail = {}; 49 | break; 50 | } 51 | const internal = node.detail.$$; 52 | // Older versions of Svelte stored props in an array 53 | const props = Array.isArray(internal.props) 54 | ? internal.props 55 | : Object.keys(internal.props); 56 | let ctx = multiCloner(node.detail.$capture_state()); 57 | if (ctx === undefined) ctx = {}; 58 | 59 | extractedDetails.detail = { 60 | attributes: props.flatMap((key) => { 61 | const value = ctx[key]; 62 | delete ctx[key]; 63 | return value === undefined 64 | ? [] 65 | : { key, value, isBound: key in internal.bound }; 66 | }), 67 | listeners: Object.entries(internal.callbacks).flatMap( 68 | ([event, value]) => 69 | value.map((obj) => ({ event, handler: obj.toString() })) 70 | ), 71 | ctx: Object.entries(ctx).map(([key, value]) => ({ key, value })), 72 | }; 73 | break; 74 | } 75 | 76 | case "element": { 77 | const element = node.detail; 78 | extractedDetails.detail = { 79 | attributes: Array.from(element.attributes).map((attr) => ({ 80 | key: attr.name, 81 | value: attr.value, 82 | })), 83 | listeners: element.__listeners 84 | ? element.__listeners.map((obj) => ({ 85 | ...obj, 86 | handler: obj.handler.toString(), 87 | })) 88 | : [], 89 | }; 90 | break; 91 | } 92 | } 93 | return extractedDetails; 94 | } 95 | 96 | function multiCloner(value, seen = new Map()) { 97 | switch (typeof value) { 98 | case "function": 99 | return { __isFunction: true, source: value.toString(), name: value.name }; 100 | case "symbol": 101 | return { __isSymbol: true, name: value.toString() }; 102 | case "object": 103 | if (value === window || value === null) return null; 104 | if (Array.isArray(value)) 105 | return value.map((obj) => multiCloner(obj, seen)); 106 | if (seen.has(value)) return {}; 107 | 108 | const obj = {}; 109 | seen.set(value, obj); 110 | 111 | for (const [key, val] of Object.entries(value)) { 112 | obj[key] = multiCloner(val, seen); 113 | } 114 | 115 | return obj; 116 | default: 117 | return value; 118 | } 119 | } 120 | 121 | const rootNodes = []; 122 | 123 | //**************************** NODE FUNCTIONS FOR DOM ***************************************** 124 | 125 | // insertNodeToDOM calls this 126 | function addNodeToDOM(node, target, anchor) { 127 | nodeMap.set(node.id, node); 128 | nodeMap.set(node.detail, node); 129 | 130 | let targetNode = nodeMap.get(target); 131 | 132 | if (!targetNode || targetNode.parentBlock != node.parentBlock) { 133 | targetNode = node.parentBlock; 134 | } 135 | 136 | node.parent = targetNode; 137 | 138 | const anchorNode = nodeMap.get(anchor); 139 | 140 | if (targetNode) { 141 | let index = -1; 142 | if (anchorNode) index = targetNode.children.indexOf(anchorNode); 143 | 144 | if (index != -1) { 145 | targetNode.children.splice(index, 0, node); 146 | } else { 147 | targetNode.children.push(node); 148 | } 149 | } else { 150 | rootNodes.push(node); 151 | } 152 | addNodeByMessage(node, anchorNode); 153 | } 154 | 155 | // SvelteInsertDOM calls this 156 | function insertNodeToDOM(element, target, anchor) { 157 | const node = { 158 | id: _id++, 159 | type: 160 | element.nodeType == 1 161 | ? "element" 162 | : element.nodeValue && element.nodeValue != " " 163 | ? "text" 164 | : "anchor", 165 | detail: element, 166 | tagName: element.nodeName.toLowerCase(), 167 | parentBlock: currentBlock, 168 | children: [], 169 | }; 170 | addNodeToDOM(node, target, anchor); 171 | 172 | for (const child of element.childNodes) { 173 | if (!nodeMap.has(child)) insertNodeToDOM(child, element); 174 | } 175 | } 176 | 177 | function removeNodeFromDOM(node) { 178 | if (!node) return; 179 | 180 | nodeMap.delete(node.id); 181 | nodeMap.delete(node.detail); 182 | 183 | console.log("this is the node:", node); 184 | const index = node.parent.children.indexOf(node); 185 | node.parent.children.splice(index, 1); 186 | node.parent = null; 187 | 188 | removeNodeByMessage(node); 189 | } 190 | 191 | //***************************** CALLBACK FUNCTIONS FOR EVENTS ***************************************** 192 | 193 | let currentBlock; 194 | 195 | function blockRegistration(e) { 196 | const { type, id, block, ...detail } = e.detail; 197 | const tagName = type == "pending" ? "await" : type; 198 | const nodeId = _id++; 199 | 200 | function updateProfile(node, type, fn, ...args) { 201 | fn(...args); 202 | } 203 | 204 | if (block.m) { 205 | const mountFn = block.m; 206 | block.m = (target, anchor) => { 207 | const parentBlock = currentBlock; 208 | let node = { 209 | id: nodeId, 210 | type: "block", 211 | detail, 212 | tagName, 213 | parentBlock, 214 | children: [], 215 | }; 216 | 217 | switch (type) { 218 | case "then": 219 | case "catch": 220 | if (!node.parentBlock) node.parentBlock = lastPromiseParent; 221 | break; 222 | 223 | case "slot": 224 | node.type = "slot"; 225 | break; 226 | 227 | case "component": 228 | const componentNode = nodeMap.get(block); 229 | if (componentNode) { 230 | nodeMap.delete(block); 231 | Object.assign(node, componentNode); 232 | } else { 233 | Object.assign(node, { 234 | type: "component", 235 | tagName: "Unknown", 236 | detail: {}, 237 | }); 238 | nodeMap.set(block, node); 239 | } 240 | 241 | Promise.resolve().then( 242 | () => 243 | node.detail.$$ && 244 | Object.keys(node.detail.$$.bound).length && 245 | updateNodeByMessage(node) 246 | ); 247 | break; 248 | } 249 | 250 | if (type == "each") { 251 | let group = nodeMap.get(parentBlock.id + id); 252 | if (!group) { 253 | group = { 254 | id: _id++, 255 | type: "block", 256 | detail: { 257 | ctx: {}, 258 | source: detail.source, 259 | }, 260 | tagName: "each", 261 | parentBlock, 262 | children: [], 263 | }; 264 | nodeMap.set(parentBlock.id + id, group); 265 | addNodeToDOM(group, target, anchor); 266 | } 267 | node.parentBlock = group; 268 | node.type = "iteration"; 269 | addNodeToDOM(node, group, anchor); 270 | } else { 271 | addNodeToDOM(node, target, anchor); 272 | } 273 | 274 | currentBlock = node; 275 | updateProfile(node, "mount", mountFn, target, anchor); 276 | currentBlock = parentBlock; 277 | }; 278 | } 279 | 280 | if (block.p) { 281 | const patchFn = block.p; 282 | block.p = (changed, ctx) => { 283 | const parentBlock = currentBlock; 284 | currentBlock = nodeMap.get(nodeId); 285 | updateNodeByMessage(currentBlock); 286 | updateProfile(currentBlock, "patch", patchFn, changed, ctx); 287 | currentBlock = parentBlock; 288 | }; 289 | } 290 | 291 | if (block.d) { 292 | const detachFn = block.d; 293 | block.d = (detach) => { 294 | const node = nodeMap.get(nodeId); 295 | 296 | if (node) { 297 | if (node.tagName == "await") lastPromiseParent = node.parentBlock; 298 | removeNodeFromDOM(node); 299 | } 300 | updateProfile(node, "detach", detachFn, detach); 301 | }; 302 | } 303 | } 304 | 305 | //this is called in response to 'SvelteRegisterComponent' message 306 | function registerSvelteComponent(event) { 307 | const { component, tagName } = event.detail; 308 | 309 | //get node content 310 | const node = nodeMap.get(component.$$.fragment); 311 | 312 | //update if already there, and add if it isn't there yet 313 | if (node) { 314 | nodeMap.delete(component.$$.fragment); 315 | 316 | node.detail = component; 317 | node.tagName = tagName; 318 | 319 | updateNodeByMessage(node); 320 | } else { 321 | nodeMap.set(component.$$.fragment, { 322 | type: "component", 323 | detail: component, 324 | tagName, 325 | }); 326 | } 327 | } 328 | 329 | //this is called in response to 'SvelteDOMInsert' message 330 | function svelteInsertDOM(event) { 331 | const { node: element, target, anchor } = event.detail; 332 | 333 | insertNodeToDOM(element, target, anchor); 334 | } 335 | 336 | function svelteRemoveDOM(event) { 337 | const node = nodeMap.get(event.detail.node); 338 | 339 | if (!node) return; 340 | removeNodeFromDOM(node); 341 | } 342 | 343 | function svelteSetDOMData(event) { 344 | const node = nodeMap.get(event.detail.node); 345 | if (!node) return; 346 | 347 | if (node.type == "anchor") node.type = "text"; 348 | 349 | updateNodeByMessage(node); 350 | } 351 | 352 | //******************* SVELTECHRON, ROLL OUT!!! ************************************** 353 | 354 | function INITIATE_SVELTECHRON(root) { 355 | root.addEventListener("SvelteRegisterBlock", blockRegistration); 356 | root.addEventListener("SvelteRegisterComponent", registerSvelteComponent); 357 | root.addEventListener("SvelteDOMInsert", svelteInsertDOM); 358 | root.addEventListener("SvelteDOMSetData", svelteSetDOMData); 359 | root.addEventListener("SvelteDOMRemove", svelteRemoveDOM); 360 | } 361 | 362 | INITIATE_SVELTECHRON(window.document); 363 | -------------------------------------------------------------------------------- /public/devtools.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /public/devtools.js: -------------------------------------------------------------------------------- 1 | chrome.devtools.panels.create('Sveltechron', null, 'index.html'); 2 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "Sveltechron", 4 | "version": "1.0.0", 5 | "description": "Browser devtools extension for debugging Svelte applications.", 6 | "background": { 7 | "type": "module", 8 | "service_worker": "serviceWorker.js", 9 | "run_at": "document_start" 10 | }, 11 | "content_scripts": [ 12 | { 13 | "matches": [""], 14 | "js": ["contentMAIN.js"], 15 | "world": "MAIN" 16 | }, 17 | { 18 | "matches": [""], 19 | "js": ["contentIsolated.js"] 20 | } 21 | ], 22 | "devtools_page": "devtools.html", 23 | "permissions": ["scripting", "activeTab"], 24 | "host_permissions": [""] 25 | } 26 | -------------------------------------------------------------------------------- /public/serviceWorker.js: -------------------------------------------------------------------------------- 1 | //let tabId and port be accessible for port connection between service worker and extension (store.ts) as well as service worker to content script connection 2 | let tabId; 3 | let port; 4 | // setup connection between service worker and extension (store.ts) 5 | chrome.runtime.onConnect.addListener(function (extensionPort) { 6 | // assign the listener function to a variable so we can remove it later 7 | const extensionListener = function (message) { 8 | switch (message.action) { 9 | case 'connect': 10 | //expose tabId and port info to service worker and content script connection 11 | tabId = message.body; 12 | port = extensionPort; 13 | chrome.tabs.reload(tabId, () => { 14 | port.postMessage('successfully connected'); 15 | }); 16 | break; 17 | default: 18 | //relay message from extension to contentScript 19 | chrome.tabs.sendMessage(tabId, message); 20 | } 21 | }; 22 | // inject the listener on receiving a message 23 | extensionPort.onMessage.addListener(extensionListener); 24 | 25 | extensionPort.onDisconnect.addListener(function () { 26 | extensionPort.onMessage.removeListener(extensionListener); 27 | console.log('Extension Disconnected'); 28 | }); 29 | }); 30 | 31 | // Relays messages from content script to the extension (store.ts) 32 | chrome.runtime.onMessage.addListener(function (message) { 33 | port.postMessage(message); 34 | }); 35 | -------------------------------------------------------------------------------- /src/App.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 |
13 | {#if connectSuccess === false} 14 | 15 | {:else} 16 | 17 | {/if} 18 |
19 | 20 | 26 | -------------------------------------------------------------------------------- /src/app.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | background-color: #242424; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | -webkit-text-size-adjust: 100%; 15 | } 16 | 17 | a { 18 | font-weight: 500; 19 | color: #646cff; 20 | text-decoration: inherit; 21 | } 22 | a:hover { 23 | color: #535bf2; 24 | } 25 | 26 | body { 27 | margin: 0; 28 | display: flex; 29 | place-items: center; 30 | min-width: 320px; 31 | min-height: 100vh; 32 | text-align: center; 33 | margin: 0 auto; 34 | } 35 | 36 | h1 { 37 | font-size: 3.2em; 38 | line-height: 1.1; 39 | } 40 | 41 | button { 42 | font-size: 1em; 43 | font-weight: 500; 44 | font-family: inherit; 45 | background-color: #1a1a1a; 46 | cursor: pointer; 47 | transition: border-color 0.25s; 48 | } 49 | 50 | @media (prefers-color-scheme: light) { 51 | :root { 52 | color: #213547; 53 | background-color: #ffffff; 54 | } 55 | a:hover { 56 | color: #747bff; 57 | } 58 | button { 59 | background-color: #f9f9f9; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/components/connect/connect.svelte: -------------------------------------------------------------------------------- 1 | 73 | 74 |
75 | 76 |
77 | 78 | 84 | -------------------------------------------------------------------------------- /src/components/mainPanel/leftPanel/LeftPanel.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | {#if $rootNodes.length} 9 |
10 | 11 |
12 | {:else} 13 |
not yet ready
14 | {/if} 15 | 16 | 22 | -------------------------------------------------------------------------------- /src/components/mainPanel/leftPanel/Tree.svelte: -------------------------------------------------------------------------------- 1 | 448 | 449 |
450 | -------------------------------------------------------------------------------- /src/components/mainPanel/mainPanel.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |
9 |
10 | 11 |
12 |
13 | 14 |
15 |
16 | 17 | 38 | -------------------------------------------------------------------------------- /src/components/mainPanel/rightPanel/RightPanel.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 |
7 |

State

8 | {#if ($hasBeenChanged = true)} 9 | {#each $stateNodeCont as { key, value }} 10 | {#if typeof value !== "object"} 11 |

{key}: {value}

12 | {:else} 13 |

{key}: {JSON.stringify(value)}

14 | {/if} 15 | {/each} 16 | {/if} 17 | {#if ($hasBeenChanged = false)} 18 | {#each $stateNodeCont as { key, value }} 19 | {#if typeof value !== "object"} 20 |

{key}: {value}

21 | {:else} 22 |

{key}: {JSON.stringify(value)}

23 | {/if} 24 | {/each} 25 | {/if} 26 |
27 |
28 |

Props

29 | {#if ($hasBeenChanged = true)} 30 | {#each $propsNodeCont as { key, value }} 31 | {#if typeof value !== "object"} 32 |

{key}: {value}

33 | {:else} 34 |

{key}: {JSON.stringify(value)}

35 | {/if} 36 | {/each} 37 | {/if} 38 | {#if ($hasBeenChanged = false)} 39 | {#each $propsNodeCont as { key, value }} 40 | {#if typeof value !== "object"} 41 |

{key}: {value}

42 | {:else} 43 |

{key}: {JSON.stringify(value)}

44 | {/if} 45 | {/each} 46 | {/if} 47 |
48 |
49 | 50 | 64 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import './app.css'; 2 | import App from './App.svelte'; 3 | 4 | const app = new App({ 5 | target: document.getElementById('app'), 6 | }); 7 | 8 | export default app; 9 | -------------------------------------------------------------------------------- /src/store.ts: -------------------------------------------------------------------------------- 1 | import { writable } from 'svelte/store'; 2 | import type { Writable } from 'svelte/store'; 3 | import type { Node, SnapShot } from './types'; 4 | 5 | export const rootNodes: Writable = writable([]); 6 | export const selected: Writable = writable(null); 7 | export const treeData = writable({}); 8 | export const connected = writable(false); 9 | export const stateNodeCont = writable([]); 10 | export const propsNodeCont = writable([]); 11 | export const hasBeenChanged = writable(false); 12 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface Message { 2 | type: string; 3 | source: string; 4 | anchor?: number; 5 | target?: number; 6 | node: Node; 7 | } 8 | 9 | export interface Node { 10 | id: number; 11 | children: Array | any[]; 12 | parent: Node | null; 13 | invalidate?: () => void; 14 | delay?: any; 15 | tagName: string; 16 | type: string; 17 | detail?: { 18 | attributes: any[]; 19 | listeners: any[]; 20 | ctx: any[]; 21 | }; 22 | } 23 | 24 | export interface SnapShot { 25 | _id: number; 26 | id: string; 27 | tagName: string; 28 | type: string; 29 | detail: { 30 | attributes: any[]; 31 | listeners: any[]; 32 | ctx: any[]; 33 | }; 34 | diff: Array; 35 | } 36 | 37 | export interface Difference { 38 | id: number; 39 | path: Array; 40 | value1: any; 41 | value2: any; 42 | } 43 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 2 | 3 | export default { 4 | // Consult https://svelte.dev/docs#compile-time-svelte-preprocess 5 | // for more information about preprocessors 6 | preprocess: [vitePreprocess()], 7 | }; 8 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | // // // jest.config.js 2 | // // module.exports = { 3 | // // transform: { 4 | // // '^.+\\.svelte$': 'jest-transform-svelte', 5 | // // '^.+\\.js$': 'babel-jest', 6 | // // }, 7 | // // setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect'], 8 | // // moduleFileExtensions: ['js', 'svelte'], 9 | // // }; 10 | 11 | // // jest.config.js 12 | // export default { 13 | // transform: { 14 | // '^.+\\.svelte$': 'jest-transform-svelte', 15 | // '^.+\\.js$': 'babel-jest', 16 | // '^.+\\.ts$': 'ts-jest', 17 | // }, 18 | // moduleFileExtensions: ['js', 'ts', 'svelte'], 19 | // testEnvironment: 'jsdom', 20 | // setupFilesAfterEnv: ['@testing-library/jest-dom/'], 21 | // }; 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/svelte/tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "useDefineForClassFields": true, 6 | "module": "ESNext", 7 | "resolveJsonModule": true, 8 | /** 9 | * Typecheck JS in `.svelte` and `.js` files by default. 10 | * Disable checkJs if you'd like to use dynamic types in JS. 11 | * Note that setting allowJs false does not prevent the use 12 | * of JS in `.svelte` files. 13 | */ 14 | "allowJs": true, 15 | "checkJs": true, 16 | "isolatedModules": true 17 | }, 18 | "include": [ 19 | "src/**/*.d.ts", 20 | "src/**/*.ts", 21 | "src/**/*.js", 22 | "src/**/*.svelte", 23 | "demo/**/*.d.ts", 24 | "demo/**/*.ts", 25 | "demo/**/*.js", 26 | "demo/**/*.svelte" 27 | ], 28 | "references": [{ "path": "./tsconfig.node.json" }] 29 | } 30 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler" 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import { svelte } from '@sveltejs/vite-plugin-svelte'; 3 | import path from 'path'; 4 | 5 | const __dirname = path.resolve(); 6 | 7 | // https://vitejs.dev/config/ 8 | export default defineConfig({ 9 | plugins: [svelte()], 10 | build: { 11 | target: 'esnext', 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | import { svelte } from '@sveltejs/vite-plugin-svelte'; 3 | 4 | export default defineConfig({ 5 | plugins: [svelte({ hot: !process.env.VITEST })], 6 | test: { 7 | /** 8 | * @author Jay Kura 9 | * includes will match __tests__ directory so that the nested tests can import tests from src/components/* folder w/o throwing an error 10 | */ 11 | include: ['__tests__/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], 12 | // allow for vitest to be accessed globally without being imported in each file like Jest 13 | globals: true, 14 | //using the jsdom implementation because it's a js implementation of many web standards, creates a browser-like environment for testing 15 | environment: 'jsdom', 16 | }, 17 | }); 18 | --------------------------------------------------------------------------------