├── .github └── workflows │ ├── compliance.yml │ └── development.yml ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── assets ├── astro-expand-collapse.gif ├── astro-inspection.gif └── astro-search.gif ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── .eslintrc.json ├── __tests__ │ ├── createTree.test.js │ ├── parseData.test.ts │ └── parseProps.test.js ├── app │ ├── App.tsx │ ├── algorithms │ │ ├── createTree.tsx │ │ ├── parseData.ts │ │ └── parseProps.ts │ ├── components │ │ ├── ComponentView.tsx │ │ ├── ElementView.tsx │ │ ├── Header.tsx │ │ └── SearchBar.tsx │ ├── containers │ │ ├── Panel.tsx │ │ └── SidePane.tsx │ ├── index.tsx │ ├── styles │ │ ├── _app.scss │ │ ├── _header.scss │ │ ├── _panel.scss │ │ ├── _searchBar.scss │ │ ├── _sidePane.scss │ │ ├── _variables.scss │ │ └── styles.scss │ └── types │ │ ├── css.d.ts │ │ └── types.ts └── extension │ ├── assets │ ├── astrospect-logo-16.png │ ├── astrospect-logo-48.png │ └── astrospect-logo.png │ ├── devtools.html │ ├── devtools.js │ ├── manifest.json │ └── panel.html ├── tsconfig.json ├── webpack.config.js └── website ├── .eslintrc.js ├── .gitignore ├── .npmrc ├── .prettierrc ├── LICENSE ├── astro.config.mjs ├── package.json ├── public ├── accessible-components.webp ├── astrospect-feature.png ├── astrospect-logo.png ├── demo.webp ├── evan.png ├── fonts │ ├── OpenSans-Bold.woff │ ├── OpenSans-Bold.woff2 │ ├── OpenSans-ExtraBold.woff │ ├── OpenSans-ExtraBold.woff2 │ ├── OpenSans-Italic.woff │ ├── OpenSans-Italic.woff2 │ ├── OpenSans-Regular.woff │ └── OpenSans-Regular.woff2 ├── jackson.png ├── john.png ├── nick.png ├── social-preview-image.png └── wcag-compliant.webp ├── src ├── assets │ └── scss │ │ ├── base │ │ ├── _breakpoint.scss │ │ ├── _button.scss │ │ ├── _color.scss │ │ ├── _container.scss │ │ ├── _font.scss │ │ ├── _list.scss │ │ ├── _outline.scss │ │ ├── _reset.scss │ │ ├── _root.scss │ │ └── _space-content.scss │ │ └── globals.scss ├── components │ ├── AboutCard.astro │ ├── CallToAction.astro │ ├── ContentMedia.astro │ ├── Feature.astro │ ├── Footer.astro │ ├── Header.astro │ ├── Hero.astro │ ├── Navigation.astro │ ├── ResponsiveToggle.astro │ └── SiteMeta.astro ├── env.d.ts ├── layouts │ ├── DefaultLayout.astro │ └── MarkdownLayout.astro └── pages │ ├── 404.astro │ ├── about │ └── [...page].astro │ ├── index.astro │ ├── markdown-page.md │ └── mdx-page.mdx ├── tailwind.config.js └── tsconfig.json /.github/workflows/compliance.yml: -------------------------------------------------------------------------------- 1 | name: Compliance 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | types: 8 | - opened 9 | - reopened 10 | - synchronize 11 | 12 | jobs: 13 | # check that the PR isn't being attempted from a feature branch to the main branch 14 | branch-compliance-check: 15 | name: Branch compliance check 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: check out repo 19 | uses: actions/checkout@v2 20 | 21 | - name: check branch name 22 | run: | 23 | if [[ "$GITHUB_HEAD_REF" == "dev" ]]; then 24 | echo "Merging from dev branch is allowed" 25 | exit 0 26 | else 27 | echo "Merging from branch other than dev is not allowed" 28 | exit 1 29 | fi 30 | -------------------------------------------------------------------------------- /.github/workflows/development.yml: -------------------------------------------------------------------------------- 1 | name: Development 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - opened 7 | - synchronize 8 | - reopened 9 | push: 10 | 11 | jobs: 12 | # tests the webpack bundle to make sure that it can be compiled without any errors 13 | build: 14 | name: Build webpack bundle 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: check out repo 18 | uses: actions/checkout@v2 19 | 20 | - name: Install dependencies 21 | run: npm install 22 | 23 | - name: Build webpack bundle 24 | run: npm run build 25 | 26 | # run the testing suite 27 | test: 28 | name: Test application 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: check out repo 32 | uses: actions/checkout@v2 33 | 34 | - name: 'set up node' 35 | uses: actions/setup-node@v2.1.5 36 | with: 37 | node-version: 16 38 | 39 | - name: 'install npm@latest' 40 | run: npm i -g npm@latest 41 | 42 | - name: 'install dependencies' 43 | uses: bahmutov/npm-install@v1 44 | 45 | - name: 'run tests' 46 | run: npm run test 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Bundles generate from Webpack 2 | src/extension/bundles/ 3 | 4 | #banish DS store 5 | .DS_Store 6 | 7 | # Logs 8 | logs 9 | *.log 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | lerna-debug.log* 14 | 15 | # Diagnostic reports (https://nodejs.org/api/report.html) 16 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 17 | 18 | # Runtime data 19 | pids 20 | *.pid 21 | *.seed 22 | *.pid.lock 23 | 24 | # Directory for instrumented libs generated by jscoverage/JSCover 25 | lib-cov 26 | 27 | # Coverage directory used by tools like istanbul 28 | coverage 29 | *.lcov 30 | 31 | # nyc test coverage 32 | .nyc_output 33 | 34 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 35 | .grunt 36 | 37 | # Bower dependency directory (https://bower.io/) 38 | bower_components 39 | 40 | # node-waf configuration 41 | .lock-wscript 42 | 43 | # Compiled binary addons (https://nodejs.org/api/addons.html) 44 | build/Release 45 | 46 | # Dependency directories 47 | node_modules/ 48 | jspm_packages/ 49 | 50 | # TypeScript v1 declaration files 51 | typings/ 52 | 53 | # TypeScript cache 54 | *.tsbuildinfo 55 | 56 | # Optional npm cache directory 57 | .npm 58 | 59 | # Optional eslint cache 60 | .eslintcache 61 | 62 | # Microbundle cache 63 | .rpt2_cache/ 64 | .rts2_cache_cjs/ 65 | .rts2_cache_es/ 66 | .rts2_cache_umd/ 67 | 68 | # Optional REPL history 69 | .node_repl_history 70 | 71 | # Output of 'npm pack' 72 | *.tgz 73 | 74 | # Yarn Integrity file 75 | .yarn-integrity 76 | 77 | # dotenv environment variables file 78 | .env 79 | .env.test 80 | 81 | # parcel-bundler cache (https://parceljs.org/) 82 | .cache 83 | 84 | # Next.js build output 85 | .next 86 | 87 | # Nuxt.js build / generate output 88 | .nuxt 89 | dist 90 | 91 | # Gatsby files 92 | .cache/ 93 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 94 | # https://nextjs.org/blog/next-9-1#public-directory-support 95 | # public 96 | 97 | # vuepress build output 98 | .vuepress/dist 99 | 100 | # Serverless directories 101 | .serverless/ 102 | 103 | # FuseBox cache 104 | .fusebox/ 105 | 106 | # DynamoDB Local files 107 | .dynamodb/ 108 | 109 | # TernJS port file 110 | .tern-port 111 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /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 | # AstroSpect 2 | 3 | 4 | 5 |
6 | 7 | 8 |
9 |
10 | 11 | Logo 12 | 13 | 14 |

