├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── dev.yml │ └── master.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── sapling ├── .eslintrc.json ├── .vscode │ ├── extensions.json │ ├── launch.json │ ├── settings.json │ └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── README.md ├── media │ ├── babel-logo-minimal.png │ ├── babel-logo-minimal.svg │ ├── chai_icon.png │ ├── chai_icon.svg │ ├── circle-arrow-right-solid.png │ ├── circle-arrow-right-solid.svg │ ├── circle-info-solid.png │ ├── circle-info-solid.svg │ ├── github-actions.png │ ├── github-actions.svg │ ├── github-icon.png │ ├── github-icon.svg │ ├── list-tree.png │ ├── list-tree.svg │ ├── mochajs-icon.png │ ├── mochajs-icon.svg │ ├── quizwall_demo.gif │ ├── react-brands.png │ ├── react-brands.svg │ ├── readme-example.png │ ├── reset.css │ ├── sapling-logo-128px.png │ ├── sapling-logo.png │ ├── store-solid.png │ ├── store-solid.svg │ ├── styles.css │ ├── twitter-logo.png │ ├── twitter-logo.svg │ ├── vscode.css │ ├── vscode.png │ ├── vscode.svg │ ├── webpack.png │ └── webpack.svg ├── package.json ├── src │ ├── SaplingParser.ts │ ├── SidebarProvider.ts │ ├── extension.ts │ ├── getNonce.ts │ ├── test │ │ ├── runTest.ts │ │ ├── suite │ │ │ ├── extension.test.ts │ │ │ ├── index.ts │ │ │ └── parser.test.ts │ │ └── test_apps │ │ │ ├── test_0 │ │ │ ├── components │ │ │ │ └── App.jsx │ │ │ └── index.js │ │ │ ├── test_1 │ │ │ ├── components │ │ │ │ ├── App.jsx │ │ │ │ └── Main.jsx │ │ │ └── index.js │ │ │ ├── test_10 │ │ │ ├── components │ │ │ │ ├── App.jsx │ │ │ │ ├── DrillCreator.jsx │ │ │ │ ├── ExerciseCreator.jsx │ │ │ │ ├── ExercisesDisplay.jsx │ │ │ │ ├── HistoryDisplay.jsx │ │ │ │ ├── Login.jsx │ │ │ │ ├── Logout.jsx │ │ │ │ ├── Nav.jsx │ │ │ │ └── Signup.jsx │ │ │ └── index.jsx │ │ │ ├── test_11 │ │ │ ├── components │ │ │ │ ├── App1.jsx │ │ │ │ └── App2.jsx │ │ │ └── index.js │ │ │ ├── test_12 │ │ │ ├── components │ │ │ │ └── Navbar.jsx │ │ │ └── pages │ │ │ │ └── index.js │ │ │ ├── test_13 │ │ │ ├── components │ │ │ │ ├── App.jsx │ │ │ │ ├── Page1.jsx │ │ │ │ ├── Page2.jsx │ │ │ │ └── Page3.jsx │ │ │ └── index.js │ │ │ ├── test_2 │ │ │ └── index.js │ │ │ ├── test_3 │ │ │ ├── App.jsx │ │ │ ├── actions │ │ │ │ └── actions.js │ │ │ ├── constants │ │ │ │ └── actionTypes.js │ │ │ ├── containers │ │ │ │ ├── ConnectedContainer.jsx │ │ │ │ └── UnconnectedContainer.jsx │ │ │ ├── index.html │ │ │ ├── index.js │ │ │ ├── reducers │ │ │ │ ├── fakeReducer.js │ │ │ │ └── index.js │ │ │ └── store.js │ │ │ ├── test_4 │ │ │ └── index.js │ │ │ ├── test_5 │ │ │ ├── components │ │ │ │ ├── Container.js │ │ │ │ ├── JS.js │ │ │ │ ├── JSX.jsx │ │ │ │ ├── TS.ts │ │ │ │ └── TSX.tsx │ │ │ └── index.js │ │ │ ├── test_6 │ │ │ ├── App2.jsx │ │ │ ├── components │ │ │ │ └── App1.jsx │ │ │ └── index.js │ │ │ ├── test_7 │ │ │ ├── components │ │ │ │ ├── App.jsx │ │ │ │ └── Main.jsx │ │ │ └── index.js │ │ │ ├── test_8 │ │ │ ├── components │ │ │ │ ├── App.jsx │ │ │ │ └── Main.jsx │ │ │ └── index.js │ │ │ └── test_9 │ │ │ ├── components │ │ │ ├── App.jsx │ │ │ └── Main.jsx │ │ │ └── index.js │ ├── types │ │ ├── ImportObj.ts │ │ └── Tree.ts │ └── webviews │ │ ├── components │ │ ├── Navbar.tsx │ │ ├── Sidebar.tsx │ │ ├── Tree.tsx │ │ └── TreeNode.tsx │ │ ├── globals.d.ts │ │ ├── pages │ │ └── sidebar.tsx │ │ ├── tsconfig.views.json │ │ └── webpack.views.config.js ├── tsconfig.json └── webpack.config.js └── website ├── .eslintrc.json ├── .gitignore ├── README.md ├── components ├── Carousel.jsx ├── Contributors.jsx ├── Jumbotron.jsx ├── Links.jsx └── Navbar.jsx ├── next.config.js ├── package.json ├── pages ├── _app.js ├── api │ └── hello.js └── index.js ├── public ├── Bootstrap_logo.svg ├── Nextjs-logo.svg ├── blurData.js ├── build_tree_demo.gif ├── favicon.ico ├── gen_tree_demo.gif ├── github-icon.svg ├── icons_demo.gif ├── medium-logo.svg ├── oslabs-icon.png ├── rebuild_on_save_demo.gif ├── sapling-logo-128px.png ├── sapling-logo.png ├── settings_theme_demo.gif ├── vercel.svg └── vscode-logo.png └── styles ├── Home.module.css └── globals.scss /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/dev.yml: -------------------------------------------------------------------------------- 1 | name: dev CI 2 | 3 | # Run this workflow on PR into dev branch 4 | on: 5 | pull_request: 6 | branches: [ dev ] 7 | 8 | jobs: 9 | build: 10 | # Run tests on ubuntu linux 11 | runs-on: ubuntu-latest 12 | 13 | # Run Mocha test suite for sapling extension 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Sapling Mocha Tests 17 | run: | 18 | cd sapling 19 | npm install 20 | npm run test-mocha 21 | -------------------------------------------------------------------------------- /.github/workflows/master.yml: -------------------------------------------------------------------------------- 1 | name: master CI/CD 2 | 3 | # Run this workflow on PR into dev branch 4 | on: 5 | pull_request: 6 | branches: [ master ] 7 | 8 | jobs: 9 | build: 10 | # Run tests on ubuntu linux 11 | runs-on: ubuntu-latest 12 | 13 | # Run full test suite for sapling extension 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Sapling Mocha Tests 17 | run: | 18 | cd sapling 19 | npm install 20 | xvfb-run -a npm test 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | 107 | # VSCode files 108 | sapling/out 109 | sapling/dist 110 | sapling/node_modules 111 | sapling/.vscode-test/ 112 | *.vsix 113 | 114 | # Package-Lock files: 115 | package-lock.json 116 | */package-lock.json -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | sapling-extension@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 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 | -------------------------------------------------------------------------------- /sapling/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 6, 6 | "sourceType": "module" 7 | }, 8 | "plugins": [ 9 | "@typescript-eslint" 10 | ], 11 | "rules": { 12 | "@typescript-eslint/naming-convention": "warn", 13 | "@typescript-eslint/semi": "warn", 14 | "curly": "warn", 15 | "eqeqeq": "warn", 16 | "no-throw-literal": "warn", 17 | "semi": "off" 18 | }, 19 | "ignorePatterns": [ 20 | "out", 21 | "dist", 22 | "**/*.d.ts", 23 | "src/test/test_apps/*" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /sapling/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dbaeumer.vscode-eslint", 6 | "eamodio.tsl-problem-matcher" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /sapling/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | 9 | { 10 | "name": "Run Extension", 11 | "type": "extensionHost", 12 | "request": "launch", 13 | "args": [ 14 | "--extensionDevelopmentPath=${workspaceFolder}" 15 | ], 16 | "outFiles": [ 17 | "${workspaceFolder}/dist/**/*.js" 18 | ], 19 | "preLaunchTask": "${defaultBuildTask}" 20 | }, 21 | { 22 | "name": "Extension Tests", 23 | "type": "extensionHost", 24 | "request": "launch", 25 | "args": [ 26 | "--extensionDevelopmentPath=${workspaceFolder}", 27 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index", 28 | ], 29 | "outFiles": [ 30 | "${workspaceFolder}/out/test/**/*.js" 31 | ], 32 | "preLaunchTask": "npm: test-watch" 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /sapling/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false, // set this to true to hide the "out" folder with the compiled JS files 5 | "dist": false // set this to true to hide the "dist" folder with the compiled JS files 6 | }, 7 | "search.exclude": { 8 | "out": true, // set this to false to include "out" folder in search results 9 | "dist": true // set this to false to include "dist" folder in search results 10 | }, 11 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 12 | "typescript.tsc.autoDetect": "off" 13 | } -------------------------------------------------------------------------------- /sapling/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": [ 10 | "$ts-webpack-watch", 11 | "$tslint-webpack-watch" 12 | ], 13 | "isBackground": true, 14 | "presentation": { 15 | "reveal": "never" 16 | }, 17 | "group": { 18 | "kind": "build", 19 | "isDefault": true 20 | } 21 | }, 22 | { 23 | "type": "npm", 24 | "script": "test-watch", 25 | "problemMatcher": "$tsc-watch", 26 | "isBackground": true, 27 | "presentation": { 28 | "reveal": "never" 29 | }, 30 | "group": "build" 31 | } 32 | ] 33 | } -------------------------------------------------------------------------------- /sapling/.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/** 4 | node_modules/** 5 | src/** 6 | .gitignore 7 | .yarnrc 8 | vsc-extension-quickstart.md 9 | **/tsconfig.json 10 | **/.eslintrc.json 11 | **/*.map 12 | **/*.ts 13 | -------------------------------------------------------------------------------- /sapling/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to the "sapling" extension will be documented in this file. 4 | 5 | Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. 6 | 7 | ## [1.2.0] - 2021-10-14 8 | 9 | ### Added 10 | - Beta support for React.lazy() import syntax and `const component = import(../components/component)` syntax 11 | - Added Generate Tree option to explorer right-click context menu when clicking on JS/JSX/TS/TSX files 12 | 13 | ## [1.1.0] - 2021-10-7 14 | 15 | ### Added 16 | - Beta support for Next.js applications - 'page' files can be parsed to show component hierarchy when using create-next-app 17 | - Basic test case for Next.js compatibility included 18 | 19 | ## [1.0.0] - [Initial Release] - 2021-10-5 20 | 21 | ### Added 22 | - Initial release of Sapling VSCode Extension 23 | - Features: 24 | - Parses React applications written in JSX syntax with ES6 import syntax 25 | - Interactive sidebar webview displays component hierarchy 26 | - Files corresponding to each component can be opened from webview 27 | - Status bar button allows tree to be built from currently opened file 28 | - Identifies and displays JSX props 29 | - Identifies components connected to Redux store when exported with `connect` from `react-redux` 30 | - Maintains tree state when sapling is opened/closed or workspace is opened/closed 31 | - Automatically updates tree view on file save 32 | - Windows / WSL / Linux / MacOS compatible -------------------------------------------------------------------------------- /sapling/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 |

