├── .gitignore ├── LICENSE ├── README.md ├── archive ├── .eslintrc.js └── webpack.config.js ├── chrome-extension ├── .eslintrc.yml ├── .prettierrc ├── @types │ ├── types.d.ts │ └── window.d.ts ├── build.ts ├── package-lock.json ├── package.json ├── playwright.config.ts ├── postcss.config.js ├── src │ ├── background.ts │ ├── contentscript.ts │ ├── detect_remix.ts │ ├── devtools │ │ ├── devtools.html │ │ └── devtools.tsx │ ├── manifest.json │ ├── noremix │ │ ├── NoRemix.css │ │ └── NoRemix.tsx │ ├── panel │ │ ├── component │ │ │ ├── List.tsx │ │ │ └── Tree.tsx │ │ ├── panel.html │ │ ├── panel.tsx │ │ ├── styles │ │ │ ├── List.css │ │ │ ├── Tree.css │ │ │ └── style.css │ │ └── treeRender │ │ │ ├── layoutParse.ts │ │ │ ├── parseDataFunc.ts │ │ │ └── urlParse.ts │ ├── popup │ │ ├── popup.html │ │ └── popup.tsx │ └── public │ │ ├── assets │ │ └── transparenticon.png │ │ └── icons │ │ ├── cropped.logo.png │ │ ├── logo.png │ │ ├── logo128.png │ │ ├── logo16.png │ │ ├── logo256.png │ │ ├── logo48.png │ │ └── notGreyscale.logo.png ├── tailwind.config.js ├── tests │ └── example.spec.ts └── tsconfig.json ├── croppedlogo.png ├── example-list.png ├── example-tree.png ├── main-page ├── Dockerfile ├── app │ ├── component │ │ ├── intro.jsx │ │ ├── socials.jsx │ │ └── team.jsx │ ├── entry.client.jsx │ ├── entry.server.jsx │ ├── images │ │ ├── adam.jpeg │ │ ├── croppedlogo.png │ │ ├── matt.jpeg │ │ ├── molly.jpeg │ │ ├── sociallogos │ │ │ ├── github-mark.png │ │ │ ├── gmail.png │ │ │ ├── linkedincropped.png │ │ │ ├── mediumsquare.png │ │ │ └── twittersocial.png │ │ ├── tim.jpeg │ │ ├── treecrop.png │ │ └── victoria.jpeg │ ├── root.tsx │ └── styles │ │ └── tailwind.css ├── consul │ └── config.json ├── package-lock.json ├── package.json ├── public │ ├── .DS_Store │ ├── favicon.ico │ └── fonts │ │ └── roboto │ │ ├── LICENSE.txt │ │ ├── Roboto-Black.ttf │ │ ├── Roboto-BlackItalic.ttf │ │ ├── Roboto-Bold.ttf │ │ ├── Roboto-BoldItalic.ttf │ │ ├── Roboto-Italic.ttf │ │ ├── Roboto-Light.ttf │ │ ├── Roboto-LightItalic.ttf │ │ ├── Roboto-Medium.ttf │ │ ├── Roboto-MediumItalic.ttf │ │ ├── Roboto-Regular.ttf │ │ ├── Roboto-Thin.ttf │ │ └── Roboto-ThinItalic.ttf ├── remix.config.js ├── scripts │ └── package.sh ├── tailwind.config.js └── tsconfig.json └── remix.md /.gitignore: -------------------------------------------------------------------------------- 1 | **/build/ 2 | **/node_modules/ 3 | example*/ 4 | main-page/.cache/ 5 | main-page/public/build 6 | **/dist 7 | .DS_Store 8 | pnpm-lock.yaml 9 | *.zip 10 | 11 | # Environement secrets 12 | .env 13 | 14 | # Ignore cache and temp files 15 | .cache/ 16 | .temp/ 17 | 18 | # Ignore Adam's example folders 19 | example*/ 20 | dist/ -------------------------------------------------------------------------------- /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 | # Remix DJ - A developer tool built specifically for the Remix framework 2 | 3 | ![Remix DJ logo: multicolored mixer sliders](croppedlogo.png) 4 | 5 | 6 | ## Installing RemixDJ 7 | 8 | RemixDJ in the Chrome store soon! 9 | 10 | Until then: 11 | 12 | - Clone our repo 🧬 13 | - Navigate to the chrome-extension folder and npm install 📀 14 | - Within the chrome-extension folder execute npm run build 🔨 15 | - In Chrome's extensions drop-down, navigate to "Manage Extensions" 🧩 16 | - Turn on "Developer mode" 🧑‍💻 17 | - In "Load Unpacked" select the "remixDJ/chrome-extension/build" folder 📦 18 | 19 | ## Using RemixDJ 20 | 21 | - Right click and select "Inspect" on any webpage 🔎 22 | - In the nav bar, navigate to RemixDJ 🗺️ 23 | 24 | ## Features 25 | 26 | - Visualize Remix unique nested routing and layout structures as a tree or list 🌳 27 | - Watch the logo change to bright colors if a site is using remix 🎨 28 | 29 | 🌲 30 | ![Remix DJ Tree: example of devtool](example-tree.png) 31 | 📂 32 | ![Remix DJ Tree: example of devtool](example-list.png) 33 | 34 | 35 | 36 | ## The Team 37 | 38 | Adam Liang 👨‍🔧 39 | 40 | - 41 | - 42 | 43 | Matt Jackson 🐟 44 | 45 | - 46 | 47 | Molly Greene 👨‍🎤 48 | 49 | - 50 | - 51 | 52 | Tim Muller 🐿 53 | 54 | - 55 | - 56 | 57 | Victoria Dillman 🌻 58 | 59 | - 60 | - 61 | -------------------------------------------------------------------------------- /archive/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | node: true, 6 | webextensions: true, 7 | amd: true, 8 | }, 9 | extends: ["eslint:recommended", "plugin:react/recommended"], 10 | parserOptions: { 11 | ecmaVersion: "latest", 12 | sourceType: "module", 13 | ecmaFeatures: { 14 | jsx: true, 15 | }, 16 | // Add TypeScript parser options 17 | project: "chrome-extension/tsconfig.json", 18 | }, 19 | overrides: [ 20 | { 21 | files: ["*.ts", "*.tsx"], 22 | extends: [ 23 | "plugin:@typescript-eslint/recommended", 24 | "plugin:react/recommended", 25 | ], 26 | parser: "@typescript-eslint/parser", 27 | parserOptions: { 28 | ecmaVersion: "latest", 29 | sourceType: "module", 30 | ecmaFeatures: { 31 | jsx: true, 32 | }, 33 | }, 34 | rules: { 35 | "@typescript-eslint/no-unused-vars": "warn", 36 | "@typescript-eslint/no-explicit-any": "off", 37 | "react/react-in-jsx-scope": "off", 38 | //no display name 39 | "react/display-name": "off", 40 | }, 41 | settings: { 42 | react: { 43 | version: "detect", 44 | }, 45 | }, 46 | }, 47 | ], 48 | rules: { 49 | "no-console": "off", 50 | "no-alert": "off", 51 | "no-unused-vars": "warn", 52 | "prefer-const": "warn", 53 | semi: ["error", "always"], 54 | quotes: ["error", "single"], 55 | "react/display-name": "off", 56 | }, 57 | settings: { 58 | react: { 59 | version: "detect", 60 | }, 61 | }, 62 | }; 63 | -------------------------------------------------------------------------------- /archive/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const path = require('path') 3 | const HtmlWebpackPlugin = require('html-webpack-plugin') 4 | const CopyPlugin = require('copy-webpack-plugin') 5 | const TerserPlugin = require('terser-webpack-plugin') 6 | 7 | module.exports = { 8 | entry: { 9 | panel: path.resolve(__dirname, 'src/panel/index.tsx'), 10 | popup: path.resolve(__dirname, 'src/popup/index.tsx'), 11 | }, 12 | output: { 13 | path: path.resolve(__dirname, 'build'), 14 | filename: '[name]/[name].bundle.js', 15 | publicPath: '/', 16 | }, 17 | devtool: 'inline-source-map', 18 | mode: 'development', 19 | module: { 20 | rules: [ 21 | { 22 | // Testing for any .js/.jsx files to be transpiled by Babel preset-react, and to transpile down 23 | // any ES6+ code down to version that can be compatible with any browser 24 | test: /\.(js|jsx)$/i, 25 | exclude: /(node_modules)/, 26 | use: { 27 | loader: 'babel-loader', 28 | options: { 29 | presets: ['@babel/preset-env', '@babel/preset-react'], 30 | cacheDireactory: true, 31 | }, 32 | }, 33 | }, 34 | { 35 | // Testing for any .css/.scss files so that webpack can fulfill the style import in 'index.js' 36 | // In order to use Tailwindcss, we need to use the 'postcss-loader' to process the css file 37 | // postcss-loader will use the 'postcss.config.js' file to process the css file and is configured with tailwindcss 38 | test: /\.(css)$/i, 39 | exclude: /(node_modules)/, 40 | use: ['style-loader', 'css-loader', 'postcss-loader'], 41 | }, 42 | { 43 | test: /\.(ts|tsx)$/, 44 | exclude: /(node_modules)/, 45 | use: ['ts-loader'], 46 | }, 47 | { 48 | test: /\.(png|jpg|gif|woff|woff2|eot|ttf|svg|ico)$/, 49 | use: [ 50 | { 51 | // loads files as base64 encoded data url if image file is less than set limit 52 | loader: 'url-loader', 53 | options: { 54 | // if file is greater than the limit (bytes), file-loader is used as fallback 55 | limit: 8192, 56 | }, 57 | }, 58 | ], 59 | }, 60 | ], 61 | }, 62 | optimization: { 63 | splitChunks: { 64 | chunks: 'all', 65 | name: 'vendor', 66 | }, 67 | minimizer: [new TerserPlugin()], 68 | }, 69 | plugins: [ 70 | new webpack.ProgressPlugin(), 71 | // Generates an HTML file based on the template we pass in to serve our webpack files 72 | // the chunks are no working properly and we have a hacky solution to our problem. Eventually should fix this 73 | new HtmlWebpackPlugin({ 74 | filename: 'panel/panel.html', 75 | template: path.resolve(__dirname, './src/panel/panel.html'), 76 | chunks: ['panel'], 77 | }), 78 | new HtmlWebpackPlugin({ 79 | filename: 'popup/popup.html', 80 | template: path.resolve(__dirname, './src/popup/popup.html'), 81 | chunks: ['popup'], 82 | }), 83 | new CopyPlugin({ 84 | patterns: [ 85 | { 86 | from: 'src/public', 87 | }, 88 | ], 89 | }), 90 | ], 91 | resolve: { 92 | // Enable importing .js and .jsx files without specifying their extension 93 | extensions: ['.js', '.jsx', '.ts', '.tsx'], 94 | }, 95 | } 96 | -------------------------------------------------------------------------------- /chrome-extension/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | browser: true 3 | es6: true 4 | node: true 5 | 6 | extends: 7 | - airbnb 8 | - airbnb/typescript 9 | - prettier 10 | 11 | parser: '@typescript-eslint/parser' 12 | 13 | plugins: 14 | - '@typescript-eslint' 15 | - jsx-a11y 16 | 17 | settings: 18 | react: 19 | version: detect 20 | -------------------------------------------------------------------------------- /chrome-extension/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true 6 | } -------------------------------------------------------------------------------- /chrome-extension/@types/types.d.ts: -------------------------------------------------------------------------------- 1 | import { NumberValue } from 'd3'; 2 | 3 | export type dataType = string | boolean | { [key: string]: any }; 4 | 5 | export type manifestObj = { 6 | name: string; 7 | max?: number; 8 | widthSet?: number; 9 | level: null | string; 10 | children: null | manifestObj[]; 11 | x: number; 12 | y: number; 13 | }; 14 | 15 | export type nodeObj = { 16 | parent: null | nodeObj; 17 | children?: nodeObj[]; 18 | x: number; 19 | y: number; 20 | descendants(): { name: string }[]; 21 | }; 22 | 23 | export type listObj = { 24 | name: string; 25 | y: number; 26 | x: number; 27 | parent: null | listObj; 28 | }; 29 | 30 | export type circleObj = { 31 | name: string; 32 | level: number; 33 | data: null | circleObj; 34 | _children: circleObj[]; 35 | }; 36 | 37 | export type parseObj = { 38 | name: string; 39 | max?: number; 40 | widthSet?: number; 41 | level: null | string; 42 | children: null | parseObj[]; 43 | }; 44 | 45 | export type windowObj = { remixManifest: { routes: manifestObj } }; 46 | 47 | export type windowObjUnderscore = { __remixManifest: { routes: manifestObj } }; 48 | -------------------------------------------------------------------------------- /chrome-extension/@types/window.d.ts: -------------------------------------------------------------------------------- 1 | // Adding possible window object to global scope in order to find it in dom 2 | 3 | declare global { 4 | interface Window { 5 | __remixManifest: any; 6 | } 7 | } 8 | 9 | export {}; 10 | -------------------------------------------------------------------------------- /chrome-extension/build.ts: -------------------------------------------------------------------------------- 1 | import * as esbuild from 'esbuild'; 2 | import * as path from 'path'; 3 | import * as fs from 'node:fs'; 4 | import { fileURLToPath } from 'url'; 5 | 6 | const baseDir = fileURLToPath(new URL('.', import.meta.url)); 7 | 8 | const extensionEntryPoints = [ 9 | path.join(baseDir, 'src', 'background.ts'), 10 | path.join(baseDir, 'src', 'contentscript.ts'), 11 | path.join(baseDir, 'src', 'detect_remix.ts'), 12 | ]; 13 | 14 | // Building core script files 15 | (async () => { 16 | await esbuild.build({ 17 | entryPoints: extensionEntryPoints, 18 | bundle: true, 19 | minify: true, 20 | outdir: path.join(baseDir, 'dist'), 21 | }); 22 | })(); 23 | 24 | // Bundling react pages 25 | const pages = ['popup', 'panel', 'devtools']; 26 | 27 | async function bundleReact(input: string, output: string) { 28 | const inputPath = path.join(baseDir, 'src', `${input}`); 29 | const outputPath = path.join(baseDir, 'dist', `${output}`); 30 | 31 | await esbuild.build({ 32 | entryPoints: [path.join(inputPath, `${input}.tsx`)], 33 | bundle: true, 34 | minify: true, 35 | loader: { 36 | '.tsx': 'tsx', 37 | }, 38 | outdir: outputPath, 39 | }); 40 | 41 | fs.copyFileSync( 42 | path.join(inputPath, `${input}.html`), 43 | path.join(outputPath, `${output}.html`), 44 | ); 45 | } 46 | 47 | pages.forEach((page) => { 48 | bundleReact(page, page); 49 | }); 50 | 51 | // Moving manifest.json and public folder to dist 52 | 53 | (async function copyFolderSync(from, to) { 54 | // creates destination folders 55 | fs.mkdirSync(to, { recursive: true }); 56 | // copy files and folders recursively 57 | fs.readdirSync(from).forEach((element) => { 58 | if (fs.lstatSync(path.join(from, element)).isFile()) { 59 | fs.copyFileSync(path.join(from, element), path.join(to, element)); 60 | } else { 61 | copyFolderSync(path.join(from, element), path.join(to, element)); 62 | } 63 | }); 64 | })(path.join(baseDir, 'src', 'public'), path.join(baseDir, 'dist', 'public')); 65 | 66 | fs.copyFileSync( 67 | path.join(baseDir, 'src', 'manifest.json'), 68 | path.join(baseDir, 'dist', 'manifest.json'), 69 | ); 70 | -------------------------------------------------------------------------------- /chrome-extension/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "remixdj", 3 | "version": "1.0.0", 4 | "description": "Developer tools for the Remix framework.", 5 | "main": "./js/background.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "node --import tsx/esm build.ts", 9 | "watch": "webpack --watch" 10 | }, 11 | "type": "module", 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/oslabs-beta/remixDJ.git" 15 | }, 16 | "author": "RemixDJ Team", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/oslabs-beta/remixDJ/issues" 20 | }, 21 | "homepage": "https://github.com/oslabs-beta/remixDJ#readme", 22 | "devDependencies": { 23 | "@playwright/test": "^1.39.0", 24 | "@types/chrome": "^0.0.248", 25 | "@types/d3": "^7.4.2", 26 | "@types/eslint": "^8.44.6", 27 | "@types/node": "^20.8.9", 28 | "@types/react": "^18.2.33", 29 | "@types/react-dom": "^18.2.14", 30 | "@types/react-svg-pan-zoom": "^3.3.7", 31 | "autoprefixer": "^10.4.16", 32 | "chrome-types": "^0.1.237", 33 | "esbuild": "^0.19.5", 34 | "eslint": "^8.52.0", 35 | "eslint-config-airbnb-typescript": "^17.1.0", 36 | "eslint-plugin-react": "^7.33.2", 37 | "playwright": "^1.39.0", 38 | "sass": "^1.69.5", 39 | "tailwindcss": "^3.3.5", 40 | "tsx": "^3.14.0", 41 | "typescript": "^5.2.2" 42 | }, 43 | "dependencies": { 44 | "d3": "^7.8.5", 45 | "react": "^18.2.0", 46 | "react-dom": "^18.2.0", 47 | "react-svg-pan-zoom": "^3.12.1" 48 | } 49 | } -------------------------------------------------------------------------------- /chrome-extension/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from '@playwright/test'; 2 | 3 | export default defineConfig({ 4 | // Look for test files in the "tests" directory, relative to this configuration file. 5 | testDir: 'tests', 6 | 7 | // Run all tests in parallel. 8 | fullyParallel: true, 9 | 10 | // Fail the build on CI if you accidentally left test.only in the source code. 11 | forbidOnly: !!process.env.CI, 12 | 13 | // Retry on CI only. 14 | retries: process.env.CI ? 2 : 0, 15 | 16 | // Opt out of parallel tests on CI. 17 | workers: process.env.CI ? 1 : undefined, 18 | 19 | // Reporter to use 20 | reporter: 'html', 21 | 22 | use: { 23 | // Base URL to use in actions like `await page.goto('/')`. 24 | baseURL: 'http://127.0.0.1:3000', 25 | 26 | // Collect trace when retrying the failed test. 27 | trace: 'on-first-retry', 28 | }, 29 | // Configure projects for major browsers. 30 | projects: [ 31 | { 32 | name: 'chromium', 33 | use: { ...devices['Desktop Chrome'] }, 34 | }, 35 | ], 36 | // Run your local dev server before starting the tests. 37 | webServer: { 38 | command: 'npm run start', 39 | url: 'http://127.0.0.1:3000', 40 | reuseExistingServer: !process.env.CI, 41 | }, 42 | }); 43 | -------------------------------------------------------------------------------- /chrome-extension/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('tailwindcss'), 4 | require('autoprefixer'), 5 | ] 6 | }; 7 | -------------------------------------------------------------------------------- /chrome-extension/src/background.ts: -------------------------------------------------------------------------------- 1 | //Adds the contentscript to all complete, non-discarded tabs on install 2 | chrome.runtime.onInstalled.addListener(async () => { 3 | for (const cs of chrome.runtime.getManifest().content_scripts) { 4 | for (const tab of await chrome.tabs.query({ 5 | discarded: false, 6 | status: 'complete', 7 | url: cs.matches, 8 | active: false, 9 | })) { 10 | try { 11 | chrome.scripting.executeScript({ 12 | target: { tabId: tab.id, allFrames: true }, 13 | files: ['contentscript.js'], 14 | }); 15 | } catch (err) { 16 | //if this triggers, you might want to add something to the query 17 | console.error( 18 | 'Error running contentscript during install on window: ', 19 | err, 20 | tab, 21 | ); 22 | } 23 | } 24 | } 25 | }); 26 | 27 | chrome.runtime.onMessage.addListener(async (message, sender, sendResponse) => { 28 | const tabs = await chrome.tabs.query({ currentWindow: true, active: true }); 29 | const tabId = tabs[0]; 30 | 31 | const newMessage = JSON.parse(message); 32 | 33 | if (newMessage.message === 'remixDetected') { 34 | chrome.action.setIcon({ 35 | tabId: tabId.id, 36 | path: { 37 | 48: 'public/icons/logo48.png', 38 | 128: 'public/icons/logo128.png', 39 | 256: 'public/icons/logo256.png', 40 | }, 41 | }); 42 | } 43 | 44 | if (newMessage.message === 'panelOpen') { 45 | if (tabs.length === 1) { 46 | try { 47 | chrome.tabs.sendMessage( 48 | tabs[0].id, 49 | JSON.stringify({ message: 'runScript' }), 50 | ); 51 | } catch (e) { 52 | //if no listener is attached, tab must be reloaded 53 | //not sure if this is needed anymore... but there could be an edge case 54 | chrome.tabs.reload(tabs[0].id); 55 | } 56 | } 57 | } 58 | }); 59 | -------------------------------------------------------------------------------- /chrome-extension/src/contentscript.ts: -------------------------------------------------------------------------------- 1 | function injectScript(file: string) { 2 | const body = document.getElementsByTagName('body')[0]; 3 | const script = document.createElement('script'); 4 | script.setAttribute('type', 'text/javascript'); 5 | script.setAttribute('src', file); 6 | body.appendChild(script); 7 | } 8 | 9 | // listen for event from injected script 10 | window.addEventListener( 11 | 'getRemixData', 12 | (e) => { 13 | try { 14 | if (!e.detail) { 15 | chrome.storage.local.set({ remixManifest: false }); 16 | } else { 17 | chrome.storage.local.set({ remixManifest: e.detail }); 18 | chrome.runtime.sendMessage( 19 | JSON.stringify({ message: 'remixDetected' }), 20 | ); 21 | } 22 | } catch (e) { 23 | console.error( 24 | 'RemixDJ Extention was installed more than once. This window stayed open and should be refreshed', 25 | ); 26 | console.error(e); 27 | } 28 | }, 29 | false, 30 | ); 31 | 32 | //listen for event from panel open 33 | chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { 34 | const newMessage = JSON.parse(message); 35 | if (newMessage.message === 'runScript') { 36 | injectScript(chrome.runtime.getURL('detect_remix.js')); 37 | } 38 | }); 39 | 40 | injectScript(chrome.runtime.getURL('detect_remix.js')); 41 | -------------------------------------------------------------------------------- /chrome-extension/src/detect_remix.ts: -------------------------------------------------------------------------------- 1 | if (window.__remixManifest) { 2 | // console.log("💿 feefifofum 👹, i smell remix in the dom 💿") 3 | window.dispatchEvent( 4 | new CustomEvent('getRemixData', { detail: window.__remixManifest }), 5 | ); 6 | } else { 7 | // console.log('❌💔 no 🚫🥺 remix😔❌'); 8 | window.dispatchEvent(new CustomEvent('getRemixData', { detail: false })); 9 | } 10 | -------------------------------------------------------------------------------- /chrome-extension/src/devtools/devtools.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Devtools Container 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /chrome-extension/src/devtools/devtools.tsx: -------------------------------------------------------------------------------- 1 | chrome.devtools.panels.create('RemixDJ', null, '../panel/panel.html', () => { 2 | chrome.runtime.sendMessage(JSON.stringify({ message: 'panelOpen' })); 3 | }); 4 | -------------------------------------------------------------------------------- /chrome-extension/src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "REMIX DJ", 4 | "description": "Developer tools for the Remix framework", 5 | "version": "0.0.1", 6 | "action": { 7 | "default_popup": "popup/popup.html", 8 | "default_icon": "public/icons/cropped.logo.png" 9 | }, 10 | "content_scripts": [ 11 | { 12 | "matches": [ 13 | "" 14 | ], 15 | "js": [ 16 | "./contentscript.js" 17 | ] 18 | } 19 | ], 20 | "background": { 21 | "service_worker": "background.js" 22 | }, 23 | "web_accessible_resources": [ 24 | { 25 | "resources": [ 26 | "detect_remix.js" 27 | ], 28 | "matches": [ 29 | "" 30 | ] 31 | } 32 | ], 33 | "icons": { 34 | "16": "public/icons/logo16.png", 35 | "48": "public/icons/logo48.png", 36 | "128": "public/icons/logo128.png", 37 | "256": "public/icons/logo256.png" 38 | }, 39 | "devtools_page": "devtools/devtools.html", 40 | "permissions": [ 41 | "scripting", 42 | "storage", 43 | "tabs", 44 | "activeTab" 45 | ], 46 | "host_permissions": [ 47 | "" 48 | ] 49 | } -------------------------------------------------------------------------------- /chrome-extension/src/noremix/NoRemix.css: -------------------------------------------------------------------------------- 1 | /* @font-face { 2 | font-family: grotesk; 3 | src: '/Users/timothymuller/Downloads/KlimTestFonts/Test desktop fonts (OTF)/Test Founders Grotesk Collection/Test Founders Grotesk/TestFoundersGrotesk-Bold.otf'; 4 | } */ 5 | 6 | /* .alert { */ 7 | /* font-family: grotesk; */ 8 | /* color: white; 9 | font-size: 46px; */ 10 | /* } */ 11 | 12 | .alert { 13 | font-size: 80px; 14 | color: #fff; 15 | text-align: center; 16 | animation: glow 1s ease-in-out infinite alternate; 17 | justify-content: center; 18 | } 19 | 20 | @keyframes glow { 21 | from { 22 | text-shadow: 0 0 10px #fff, 0 0 20px #fff, 0 0 30px #e60073, 23 | 0 0 40px #e60073, 0 0 50px #e60073, 0 0 60px #e60073, 0 0 70px #e60073; 24 | } 25 | 26 | to { 27 | text-shadow: 0 0 20px #fff, 0 0 30px #ff4da6, 0 0 40px #ff4da6, 28 | 0 0 50px #ff4da6, 0 0 60px #ff4da6, 0 0 70px #ff4da6, 0 0 80px #ff4da6; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /chrome-extension/src/noremix/NoRemix.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './NoRemix.css'; 3 | 4 | function NoRemix() { 5 | return
No Remix Detected Here!
; 6 | } 7 | 8 | export default NoRemix; 9 | -------------------------------------------------------------------------------- /chrome-extension/src/panel/component/List.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | import { manifestObj, parseObj } from '../../../@types/types' 3 | import '../styles/List.css' 4 | import parseData from '../treeRender/parseDataFunc' 5 | 6 | function List() { 7 | const [manifest, setManifest] = useState< 8 | { routes: manifestObj } | null | Record 9 | >({}) 10 | useEffect(() => { 11 | async function fetchData() { 12 | // getting data from chrome localstorage 13 | await chrome.storage.local.get(['remixManifest']).then((res) => { 14 | setManifest(res.remixManifest) 15 | }) 16 | } 17 | fetchData() 18 | }, []) 19 | 20 | const data: parseObj = parseData(manifest.routes) 21 | function renderData(data: parseObj) { 22 | // recurse through the object and take each name and make it a summary element 23 | // then recurse through the children and for each element take the name property and make it a child of the parent 24 | 25 | if (!data.children || data.children.length === 0) { 26 | return
  • {data.name}
  • 27 | } 28 | 29 | return ( 30 |
      31 |
      32 | {data.name} 33 | {data.children.map((child) => { 34 | return renderData(child) 35 | })} 36 |
      37 |
    38 | ) 39 | } 40 | 41 | return ( 42 |
    43 | {renderData(data)} 44 |
    45 | ) 46 | } 47 | 48 | export default List 49 | -------------------------------------------------------------------------------- /chrome-extension/src/panel/component/Tree.tsx: -------------------------------------------------------------------------------- 1 | import * as d3 from 'd3' 2 | import React, { useEffect, useRef, useState } from 'react' 3 | import { circleObj, listObj, manifestObj, nodeObj } from '../../../@types/types' 4 | import '../styles/Tree.css' 5 | import parseData from '../treeRender/parseDataFunc.js' 6 | 7 | function Tree() { 8 | const [manifest, setManifest] = useState< 9 | { routes: manifestObj } | null | Record 10 | >({}) 11 | const [cssHeight, setCssHeight] = useState(1000) 12 | const [cssWidth, setCssWidth] = useState(1000) 13 | 14 | useEffect(() => { 15 | async function fetchData() { 16 | // getting data from chrome localstorage 17 | await chrome.storage.local.get(['remixManifest']).then((res) => { 18 | setManifest(res.remixManifest) 19 | console.log('Manifest received') 20 | }) 21 | } 22 | fetchData() 23 | }, []) 24 | 25 | const ref = useRef() 26 | useEffect(() => { 27 | const treeData = parseData(manifest.routes) 28 | 29 | // Setting up the D3 Graph: 30 | if (treeData.children.length !== 0) { 31 | // Updating size of the tree panel based on how many nodes 32 | const margin = { top: 10, right: 120, bottom: 10, left: 40 }, 33 | width = Math.max( 34 | treeData.widthSet * 600 - margin.right - margin.left, 35 | 960, 36 | ), 37 | height = Math.max(treeData.max * 70 - margin.top - margin.bottom, 400) 38 | 39 | const treemap = d3.tree().size([height, width]) 40 | const nodesEarly = d3.hierarchy(treeData, (d: manifestObj) => d.children) 41 | const nodes: nodeObj = treemap(nodesEarly) 42 | 43 | // Appending each node and svg element 44 | const svg = d3 45 | .select(ref.current) 46 | .append('svg') 47 | .attr('width', width + margin.left + margin.right) 48 | .attr('height', height + margin.top + margin.bottom), 49 | g = svg 50 | .append('g') 51 | .attr( 52 | 'transform', 53 | 'translate(' + margin.left + ',' + margin.top + ')', 54 | ) 55 | 56 | const node = g 57 | .selectAll('.node') 58 | .data(nodes.descendants()) 59 | .enter() 60 | .append('g') 61 | .attr( 62 | 'class', 63 | (d: manifestObj) => 64 | 'node' + (d.children ? ' node--internal' : ' node--leaf'), 65 | ) 66 | .attr( 67 | 'transform', 68 | (d: manifestObj) => 'translate(' + d.y + ',' + d.x + ')', 69 | ) 70 | 71 | g.selectAll('.link') 72 | .data(nodes.descendants().slice(1)) 73 | .enter() 74 | .append('path') 75 | .attr('class', 'link') 76 | .style('stroke', 'white') 77 | .style('stroke-width', 1) 78 | .style('fill', 'none') 79 | .attr('d', (d: listObj) => { 80 | return ( 81 | 'M' + 82 | d.y + 83 | ',' + 84 | d.x + 85 | 'C' + 86 | (d.y + d.parent.y) / 2 + 87 | ',' + 88 | d.x + 89 | ' ' + 90 | (d.y + d.parent.y) / 2 + 91 | ',' + 92 | d.parent.x + 93 | ' ' + 94 | d.parent.y + 95 | ',' + 96 | d.parent.x 97 | ) 98 | }) 99 | 100 | node 101 | .append('circle') 102 | .attr('r', 2.5) 103 | .style('stroke', (d: circleObj) => d.data.level) 104 | .style('fill', (d: circleObj) => d.data.level) 105 | .attr('fill', (d: circleObj) => (d._children ? '#555' : '#999')) 106 | .attr('stroke-width', 10) 107 | 108 | node 109 | .append('text') 110 | .attr('dy', '0.31em') 111 | .attr('x', (d: circleObj) => (d._children ? -9 : 9)) 112 | .attr('text-anchor', (d: circleObj) => (d._children ? 'end' : 'start')) 113 | .text((d: circleObj) => d.data.name) 114 | .clone(true) 115 | .lower() 116 | .attr('stroke-linejoin', 'round') 117 | .attr('stroke-width', 3) 118 | 119 | const nodesAndText = d3.selectAll('.node, .text') 120 | nodesAndText.raise() 121 | 122 | setCssHeight(height + 15) 123 | setCssWidth(width + 150) 124 | } 125 | }, [manifest]) 126 | 127 | return ( 128 |
    129 | 134 |
    135 | ) 136 | } 137 | 138 | export default Tree 139 | -------------------------------------------------------------------------------- /chrome-extension/src/panel/panel.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Panel Insert 8 | 9 | 10 | 11 |
    12 | 13 | 14 | -------------------------------------------------------------------------------- /chrome-extension/src/panel/panel.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import NoRemix from '../noremix/NoRemix'; 4 | import { windowObj } from '../../@types/types.js'; 5 | import List from './component/List'; 6 | import Tree from './component/Tree'; 7 | import './styles/style.css'; 8 | 9 | const App = (): JSX.Element => { 10 | const [comp, setComp] = useState(); 11 | const [mainComp, setMainComp] = useState(null); 12 | const [content, setContent] = useState< 13 | windowObj | null | Record 14 | >({}); 15 | const [loading, setLoading] = useState(true); 16 | 17 | // Retrieving manifest from the chrome storage (via the background page) 18 | useEffect(() => { 19 | async function fetchData() { 20 | await chrome.storage.local 21 | .get(['remixManifest']) 22 | .then((res: windowObj) => { 23 | setContent(res); 24 | setLoading(false); 25 | }); 26 | } 27 | fetchData(); 28 | }, []); 29 | 30 | // Rendering component based on manifest 31 | useEffect(() => { 32 | if ( 33 | !loading && 34 | content && 35 | content.remixManifest && 36 | content.remixManifest.routes 37 | ) { 38 | setMainComp( 39 |
    40 |
    41 | 44 | 47 |
    48 |
    {comp}
    49 |
    50 |
    , 51 | ); 52 | } else if (!loading) { 53 | setMainComp( 54 |
    55 | 56 |
    , 57 | ); 58 | } else { 59 | setMainComp(
    ); 60 | } 61 | }, [loading, content, comp]); 62 | 63 | const changeTree = () => { 64 | setComp(); 65 | }; 66 | const changeList = () => { 67 | setComp(); 68 | }; 69 | 70 | return
    {mainComp}
    ; 71 | }; 72 | 73 | const rootContainer = document.getElementById('root'); 74 | const root = createRoot(rootContainer); 75 | root.render(); 76 | -------------------------------------------------------------------------------- /chrome-extension/src/panel/styles/List.css: -------------------------------------------------------------------------------- 1 | #listData { 2 | color: whitesmoke; 3 | } 4 | #listData ul { 5 | list-style-type: none; 6 | padding-left: 0; 7 | } 8 | #listData details, li { 9 | margin-left: 20px; 10 | } 11 | 12 | #listContainer { 13 | margin: 5em; 14 | padding: 0; 15 | } 16 | -------------------------------------------------------------------------------- /chrome-extension/src/panel/styles/Tree.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: rgb(30, 30, 30); 3 | margin: 10px; 4 | } 5 | 6 | .display { 7 | margin: 10px; 8 | overflow: scroll; 9 | position: relative; 10 | } 11 | 12 | .svg { 13 | position: absolute; 14 | } 15 | 16 | g>text { 17 | fill: white; 18 | text-shadow: 0 1px 0 rgb(30, 30, 30), 0 -1px 0 rgb(30, 30, 30), 1px 0 0 rgb(30, 30, 30), -1px 0 0 rgb(30, 30, 30) 19 | } 20 | 21 | .link { 22 | fill: none; 23 | stroke: white; 24 | stroke-opacity: 0.4; 25 | stroke-width: 1.5px; 26 | } 27 | 28 | svg { 29 | overflow-x: scroll; 30 | } 31 | 32 | -------------------------------------------------------------------------------- /chrome-extension/src/panel/styles/style.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | button { 6 | color: whitesmoke; 7 | font-size: 1.5em; 8 | } 9 | 10 | .tabs { 11 | display: grid; 12 | grid-template-columns: 1fr 1fr; 13 | } 14 | 15 | p { 16 | color: whitesmoke; 17 | } 18 | 19 | h1 { 20 | color: whitesmoke; 21 | font-size: 5em; 22 | } 23 | -------------------------------------------------------------------------------- /chrome-extension/src/panel/treeRender/layoutParse.ts: -------------------------------------------------------------------------------- 1 | import { parseObj } from '../../../@types/types' 2 | // This function transforms the data pulled from the window.__remixManifest object into a nested object of parent and child nodes 3 | export default function layoutParse(remixManifest: parseObj) { 4 | // This function is used in the 'keySplitter' function below to re-join array elements with opening & closing brackets 5 | function joiner(arrOfStrings: string[], char: string, i = 0): string[] { 6 | if (i === arrOfStrings.length) return arrOfStrings 7 | 8 | let temp = '' 9 | let currentEl = arrOfStrings[i] 10 | let nextEl = arrOfStrings[i + 1] 11 | 12 | if (currentEl[currentEl.length - 1] === '[') { 13 | temp += currentEl + char + nextEl 14 | arrOfStrings.splice(i, 2, temp) 15 | return joiner(arrOfStrings, char, i) 16 | } else return joiner(arrOfStrings, char, (i += 1)) 17 | } 18 | 19 | // This function generates an array of arrays, each subarray containing routes broken up with '.' or '/' (outside of []'s) 20 | function keySplitter(remixManifest: parseObj) { 21 | let myKeys = [] 22 | for (const key in remixManifest) { 23 | if (key != 'root') { 24 | // For keys without escape brackets, split by '.' or '/' and push into myKeys array 25 | if (!key.includes('[')) { 26 | myKeys.push(key.split(/[/.]/)) 27 | } 28 | 29 | // For keys with escape brackets: 30 | if (key.includes('[')) { 31 | // Split by dot to account for potentially non-escaped dots within Remix format 32 | if (key.includes('.')) { 33 | } 34 | let splitKeyDot = key.split('.') 35 | // Re-join with joiner function 36 | splitKeyDot = joiner(splitKeyDot, '.') 37 | // After splitting and re-joining by dots, do the same for slashes 38 | const holder: string[] = [] 39 | splitKeyDot.forEach((el) => { 40 | if (!el.includes('/')) { 41 | holder.push(el) 42 | } 43 | if (el.includes('/')) { 44 | let splitKeySlash = el.split('/') 45 | splitKeySlash = joiner(splitKeySlash, '/') 46 | holder.push(...splitKeySlash) 47 | } 48 | }) 49 | myKeys.push(holder) 50 | } 51 | } 52 | } 53 | return myKeys 54 | } 55 | 56 | const myKeys = keySplitter(remixManifest) 57 | 58 | // The newObj will contain all of our routes. Starts with a root node which has a child array for additional routes 59 | let newObj: parseObj = { 60 | name: 'root', 61 | children: [], 62 | max: 0, 63 | widthSet: 1, 64 | level: null, 65 | } 66 | 67 | // cache for color assignment to each node. colors are matched to the remix.run website color scheme. 68 | const colors: { [key: string]: string } = { 69 | 0: 'rgb(225, 81, 86)', // red 70 | 1: 'rgb(246, 206, 75)', // yellow 71 | 2: 'rgb(135, 214, 117)', // green 72 | 3: 'rgb(121, 236, 232)', // turequoise 73 | 4: 'rgb(82, 144, 247)', // dark blue 74 | 5: 'rgb(199, 72, 204)', // magenta 75 | 6: 'rgb(230, 134, 149)', // lt pink 76 | 7: 'rgb(224, 92, 115)', // dk pink 77 | } 78 | 79 | for (let i = 0; i < myKeys.length; i++) { 80 | let pathString = newObj.children 81 | for (let j = 0; j < myKeys[i].length; j++) { 82 | let path = pathString 83 | newObj.widthSet = j 84 | if (!path.find((e) => e.name === myKeys[i][j])) { 85 | if (myKeys[i][j].slice(-1) !== '_') { 86 | path.push({ name: myKeys[i][j], children: [], level: colors[j % 8] }) 87 | } 88 | } 89 | let numbah 90 | newObj.max = Math.max(newObj.max, path.length) 91 | for (let k = 0; k < path.length; k++) { 92 | if (path[k].name === myKeys[i][j]) { 93 | numbah = k 94 | } 95 | } 96 | pathString = pathString[numbah]['children'] 97 | } 98 | } 99 | const treeData = newObj 100 | return treeData 101 | } 102 | -------------------------------------------------------------------------------- /chrome-extension/src/panel/treeRender/parseDataFunc.ts: -------------------------------------------------------------------------------- 1 | import { parseObj } from '../../../@types/types' 2 | 3 | // This function transforms the data pulled from the window.__remixManifest object into a nested object of parent and child nodes 4 | export default function parseData(remixManifest: parseObj) { 5 | // This function is used in the 'keySplitter' function below to re-join array elements with opening & closing brackets 6 | function joiner(arrOfStrings: string[], char: string, i = 0): string[] { 7 | if (i === arrOfStrings.length - 1) return arrOfStrings 8 | 9 | let temp = '' 10 | const currentEl = arrOfStrings[i] 11 | const nextEl = arrOfStrings[i + 1] 12 | 13 | if (currentEl.includes('[') && nextEl.includes(']')) { 14 | temp += currentEl + char + nextEl 15 | arrOfStrings.splice(i, 2, temp) 16 | return joiner(arrOfStrings, char, i) 17 | } else return joiner(arrOfStrings, char, (i += 1)) 18 | } 19 | 20 | // This function generates an array of arrays, with each subarray containing routes broken up with '.' or '/' (outside of []'s) 21 | function keySplitter(remixManifest: parseObj) { 22 | const myKeys = [] 23 | for (const key in remixManifest) { 24 | if (key != 'root') { 25 | // For keys without escape brackets, split by '.' or '/' and push into myKeys array 26 | if (!key.includes('[')) { 27 | myKeys.push(key.split(/[/.]/)) 28 | } 29 | 30 | // For keys with escape brackets: 31 | if (key.includes('[')) { 32 | // Split by dot to account for potentially non-escaped dots 33 | let splitKeyDot = key.split('.') 34 | // Re-join with joiner function 35 | splitKeyDot = joiner(splitKeyDot, '.') 36 | // After splitting and re-joining by dots, do the same for slashes 37 | const holder: string[] = [] 38 | splitKeyDot.forEach((el) => { 39 | if (!el.includes('/')) { 40 | holder.push(el) 41 | } 42 | if (el.includes('/')) { 43 | let splitKeySlash = el.split('/') 44 | splitKeySlash = joiner(splitKeySlash, '/') 45 | holder.push(...splitKeySlash) 46 | } 47 | }) 48 | myKeys.push(holder) 49 | } 50 | } 51 | } 52 | return myKeys 53 | } 54 | 55 | const myKeys = keySplitter(remixManifest) 56 | 57 | // The newObj will contain all of our routes. Starts with a root node which has a child array for additional routes 58 | const newObj: parseObj = { 59 | name: 'root', 60 | children: [], 61 | max: 0, 62 | widthSet: 1, 63 | level: null, 64 | } 65 | 66 | // cache for color assignment to each node. colors are matched to the remix.run website color scheme. 67 | const colors: { [key: string]: string } = { 68 | 0: 'rgb(225, 81, 86)', // red 69 | 1: 'rgb(246, 206, 75)', // yellow 70 | 2: 'rgb(135, 214, 117)', // green 71 | 3: 'rgb(121, 236, 232)', // turequoise 72 | 4: 'rgb(82, 144, 247)', // dark blue 73 | 5: 'rgb(199, 72, 204)', // magenta 74 | 6: 'rgb(230, 134, 149)', // lt pink 75 | 7: 'rgb(224, 92, 115)', // dk pink 76 | } 77 | 78 | for (let i = 0; i < myKeys.length; i++) { 79 | let pathString = newObj.children 80 | for (let j = 0; j < myKeys[i].length; j++) { 81 | const path = pathString 82 | newObj.widthSet = j 83 | if (!path.find((e) => e.name === myKeys[i][j])) { 84 | path.push({ name: myKeys[i][j], children: [], level: colors[j % 8] }) 85 | } 86 | let numbah 87 | newObj.max = Math.max(newObj.max, path.length) 88 | for (let k = 0; k < path.length; k++) { 89 | if (path[k].name === myKeys[i][j]) { 90 | numbah = k 91 | } 92 | } 93 | pathString = pathString[numbah]['children'] 94 | } 95 | } 96 | const treeData = newObj 97 | return treeData 98 | } 99 | -------------------------------------------------------------------------------- /chrome-extension/src/panel/treeRender/urlParse.ts: -------------------------------------------------------------------------------- 1 | import { parseObj } from '../../../@types/types' 2 | // This function transforms the data pulled from the window.__remixManifest object into a nested object of parent and child nodes 3 | export default function layoutParse(remixManifest: parseObj) { 4 | // This function is used in the 'keySplitter' function below to re-join array elements with opening & closing brackets 5 | function joiner(arrOfStrings: string[], char: string, i = 0): string[] { 6 | if (i === arrOfStrings.length) return arrOfStrings 7 | 8 | let temp = '' 9 | let currentEl = arrOfStrings[i] 10 | let nextEl = arrOfStrings[i + 1] 11 | 12 | if (currentEl[currentEl.length - 1] === '[') { 13 | temp += currentEl + char + nextEl 14 | arrOfStrings.splice(i, 2, temp) 15 | return joiner(arrOfStrings, char, i) 16 | } else return joiner(arrOfStrings, char, (i += 1)) 17 | } 18 | 19 | // This function generates an array of arrays, with each subarray containing routes broken up with '.' or '/' (outside of []'s) 20 | function keySplitter(remixManifest: parseObj) { 21 | let myKeys = [] 22 | for (const key in remixManifest) { 23 | if (key != 'root') { 24 | // For keys without escape brackets, split by '.' or '/' and push into myKeys array 25 | if (!key.includes('[')) { 26 | myKeys.push(key.split(/[/.]/)) 27 | } 28 | 29 | // For keys with escape brackets: 30 | if (key.includes('[')) { 31 | // Split by dot to account for potentially non-escaped dots 32 | if (key.includes('.')) { 33 | } 34 | let splitKeyDot = key.split('.') 35 | // Re-join with joiner function 36 | splitKeyDot = joiner(splitKeyDot, '.') 37 | // After splitting and re-joining by dots, do the same for slashes 38 | const holder: string[] = [] 39 | splitKeyDot.forEach((el) => { 40 | if (!el.includes('/')) { 41 | holder.push(el) 42 | } 43 | if (el.includes('/')) { 44 | let splitKeySlash = el.split('/') 45 | splitKeySlash = joiner(splitKeySlash, '/') 46 | holder.push(...splitKeySlash) 47 | } 48 | }) 49 | myKeys.push(holder) 50 | } 51 | } 52 | } 53 | return myKeys 54 | } 55 | 56 | const myKeys = keySplitter(remixManifest) 57 | 58 | // The newObj will contain all of our routes. Starts with a root node which has a child array for additional routes 59 | let newObj: parseObj = { 60 | name: 'root', 61 | children: [], 62 | max: 0, 63 | widthSet: 1, 64 | level: null, 65 | } 66 | 67 | // cache for color assignment to each node. colors are matched to the remix.run website color scheme. 68 | const colors: { [key: string]: string } = { 69 | 0: 'rgb(225, 81, 86)', // red 70 | 1: 'rgb(246, 206, 75)', // yellow 71 | 2: 'rgb(135, 214, 117)', // green 72 | 3: 'rgb(121, 236, 232)', // turequoise 73 | 4: 'rgb(82, 144, 247)', // dark blue 74 | 5: 'rgb(199, 72, 204)', // magenta 75 | 6: 'rgb(230, 134, 149)', // lt pink 76 | 7: 'rgb(224, 92, 115)', // dk pink 77 | } 78 | 79 | for (let i = 0; i < myKeys.length; i++) { 80 | let pathString = newObj.children 81 | for (let j = 0; j < myKeys[i].length; j++) { 82 | let path = pathString 83 | newObj.widthSet = j 84 | if (!path.find((e) => e.name === myKeys[i][j])) { 85 | if (myKeys[i][j].slice(0) !== '_') { 86 | path.push({ name: myKeys[i][j], children: [], level: colors[j % 8] }) 87 | } 88 | } 89 | let numbah 90 | newObj.max = Math.max(newObj.max, path.length) 91 | for (let k = 0; k < path.length; k++) { 92 | if (path[k].name === myKeys[i][j]) { 93 | numbah = k 94 | } 95 | } 96 | pathString = pathString[numbah]['children'] 97 | } 98 | } 99 | const treeData = newObj 100 | return treeData 101 | } 102 | -------------------------------------------------------------------------------- /chrome-extension/src/popup/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | PopUp 8 |