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 |
137 |
138 | available props (hover)
139 |
140 |
141 | open file (click)
142 |
143 |
144 | Redux store connection
145 |
146 |
147 | Navbar error in file (matches the error color of your theme)
148 |
149 |
150 | Navbar: currently open file
151 |
152 |
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 |
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
42 |
--------------------------------------------------------------------------------
/sapling/media/webpack.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/sapling/509652cb7aef69be901f898282e362afb80fedd9/sapling/media/webpack.png
--------------------------------------------------------------------------------
/sapling/media/webpack.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
103 | );
104 | };
105 |
106 | export default Sidebar;
107 |
--------------------------------------------------------------------------------
/sapling/src/webviews/components/Tree.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { useEffect, useState } from 'react';
3 |
4 | // import for the nodes
5 | import TreeNode from './TreeNode';
6 |
7 | const Tree = ({ data, first }: any) => {
8 | // Render section
9 | return (
10 | <>
11 | {/* Checks if the current iteration is the first time being run (adding in ul if not, and without ul if it is the first time) */}
12 | {first ? data.map((tree: any) => {
13 | return ;
14 | }):
15 |
20 | }
21 | >
22 | );
23 | };
24 |
25 | export default Tree;
26 |
--------------------------------------------------------------------------------
/sapling/src/webviews/components/TreeNode.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { useState, useEffect, Fragment } from 'react';
3 | import * as ReactDOM from 'react-dom';
4 |
5 | // import tree for recursive calls
6 | import Tree from './Tree';
7 |
8 | // imports for the icons
9 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
10 | import { faInfoCircle, faArrowCircleRight, faStore } from '@fortawesome/free-solid-svg-icons';
11 |
12 | // imports for the tooltip
13 | import Tippy from '@tippy.js/react';
14 | import 'tippy.js/dist/tippy.css';
15 |
16 | const TreeNode = ({ node }: any) => {
17 | // state variables for the users current active file and the expanded value (boolean) of the node
18 | const [currFile, setCurrFile] = useState(false);
19 | const [expanded, setExpanded] = useState(node.expanded);
20 |
21 | // useEffect that will add an event listener for 'message' to each node, in order to show which file the user is currently working in
22 | useEffect(() => {
23 | window.addEventListener('message', (event) => {
24 | const message = event.data;
25 | switch (message.type) {
26 | case("current-tab"): {
27 | // If the current node's filePath is the same as the user's current actie window, change state to true, else, change state to false
28 | if (message.value === node.filePath) {
29 | setCurrFile(true);
30 | } else {
31 | setCurrFile(false);
32 | }
33 | }
34 | }
35 | });
36 | // Send message to the extension for the bolding
37 | tsvscode.postMessage({
38 | type: "onBoldCheck",
39 | value: null
40 | });
41 | }, []);
42 |
43 | // Function that will capture the file path of the current node clicked on + send a message to the extension
44 | const viewFile = () => {
45 | // Edge case to verify that there is in fact a file path for the current node
46 | if (node.filePath) {
47 | tsvscode.postMessage({
48 | type: "onViewFile",
49 | value: node.filePath
50 | });
51 | }
52 | };
53 |
54 | // Function that generates the props for each node
55 | const propsGenerator = () => {
56 | // Case when there are no props present on the node
57 | if (Object.keys(node.props).length === 0) {
58 | return
None
;
59 | }
60 | // Case when there are props to loop through on the node
61 | return Object.keys(node.props).map(prop => {
62 | return
{prop}
;
63 | });
64 | };
65 |
66 | // Variable that holds the props that will be fed into the tooltip (Tippy)
67 | const propsList = propsGenerator();
68 |
69 | // Variable that holds the logic of whether the current node has children or not
70 | const child = node.children.length > 0 ? true: false;
71 |
72 | // onClick method for each node that will change the expanded/collapsed structure + send a message to the extension
73 | const toggleNode = () => {
74 | // Set state with the opposite of what is currently saved in state (expanded)
75 | const newExpanded = !expanded;
76 | setExpanded(newExpanded);
77 | // Send a message to the extension on the changed checked value of the current node
78 | tsvscode.postMessage({
79 | type: "onNodeToggle",
80 | value: {id: node.id, expanded: newExpanded}
81 | });
82 | };
83 |
84 | const classString = "tree_label" + (node.error ? " node_error" : "");
85 |
86 | // Render section
87 | return (
88 | <>
89 | {/* Conditional to check whether there are children or not on the current node */}
90 | {child ? (
91 |
92 |
93 | {/* Checks for the user's current active file */}
94 | {currFile ?
95 |
96 | : }
97 | {/* Checks to make sure there are no thirdParty or reactRouter node_icons */}
98 | {!node.thirdParty && !node.reactRouter ? (
99 |
100 | {node.reduxConnect ?
101 | Connected to Redux Store}>
102 |
103 |
104 | : null}
105 | Props available:{propsList}}>
106 |
107 |
108 |
109 |
110 | ): null}
111 |
112 |
113 | ):
114 |
115 | {/* Checks for the user's current active file */}
116 | {currFile ?
117 | {node.name}
118 | : {node.name}
119 | }
120 | {/* Checks to make sure there are no thirdParty or reactRouter node_icons */}
121 | {!node.thirdParty && !node.reactRouter ? (
122 |
123 | {node.reduxConnect ?
124 | Connected to Redux Store}>
125 |
126 |
127 | : null}
128 | Props available:{propsList}}>
129 |
130 |
131 |
132 |
133 | ): null}
134 |
135 | }
136 | >
137 | );
138 | };
139 |
140 | export default TreeNode;
141 |
--------------------------------------------------------------------------------
/sapling/src/webviews/globals.d.ts:
--------------------------------------------------------------------------------
1 | import * as _vscode from "vscode";
2 |
3 | declare global {
4 | const tsvscode: {
5 | postMessage: ({ type: string, value: any }) => void;
6 | };
7 | }
8 |
9 | export default tsvscode;
--------------------------------------------------------------------------------
/sapling/src/webviews/pages/sidebar.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as ReactDOM from 'react-dom';
3 |
4 | // Component import
5 | import Sidebar from '../components/Sidebar';
6 |
7 | // CSS import
8 | import '../../../media/styles.css';
9 |
10 | ReactDOM.render(, document.getElementById('root'));
11 |
--------------------------------------------------------------------------------
/sapling/src/webviews/tsconfig.views.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "./dist/",
4 | "lib": [
5 | "dom"
6 | ],
7 | "noImplicitAny": true,
8 | "module": "es6",
9 | "target": "es5",
10 | "jsx": "react",
11 | "allowJs": true,
12 | "moduleResolution": "node",
13 | "sourceMap": true,
14 | }
15 | }
--------------------------------------------------------------------------------
/sapling/src/webviews/webpack.views.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | module.exports = {
4 | entry: path.resolve(__dirname, './pages/sidebar.tsx'),
5 | mode: 'production',
6 | module: {
7 | rules: [
8 | {
9 | test: /\.tsx?$/,
10 | use: {
11 | loader: 'ts-loader',
12 | options: {
13 | configFile: 'tsconfig.views.json'
14 | },
15 | },
16 | exclude: /node_modules/,
17 | },
18 | {
19 | test: /\.css$/,
20 | use: [
21 | {
22 | loader: "style-loader"
23 | },
24 | {
25 | loader: "css-loader"
26 | }
27 | ]
28 | }
29 | ],
30 | },
31 | resolve: {
32 | extensions: ['.tsx', '.ts', '.js'],
33 | },
34 | output: {
35 | filename: 'sidebar.js',
36 | path: path.resolve(__dirname, '../../dist'),
37 | },
38 | };
--------------------------------------------------------------------------------
/sapling/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "target": "es6",
5 | "outDir": "out",
6 | "lib": [
7 | "es6",
8 | "dom"
9 | ],
10 | "jsx": "react",
11 | "sourceMap": true,
12 | "rootDir": "src",
13 | "strict": false /* enable all strict type-checking options */,
14 | /* Additional Checks */
15 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
16 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
17 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
18 | },
19 | "exclude": [
20 | "node_modules",
21 | ".vscode-test",
22 | "src/webviews",
23 | "src/test/test_apps"
24 | ]
25 | }
26 |
27 |
--------------------------------------------------------------------------------
/sapling/webpack.config.js:
--------------------------------------------------------------------------------
1 | //@ts-check
2 |
3 | 'use strict';
4 |
5 | const path = require('path');
6 |
7 | /**@type {import('webpack').Configuration}*/
8 | const config = {
9 | target: 'node', // vscode extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/
10 | mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production')
11 |
12 | entry: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/
13 |
14 | output: {
15 | // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/
16 | path: path.resolve(__dirname, 'dist'),
17 | filename: 'extension.js',
18 | libraryTarget: 'commonjs2'
19 | },
20 | devtool: 'nosources-source-map',
21 | externals: {
22 | vscode: 'commonjs vscode' // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/
23 | // modules added here also need to be added in the .vsceignore file
24 | },
25 | resolve: {
26 | // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader
27 | extensions: ['.ts', '.js', '.tsx']
28 | },
29 | module: {
30 | rules: [
31 | {
32 | test: /\.(ts|tsx)$/,
33 | exclude: /node_modules/,
34 | use: [
35 | {
36 | loader: 'ts-loader'
37 | }
38 | ]
39 | },
40 | ]
41 | }
42 | };
43 | module.exports = config;
--------------------------------------------------------------------------------
/website/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/website/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env.local
29 | .env.development.local
30 | .env.test.local
31 | .env.production.local
32 |
33 | # vercel
34 | .vercel
35 |
--------------------------------------------------------------------------------
/website/README.md:
--------------------------------------------------------------------------------
1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
2 |
3 | ## Getting Started
4 |
5 | First, run the development server:
6 |
7 | ```bash
8 | npm run dev
9 | # or
10 | yarn dev
11 | ```
12 |
13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
14 |
15 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file.
16 |
17 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`.
18 |
19 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
20 |
21 | ## Learn More
22 |
23 | To learn more about Next.js, take a look at the following resources:
24 |
25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
27 |
28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
29 |
30 | ## Deploy on Vercel
31 |
32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
33 |
34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
35 |
--------------------------------------------------------------------------------
/website/components/Carousel.jsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image';
2 | import firstSlide from '../public/gen_tree_demo.gif';
3 | import secondSlide from '../public/icons_demo.gif';
4 | import thirdSlide from '../public/rebuild_on_save_demo.gif';
5 | import fourthSlide from '../public/build_tree_demo.gif';
6 | import fifthSlide from '../public/settings_theme_demo.gif';
7 | import blurData from '../public/blurData.js';
8 |
9 |
10 | const Carousel = () => {
11 | return (
12 |
13 |
14 |
Feature Demo
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
Open a root component to structure your app's files so they match its dependency relationships.
28 |
29 |
30 |
31 |
32 |
33 |
Use Sapling's intuitive icons to get a list of props available to each component, see which components are connected to your Redux store, and open the file you wish to edit.
34 |
35 |
36 |
37 |
38 |
39 |
Sapling is highly responsive, and notices whenever you edit and save a file.
40 |
41 |
42 |
43 |
44 |
45 |
Rebuild the tree with your currently open file as the root. Note that Sapling retains its expanded state between sessions.
46 |
47 |
48 |
49 |
50 |
51 |
Toggle the display of third-party and React Router components, and watch as Sapling's theme changes to match your preferences.
React is a powerful tool for building your frontend applications, but at scale navigating the hierarchy of your components can become frustrating. Sapling's intuitive interface reflects the hierarchical nature of your app, so you'll never have to think twice about navigation again.