15 | A Chrome DevTools Extension For Astro Developers 16 |
17 | Explore AstroSpect » 18 |
19 | Read our Medium Article » 20 |

21 |
22 |
23 | 24 | [![Contributors][contributors-shield]][contributors-url] 25 | [![Forks][forks-shield]][forks-url] 26 | [![Stargazers][stars-shield]][stars-url] 27 | [![MIT License][license-shield]][license-url] 28 | [![Issues][issues-shield]][issues-url] 29 | [![LinkedIn][linkedin-shield]][linkedin-url] 30 | 31 | 32 | 33 |
34 | 35 | 36 |
37 | Table of Contents 38 |
    39 |
  1. 40 | Summary 41 | 44 |
  2. 45 |
  3. 46 | Getting Started 47 | 52 |
  4. 53 |
  5. About
  6. 54 | 57 |
  7. Roadmap
  8. 58 |
  9. Contributions
  10. 59 |
  11. Acknowledgments
  12. 60 | 64 |
65 |
66 | 67 |
68 | 69 | ## Summary 70 | 71 | AstroSpect (Astro Inspection) is a Chrome Developer Tool Extension that allows developers to inspect and debug Astro websites more efficiently. With AstroSpect, developers can display a tree diagram of all elements on a page, including static HTML files and hydrated components known as Astro Islands, on a panel. The "All Elements" tab shows all elements, while the "Islands Only" tab displays only Astro Islands. Clicking on an Astro Island reveals information about the component, such as its type, client directive, and props, in a side-pane. AstroSpect features expand and collapse options to open and close tree nodes and a search function for quicker navigation and debugging. 72 | 73 | ### - Built With 74 | 75 |
76 | 77 | [![Astro][astro-shield]][astro-url] 78 | [![React][react-shield]][react-url] 79 | [![Sass][sass-shield]][sass-url] 80 | [![TypeScript][typescript-shield]][typescript-url] 81 | [![Webpack][webpack-shield]][webpack-url] 82 | 83 |
84 |
85 | 86 | 87 | ## Getting Started 88 | 89 | AstroSpect is available for download as a Google Chrome Extension. You can also clone or fork this repo and add it as your own extension manually. 90 | 91 | ### - Prerequisites 92 | 93 | - Chrome Browser 94 | - Astro 2.0 95 | - VS Code 96 | 97 | ### - Installation 98 | 99 | Option 1: Download as a chrome extension 100 | 101 | Option 2: Manually Download 102 | 103 |
    104 |
  1. Fork or Clone this repo
  2. 105 |
  3. In the terminal: npm run build
  4. 106 |
  5. Navigate to chrome://extensions/
  6. 107 |
  7. Click Load unpacked button
  8. 108 |
  9. Upload the extension folder
  10. 109 |