7 | 8 | Logo 9 | 10 | 11 |

Sapling

12 | 13 |

14 | A convenient way to traverse your React application. 15 |
16 | Explore the docs » 17 |
18 |
19 | Report Bug 20 | · 21 | Request Feature 22 |

23 | 24 |

25 | 26 | Visual Studio Marketplace Version 27 | 28 | Visual Studio Marketplace Installs 29 | 30 | GitHub Repo stars 31 | 32 | GitHub forks 33 | 34 | 35 |
36 | 37 | 40 | 41 | 42 | 43 | 44 |

45 |

46 | 47 |
48 | 49 | 50 |
51 | Table of Contents 52 |
    53 |
  1. 54 | About The Project 55 | 58 |
  2. 59 |
  3. Installation
  4. 60 |
  5. 61 | Getting Started
  6. 62 |
  7. Usage
  8. 63 |
  9. Extension Settings
  10. 64 |
  11. License
  12. 65 |
  13. Creators
  14. 66 |
  15. Contact
  16. 67 |
  17. Acknowledgements
  18. 68 |
69 |
70 | 71 |
72 | 73 | ## About The Project 74 | 75 |
76 |

77 | 78 |

79 |
80 | 81 | Sapling is a VS Code extension built with React developers in mind. As your codebase grows, your native file structure becomes less and less intuitive. Wouldn't it be nice to have a file structure that represents the actual relationships between the components and containers in your application? Wouldn't you like a quick reference to your available props, and an indication of routes and conditional rendering? 82 | 83 | With Sapling, you don't have to guess at the parent component of your current file. Sapling is an interactive hierarchical dependency tree that lives directly within your VS Code IDE, as accessible as the native file system. You can build your tree by selecting any component file as the root and get information about the available props at any level. It also provides visual indication of Javascript syntax or import errors in your files, and shows you which components are connected to your Redux store. 84 | 85 | 86 | ### Built With 87 | 110 | 111 | ## Installation 112 | 113 | Installing from VS Code Extension Marketplace: 114 | 115 | 1. If needed, install Visual Studio Code for Windows (7+), macOS (Sierra+), or Linux (details). 116 | 117 | 2. Install the Sapling extension for Visual Studio Code. Search for 'sapling' in the VS Code extensions tab, or click [here](https://marketplace.visualstudio.com/items?itemName=team-sapling.sapling). 118 | 119 | 3. Once complete, you'll see Sapling appear in your sidebar. You can now begin using Sapling! Check out the Getting Started below for information on how to get started. 120 | 121 | To install sapling for development, please see the contributing section below. 122 | 123 | ## Getting Started 124 | 125 | 1. After installing VSCode Extension, you will see the extension on your sidebar. Click the "Choose a File" button. 126 | 127 | 2. Your file explorer window will launch. Select an entrypoint, a file where the parent component for the rest of your application is rendered. 128 | 129 | 3. Your sidebar will now display a component tree. 130 | 131 | ## Usage 132 | 133 | After installing, click the Sapling tree icon in your extension sidebar (image of icon here). From there, you can either click "Choose a file" to select your root component, or build your tree directly from a file open in your editor with the "Build Tree" button on the right hand side of your status bar. Click the + and - buttons to expand and collapse subsets of your tree, and hover over the information icon (image of icon here) to get a list of props available to that component. You can also press the view file icon (image of icon here) to open the file where the component is defined in your editor window. Below is a quick-reference legend for icon and text format meanings. If you prefer not to view React Router or other third-party components imported to your app, you can disable either of these in the VS Code Extension Settings. 134 | 135 | Icon Legend in Sapling Tree View: 136 | 153 | 154 | Sapling can currently display React apps made with JSX/TSX and ES6 import syntax. 155 | 156 | Sapling will detect React components invoked using JSX tag syntax and React-Router component syntax, where React is imported in a file: 157 | 158 | ```JSX 159 | // Navbar will be detected as a child of the current file 160 | 161 | 162 | // As above 163 | 164 | 165 | // Route and Navbar will be detected as child components of the current file 166 | 167 | 168 | // Route and App will be detected as child components of the current file 169 | 170 | ``` 171 | 172 | Sapling will detect the names of inline props for JSX components it identifies: 173 | 174 | ```JSX 175 | // props 'userId' and 'userName' will be listed for Navbar in Sapling 176 | 177 | ``` 178 | 179 | Sapling can identify components connected to the Redux store, when 'connect' is imported from 'react-redux', and the component is the export default of the file: 180 | 181 | ```JSX 182 | // App.jsx 183 | import React from 'react'; 184 | import { connect } from 'react-redux'; 185 | 186 | const mapStateToProps = ... 187 | const mapDispatchToProps = ... 188 | 189 | const App = (props) => { 190 | return

This is the App