110 | 111 | ### - Usage 112 | 113 | 1. Start or open an Astro project. 114 | 2. Inspect the Astro webpage 115 | 3. Open the AstroSpect Tab 116 | 4. "ALL ELEMENTS" displays every element on the page 117 | 5. "ISLANDS ONLY" displays hydrated components with client directives 118 | 119 |
120 |

Inspect

121 | 122 | Astro Inspection 124 | 125 |

Expand & Collapse

126 | 127 | Astro Expand and Collapse 129 | 130 |

Search

131 | 132 | Astro Search 134 | 135 |
136 | 137 |
138 | 139 | ## About 140 | 141 | ### - Astro 142 | 143 | Astro is the all-in-one web framework designed for speed. Pull your content from anywhere and deploy everywhere, all powered by your favorite UI components and libraries. AstroSpect allow developers to inspect the Island Architecture of Astro websites in conjunction with other frameworks (React, Preact, Svelte, Vue, Solid, Lit and more). [Check out Astro to build your next website](https://astro.build/). 144 | 145 |
146 | 147 | ## Roadmap 148 | 149 | - [x] Inspect Astro Websites with AstroSpect Chrome Extension 150 | - [x] Display all HTML elements in "All Elements" tab 151 | - [x] Display Hydrated Components in "Islands Only" tab 152 | - [x] Display Component's Type, Client Directive, and Props in side pane 153 | - [x] Expand and Collapse feature opens and closes all tree nodes 154 | - [x] Search feature highlights the inputted text 155 | - [x] Tracking buttons that directs user to every highlighted word 156 | - [ ] Auto-reload when navigating to a different page 157 | - [ ] Display Framework associated with each island on side pane 158 | - [ ] Access nanostores of all components 159 | - [ ] Display state of components in side pane 160 | - [ ] Highlight over islands when clicked in the panel 161 | - [ ] Highlight over islands when clicked in the panel 162 | 163 | Check out the [open issues](https://github.com/oslabs-beta/AstroSpect/issues) for a full list of proposed features (and known issues). 164 | 165 |
166 | 167 | ## Contributions 168 | 169 | All contributions are hightly welcomed and appreciated here at AstroSpect. We are open to suggestions so please submit an issue and the AstroSpect team will get back to you as soon as possible. If you would like to add a new feature, please follow the steps below. 170 | 171 | #### STEP 1 — Fork and Clone the repository 172 | 173 | ``` 174 | git clone https://github.com/oslabs-beta/AstroSpect.git 175 | ``` 176 | 177 | #### STEP 2 — Create a Feature Branch 178 | 179 | ``` 180 | git checkout -b [name]/[feature-name] 181 | ``` 182 | 183 | #### STEP 3 — Install Dependencies 184 | 185 | ``` 186 | npm install 187 | ``` 188 | 189 | #### STEP 4 — Bundle the Code 190 | 191 | ``` 192 | npm run build 193 | ``` 194 | 195 | #### STEP 5 — Upload Extension Folder to Chrome 196 | 197 | 1. Navigate to chrome://extensions/ using chrome 198 | 2. Click Load unpacked button 199 | 3. Upload extension folder 200 | 4. Test Extension by inspecting an Astro webpage 201 | 202 | #### STEP 6 — Add and Commit 203 | 204 | ``` 205 | git add [file-name] 206 | git commit -m "describe your new feature" 207 | ``` 208 | 209 | #### STEP 7 — Submit a Pull Request 210 | 211 | Merge your feature branch into dev 212 | 213 | #### STEP 8 — Create an Issue 214 | 215 | Click the Issues tab and create a new issue 216 | 217 |
218 | 219 | ## Acknowledgments 220 | 221 | ### - Community 222 | 223 | - [OpenSource Labs](https://opensourcelabs.io/) 224 | - [Astro Discord](https://discord.com/invite/grF4GTXXYm) 225 | - [AstroSpect Users](https://www.astrospect.dev/) 226 | 227 | Distributed under the MIT License. See `LICENSE` for more information. 228 | 229 | ### - Authors 230 | 231 | 232 | 233 | 240 | 247 | 253 | 259 | 260 |
234 |

🧑🏻‍🚀

235 |

Evan Jones

236 | 237 |
238 | 239 |
241 |

🧑🏼‍🚀

242 |

Nicholas Park

243 | 244 |
245 | 246 |

👨🏻‍🚀

248 |

John Roman

249 | 250 |
251 | 252 |

👩🏼‍🚀

254 |

Jackson Ta

255 | 256 |
257 | 258 |
261 | 262 |

(back to top)

263 | 264 | [github-shield]: https://img.shields.io/badge/-Github-808080?logo=github 265 | [contributors-shield]: https://img.shields.io/github/contributors/oslabs-beta/AstroSpect?color=navy&label=Contributors 266 | [contributors-url]: https://github.com/oslabs-beta/AstroSpect/graphs/contributors 267 | [forks-shield]: https://img.shields.io/github/forks/oslabs-beta/AstroSpect?color=gold&label=Forks 268 | [forks-url]: https://github.com/oslabs-beta/AstroSpect/forks 269 | [stars-shield]: https://img.shields.io/github/stars/oslabs-beta/AstroSpect?color=%234B0082&label=Stars 270 | [stars-url]: https://github.com/oslabs-beta/AstroSpect/stargazers 271 | [issues-shield]: https://img.shields.io/github/issues/oslabs-beta/AstroSpect?color=%23483D8B&label=Issues 272 | [issues-url]: https://github.com/oslabs-beta/AstroSpect/issues 273 | [license-shield]: https://img.shields.io/github/license/oslabs-beta/AstroSpect?color=%09%23FF8C00&label=License 274 | [license-url]: https://github.com/oslabs-beta/AstroSpect/blob/master/LICENSE 275 | [linkedin-shield]: https://img.shields.io/badge/-LinkedIn-0072b1?logo=linkedin 276 | [linkedin-url]: https://www.linkedin.com/company/astrospect 277 | [astro-shield]: https://img.shields.io/badge/-ASTRO-4c00b0?logo=astro 278 | [astro-url]: https://astro.build/ 279 | [react-shield]: https://img.shields.io/badge/-REACT-333333?logo=react 280 | [react-url]: https://reactjs.org/ 281 | [sass-shield]: https://img.shields.io/badge/-SASS-FFC0CB?logo=sass 282 | [sass-url]: https://sass-lang.com/ 283 | [typescript-shield]: https://img.shields.io/badge/-TYPESCRIPT-e6e6e6?logo=typescript 284 | [typescript-url]: https://www.typescriptlang.org 285 | [webpack-url]: https://webpack.js.org/ 286 | [webpack-shield]: https://img.shields.io/badge/-WEBPACK-1e3f66?logo=webpack 287 | -------------------------------------------------------------------------------- /assets/astro-expand-collapse.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/AstroSpect/8e62314368b99e39a2d0fd87e77652a3d14e2a49/assets/astro-expand-collapse.gif -------------------------------------------------------------------------------- /assets/astro-inspection.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/AstroSpect/8e62314368b99e39a2d0fd87e77652a3d14e2a49/assets/astro-inspection.gif -------------------------------------------------------------------------------- /assets/astro-search.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/AstroSpect/8e62314368b99e39a2d0fd87e77652a3d14e2a49/assets/astro-search.gif -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | }; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "astro-dev-tools", 3 | "version": "1.0.0", 4 | "description": "Astro Dev Tools OSP", 5 | "main": "index.js", 6 | "directories": { 7 | "doc": "docs" 8 | }, 9 | "scripts": { 10 | "test": "jest --verbose", 11 | "start": "webpack-dev-server --open", 12 | "build": "webpack", 13 | "lint": "eslint . --ext .tsx" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/oslabs-beta/astro-dev-tools.git" 18 | }, 19 | "author": "Evan Jones, Nicholas Park, John Roman, Jackson Ta", 20 | "license": "ISC", 21 | "bugs": { 22 | "url": "https://github.com/oslabs-beta/astro-dev-tools/issues" 23 | }, 24 | "homepage": "https://github.com/oslabs-beta/astro-dev-tools#readme", 25 | "dependencies": { 26 | "@emotion/react": "^11.10.6", 27 | "@emotion/styled": "^11.10.6", 28 | "@mui/icons-material": "^5.11.9", 29 | "@mui/lab": "^5.0.0-alpha.120", 30 | "@mui/material": "^5.11.10", 31 | "@types/react": "^18.0.28", 32 | "@types/react-dom": "^18.0.11", 33 | "file-loader": "^6.2.0", 34 | "react": "^18.2.0", 35 | "react-dom": "^18.2.0", 36 | "react-router-dom": "^6.9.0", 37 | "ts-loader": "^9.4.2", 38 | "url-loader": "^4.1.1", 39 | "vite": "^4.1.4" 40 | }, 41 | "devDependencies": { 42 | "@babel/core": "^7.21.0", 43 | "@babel/preset-env": "^7.20.2", 44 | "@babel/preset-react": "^7.18.6", 45 | "@babel/preset-typescript": "^7.21.4", 46 | "@jest/globals": "^29.5.0", 47 | "@testing-library/react": "^14.0.0", 48 | "@types/jest": "^29.5.0", 49 | "@typescript-eslint/eslint-plugin": "^5.54.1", 50 | "@typescript-eslint/parser": "^5.58.0", 51 | "babel-loader": "^9.1.2", 52 | "babel-preset-react-app": "^10.0.1", 53 | "css-loader": "^6.7.3", 54 | "eslint": "^8.35.0", 55 | "eslint-config-prettier": "^8.8.0", 56 | "eslint-config-standard-with-typescript": "^34.0.0", 57 | "eslint-plugin-import": "^2.27.5", 58 | "eslint-plugin-n": "^15.6.1", 59 | "eslint-plugin-promise": "^6.1.1", 60 | "eslint-plugin-react": "^7.32.2", 61 | "html-webpack-plugin": "^5.5.0", 62 | "jest": "^29.5.0", 63 | "jsdom": "^21.1.1", 64 | "jsdom-global": "^3.0.2", 65 | "prettier": "^2.8.7", 66 | "sass": "^1.58.3", 67 | "sass-loader": "^13.2.0", 68 | "style-loader": "^3.3.1", 69 | "ts-jest": "^29.1.0", 70 | "typescript": "^4.9.5", 71 | "webpack-cli": "^5.0.1", 72 | "webpack-dev-server": "^4.11.1" 73 | }, 74 | "quokka": { 75 | "plugins": [ 76 | "jsdom-quokka-plugin" 77 | ], 78 | "babel": { 79 | "presets": [ 80 | "react-app" 81 | ] 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": ["eslint:recommended"], 7 | "overrides": [], 8 | "parser": "@typescript-eslint/parser", 9 | "parserOptions": { 10 | "ecmaVersion": "latest", 11 | "sourceType": "module", 12 | "project": "./tsconfig.json" 13 | }, 14 | "plugins": ["react", "import", "@typescript-eslint"], 15 | "ignorePatterns": ["__tests__", "extension"], 16 | "rules": { 17 | "semi": ["off", "always"], 18 | "@typescript-eslint/semi": "off", 19 | "no-unexpected-multiline": "error" 20 | }, 21 | "globals": { 22 | "JSX": "readonly", 23 | "React": "readonly" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/__tests__/createTree.test.js: -------------------------------------------------------------------------------- 1 | // tests createTree algorithm 2 | const jsdom = require('jsdom'); 3 | const { JSDOM } = jsdom; 4 | // import { TreeItem } from '@mui/lab'; 5 | // import createTree from '../app/algorithms/createTree'; 6 | 7 | xdescribe('createTree', () => { 8 | beforeEach(() => { 9 | // Reset any side effects from previous tests. 10 | // Assumes you have reset functions implemented for addId and addIslandData. 11 | addId.reset(); 12 | addIslandData.reset(); 13 | }); 14 | 15 | test('creates a leaf tree item for a node without children', () => { 16 | const node = document.createElement('div'); 17 | const result = createTree(node, '0'); 18 | const { getByText } = render(result); 19 | 20 | expect(getByText('div')).toBeInTheDocument(); 21 | }); 22 | 23 | it('should have three elements in islands array', () => { 24 | // create a DOM environment and load a test document 25 | const dom = new JSDOM(` 26 | 27 | 28 | 29 | 30 |
  • React

    React

    -- UI library

  • 31 |
    32 |

    33 |
  • Sass

    Sass

    -- CSS pre-processor

  • 34 |
  • Svelte

    Svelte

    -- trendy frontend library

  • 35 | 36 | 37 | `); 38 | const document = dom.window.document; 39 | 40 | expect(islands).toHaveLength(3); 41 | expect(islands[0].framework).not.toBe(null); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /src/__tests__/parseData.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect } from '@jest/globals'; 2 | // import jsdom from 'jsdom'; 3 | const jsdom = require('jsdom'); 4 | const { JSDOM } = jsdom; 5 | require('jsdom-global')(); 6 | global.DOMParser = window.DOMParser; 7 | 8 | describe('parse data tests', () => { 9 | it('should return an object', () => { 10 | // create fake DOM tree string that would be returned from Chrome API 11 | const html: string = new JSDOM(` 12 | 13 | 14 | 15 | 16 |
  • React

    React

    -- UI library

  • 17 |
    18 |

    19 |
  • Sass

    Sass

    -- CSS pre-processor

  • 20 |
  • Svelte

    Svelte

    -- trendy frontend library

  • 21 | 22 | 23 | `); 24 | 25 | const parser = new window.DOMParser(); 26 | const stringToDoc: {} = parser.parseFromString(html, 'text/html'); 27 | 28 | expect(stringToDoc && typeof stringToDoc === 'object').toBe(true); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/__tests__/parseProps.test.js: -------------------------------------------------------------------------------- 1 | // tests parseProps algorithm 2 | const parseProps = require('../app/algorithms/parseProps').default; 3 | 4 | describe('parseProps ', () => { 5 | it('returns object for one prop', () => { 6 | const unparsedProps = '{"hello": "world"}'; 7 | 8 | const parsedProps = parseProps(unparsedProps); 9 | 10 | expect(parsedProps).toEqual({ hello: 'world' }); 11 | }); 12 | 13 | it('returns object for multiple, properly formatted, non-nested props', () => { 14 | // create a DOM environment and load a test document 15 | 16 | const unparsedProps = '{"hello": "world", "number": 2}'; 17 | 18 | const parsedProps = parseProps(unparsedProps); 19 | 20 | expect(parsedProps).toEqual({ hello: 'world', number: 2 }); 21 | }); 22 | 23 | it('returns object for multiple, properly formatted, nested props', () => { 24 | // create a DOM environment and load a test document 25 | const unparsedProps = `{"labels": {"useLight": "Use light theme","useDark": "Use dark theme"},"isInsideHeader": true,"class": "astro-2W66RQV5"}`; 26 | 27 | const parsedProps = parseProps(unparsedProps); 28 | 29 | const targetObj = { 30 | labels: { 31 | useLight: 'Use light theme', 32 | useDark: 'Use dark theme', 33 | }, 34 | isInsideHeader: true, 35 | class: 'astro-2W66RQV5', 36 | }; 37 | 38 | expect(parsedProps).toEqual(targetObj); 39 | }); 40 | 41 | it('returns object for multiple, properly formatted, nested props', () => { 42 | // create a DOM environment and load a test document 43 | 44 | const unparsedProps = `{"labels":[0,{"useLight":[0,"Use light theme"],"useDark":[0,"Use dark theme"]}],"isInsideHeader":[0,true],"class":[0,"astro-2W66RQV5"]}`; 45 | 46 | const parsedProps = parseProps(unparsedProps); 47 | 48 | const targetObj = { 49 | labels: { 50 | useLight: 'Use light theme', 51 | useDark: 'Use dark theme', 52 | }, 53 | isInsideHeader: true, 54 | class: 'astro-2W66RQV5', 55 | }; 56 | 57 | expect(parsedProps).toEqual(targetObj); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /src/app/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // @ts-ignore 3 | import Panel from './containers/Panel'; 4 | // @ts-ignore 5 | import SidePane from './containers/SidePane'; 6 | import { useState, useEffect } from 'react'; 7 | import parseData from './algorithms/parseData'; 8 | import Header from './components/Header'; 9 | import { 10 | CurrentComp, 11 | IslandData, 12 | AddIslandData, 13 | HandleClick, 14 | AddId, 15 | } from './types/types'; 16 | 17 | const App: React.FC = (): JSX.Element => { 18 | //body data raw document nodes from target html 19 | const [bodyData, setBodyData] = useState(null); 20 | //prop info displayed in sidepane for the node that is clicked 21 | const [currentComp, setCurrentComp] = useState(null); 22 | //An object with prop data that is referenced when setting current comp 23 | const [islandData, setIslandData] = useState({}); 24 | const [idSet, setIdSet] = useState>(new Set()); 25 | const [idArray, setIdArray] = useState([]); 26 | 27 | // set the currentComp when a node is selected so we can display the Astro Island information if the node is an Island 28 | const handleClick: HandleClick = (event, id) => { 29 | if (islandData[id]) { 30 | setCurrentComp(islandData[id]); 31 | } else setCurrentComp(null); 32 | }; 33 | 34 | // function to add astro island nodes to state when parsing dom 35 | const addIslandData: AddIslandData = (astroIsland, id) => { 36 | setIslandData((prevIslandData) => ({ 37 | ...prevIslandData, 38 | [id]: astroIsland, 39 | })); 40 | }; 41 | 42 | // adds id of each node in tree to an array of all ids 43 | const addId: AddId = (id) => { 44 | if (!idSet.has(id)) { 45 | setIdSet(new Set(idSet.add(id))); 46 | const idArray: string[] = Array.from(idSet); 47 | setIdArray([...idArray]); 48 | } 49 | }; 50 | 51 | // parse the data from the DOM of the target page so we can pass in the DOM representation when creating the MUI tree 52 | useEffect((): void => { 53 | (async function fetchData(): Promise { 54 | const data: Document = await parseData(); 55 | setBodyData(data); 56 | })(); 57 | }, []); 58 | 59 | // if bodyData is not fetched, renders Loading screen 60 | return ( 61 | <> 62 |
    63 |
    64 | {!bodyData &&
    Loading...
    } 65 | {bodyData && ( 66 | 73 | )} 74 | {bodyData && } 75 |
    76 | 77 | ); 78 | }; 79 | 80 | export default App; 81 | -------------------------------------------------------------------------------- /src/app/algorithms/createTree.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TreeItem from '@mui/lab/TreeItem'; 3 | import parseProps from './parseProps'; 4 | import { AddId, AddIslandData } from '../types/types'; 5 | 6 | const createTree = ( 7 | node: any, 8 | id: string, 9 | addId: AddId, 10 | addIslandData: AddIslandData 11 | ) => { 12 | //Recursive function to store tree structure in panel (all elements & all islands) 13 | const inner = ( 14 | node: any, 15 | id: string, 16 | addId: AddId, 17 | addIslandData: AddIslandData, 18 | fontColor: string = '#F5F5F5' 19 | ) => { 20 | // adds id to idArray, required for expandAll functionality 21 | addId(id); 22 | 23 | // Stores ASTRO-ISLAND data in islandData state (from app) 24 | if (node.nodeName === 'ASTRO-ISLAND') { 25 | // parse props attribute of astro-island element 26 | const parsedProps = parseProps(node.attributes.props.value); 27 | // creates island object, with client directive and props info 28 | const island = { 29 | client: node.attributes.client.value, 30 | props: parsedProps, 31 | }; 32 | // saves island object to an object of all island objects with its ID and parsed props 33 | addIslandData(island, id); 34 | // parses component-url attrbute to give astro-island descriptive name 35 | let componentFile = node.attributes['component-url'].value; 36 | let lastIndex: number = NaN; 37 | for (let i = componentFile.length - 1; i > 0; i--) { 38 | if (componentFile[i] === '.') lastIndex = i; 39 | if (componentFile[i] === '/') { 40 | if (lastIndex) componentFile = componentFile.slice(i + 1, lastIndex); 41 | else componentFile = componentFile.slice(i + 1); 42 | break; 43 | } 44 | } 45 | //if the current astro island has no children, save treeitem. 46 | if (!node.children) { 47 | const islandTreeItem = ( 48 | 54 | ); 55 | allIslands.push(islandTreeItem); 56 | return islandTreeItem; 57 | } else { 58 | // when astro island has children, returns parent TreeItem (orange) & recursively traverses through children 59 | const children = Array.from(node.children); 60 | const islandTreeItem = ( 61 | 67 | {children.map((child, index) => 68 | inner(child, `${id}-${index}`, addId, addIslandData, '#d494ffa6') 69 | )} 70 | 71 | ); 72 | allIslands.push(islandTreeItem); 73 | return islandTreeItem; 74 | } 75 | } 76 | 77 | // If node has no children, return node 78 | if (!node.children) { 79 | return ( 80 | 86 | ); 87 | } else { 88 | const children = Array.from(node.children); 89 | // If node has children, recurse through function with each child node 90 | return ( 91 | 97 | {children.map((child, index) => 98 | inner(child, `${id}-${index}`, addId, addIslandData, fontColor) 99 | )} 100 | 101 | ); 102 | } 103 | }; 104 | 105 | // array of all islands (declared outside of the inner function because we don't want this array to be wiped with each recursive function call) 106 | const allIslands: JSX.Element[] = []; 107 | // invokes inner, which will return dom tree of all elements. 108 | const allElements: JSX.Element = inner( 109 | node, 110 | id, 111 | addId, 112 | addIslandData 113 | ); 114 | //return all elements (to be rendered in ElementsView) & all islands (to be rendered in ComponentView) 115 | return { 116 | allElements, 117 | allIslands, 118 | }; 119 | }; 120 | 121 | export default createTree; 122 | -------------------------------------------------------------------------------- /src/app/algorithms/parseData.ts: -------------------------------------------------------------------------------- 1 | declare const chrome: any; 2 | 3 | // parses html of target page in order to construct tree 4 | const parseData = async (): Promise => { 5 | // gets html of target page using Chrome API methods 6 | const html: string = await new Promise((resolve, reject) => { 7 | chrome.devtools.inspectedWindow.eval( 8 | 'document.documentElement.outerHTML', 9 | (result: string, exception: string) => { 10 | if (exception) { 11 | reject(exception); 12 | } else { 13 | resolve(result); 14 | } 15 | } 16 | ); 17 | }); 18 | 19 | // parses HTML string into document object 20 | const parser = new DOMParser(); 21 | const stringToDoc: Document = parser.parseFromString(html, 'text/html'); 22 | 23 | // returns document object 24 | return stringToDoc; 25 | }; 26 | 27 | export default parseData; 28 | -------------------------------------------------------------------------------- /src/app/algorithms/parseProps.ts: -------------------------------------------------------------------------------- 1 | // parses props of astro islands for display in side pane 2 | const parseProps = (attribute: string): Record => { 3 | // parses JSON string of props attribute 4 | const parsed: { [k: string]: any } = JSON.parse(attribute); 5 | 6 | // recursively parses nested props to get rid of unnecessary data from astro-island props 7 | // example of how the JSON is structured coming in: { "name": "[0, "Commander Roman"]" } 8 | // output after running JSON object through parseProps: { name: "Commander Roman" } 9 | const spreader = (obj: { [k: string]: any }): void => { 10 | for (const key in obj) { 11 | if (Array.isArray(obj[key])) { 12 | let newVal: any[] = obj[key].slice(1); 13 | obj[key] = newVal[0]; 14 | spreader(obj[key]); 15 | } 16 | } 17 | }; 18 | 19 | // calls spreader helper function 20 | spreader(parsed); 21 | 22 | // returns object of parsed props 23 | return parsed; 24 | }; 25 | 26 | export default parseProps; 27 | -------------------------------------------------------------------------------- /src/app/components/ComponentView.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TreeView from '@mui/lab/TreeView'; 3 | import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; 4 | import ChevronRightIcon from '@mui/icons-material/ChevronRight'; 5 | import { ComponentViewProps } from '../types/types'; 6 | 7 | const ComponentView: React.FC = (props): JSX.Element => { 8 | const { componentData, handleToggle, expanded, handleClick } = props; 9 | 10 | return ( 11 | <> 12 | {componentData.length === 0 && ( 13 |
    No Astro Islands found.
    14 | )} 15 | {componentData.length > 0 && ( 16 | } 19 | defaultExpandIcon={} 20 | onNodeSelect={handleClick} 21 | onNodeToggle={handleToggle} 22 | expanded={expanded} 23 | sx={{ 24 | height: '100vh', 25 | flexGrow: 1, 26 | width: 'auto', 27 | overflowY: 'auto', 28 | }} 29 | > 30 | {componentData} 31 | 32 | )} 33 | 34 | ); 35 | }; 36 | 37 | export default ComponentView; 38 | -------------------------------------------------------------------------------- /src/app/components/ElementView.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TreeView from '@mui/lab/TreeView'; 3 | import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; 4 | import ChevronRightIcon from '@mui/icons-material/ChevronRight'; 5 | import { ElementViewProps } from '../types/types'; 6 | 7 | const ElementView: React.FC = (props): JSX.Element => { 8 | const { handleClick, expanded, handleToggle, elementData } = props; 9 | 10 | // returns the completed tree 11 | return ( 12 | <> 13 | } 16 | defaultExpandIcon={} 17 | onNodeSelect={handleClick} 18 | onNodeToggle={handleToggle} 19 | expanded={expanded} 20 | sx={{ 21 | height: '100vh', 22 | flexGrow: 1, 23 | width: 'auto', 24 | overflowY: 'auto', 25 | }} 26 | > 27 | {elementData} 28 | 29 | 30 | ); 31 | }; 32 | 33 | export default ElementView; 34 | -------------------------------------------------------------------------------- /src/app/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import AppBar from '@mui/material/AppBar'; 3 | import Box from '@mui/material/Box'; 4 | import Toolbar from '@mui/material/Toolbar'; 5 | import Typography from '@mui/material/Typography'; 6 | import GitHubIcon from '@mui/icons-material/GitHub'; 7 | import WebIcon from '@mui/icons-material/Web'; 8 | import Logo from '../../extension/assets/astrospect-logo.png'; 9 | 10 | const Header: React.FC = (): JSX.Element => { 11 | return ( 12 | // possibly pass in the system light/dark mode preferences into the props? 13 | 14 | 15 | 19 | 20 | 26 |
    AstroSpect
    27 | AstroSpect logo 28 |
    29 | 30 | {/* media query that gets rid of the 'GitHub' and 'Site' text? */} 31 |
    32 | 53 | 54 | 69 |
    70 |
    71 |
    72 |
    73 | ); 74 | }; 75 | 76 | export default Header; 77 | -------------------------------------------------------------------------------- /src/app/components/SearchBar.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Button from '@mui/material/Button'; 3 | import SearchIcon from '@mui/icons-material/Search'; 4 | import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; 5 | import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; 6 | import { SearchBarProps } from '../types/types'; 7 | 8 | // search bar in panel 9 | const SearchBar: React.FC = ( 10 | props: SearchBarProps 11 | ): JSX.Element => { 12 | const { handleExpandClick, expanded } = props; 13 | const [found, setFound] = useState([]); 14 | const [current, setCurrent] = useState(0); 15 | const [searchVal, setSearchVal] = useState(''); 16 | 17 | // searches through the tree to find text that matches the value of the text input 18 | const search = (): void => { 19 | // sets found and current to initial values of new search 20 | setFound([]); 21 | setCurrent(0); 22 | // assigns the text input value to a variable 23 | let textToSearch: string = ( 24 | document.getElementById('text-to-search') as HTMLInputElement 25 | ).value; 26 | 27 | setSearchVal(textToSearch); 28 | // assign the searched text (tree view) to a variable 29 | const searchContents = document.querySelectorAll('.MuiTreeItem-label'); 30 | // changing textToSearch to be a string that can be used as literal string in a regular expression without any unintended special meaning 31 | textToSearch = textToSearch.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); 32 | // object created which contains the escaped search string and flags 'gi' (means search is global and case sensitive) 33 | const pattern: RegExp = new RegExp(`${textToSearch}`, 'gi'); 34 | // sets new array to be filled with matched elements 35 | const newArr: HTMLElement[] = []; 36 | // loop through each element in searchContents 37 | searchContents.forEach((item) => { 38 | const htmlItem = item as HTMLElement; 39 | if (htmlItem.textContent && htmlItem.textContent.match(pattern)) { 40 | newArr.push(htmlItem); 41 | } 42 | // get the textContent inside each element 43 | // replace any matches of the pattern with a highlighted version of the match 44 | const content = htmlItem.textContent || ''; 45 | const highlightedText: string = content.replace( 46 | pattern, 47 | (match) => `${match}` // performs the highlighting 48 | ); 49 | // this replaces the original text content with the highlighted version in the actual HTML of the page 50 | item.innerHTML = highlightedText; 51 | }); 52 | // sets found to the array of matching elements 53 | setFound([...newArr]); 54 | // scrolls automatically to first found element 55 | if (found[current]) found[current].scrollIntoView(); 56 | }; 57 | 58 | // scroll to next found element 59 | const scrollNext = (): void => { 60 | current < found.length - 1 ? setCurrent(current + 1) : setCurrent(0); 61 | found[current].scrollIntoView(); 62 | }; 63 | 64 | // scrolls to previous found element 65 | const scrollPrev = (): void => { 66 | current === 0 ? setCurrent(found.length - 1) : setCurrent(current - 1); 67 | found[current].scrollIntoView(); 68 | }; 69 | 70 | // // searchs when input field is updated 71 | // const handleInputChange = () => { 72 | // search(); 73 | // }; 74 | 75 | return ( 76 |