191 | } 192 | 193 | // Sapling will detect App as connected to the Redux store 194 | export default connect(mapStateToProps, mapDispatchToProps)(App); 195 | ``` 196 | 197 | ### Note 198 | Sapling prioritizes file dependencies over component dependencies. Consider the following JSX contained in the file App.jsx: 199 | 200 | ```JSX 201 | // App.jsx 202 | import React from 'react'; 203 | import Home from './Home'; 204 | import Navbar from './Navbar'; 205 | 206 | class App extends Component { 207 | 208 | render ( 209 | return { 210 | 211 | 212 | 213 | }) 214 | } 215 | ``` 216 | 217 | Sapling will display Home and Navbar as siblings, both children of App: (image of actual Sapling here) 218 | 219 |
220 | 221 | 222 | ## Extension Settings 223 | 224 | This extension contributes the following settings: 225 | 226 | * `sapling.view.reactRouter`: enable/disable React Router component nodes 227 | * `sapling.view.thirdParty`: enable/disable all third party component nodes 228 | 229 | ## License 230 | 231 | Distributed under the MIT License. See [`LICENSE`](https://github.com/oslabs-beta/sapling/blob/master/LICENSE) for more information. 232 | 233 | ## Creators 234 | 235 | * [Charles Gutwirth](https://github.com/charlesgutwirth) 236 | * [Jordan Hisel](https://github.com/jo-cella) 237 | * [Lindsay Baird](https://github.com/labaird) 238 | * [Paul Coster](https://github.com/PLCoster) 239 | 240 | ## Contact 241 | 242 | Twitter: [@TeamSapling](https://twitter.com/teamsapling) | Email: saplingextension@gmail.com 243 | 244 | GitHub: [https://github.com/oslabs-beta/sapling/](https://github.com/oslabs-beta/sapling/) 245 | 246 | ## Acknowledgements 247 | * Parsing Strategy inspired by [React Component Hierarchy](https://www.npmjs.com/package/react-component-hierarchy) 248 | * Interactive tree view styling adapted from [Pure CSS Tree Menu](https://codepen.io/bisserof/pen/fdtBm) 249 | * Icons from [Font Awesome](https://fontawesome.com) 250 | * Tooltips with [Tippy](https://www.npmjs.com/package/@tippy.js/react) 251 | * [Best README Template](https://github.com/othneildrew/Best-README-Template) 252 | * Sapling Logo from [Freepik](https://www.freepik.com/vectors/tree) 253 | * Readme badges from [shields.io](https://shields.io/) -------------------------------------------------------------------------------- /sapling/media/babel-logo-minimal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/sapling/509652cb7aef69be901f898282e362afb80fedd9/sapling/media/babel-logo-minimal.png -------------------------------------------------------------------------------- /sapling/media/babel-logo-minimal.svg: -------------------------------------------------------------------------------- 1 | Minimal Babel Logo 2 | -------------------------------------------------------------------------------- /sapling/media/chai_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/sapling/509652cb7aef69be901f898282e362afb80fedd9/sapling/media/chai_icon.png -------------------------------------------------------------------------------- /sapling/media/chai_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Chai Logo (C) 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /sapling/media/circle-arrow-right-solid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/sapling/509652cb7aef69be901f898282e362afb80fedd9/sapling/media/circle-arrow-right-solid.png -------------------------------------------------------------------------------- /sapling/media/circle-arrow-right-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sapling/media/circle-info-solid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/sapling/509652cb7aef69be901f898282e362afb80fedd9/sapling/media/circle-info-solid.png -------------------------------------------------------------------------------- /sapling/media/circle-info-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sapling/media/github-actions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/sapling/509652cb7aef69be901f898282e362afb80fedd9/sapling/media/github-actions.png -------------------------------------------------------------------------------- /sapling/media/github-actions.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /sapling/media/github-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/sapling/509652cb7aef69be901f898282e362afb80fedd9/sapling/media/github-icon.png -------------------------------------------------------------------------------- /sapling/media/github-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /sapling/media/list-tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/sapling/509652cb7aef69be901f898282e362afb80fedd9/sapling/media/list-tree.png -------------------------------------------------------------------------------- /sapling/media/list-tree.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /sapling/media/mochajs-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/sapling/509652cb7aef69be901f898282e362afb80fedd9/sapling/media/mochajs-icon.png -------------------------------------------------------------------------------- /sapling/media/mochajs-icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sapling/media/quizwall_demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/sapling/509652cb7aef69be901f898282e362afb80fedd9/sapling/media/quizwall_demo.gif -------------------------------------------------------------------------------- /sapling/media/react-brands.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/sapling/509652cb7aef69be901f898282e362afb80fedd9/sapling/media/react-brands.png -------------------------------------------------------------------------------- /sapling/media/react-brands.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sapling/media/readme-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/sapling/509652cb7aef69be901f898282e362afb80fedd9/sapling/media/readme-example.png -------------------------------------------------------------------------------- /sapling/media/reset.css: -------------------------------------------------------------------------------- 1 | 2 | html { 3 | box-sizing: border-box; 4 | font-size: 13px; 5 | } 6 | 7 | *, 8 | *:before, 9 | *:after { 10 | box-sizing: inherit; 11 | } 12 | 13 | body, 14 | h1, 15 | h2, 16 | h3, 17 | h4, 18 | h5, 19 | h6, 20 | p, 21 | ol, 22 | ul { 23 | margin: 0; 24 | padding: 0; 25 | font-weight: normal; 26 | } 27 | 28 | img { 29 | max-width: 100%; 30 | height: auto; 31 | } 32 | 33 | html, 34 | body { 35 | height: 100%; 36 | display: flex; 37 | flex-direction: column; 38 | } -------------------------------------------------------------------------------- /sapling/media/sapling-logo-128px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/sapling/509652cb7aef69be901f898282e362afb80fedd9/sapling/media/sapling-logo-128px.png -------------------------------------------------------------------------------- /sapling/media/sapling-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/sapling/509652cb7aef69be901f898282e362afb80fedd9/sapling/media/sapling-logo.png -------------------------------------------------------------------------------- /sapling/media/store-solid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/sapling/509652cb7aef69be901f898282e362afb80fedd9/sapling/media/store-solid.png -------------------------------------------------------------------------------- /sapling/media/store-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sapling/media/styles.css: -------------------------------------------------------------------------------- 1 | #root { 2 | margin: 0; 3 | height: 100%; 4 | background-color: var(--vscode-menu-background); 5 | } 6 | 7 | .line_break { 8 | border: none; 9 | border-top: 2px solid; 10 | opacity: 40%; 11 | margin: 0; 12 | } 13 | 14 | .sidebar { 15 | display: flex; 16 | flex-direction: column; 17 | height: 100%; 18 | } 19 | 20 | .navbar { 21 | flex-grow: 0; 22 | padding: 10px 0px; 23 | } 24 | 25 | .tree_view { 26 | overflow-x: scroll; 27 | flex-grow: 1; 28 | } 29 | 30 | .node_error { 31 | color: var(--vscode-editorError-foreground); 32 | } 33 | 34 | .inputfile { 35 | width: 0.1px; 36 | height: 0.1px; 37 | opacity: 0; 38 | overflow: hidden; 39 | position: absolute; 40 | z-index: -1; 41 | } 42 | 43 | .inputfile + label { 44 | padding: .4em; 45 | font-size: 1em; 46 | font-weight: 700; 47 | color: var(--vscode-button-foreground); 48 | background: var(--vscode-button-background); 49 | display: inline-block; 50 | margin-left: .8em; 51 | margin-top: .2em; 52 | border-radius: 5px; 53 | transition: .2s; 54 | } 55 | 56 | .inputfile + label:hover { 57 | background: var(--vscode-button-hoverBackground); 58 | } 59 | 60 | .inputfile + label { 61 | cursor: pointer; 62 | } 63 | 64 | .node_icons { 65 | margin-left: 5px; 66 | opacity: 40%; 67 | color: var(--vscode-button-background); 68 | } 69 | 70 | .node_icons:hover { 71 | opacity: 100%; 72 | color: var(--vscode-button-background); 73 | } 74 | 75 | .redux_connect { 76 | margin-left: 5px; 77 | color: var(--vscode-button-hoverBackground); 78 | opacity: 100%; 79 | } 80 | 81 | /* ————————————————————– 82 | Tree core styles 83 | */ 84 | .tree_beginning { margin: 1em 1em 1em 2em; } 85 | 86 | .tree_beginning input { 87 | position: absolute; 88 | clip: rect(0, 0, 0, 0); 89 | } 90 | 91 | .tree_beginning input ~ ul { display: none; } 92 | 93 | .tree_beginning input:checked ~ ul { display: block; } 94 | 95 | /* ————————————————————– 96 | Tree rows 97 | */ 98 | 99 | .tree_view li { 100 | white-space: nowrap; 101 | } 102 | 103 | .tree_beginning li { 104 | line-height: 1.2; 105 | position: relative; 106 | padding: 0 0 1em 1em; 107 | } 108 | 109 | .tree_beginning ul li { padding: 1em 0 0 1em; } 110 | 111 | .tree_beginning > li:last-child { padding-bottom: 0; } 112 | 113 | /* ————————————————————– 114 | Tree labels 115 | */ 116 | .tree_label { 117 | position: relative; 118 | display: inline-block; 119 | } 120 | 121 | label.tree_label { cursor: pointer; } 122 | 123 | label.tree_label:hover { color: #666; } 124 | 125 | /* ————————————————————– 126 | Tree expanded icon 127 | */ 128 | label.tree_label:before { 129 | /* background: rgb(220, 28, 28); */ 130 | background: var(--vscode-button-background); 131 | color: #fff; 132 | position: relative; 133 | z-index: 1; 134 | float: left; 135 | margin: 0 1em 0 -2em; 136 | width: 1.05em; 137 | height: 1.05em; 138 | border-radius: 1em; 139 | content: '+'; 140 | text-align: center; 141 | line-height: .9em; 142 | } 143 | 144 | :checked ~ label.tree_label:before { content: '–'; } 145 | 146 | /* ————————————————————– 147 | Tree branches 148 | */ 149 | .tree_beginning li:before { 150 | position: absolute; 151 | top: 0; 152 | bottom: 0; 153 | left: -.55em; 154 | display: block; 155 | width: 0; 156 | border-left: 1px solid #777; 157 | content: ""; 158 | } 159 | 160 | .tree_label:after { 161 | position: absolute; 162 | top: 0; 163 | left: -1.55em; 164 | display: block; 165 | height: 0.5em; 166 | width: 1em; 167 | border-bottom: 1px solid #777; 168 | border-left: 1px solid #777; 169 | border-radius: 0 0 0 .3em; 170 | content: ''; 171 | } 172 | 173 | label.tree_label:after { border-bottom: 0; } 174 | 175 | :checked ~ label.tree_label:after { 176 | border-radius: 0 .3em 0 0; 177 | border-top: 1px solid #777; 178 | border-right: 1px solid #777; 179 | border-bottom: 0; 180 | border-left: 0; 181 | left: -1.5em; 182 | bottom: 0; 183 | top: 0.5em; 184 | height: auto; 185 | } 186 | 187 | .tree_beginning li:last-child:before { 188 | height: 1em; 189 | bottom: auto; 190 | } 191 | 192 | .tree_beginning > li:last-child:before { display: none; } 193 | 194 | .tree_custom { 195 | display: block; 196 | background: #eee; 197 | padding: 1em; 198 | border-radius: 0.3em; 199 | } 200 | 201 | ul, 202 | li { 203 | list-style-type: none; 204 | } -------------------------------------------------------------------------------- /sapling/media/twitter-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/sapling/509652cb7aef69be901f898282e362afb80fedd9/sapling/media/twitter-logo.png -------------------------------------------------------------------------------- /sapling/media/twitter-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | -------------------------------------------------------------------------------- /sapling/media/vscode.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --container-paddding: 20px; 3 | --input-padding-vertical: 6px; 4 | --input-padding-horizontal: 4px; 5 | --input-margin-vertical: 4px; 6 | --input-margin-horizontal: 0; 7 | } 8 | 9 | body { 10 | /* padding: 0 var(--container-paddding); */ 11 | color: var(--vscode-foreground); 12 | font-size: var(--vscode-font-size); 13 | font-weight: var(--vscode-font-weight); 14 | font-family: var(--vscode-font-family); 15 | background-color: var(--vscode-editor-background); 16 | } 17 | 18 | /* ol, 19 | ul { 20 | padding-left: var(--container-paddding); 21 | } */ 22 | 23 | body > *, 24 | form > * { 25 | margin-block-start: var(--input-margin-vertical); 26 | margin-block-end: var(--input-margin-vertical); 27 | } 28 | 29 | *:focus { 30 | outline-color: var(--vscode-focusBorder) !important; 31 | } 32 | 33 | a { 34 | color: var(--vscode-textLink-foreground); 35 | } 36 | 37 | a:hover, 38 | a:active { 39 | color: var(--vscode-textLink-activeForeground); 40 | } 41 | 42 | code { 43 | font-size: var(--vscode-editor-font-size); 44 | font-family: var(--vscode-editor-font-family); 45 | } 46 | 47 | button { 48 | border: none; 49 | padding: var(--input-padding-vertical) var(--input-padding-horizontal); 50 | width: 100%; 51 | text-align: center; 52 | outline: 1px solid transparent; 53 | outline-offset: 2px !important; 54 | color: var(--vscode-button-foreground); 55 | background: var(--vscode-button-background); 56 | } 57 | 58 | /* span { 59 | color: #1e1e1e; 60 | color: #d4d4d4; 61 | color: #9cdcfe; 62 | color: #d19a66; 63 | color: #dcdcaa; 64 | color: #c586c0; 65 | color: #d4d4d4; 66 | color: #dcdcaa; 67 | color: #b5cea8; 68 | color: #ce9178; 69 | color: #6a9955; 70 | color: #d4d4d4; 71 | color: #569cd6; 72 | } */ 73 | 74 | button:hover { 75 | cursor: pointer; 76 | background: var(--vscode-button-hoverBackground); 77 | } 78 | 79 | button:focus { 80 | outline-color: var(--vscode-focusBorder); 81 | } 82 | 83 | button.secondary { 84 | color: var(--vscode-button-secondaryForeground); 85 | background: var(--vscode-button-secondaryBackground); 86 | } 87 | 88 | button.secondary:hover { 89 | background: var(--vscode-button-secondaryHoverBackground); 90 | } 91 | 92 | input:not([type="checkbox"]), 93 | textarea { 94 | display: block; 95 | width: 100%; 96 | border: none; 97 | font-family: var(--vscode-font-family); 98 | padding: var(--input-padding-vertical) var(--input-padding-horizontal); 99 | color: var(--vscode-input-foreground); 100 | outline-color: var(--vscode-input-border); 101 | background-color: var(--vscode-input-background); 102 | } 103 | 104 | input::placeholder, 105 | textarea::placeholder { 106 | color: var(--vscode-input-placeholderForeground); 107 | } -------------------------------------------------------------------------------- /sapling/media/vscode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/sapling/509652cb7aef69be901f898282e362afb80fedd9/sapling/media/vscode.png -------------------------------------------------------------------------------- /sapling/media/vscode.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /sapling/media/webpack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/sapling/509652cb7aef69be901f898282e362afb80fedd9/sapling/media/webpack.png -------------------------------------------------------------------------------- /sapling/media/webpack.svg: -------------------------------------------------------------------------------- 1 | icon-square-small -------------------------------------------------------------------------------- /sapling/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sapling", 3 | "displayName": "Sapling", 4 | "description": "React Component Hierarchy Visualizer", 5 | "repository": "https://github.com/oslabs-beta/sapling", 6 | "icon": "media/sapling-logo-128px.png", 7 | "publisher": "team-sapling", 8 | "version": "1.2.0", 9 | "engines": { 10 | "vscode": "^1.60.0" 11 | }, 12 | "categories": [ 13 | "Visualization" 14 | ], 15 | "keywords": [ 16 | "react", 17 | "component hierarchy", 18 | "devtools" 19 | ], 20 | "activationEvents": [ 21 | "onStartupFinished" 22 | ], 23 | "main": "./dist/extension.js", 24 | "contributes": { 25 | "viewsContainers": { 26 | "activitybar": [ 27 | { 28 | "id": "sapling-sidebar-view", 29 | "title": "sapling", 30 | "icon": "media/list-tree.svg" 31 | } 32 | ] 33 | }, 34 | "views": { 35 | "sapling-sidebar-view": [ 36 | { 37 | "type": "webview", 38 | "id": "sapling-sidebar", 39 | "name": "sapling", 40 | "icon": "media/list-tree.svg", 41 | "contextualTitle": "sapling" 42 | } 43 | ] 44 | }, 45 | "commands": [ 46 | { 47 | "command": "sapling.refresh", 48 | "category": "Sapling", 49 | "title": "Refresh" 50 | }, 51 | { 52 | "command": "sapling.generateTree", 53 | "category": "Sapling", 54 | "title": "Generate Tree" 55 | } 56 | ], 57 | "menus": { 58 | "commandPalette": [ 59 | { 60 | "command": "sapling.generateTree", 61 | "when": "resourceLangId == javascript || resourceLangId == javascriptreact || resourceLangId == typescript || resourceLangId == typescriptreact" 62 | } 63 | ], 64 | "explorer/context": [ 65 | { 66 | "when": "resourceLangId == javascript || resourceLangId == javascriptreact || resourceLangId == typescript || resourceLangId == typescriptreact", 67 | "command": "sapling.generateTree", 68 | "group": "sapling" 69 | } 70 | ] 71 | }, 72 | "configuration": { 73 | "title": "Sapling", 74 | "properties": { 75 | "sapling.view.thirdParty": { 76 | "type": "boolean", 77 | "default": true, 78 | "description": "Show Third Party components in the tree view." 79 | }, 80 | "sapling.view.reactRouter": { 81 | "type": "boolean", 82 | "default": true, 83 | "description": "Show React Router components in the tree view." 84 | } 85 | } 86 | } 87 | }, 88 | "scripts": { 89 | "vscode:prepublish": "npm run package", 90 | "build": "npm-run-all -p build:*", 91 | "build:extension": "webpack --mode production", 92 | "build:sidebar": "webpack --config ./src/webviews/webpack.views.config.js", 93 | "watch": "npm-run-all -p watch:*", 94 | "watch:extension": "webpack --watch", 95 | "watch:sidebar": "webpack --watch --config ./src/webviews/webpack.views.config.js", 96 | "package": "webpack --mode production --devtool hidden-source-map && webpack --config ./src/webviews/webpack.views.config.js --devtool hidden-source-map", 97 | "test-compile": "tsc -p ./", 98 | "test-watch": "tsc -watch -p ./", 99 | "pretest": "npm run test-compile && npm run lint", 100 | "lint": "eslint src --ext ts", 101 | "test": "node ./out/test/runTest.js", 102 | "test-mocha": "npm run pretest && npx mocha out/test/suite/parser.test.js" 103 | }, 104 | "devDependencies": { 105 | "@babel/parser": "^7.15.7", 106 | "@babel/types": "^7.15.6", 107 | "@testing-library/react": "^12.1.0", 108 | "@types/chai": "^4.2.22", 109 | "@types/glob": "^7.1.3", 110 | "@types/jsdom": "^16.2.13", 111 | "@types/mocha": "^8.2.2", 112 | "@types/node": "14.x", 113 | "@types/react": "^17.0.21", 114 | "@types/react-dom": "^17.0.9", 115 | "@types/vscode": "^1.60.0", 116 | "@typescript-eslint/eslint-plugin": "^4.26.0", 117 | "@typescript-eslint/parser": "^4.26.0", 118 | "chai": "^4.3.4", 119 | "css-loader": "^6.2.0", 120 | "eslint": "^7.27.0", 121 | "glob": "^7.1.7", 122 | "global-jsdom": "^8.2.0", 123 | "jsdom": "^17.0.0", 124 | "mocha": "^8.4.0", 125 | "npm-run-all": "^4.1.5", 126 | "style-loader": "^3.2.1", 127 | "ts-loader": "^9.2.2", 128 | "typescript": "^4.3.2", 129 | "vscode-test": "^1.5.2", 130 | "webpack": "^5.38.1", 131 | "webpack-cli": "^4.7.0" 132 | }, 133 | "dependencies": { 134 | "@fortawesome/fontawesome-svg-core": "^1.2.36", 135 | "@fortawesome/free-solid-svg-icons": "^5.15.4", 136 | "@fortawesome/react-fontawesome": "^0.1.15", 137 | "@tippy.js/react": "^3.1.1", 138 | "react": "^17.0.2", 139 | "react-dom": "^17.0.2" 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /sapling/src/SaplingParser.ts: -------------------------------------------------------------------------------- 1 | import * as babelParser from '@babel/parser'; 2 | import * as path from 'path'; 3 | import * as fs from 'fs'; 4 | import { getNonce } from "./getNonce"; 5 | import { Tree } from './types/Tree'; 6 | import { ImportObj } from './types/ImportObj'; 7 | import { File } from '@babel/types'; 8 | 9 | export class SaplingParser { 10 | entryFile: string; 11 | tree: Tree | undefined; 12 | 13 | constructor(filePath: string) { 14 | // Fix when selecting files in wsl file system 15 | this.entryFile = filePath; 16 | if (process.platform === 'linux' && this.entryFile.includes('wsl$')) { 17 | this.entryFile = path.resolve(filePath.split(path.win32.sep).join(path.posix.sep)); 18 | this.entryFile = '/' + this.entryFile.split('/').slice(3).join('/'); 19 | // Fix for when running wsl but selecting files held on windows file system 20 | } else if (process.platform === 'linux' && (/[a-zA-Z]/).test(this.entryFile[0])) { 21 | const root = `/mnt/${this.entryFile[0].toLowerCase()}`; 22 | this.entryFile = path.join(root, filePath.split(path.win32.sep).slice(1).join(path.posix.sep)); 23 | } 24 | 25 | this.tree = undefined; 26 | // Break down and reasemble given filePath safely for any OS using path? 27 | } 28 | 29 | // Public method to generate component tree based on current entryFile 30 | public parse() : Tree { 31 | // Create root Tree node 32 | const root = { 33 | id: getNonce(), 34 | name: path.basename(this.entryFile).replace(/\.(t|j)sx?$/, ''), 35 | fileName: path.basename(this.entryFile), 36 | filePath : this.entryFile, 37 | importPath: '/', // this.entryFile here breaks windows file path on root e.g. C:\\ is detected as third party 38 | expanded: false, 39 | depth: 0, 40 | count: 1, 41 | thirdParty: false, 42 | reactRouter: false, 43 | reduxConnect: false, 44 | children: [], 45 | parentList: [], 46 | props: {}, 47 | error: '' 48 | }; 49 | 50 | this.tree = root; 51 | this.parser(root); 52 | return this.tree; 53 | } 54 | 55 | public getTree() : Tree { 56 | return this.tree; 57 | } 58 | 59 | // Set Sapling Parser with a specific Data Tree (from workspace state) 60 | public setTree(tree : Tree) : void { 61 | this.entryFile = tree.filePath; 62 | this.tree = tree; 63 | } 64 | 65 | public updateTree(filePath : string) : Tree { 66 | let children = []; 67 | 68 | const getChildNodes = (node: Tree) : void => { 69 | const { depth, filePath, expanded } = node; 70 | children.push({ depth, filePath, expanded }); 71 | }; 72 | 73 | const matchExpand = (node: Tree) : void => { 74 | for (let i = 0 ; i < children.length ; i += 1) { 75 | const oldNode = children[i]; 76 | if (oldNode.depth === node.depth && oldNode.filePath === node.filePath && oldNode.expanded) { 77 | node.expanded = true; 78 | } 79 | } 80 | }; 81 | 82 | const callback = (node: Tree) : void => { 83 | if (node.filePath === filePath) { 84 | node.children.forEach(child => { 85 | this.#traverseTree(getChildNodes, child); 86 | }); 87 | 88 | const newNode = this.parser(node); 89 | 90 | this.#traverseTree(matchExpand, newNode); 91 | 92 | children = []; 93 | } 94 | }; 95 | 96 | this.#traverseTree(callback, this.tree); 97 | 98 | return this.tree; 99 | } 100 | 101 | // Traverses the tree and changes expanded property of node whose id matches provided id 102 | public toggleNode(id : string, expanded : boolean) : Tree { 103 | const callback = (node) => { 104 | if (node.id === id) { 105 | node.expanded = expanded; 106 | } 107 | }; 108 | 109 | this.#traverseTree(callback, this.tree); 110 | 111 | return this.tree; 112 | } 113 | 114 | // Traverses all nodes of current component tree and applies callback to each node 115 | #traverseTree(callback : Function, node : Tree = this.tree) : void { 116 | if (!node) { 117 | return; 118 | } 119 | 120 | callback(node); 121 | 122 | node.children.forEach( (childNode) => { 123 | this.#traverseTree(callback, childNode); 124 | }); 125 | } 126 | 127 | // Recursively builds the React component tree structure starting from root node 128 | private parser(componentTree: Tree) : Tree { 129 | 130 | // If import is a node module, do not parse any deeper 131 | if (!['\\', '/', '.'].includes(componentTree.importPath[0])) { 132 | componentTree.thirdParty = true; 133 | if (componentTree.fileName === 'react-router-dom' || componentTree.fileName === 'react-router') { 134 | componentTree.reactRouter = true; 135 | } 136 | return; 137 | } 138 | 139 | // Check that file has valid fileName/Path, if not found, add error to node and halt 140 | const fileName = this.getFileName(componentTree); 141 | if (!fileName) { 142 | componentTree.error = 'File not found.'; 143 | return; 144 | } 145 | 146 | // If current node recursively calls itself, do not parse any deeper: 147 | if (componentTree.parentList.includes(componentTree.filePath)) { 148 | return; 149 | } 150 | 151 | // Create abstract syntax tree of current component tree file 152 | let ast: babelParser.ParseResult; 153 | try { 154 | ast = babelParser.parse(fs.readFileSync(path.resolve(componentTree.filePath), 'utf-8'), { 155 | sourceType: 'module', 156 | tokens: true, 157 | plugins: [ 158 | 'jsx', 159 | 'typescript', 160 | ] 161 | }); 162 | } catch (err) { 163 | componentTree.error = 'Error while processing this file/node'; 164 | return componentTree; 165 | } 166 | 167 | // Find imports in the current file, then find child components in the current file 168 | const imports = this.getImports(ast.program.body); 169 | 170 | // Get any JSX Children of current file: 171 | componentTree.children = this.getJSXChildren(ast.tokens, imports, componentTree); 172 | 173 | // Check if current node is connected to the Redux store 174 | componentTree.reduxConnect = this.checkForRedux(ast.tokens, imports); 175 | 176 | // Recursively parse all child components 177 | componentTree.children.forEach(child => this.parser(child)); 178 | 179 | return componentTree; 180 | } 181 | 182 | // Finds files where import string does not include a file extension 183 | private getFileName(componentTree: Tree) : string | undefined { 184 | const ext = path.extname(componentTree.filePath); 185 | let fileName = componentTree.fileName; 186 | 187 | if (!ext) { 188 | // Try and find file extension that exists in directory: 189 | const fileArray = fs.readdirSync(path.dirname(componentTree.filePath)); 190 | const regEx = new RegExp(`${componentTree.fileName}.(j|t)sx?$`); 191 | fileName = fileArray.find(fileStr => fileStr.match(regEx)); 192 | fileName ? componentTree.filePath += path.extname(fileName) : null; 193 | } 194 | 195 | return fileName; 196 | } 197 | 198 | // Extracts Imports from current file 199 | // const Page1 = lazy(() => import('./page1')); -> is parsed as 'ImportDeclaration' 200 | // import Page2 from './page2'; -> is parsed as 'VariableDeclaration' 201 | private getImports(body : {[key : string]: any}[]) : ImportObj { 202 | const bodyImports = body.filter(item => item.type === 'ImportDeclaration' || 'VariableDeclaration'); 203 | // console.log('bodyImports are: ', bodyImports); 204 | return bodyImports.reduce((accum, curr) => { 205 | // Import Declarations: 206 | if (curr.type === 'ImportDeclaration') { 207 | curr.specifiers.forEach( i => { 208 | accum[i.local.name] = { 209 | importPath: curr.source.value, 210 | importName: (i.imported)? i.imported.name : i.local.name 211 | }; 212 | }); 213 | } 214 | // Imports Inside Variable Declarations: // Not easy to deal with nested objects 215 | if (curr.type === 'VariableDeclaration') { 216 | const importPath = this.findVarDecImports(curr.declarations[0]); 217 | if (importPath) { 218 | const importName = curr.declarations[0].id.name; 219 | accum[curr.declarations[0].id.name] = { 220 | importPath, 221 | importName 222 | }; 223 | } 224 | } 225 | return accum; 226 | }, {}); 227 | } 228 | 229 | // Recursive helper method to find import path in Variable Declaration 230 | private findVarDecImports(ast: {[key: string]: any}) { 231 | // Base Case, find import path in variable declaration and return it, 232 | if (ast.hasOwnProperty('callee') && ast.callee.type === 'Import') { 233 | return ast.arguments[0].value; 234 | } 235 | 236 | // Otherwise look for imports in any other non null/undefined objects in the tree: 237 | for (let key in ast) { 238 | if (ast.hasOwnProperty(key) && typeof ast[key] === 'object' && ast[key]) { 239 | const importPath = this.findVarDecImports(ast[key]); 240 | if (importPath) { 241 | return importPath; 242 | } 243 | } 244 | } 245 | 246 | return false; 247 | } 248 | 249 | // Finds JSX React Components in current file 250 | private getJSXChildren(astTokens: any[], importsObj : ImportObj, parentNode: Tree) : Tree[] { 251 | let childNodes: {[key : string]: Tree} = {}; 252 | let props : {[key : string]: boolean} = {}; 253 | let token : {[key: string]: any}; 254 | 255 | for (let i = 0; i < astTokens.length; i++) { 256 | // Case for finding JSX tags eg 257 | if (astTokens[i].type.label === 'jsxTagStart' 258 | && astTokens[i + 1].type.label === 'jsxName' 259 | && importsObj[astTokens[i + 1].value]) { 260 | token = astTokens[i + 1]; 261 | props = this.getJSXProps(astTokens, i + 2); 262 | childNodes = this.getChildNodes(importsObj, token, props, parentNode, childNodes); 263 | 264 | // Case for finding components passed in as props e.g. 265 | } else if (astTokens[i].type.label === 'jsxName' 266 | && (astTokens[i].value === 'component' || astTokens[i].value === 'children') 267 | && importsObj[astTokens[i + 3].value]) { 268 | token = astTokens[i + 3]; 269 | childNodes = this.getChildNodes(importsObj, token, props, parentNode, childNodes); 270 | } 271 | } 272 | 273 | return Object.values(childNodes); 274 | } 275 | 276 | private getChildNodes(imports : ImportObj, 277 | astToken : {[key: string]: any}, props : {[key : string]: boolean}, 278 | parent : Tree, children : {[key : string] : Tree}) : {[key : string] : Tree} { 279 | 280 | if (children[astToken.value]) { 281 | children[astToken.value].count += 1; 282 | children[astToken.value].props = {...children[astToken.value].props, ...props}; 283 | } else { 284 | // Add tree node to childNodes if one does not exist 285 | children[astToken.value] = { 286 | id: getNonce(), 287 | name: imports[astToken.value]['importName'], 288 | fileName: path.basename(imports[astToken.value]['importPath']), 289 | filePath: path.resolve(path.dirname(parent.filePath), imports[astToken.value]['importPath']), 290 | importPath: imports[astToken.value]['importPath'], 291 | expanded: false, 292 | depth: parent.depth + 1, 293 | thirdParty: false, 294 | reactRouter: false, 295 | reduxConnect: false, 296 | count: 1, 297 | props: props, 298 | children: [], 299 | parentList: [parent.filePath].concat(parent.parentList), 300 | error: '', 301 | }; 302 | } 303 | 304 | return children; 305 | } 306 | 307 | // Extracts prop names from a JSX element 308 | private getJSXProps(astTokens: {[key: string]: any}[], j : number) : {[key : string]: boolean} { 309 | const props = {}; 310 | while (astTokens[j].type.label !== "jsxTagEnd") { 311 | if (astTokens[j].type.label === "jsxName" && astTokens[j + 1].value === "=") { 312 | props[astTokens[j].value] = true; 313 | } 314 | j += 1; 315 | } 316 | return props; 317 | } 318 | 319 | // Checks if current Node is connected to React-Redux Store 320 | private checkForRedux(astTokens: any[], importsObj : ImportObj) : boolean { 321 | // Check that react-redux is imported in this file (and we have a connect method or otherwise) 322 | let reduxImported = false; 323 | let connectAlias; 324 | Object.keys(importsObj).forEach( key => { 325 | if (importsObj[key].importPath === 'react-redux' && importsObj[key].importName === 'connect') { 326 | reduxImported = true; 327 | connectAlias = key; 328 | } 329 | }); 330 | 331 | if (!reduxImported) { 332 | return false; 333 | } 334 | 335 | // Check that connect method is invoked and exported in the file 336 | for (let i = 0; i < astTokens.length; i += 1) { 337 | if (astTokens[i].type.label === 'export' && astTokens[i + 1].type.label === 'default' && astTokens[i + 2].value === connectAlias) { 338 | return true; 339 | } 340 | } 341 | return false; 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /sapling/src/SidebarProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { getNonce } from "./getNonce"; 3 | import { SaplingParser } from './SaplingParser'; 4 | import { Tree } from "./types/Tree"; 5 | 6 | // Sidebar class that creates a new instance of the sidebar + adds functionality with the parser 7 | export class SidebarProvider implements vscode.WebviewViewProvider { 8 | _view?: vscode.WebviewView; 9 | _doc?: vscode.TextDocument; 10 | parser: SaplingParser | undefined; 11 | private readonly _extensionUri: vscode.Uri; 12 | private readonly context: vscode.ExtensionContext; 13 | 14 | constructor(context: vscode.ExtensionContext) { 15 | this.context = context; 16 | this._extensionUri = context.extensionUri; 17 | // Check for sapling state in workspace and set tree with previous state 18 | const state: Tree | undefined = context.workspaceState.get('sapling'); 19 | if (state) { 20 | this.parser = new SaplingParser(state.filePath); 21 | this.parser.setTree(state); 22 | } 23 | } 24 | 25 | // Instantiate the connection to the webview 26 | public resolveWebviewView(webviewView: vscode.WebviewView) { 27 | this._view = webviewView; 28 | 29 | webviewView.webview.options = { 30 | // Allow scripts in the webview 31 | enableScripts: true, 32 | localResourceRoots: [this._extensionUri], 33 | }; 34 | 35 | // Event listener that triggers any moment that the user changes his/her settings preferences 36 | vscode.workspace.onDidChangeConfiguration((e) => { 37 | // Get the current settings specifications the user selects 38 | const settings = vscode.workspace.getConfiguration('sapling'); 39 | // Send a message back to the webview with the data on settings 40 | webviewView.webview.postMessage({ 41 | type: "settings-data", 42 | value: settings.view 43 | }); 44 | }); 45 | 46 | // Event listener that triggers whenever the user changes their current active window 47 | vscode.window.onDidChangeActiveTextEditor((e) => { 48 | // Post a message to the webview with the file path of the user's current active window 49 | webviewView.webview.postMessage({ 50 | type: "current-tab", 51 | value: e ? e.document.fileName : undefined 52 | }); 53 | }); 54 | 55 | // Event listener that triggers whenever the user saves a document 56 | vscode.workspace.onDidSaveTextDocument((document) => { 57 | // Edge case that avoids sending messages to the webview when there is no tree currently populated 58 | if (!this.parser) { 59 | return; 60 | } 61 | // Post a message to the webview with the newly parsed tree 62 | this.parser.updateTree(document.fileName); 63 | this.updateView(); 64 | }); 65 | 66 | // Reaches out to the project file connector function below 67 | webviewView.webview.html = this._getHtmlForWebview(webviewView.webview); 68 | 69 | // Message switch case that will listen for messages sent from the webview 70 | webviewView.webview.onDidReceiveMessage(async (data) => { 71 | // Switch cases based on the type sent as a message 72 | switch (data.type) { 73 | // Case when the user selects a file to begin a tree 74 | case "onFile": { 75 | // Edge case if the user sends in nothing 76 | if (!data.value) { 77 | return; 78 | } 79 | // Run an instance of the parser 80 | this.parser = new SaplingParser(data.value); 81 | this.parser.parse(); 82 | this.updateView(); 83 | break; 84 | } 85 | 86 | // Case when clicking on tree to open file 87 | case "onViewFile": { 88 | if (!data.value) { 89 | return; 90 | } 91 | // Open and the show the user the file they want to see 92 | const doc = await vscode.workspace.openTextDocument(data.value); 93 | const editor = await vscode.window.showTextDocument(doc, {preserveFocus: false, preview: false}); 94 | break; 95 | } 96 | 97 | // Case when sapling becomes visible in sidebar 98 | case "onSaplingVisible": { 99 | if (!this.parser) { 100 | return; 101 | } 102 | // Get and send the saved tree to the webview 103 | this.updateView(); 104 | break; 105 | } 106 | 107 | // Case to retrieve the user's settings 108 | case "onSettingsAcquire": { 109 | // use getConfiguration to check what the current settings are for the user 110 | const settings = await vscode.workspace.getConfiguration('sapling'); 111 | // send a message back to the webview with the data on settings 112 | webviewView.webview.postMessage({ 113 | type: "settings-data", 114 | value: settings.view 115 | }); 116 | break; 117 | } 118 | 119 | // Case that changes the parser's recorded node expanded/collapsed structure 120 | case "onNodeToggle": { 121 | // let the parser know that the specific node clicked changed it's expanded value, save in state 122 | this.context.workspaceState.update( 123 | 'sapling', 124 | this.parser.toggleNode(data.value.id, data.value.expanded) 125 | ); 126 | break; 127 | } 128 | 129 | // Message sent to the webview to bold the active file 130 | case "onBoldCheck": { 131 | // Check there is an activeText Editor 132 | const fileName = vscode.window.activeTextEditor ? vscode.window.activeTextEditor.document.fileName: null; 133 | // Message sent to the webview to bold the active file 134 | if (fileName) { 135 | this._view.webview.postMessage({ 136 | type: "current-tab", 137 | value: fileName 138 | }); 139 | } 140 | break; 141 | } 142 | } 143 | }); 144 | } 145 | 146 | // Called when Generate Tree command triggered by status button or explorer context menu 147 | public statusButtonClicked = (uri: vscode.Uri | undefined) => { 148 | let fileName; 149 | // If status menu button clicked, no uri, get active file uri 150 | if (!uri) { 151 | fileName = vscode.window.activeTextEditor.document.fileName; 152 | } else { 153 | fileName = uri.path; 154 | } 155 | 156 | // Parse new tree with file as root 157 | if (fileName) { 158 | this.parser = new SaplingParser(fileName); 159 | this.parser.parse(); 160 | this.updateView(); 161 | } 162 | }; 163 | 164 | // revive statement for the webview panel 165 | public revive(panel: vscode.WebviewView) { 166 | this._view = panel; 167 | } 168 | 169 | // Helper method to send updated tree data to view, and saves current tree to workspace 170 | private updateView() { 171 | // Save current state of tree to workspace state: 172 | const tree = this.parser.getTree(); 173 | this.context.workspaceState.update('sapling', tree); 174 | // Send updated tree to webview 175 | this._view.webview.postMessage({ 176 | type: "parsed-data", 177 | value: tree 178 | }); 179 | } 180 | 181 | // paths and return statement that connects the webview to React project files 182 | private _getHtmlForWebview(webview: vscode.Webview) { 183 | const styleResetUri = webview.asWebviewUri( 184 | vscode.Uri.joinPath(this._extensionUri, "media", "reset.css") 185 | ); 186 | const styleVSCodeUri = webview.asWebviewUri( 187 | vscode.Uri.joinPath(this._extensionUri, "media", "vscode.css") 188 | ); 189 | const styleMainUri = webview.asWebviewUri( 190 | vscode.Uri.joinPath(this._extensionUri, "media", "styles.css") 191 | ); 192 | 193 | const scriptUri = webview.asWebviewUri( 194 | vscode.Uri.joinPath(this._extensionUri, "dist", "sidebar.js") 195 | ); 196 | 197 | // Use a nonce to only allow a specific script to be run. 198 | const nonce = getNonce(); 199 | 200 | return ` 201 | 202 | 203 | 204 | 208 | 213 | 214 | 215 | 216 | 217 | 220 | 221 | 222 |
223 | 224 | 225 | `; 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /sapling/src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { SidebarProvider } from './SidebarProvider'; 3 | 4 | // Sapling extension is activated after vscode startup 5 | export function activate(context: vscode.ExtensionContext) { 6 | // instantiating the sidebar webview 7 | const sidebarProvider = new SidebarProvider(context); 8 | 9 | // Create Build Tree Status Bar Button 10 | const item = vscode.window.createStatusBarItem( 11 | vscode.StatusBarAlignment.Right 12 | ); 13 | item.tooltip = 'Generate hierarchy tree from current file'; 14 | item.text = '$(list-tree) Build Tree'; 15 | item.command = 'sapling.generateTree'; 16 | item.show(); 17 | 18 | // Register Sapling Sidebar Webview View 19 | context.subscriptions.push( 20 | vscode.window.registerWebviewViewProvider( 21 | "sapling-sidebar", 22 | sidebarProvider 23 | ) 24 | ); 25 | 26 | // Register command to generate tree from current file on status button click or from explorer context 27 | context.subscriptions.push( 28 | vscode.commands.registerCommand("sapling.generateTree", async (uri: vscode.Uri | undefined) => { 29 | await vscode.commands.executeCommand('workbench.view.extension.sapling-sidebar-view'); 30 | sidebarProvider.statusButtonClicked(uri); 31 | }) 32 | ); 33 | 34 | // setting up a hotkey to refresh the extension without manual refresh -- for developer use 35 | // context.subscriptions.push( 36 | // vscode.commands.registerCommand('sapling.refresh', async () => { 37 | // // async call to close the sidebar 38 | // await vscode.commands.executeCommand('workbench.action.closeSidebar'); 39 | // // async call to open the extension 40 | // await vscode.commands.executeCommand('workbench.view.extension.sapling-sidebar-view'); 41 | // // open the webdev tools on create (ID for Open Webdev Tools) 42 | // setTimeout(() => { 43 | // vscode.commands.executeCommand('workbench.action.webview.openDeveloperTools'); 44 | // }, 500); 45 | // }) 46 | // ); 47 | } 48 | 49 | // this method is called when your extension is deactivated 50 | export function deactivate() {} 51 | -------------------------------------------------------------------------------- /sapling/src/getNonce.ts: -------------------------------------------------------------------------------- 1 | export function getNonce() : string { 2 | let text : string = ""; 3 | const possible : string = 4 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 5 | for (let i = 0; i < 32; i++) { 6 | text += possible.charAt(Math.floor(Math.random() * possible.length)); 7 | } 8 | return text; 9 | } 10 | -------------------------------------------------------------------------------- /sapling/src/test/runTest.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | import { runTests } from 'vscode-test'; 4 | 5 | async function main() { 6 | try { 7 | // The folder containing the Extension Manifest package.json 8 | // Passed to `--extensionDevelopmentPath` 9 | const extensionDevelopmentPath = path.resolve(__dirname, '../../'); 10 | 11 | // The path to test runner 12 | // Passed to --extensionTestsPath 13 | const extensionTestsPath = path.resolve(__dirname, './suite/index'); 14 | 15 | // Download VS Code, unzip it and run the integration test 16 | await runTests({ extensionDevelopmentPath, extensionTestsPath }); 17 | } catch (err) { 18 | console.error('Failed to run tests'); 19 | process.exit(1); 20 | } 21 | } 22 | 23 | main(); 24 | -------------------------------------------------------------------------------- /sapling/src/test/suite/extension.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, suite , test, before} from 'mocha'; 2 | import { expect } from 'chai'; 3 | 4 | // You can import and use all API from the 'vscode' module 5 | // as well as import your extension to test it 6 | import * as vscode from 'vscode'; 7 | // import * as myExtension from '../../extension'; 8 | 9 | suite('Extension Test Suite', () => { 10 | vscode.window.showInformationMessage('Start all tests.'); 11 | 12 | describe('Sapling loads correctly', () => { 13 | let saplingExtension; 14 | before (() => { 15 | saplingExtension = vscode.extensions.getExtension('team-sapling.sapling'); 16 | }); 17 | 18 | test('Sapling is registered as an extension', () => { 19 | expect(saplingExtension).to.not.be.undefined; 20 | }); 21 | 22 | test('Sapling extension is activated after VSCode startup', () => { 23 | expect(saplingExtension.isActive).to.be.true; 24 | }); 25 | 26 | test('Sapling extension package.json exists', () => { 27 | expect(saplingExtension.packageJSON).to.not.be.undefined; 28 | }); 29 | }); 30 | 31 | // describe('It registers saplings commands successfully', () => { 32 | // let commandList; 33 | // before( (done) => { 34 | // vscode.commands.getCommands().then(commands => { 35 | // commandList = commands; 36 | // done(); 37 | // }); 38 | // }); 39 | 40 | // test('It registers the sapling.generateTree command', () => { 41 | // expect(commandList).to.be.an('array').that.does.include('sapling.generateTree'); 42 | // }); 43 | // }); 44 | }); 45 | -------------------------------------------------------------------------------- /sapling/src/test/suite/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as Mocha from 'mocha'; 3 | import * as glob from 'glob'; 4 | 5 | export function run(): Promise { 6 | // Create the mocha test 7 | const mocha = new Mocha({ 8 | ui: 'tdd', 9 | color: true 10 | }); 11 | 12 | const testsRoot = path.resolve(__dirname, '..'); 13 | 14 | return new Promise((c, e) => { 15 | glob('**/**.test.js', { cwd: testsRoot }, (err, files) => { 16 | if (err) { 17 | return e(err); 18 | } 19 | 20 | // Add files to the test suite 21 | files.forEach(f => mocha.addFile(path.resolve(testsRoot, f))); 22 | 23 | try { 24 | // Run the mocha test 25 | mocha.run(failures => { 26 | if (failures > 0) { 27 | e(new Error(`${failures} tests failed.`)); 28 | } else { 29 | c(); 30 | } 31 | }); 32 | } catch (err) { 33 | console.error(err); 34 | e(err); 35 | } 36 | }); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /sapling/src/test/test_apps/test_0/components/App.jsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'react'; 2 | 3 | class App extends Component { 4 | render () { 5 | return ( 6 |
I am App.
7 | ) 8 | } 9 | } 10 | 11 | export default App; 12 | -------------------------------------------------------------------------------- /sapling/src/test/test_apps/test_0/index.js: -------------------------------------------------------------------------------- 1 | import { prependOnceListener } from 'process'; 2 | import React from 'react'; 3 | import { render } from 'react-dom'; 4 | 5 | // Import React Components 6 | import App from './components/App.jsx'; 7 | 8 | // TEST 0 - Simple React App with one App Component 9 | 10 | render( 11 |
12 | 13 |
, document.getElementById('root') 14 | ); -------------------------------------------------------------------------------- /sapling/src/test/test_apps/test_1/components/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import Main from './Main.jsx' 4 | 5 | class App extends Component { 6 | render () { 7 | return ( 8 |
9 |
I am App.
10 |
11 |
12 | ) 13 | } 14 | } 15 | 16 | export default App; 17 | -------------------------------------------------------------------------------- /sapling/src/test/test_apps/test_1/components/Main.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | class Main extends Component { 4 | render () { 5 | return ( 6 |
I am App.
7 | ) 8 | } 9 | } 10 | 11 | export default Main; 12 | -------------------------------------------------------------------------------- /sapling/src/test/test_apps/test_1/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | 4 | // Import React Components 5 | import App from './components/App.jsx'; 6 | 7 | // TEST 1 - Simple App with two components, App and Main, App renders Main 8 | 9 | render( 10 |
11 | 12 |
, document.getElementById('root') 13 | ); 14 | -------------------------------------------------------------------------------- /sapling/src/test/test_apps/test_10/components/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Switch, Route, Redirect } from 'react-router-dom'; 3 | 4 | // Import React Components 5 | import Nav from './Nav.jsx'; 6 | import ExercisesDisplay from './ExercisesDisplay.jsx'; 7 | import ExerciseCreator from './ExerciseCreator.jsx'; 8 | import DrillCreator from './DrillCreator.jsx'; 9 | import HistoryDisplay from './HistoryDisplay.jsx'; 10 | import Signup from './Signup.jsx'; 11 | import Login from './Login.jsx'; 12 | import Logout from './Logout.jsx'; 13 | 14 | // App Component 15 | const App = () => { 16 | const [userInfo, setUserInfo] = useState({ name: '', email: '' }); 17 | 18 | return ( 19 |
20 |
37 | ); 38 | }; 39 | 40 | export default App; -------------------------------------------------------------------------------- /sapling/src/test/test_apps/test_10/components/DrillCreator.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { useParams, Link, Redirect } from 'react-router-dom'; 3 | 4 | const DrillCreator = () => { 5 | const { id } = useParams(); 6 | const [drillData, setDrillData] = useState({}); 7 | const [redirect, setRedirect] = useState(false); 8 | const [formVals, setFormVals] = useState({ 9 | exercise_id: id, 10 | weight: '', 11 | sets: '', 12 | reps: '', 13 | rest_interval: '', 14 | }); 15 | 16 | // Helper function to update state formVals on form change 17 | const updateFormVal = (key, val) => { 18 | setFormVals({ ...formVals, [key]: val }); 19 | }; 20 | 21 | // TODO MAKE REAL API CALL OR LIFT STATE TO APP 22 | // Is there a route for creating a drill? I only see createExercise 23 | const getExercise = () => { 24 | fetch(`/api/exercise/${id}`) 25 | .then((response) => { 26 | if (response.status === 200) { 27 | return response.json(); 28 | } 29 | throw new Error('Error when trying to get exercise details'); 30 | }) 31 | .then((data) => { 32 | console.log('exercise drill data is', data); 33 | setDrillData(data); 34 | }) 35 | .catch((error) => console.error(error)); 36 | }; 37 | 38 | // Get exercise data for drill info (CURRENTLY FAKE DATA) 39 | useEffect(() => { 40 | console.log('Getting data from server for drill'); 41 | getExercise(); 42 | }, []); 43 | 44 | // Function to submit drill form data to server, create new drill 45 | const createDrill = () => { 46 | console.log('trying to create new drill', formVals); 47 | 48 | fetch('/api/drill', { 49 | method: 'POST', 50 | headers: { 51 | 'Content-Type': 'application/json', 52 | }, 53 | body: JSON.stringify(formVals), 54 | }) 55 | .then((response) => { 56 | console.log('drill create response', response.status); 57 | if (response.status === 201) { 58 | return response.json(); 59 | } 60 | throw new Error('error when trying to create a drill'); 61 | }) 62 | .then((data) => { 63 | console.log('response is 201, data is', data); 64 | setRedirect(true); 65 | }) 66 | .catch((error) => console.error(error)); 67 | }; 68 | 69 | const { weight, sets, reps, rest_interval } = formVals; 70 | 71 | // Redirect to home page if drill created successfully 72 | if (redirect === true) { 73 | return ; 74 | } 75 | 76 | return ( 77 |
78 |

Create a new drill:

79 |

80 | Exercise Name: {drillData.name}

81 |

82 | Exercise Description: {drillData.description}

83 |

84 | Exercise Type: {drillData.type}

85 |

86 | Last Weight (LBs): {drillData.last_weight}

87 |

88 | Last Reps: {drillData.last_reps}

89 |

90 | Last Sets: {drillData.last_sets}

91 |

92 | Last Rest (Mins): {drillData.last_rest} 93 |

94 | 95 | {/* DRILL INPUT FORM */} 96 |
{ 98 | e.preventDefault(); 99 | createDrill(); 100 | }} 101 | > 102 | 103 | {/* DRILL WEIGHT INPUT */} 104 | 121 |
122 | 123 | {/* DRILL SETS INPUT */} 124 | 141 |
142 | 143 | {/* DRILL REPS INPUT */} 144 | 161 |
162 | 163 | {/*DRILL REST INPUT */} 164 | 181 |
182 | 183 | {/* FORM SUBMIT BUTTON */} 184 | 189 | 190 | {/* FORM CANCEL BUTTON */} 191 | 192 | 197 | 198 | 199 |
200 |
201 | ); 202 | }; 203 | 204 | export default DrillCreator; 205 | -------------------------------------------------------------------------------- /sapling/src/test/test_apps/test_10/components/ExerciseCreator.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Link, Redirect } from 'react-router-dom'; 3 | 4 | // React element allowing users to create a new exercise via form 5 | const ExerciseCreator = () => { 6 | const [redirect, setRedirect] = useState(false); 7 | const [formVals, setFormVals] = useState({ 8 | name: '', 9 | description: '', 10 | type_id: '1', 11 | init_weight: '', 12 | init_reps: '', 13 | init_sets: '', 14 | init_rest: '', 15 | }); 16 | 17 | // Helper function to update state formVals on form change 18 | const updateFormVal = (key, val) => { 19 | setFormVals({ ...formVals, [key]: val }); 20 | }; 21 | 22 | // Function to submit new exercise form data to server for processing 23 | const createExercise = () => { 24 | console.log('Trying to create exercise: ', formVals); 25 | fetch('/api/exercise', { 26 | method: 'POST', 27 | headers: { 28 | 'Content-Type': 'application/json', 29 | }, 30 | body: JSON.stringify(formVals), 31 | }) 32 | .then((response) => { 33 | // If creation successful, redirect to exercises 34 | console.log('CREATE RESPONSE: ', response.status); 35 | if (response.status === 200) { 36 | return response.json(); 37 | } 38 | throw new Error('Error when trying to login a user!'); 39 | }).then((data) => { 40 | console.log('Added new exercise: ', data); 41 | setRedirect(true); 42 | }) 43 | .catch((err) => console.error(err)); 44 | }; 45 | 46 | const { 47 | name, description, type, init_weight, init_reps, init_sets, init_rest, 48 | } = formVals; 49 | 50 | // If successfully created new exercise, redirect to '/' route: 51 | if (redirect) { 52 | return ; 53 | } 54 | 55 | return ( 56 |
57 |

Create a new Exercise:

58 | 59 | {/* NEW EXERCISE FORM */} 60 |
{ 62 | e.preventDefault(); 63 | createExercise(); 64 | }} 65 | > 66 | 67 | {/* EXERCISE NAME INPUT */} 68 | 69 | { 74 | console.log('Updated createEx formVals: ', e.target.value); 75 | updateFormVal('name', e.target.value); 76 | }} 77 | value={name} 78 | name="name" 79 | required 80 | /> 81 |
82 | 83 | {/* EXERCISE TYPE INPUT */} 84 | 85 | 110 |
111 | 112 | {/* EXERCISE DESCRIPTION INPUT */} 113 | 114 |