Repository: martinascharrer/vuensight
Branch: main
Commit: e3f6681dacdd
Files: 96
Total size: 110.7 KB
Directory structure:
gitextract_z_w9sqhw/
├── .editorconfig
├── .eslintrc
├── .github/
│ └── workflows/
│ └── node.js.yml
├── .gitignore
├── .nvmrc
├── LICENSE.txt
├── README.md
├── package.json
├── packages/
│ ├── app/
│ │ ├── .browserslistrc
│ │ ├── .editorconfig
│ │ ├── .eslintrc.js
│ │ ├── .gitignore
│ │ ├── .npmignore
│ │ ├── LICENSE.txt
│ │ ├── README.md
│ │ ├── babel.config.js
│ │ ├── jest.config.js
│ │ ├── package.json
│ │ ├── public/
│ │ │ └── index.html
│ │ ├── server/
│ │ │ ├── index.ts
│ │ │ └── tsconfig.pkg.json
│ │ ├── src/
│ │ │ ├── App.vue
│ │ │ ├── assets/
│ │ │ │ └── css/
│ │ │ │ ├── border-radius.css
│ │ │ │ ├── box-shadow.css
│ │ │ │ ├── color.css
│ │ │ │ ├── font.css
│ │ │ │ ├── icon.css
│ │ │ │ └── spacing.css
│ │ │ ├── components/
│ │ │ │ ├── CardCommunicationChannel.test.ts
│ │ │ │ ├── CardCommunicationChannel.vue
│ │ │ │ ├── ForceGraph.vue
│ │ │ │ ├── MenuCommunication.vue
│ │ │ │ ├── SidebarCommunication.vue
│ │ │ │ ├── SidebarCommunicationEventsTab.vue
│ │ │ │ ├── SidebarCommunicationPropsTab.vue
│ │ │ │ ├── SidebarCommunicationSlotsTab.vue
│ │ │ │ ├── base/
│ │ │ │ │ ├── BaseArrowIcon.vue
│ │ │ │ │ ├── BaseBadge.vue
│ │ │ │ │ ├── BaseCard.vue
│ │ │ │ │ ├── BaseCheckIcon.vue
│ │ │ │ │ ├── BaseDelimiter.vue
│ │ │ │ │ ├── BaseDropdown.vue
│ │ │ │ │ ├── BaseIcon.vue
│ │ │ │ │ ├── BaseIconButton.vue
│ │ │ │ │ ├── BaseList.vue
│ │ │ │ │ ├── BaseLoadingSpinner.vue
│ │ │ │ │ ├── BaseRadioButtonGroup.vue
│ │ │ │ │ └── BaseSubNav.vue
│ │ │ │ ├── icons/
│ │ │ │ │ ├── IconCross.vue
│ │ │ │ │ ├── IconFilter.vue
│ │ │ │ │ ├── IconSearch.vue
│ │ │ │ │ └── IconSelect.vue
│ │ │ │ └── layout/
│ │ │ │ └── LayoutSplitView.vue
│ │ │ ├── composables/
│ │ │ │ └── fetch.ts
│ │ │ ├── main.ts
│ │ │ ├── router/
│ │ │ │ └── index.ts
│ │ │ ├── services/
│ │ │ │ └── parser.ts
│ │ │ ├── shims-vue.d.ts
│ │ │ ├── types/
│ │ │ │ ├── color.ts
│ │ │ │ ├── force.ts
│ │ │ │ └── nodeSizeAttributeType.ts
│ │ │ └── views/
│ │ │ └── PageCommunication.vue
│ │ └── tsconfig.json
│ ├── cli/
│ │ ├── .eslintrc
│ │ ├── .gitignore
│ │ ├── .npmignore
│ │ ├── LICENSE.txt
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ └── index.ts
│ │ └── tsconfig.pkg.json
│ ├── parser/
│ │ ├── .eslintrc
│ │ ├── .gitignore
│ │ ├── .npmignore
│ │ ├── LICENSE.txt
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── index.ts
│ │ │ ├── utils/
│ │ │ │ ├── files.test.ts
│ │ │ │ ├── files.ts
│ │ │ │ ├── kababize.ts
│ │ │ │ ├── kebabize.test.ts
│ │ │ │ ├── vue.test.ts
│ │ │ │ └── vue.ts
│ │ │ └── vue/
│ │ │ ├── analyzer.ts
│ │ │ ├── communication-channels.test.ts
│ │ │ ├── communication-channels.ts
│ │ │ └── dependencies.ts
│ │ ├── test/
│ │ │ ├── parsing-process.test.ts
│ │ │ └── project/
│ │ │ ├── Child.vue
│ │ │ └── Parent.vue
│ │ └── tsconfig.pkg.json
│ └── types/
│ ├── index.d.ts
│ └── package.json
├── tsconfig.build.json
└── tsconfig.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .editorconfig
================================================
[*.{js,jsx,ts,tsx,vue}]
indent_style = space
indent_size = 4
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true
max_line_length = 100
================================================
FILE: .eslintrc
================================================
{
"root": true,
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint"
],
"ignorePatterns": ["dist/**/*.js", "bin/**/*.js"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
],
"rules": {
"semi": ["error", "always"],
"max-len": ["error", 120]
}
}
================================================
FILE: .github/workflows/node.js.yml
================================================
name: Push (lint + unit tests)
on: [push]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.x]
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- name: npm install, build, and test
run: |
npm install
npm run build
npm run lint
npm run test
env:
CI: true
================================================
FILE: .gitignore
================================================
node_modules
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
================================================
FILE: .nvmrc
================================================
18.6
================================================
FILE: LICENSE.txt
================================================
MIT License
Copyright (c) 2021-2022 Martina Scharrer
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# vuensight 👀
Visualize Vue.js **component relationships** and **communication channels**, i.e. props, events and slots. This tool operates on the
command line and is made for developers. The aim of vuensight is to provide visual insight into the components of a
Vue.js project and to support developers before and during refactoring, e.g. by visually analyzing which prop is used
in which parent component or by highlighting unused components or channels.
An example visualization of vuensight itself:

This tool is built on top of the two awesome packages:
- [dependency-cruiser](https://github.com/sverweij/dependency-cruiser) for building the dependency tree
- [vue-docgen-api](https://github.com/vue-styleguidist/vue-styleguidist/tree/dev/packages/vue-docgen-api) for parsing the Vue files
## Getting started 🚀
### Install
First, install the cli package either locally in the project you want to visualize:
```
npm i -D @vuensight/cli
```
Or globally on your machine if you intend to visualize multiple projects:
```
npm i -g @vuensight/cli
```
### Run
Then run the tool in your project folder:
```
vuensight
```
#### Options
- `--dir` or `-d` (optional): Specify the directory that should be parsed relative from your current working directory, default is `src`
- `--port` or `-p` (optional): Start the application in a different port, default is 4444
- `--webpack-config` or `-wpc` (optional): Specify the path to your webpack-config (from your current working directory). This is particularly important if you use aliases.
- `--ts-config` or `-tsc` (optional): Specify the path to your TypeScript config file (from your current working directory).
An example usage:
```
vuensight --dir resources/js --port 9999 --webpack-config ./webpack-config.json --ts-config ./tsconfig.json
```
## Licencse
[MIT](LICENSE.txt)
## Development
### Requirements
- `npm version >= 7` (the project is a monorepo and uses npm workspaces which require at least npm version 7)
### Installing dependencies
- `npm i` (in root directory) to install all dependencies of all packages
- `npm i <package-name>` to add a global dependency for all packages
- `npm i <package-name> --workspace @vuensight/<vuensight-package-name>` to add a new dependency to a specific package
### Build packages
- `npm run build` in root folder (to build all packages at the same time)
- `npm run build` in each package
#### or use a watcher
- `npm run build:watch` in every package separately
### Link locally
For testing vuensight locally in a Vue project run `npm link` in the cli package and `npm link @vuensight/cli` in the
project you want to test it on. Make sure you use a correct node version and ran `npm i` in the root directory
beforehand as this links the packages together internally.
### Unit tests
- `npm run test` in root (to run tests for all packages)
- `npm run test` in each package
### Publish
- `npm publish` in each package
================================================
FILE: package.json
================================================
{
"name": "vuensight",
"description": "`vuensight` is a cli tool for parsing and visualizing Vue.js projects. The ultimate goal is to visualize the components communication e.g. their props, events and slots in an interactive web app. **This project is currently a work in progress!**",
"scripts": {
"build": "npm run build --workspaces",
"lint": "npm run lint --workspaces",
"test": "npm run test --workspaces"
},
"repository": {
"type": "git",
"url": "git+https://github.com/martinascharrer/vuensight.git"
},
"author": "Martina Scharrer",
"license": "(MIT)",
"bugs": {
"url": "https://github.com/martinascharrer/vuensight/issues"
},
"homepage": "https://github.com/martinascharrer/vuensight#readme",
"workspaces": [
"./packages/types",
"./packages/parser",
"./packages/app",
"./packages/cli"
],
"devDependencies": {
"@types/node": "^17.0.22",
"@typescript-eslint/eslint-plugin": "^5.16.0",
"@typescript-eslint/parser": "^5.16.0",
"eslint": "^8.11.0",
"jest": "^27.5.1",
"jest-cli": "^27.5.1",
"ts-jest": "^27.1.3",
"typescript": "^4.6.2"
}
}
================================================
FILE: packages/app/.browserslistrc
================================================
> 1%
last 2 versions
not dead
================================================
FILE: packages/app/.editorconfig
================================================
[*.{js,jsx,ts,tsx,vue}]
indent_style = space
indent_size = 2
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true
max_line_length = 120
================================================
FILE: packages/app/.eslintrc.js
================================================
module.exports = {
root: true,
env: {
node: true,
},
extends: [
'plugin:vue/vue3-essential',
'@vue/airbnb',
'@vue/typescript/recommended',
],
parserOptions: {
ecmaVersion: 2020,
},
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'max-len': ['error', 120],
'import/no-unresolved': 'warn',
'import/extensions': 'warn',
},
overrides: [
{
files: [
'**/__tests__/*.{j,t}s?(x)',
'**/tests/unit/**/*.spec.{j,t}s?(x)',
],
env: {
jest: true,
},
},
],
};
================================================
FILE: packages/app/.gitignore
================================================
.DS_Store
node_modules
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
/dist
tsconfig.tsbuildinfo
================================================
FILE: packages/app/.npmignore
================================================
*
!dist/app/**/*
dist/app/**/*.js.map
!dist/server/*.js
!dist/server/*.d.ts
!package.json
!readme.md
================================================
FILE: packages/app/LICENSE.txt
================================================
MIT License
Copyright (c) 2021-2022 Martina Scharrer
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: packages/app/README.md
================================================
# @vuensight/app
> ⚠️ **General information about usage and setup of `vuensight` can be found [here](https://github.com/martinascharrer/vuensight)**
The `app` package visualizes the parsed Vue project as force-directed graph.
The visualization is built with [d3.js](https://d3js.org/) and [webCola](https://github.com/tgdwyer/WebCola).
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Compiles and minifies for production
```
npm run build
```
### Run your unit tests
```
npm run test:unit
```
### Lints and fixes files
```
npm run lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).
## Licencse
[MIT](LICENSE.txt)
================================================
FILE: packages/app/babel.config.js
================================================
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset',
],
};
================================================
FILE: packages/app/jest.config.js
================================================
module.exports = {
preset: '@vue/cli-plugin-unit-jest/presets/typescript-and-babel',
transform: {
'^.+\\.vue$': '@vue/vue3-jest',
},
testMatch: ['**/__tests__/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[jt]s?(x)'],
moduleFileExtensions: [
'js',
'ts',
],
};
================================================
FILE: packages/app/package.json
================================================
{
"name": "@vuensight/app",
"version": "0.3.3",
"main": "dist/server/index.js",
"description": "The front-end of @vuensight that visualizes Vue.js projects.",
"repository": {
"type": "git",
"url": "git+https://github.com/martinascharrer/vuensight.git"
},
"author": "Martina Scharrer",
"license": "(MIT)",
"bugs": {
"url": "https://github.com/martinascharrer/vuensight/issues"
},
"homepage": "https://github.com/martinascharrer/vuensight#readme",
"scripts": {
"serve": "vue-cli-service serve",
"build": "rm -rf dist/ && echo 'Building app...' && vue-cli-service build --dest dist/app && echo 'Building server...' && cd server && tsc -p tsconfig.pkg.json",
"build:watch": "vue-cli-service build --watch",
"test": "vue-cli-service test:unit",
"lint": "vue-cli-service lint",
"prepublish": "npm run build"
},
"dependencies": {
"@vuensight/parser": "^0.1.4",
"@vuensight/types": "^0.1.0",
"@vueuse/core": "^8.3.1",
"connect-history-api-fallback": "^1.6.0",
"core-js": "^3.21.1",
"d3": "^5.16.0",
"express": "^4.17.3",
"vue": "^3.2.31",
"vue-router": "^4.0.14",
"webcola": "^3.4.0"
},
"devDependencies": {
"@types/connect-history-api-fallback": "^1.3.5",
"@types/d3": "^7.1.0",
"@types/express": "^4.17.13",
"@vue/cli-plugin-babel": "^5.0.4",
"@vue/cli-plugin-eslint": "^5.0.4",
"@vue/cli-plugin-router": "^5.0.4",
"@vue/cli-plugin-typescript": "^5.0.4",
"@vue/cli-plugin-unit-jest": "^5.0.4",
"@vue/cli-service": "^5.0.4",
"@vue/compiler-sfc": "^3.2.31",
"@vue/eslint-config-airbnb": "^6.0.0",
"@vue/eslint-config-typescript": "^10.0.0",
"@vue/test-utils": "^2.0.0-rc.17",
"@vue/vue3-jest": "^27.0.0",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-vue": "^8.5.0",
"sass": "^1.49.9",
"sass-loader": "^12.6.0"
}
}
================================================
FILE: packages/app/public/index.html
================================================
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
================================================
FILE: packages/app/server/index.ts
================================================
import history from 'connect-history-api-fallback';
import express from 'express';
import { join } from 'path';
import { parse } from '@vuensight/parser';
export default async (directory: string, port?: number, webpackConfigPath?: string, tsConfigPath?: string) => {
const app = express();
const localPort = port || 4444;
app.get('/parse-result', async (request, response) => {
const parseResult = await parse(directory, 'vue', webpackConfigPath, tsConfigPath);
response.json(parseResult);
});
app.use(history());
app.use(express.static(join(__dirname, '../app')));
app.listen((+localPort), () => {
console.log(`👀 vuensight: http://localhost:${localPort}`);
});
};
================================================
FILE: packages/app/server/tsconfig.pkg.json
================================================
{
"extends": "../../../tsconfig.build.json",
"compilerOptions": {
"outDir": "../dist/server"
},
"include": [
"./**/*.ts"
]
}
================================================
FILE: packages/app/src/App.vue
================================================
<template>
<router-view/>
</template>
<style lang="scss">
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,400;0,700;1,400&display=swap');
@import 'assets/css/border-radius.css';
@import 'assets/css/box-shadow.css';
@import 'assets/css/color.css';
@import 'assets/css/font.css';
@import 'assets/css/icon.css';
@import 'assets/css/spacing.css';
*, *::before, *::after {
box-sizing: border-box;
}
* {
margin: 0;
}
html, body {
height: 100%;
}
input, button, textarea, select {
font: inherit;
border: none;
background: transparent;
text-align: inherit;
padding: 0;
&:hover {
border: none;
outline: none;
}
}
button {
cursor: pointer;
}
.input {
display: flex;
align-items: center;
gap: var(--spacing--s);
background: white;
border: none;
border-radius: var(--border-radius--s);
box-shadow: var(--box-shadow--s);
padding: var(--spacing--xs) var(--spacing--m);
min-width: var(--spacing--2xl);
height: var(--spacing--3xl);
&:hover {
outline: 2px solid var(--yellow-30);
}
}
p, h1, h2, h3, h4, h5, h6 {
overflow-wrap: break-word;
}
#root, #__next {
isolation: isolate;
}
body {
font-size: var(--font-size--m);
font-family: NotoSans, Arial, Helvetica, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: var(--navy-90);
line-height: 1.5;
}
</style>
================================================
FILE: packages/app/src/assets/css/border-radius.css
================================================
:root {
--border-radius--xs: 2px;
--border-radius--s: 5px;
--border-radius--m: 10px;
--border-radius--l: 15px;
--border-radius--xl: 20px;
--border-radius--2xl: 25px;
}
================================================
FILE: packages/app/src/assets/css/box-shadow.css
================================================
:root {
--box-shadow--xs: 0px 0px 2px rgba(0,0,0,0.2);
--box-shadow--s: 0px 0px 4px rgba(0,0,0,0.23);
--box-shadow--m: 0px 0px 8px rgba(0,0,0,0.3);
}
================================================
FILE: packages/app/src/assets/css/color.css
================================================
:root {
--mint-grey: #CBE5DE;
--mint-10: #CDF6EB;
--mint-30: #A0E2CF;
--mint-50: #61c799;
--mint-70: #369D6F;
--red-grey: #E9D0C9;
--red-10: #F6DCD4;
--red-30: #FFB7A1;
--red-50: #F57A5F;
--red-70: #C54C31;
--purple-grey: #c7b1c9;
--purple-30: #E9BCEF;
--purple-50: #C37ECD;
--purple-70: #854D8D;
--yellow-30: #F6D697;
--yellow-50: #EDAB2C;
--grey-5: #FaFaFa;
--grey-10: #F2F2F2;
--grey-15: #d7d7d7;
--grey-20: #CECECE;
--grey-30: #9a9a9a;
--grey-50: #7c7c7c;
--navy-50: #7A8497;
--navy-90: #212c41;
}
================================================
FILE: packages/app/src/assets/css/font.css
================================================
:root {
font-size: 16px;
--font-size--3xs: 0.35em;
--font-size--2xs: 0.5em;
--font-size--xs: 0.75em;
--font-size--s: 0.875em;
--font-size--m: 1em;
--font-size--l: 1.125em;
--font-size--xl: 1.25em;
--font-size--2xl: 1.5em;
--font-size--3xl: 2em;
--font-size--4xl: 2.75em;
}
================================================
FILE: packages/app/src/assets/css/icon.css
================================================
:root {
--icon--xs: 0.75rem;
--icon--s: 1rem;
--icon--m: 1.25rem;
--icon--l: 1.5rem;
--icon--xl: 1.75rem;
--icon--2xl: 2rem;
}
================================================
FILE: packages/app/src/assets/css/spacing.css
================================================
:root {
--spacing--2xs: 0.125rem;
--spacing--xs: 0.25rem;
--spacing--s: 0.5rem;
--spacing--m: 0.75rem;
--spacing--l: 1rem;
--spacing--xl: 1.5rem;
--spacing--2xl: 2rem;
--spacing--3xl: 2.75rem;
--spacing--4xl: 4rem;
--spacing--5xl: 6rem;
}
================================================
FILE: packages/app/src/components/CardCommunicationChannel.test.ts
================================================
// eslint-disable-next-line import/no-extraneous-dependencies
import { mount, MountingOptions } from '@vue/test-utils';
import CardCommunicationChannel from '@/components/CardCommunicationChannel.vue';
describe('CardCommunicationChannel.vue', () => {
it('renders the card with correct name and counter', () => {
const wrapper = mount(CardCommunicationChannel, {
props: {
channel: {
name: 'foo prop',
},
dependents: [
{
name: 'Bar',
fullPath: 'foo/bar.vue',
},
{
name: 'Test',
fullPath: 'foo/test.vue',
},
],
},
} as MountingOptions<any>); // eslint-disable-line @typescript-eslint/no-explicit-any
expect(wrapper.find('[data-qa="name"]').text()).toBe('foo prop');
expect(wrapper.find('[data-qa="counter"]').text()).toBe('2');
});
});
================================================
FILE: packages/app/src/components/CardCommunicationChannel.vue
================================================
<template>
<base-card
class="cardCommunicationChannel"
:class="{
[`cardCommunicationChannel--${color}`]: color,
'cardCommunicationChannel--selected': isSelected,
'cardCommunicationChannel--disabled': dependents.length === 0
}"
:disabled="dependents.length === 0"
>
<template #header>
<div class="cardCommunicationChannel__header">
<base-check-icon
:color="color"
:is-checked="isSelected"
:is-disabled="dependents.length === 0"
/>
<p data-qa="name">{{ channel.name }}</p>
<base-badge data-qa="counter">{{ dependents.length }}</base-badge>
<base-badge :color="`light-${color}`" v-if="channel.mixin">mixin</base-badge>
</div>
</template>
<template
v-if="channel.type || channel.default || channel.required || channel.mixin"
#body
>
<template v-if="channel.type">
type: {{ channel.type.name }}
</template>
<template v-if="channel.default">
<base-delimiter :color="color" /> default: {{ channel.default }}
</template>
<template v-if="channel.required">
<base-delimiter :color="color" /> required: {{ channel.required }}
</template>
<template v-if="channel.mixin">
<base-delimiter :color="color" /> mixin: {{ channel.mixin.name }}
</template>
</template>
<template v-if="dependents.length > 0" #footer>
used in:
<base-list
v-if="dependents.length > 0" :color="color"
:items="dependents.map(dep => dep.name)"
/>
</template>
</base-card>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
import BaseBadge from '@/components/base/BaseBadge.vue';
import BaseCard from '@/components/base/BaseCard.vue';
import BaseCheckIcon from '@/components/base/BaseCheckIcon.vue';
import BaseDelimiter from '@/components/base/BaseDelimiter.vue';
import BaseList from '@/components/base/BaseList.vue';
import { Prop, Dependent } from '@vuensight/types';
import { Color } from '@/types/color';
export default defineComponent({
name: 'CardCommunicationChannel',
props: {
channel: {
type: Object as PropType<Prop>,
required: true,
},
dependents: {
type: Array as PropType<Array<Dependent>>,
required: true,
},
color: {
type: String as PropType<Color>,
default: 'mint',
},
isSelected: {
type: Boolean,
default: false,
},
},
components: {
BaseBadge,
BaseCard,
BaseCheckIcon,
BaseDelimiter,
BaseList,
},
});
</script>
<style lang="scss">
.cardCommunicationChannel {
cursor: pointer;
&--mint.cardCommunicationChannel--selected {
outline: 2px solid var(--mint-50);
}
&--red.cardCommunicationChannel--selected {
outline: 2px solid var(--red-50);
}
&--purple.cardCommunicationChannel--selected {
outline: 2px solid var(--purple-50);
}
&:hover {
box-shadow: var(--box-shadow--m);
}
&--disabled {
color: var(--grey-30);
cursor: default;
box-shadow: var(--box-shadow--xs);
&:hover {
box-shadow: var(--box-shadow--xs);
}
}
&__header {
display: flex;
gap: var(--spacing--m);
align-items: center;
}
}
</style>
================================================
FILE: packages/app/src/components/ForceGraph.vue
================================================
<template>
<svg ref="graphRef" class="forceGraph"></svg>
</template>
<script>
import {
defineComponent, ref, onMounted, watch, computed,
} from 'vue';
import * as d3 from 'd3';
import * as cola from 'webcola';
import nodeSizeAttributeType from '@/types/nodeSizeAttributeType';
const SMALL_CIRCLE_RADIUS = 2;
const DISTANCE_BETWEEN_SMALL_CIRCLES = 5;
const NODE_SIZE_NO_FILTER = 15;
export default defineComponent({
name: 'ForceGraph',
props: {
data: {
type: Object,
required: true,
},
selectedChannelType: {
type: String,
default: null,
},
selectedChannel: {
type: Object,
default: null,
},
nodeSizeAttribute: {
type: String,
default: 'props',
},
searchString: {
type: String,
default: '',
},
width: {
type: Number,
default: 500,
},
height: {
type: Number,
default: 500,
},
},
setup(props, { emit }) {
const graphRef = ref(null);
const selectedNode = ref('');
const transitionTime = null;
const nodes = props.data.nodes.map((d) => ({ ...d, id: d.fullPath }));
const indices = new Map(nodes.map((d) => [d.fullPath, d]));
const links = props.data.links.map((d) => Object.assign(Object.create(d), {
source: indices.get(d.source),
target: indices.get(d.target),
}));
const nodeSizeAttributeScale = computed(() => {
if (props.nodeSizeAttribute === nodeSizeAttributeType.NONE) return (d) => d;
const attributeCounts = props.nodeSizeAttribute === nodeSizeAttributeType.CHANNELS
? nodes.map((node) => node.props.length + node.events.length + node.slots.length)
: nodes.map((node) => node[props.nodeSizeAttribute].length);
return d3.scaleLinear(d3.extent(attributeCounts), [8, 30]);
});
const calculateNodeSize = (d) => {
if (props.nodeSizeAttribute === nodeSizeAttributeType.NONE) return NODE_SIZE_NO_FILTER;
const value = props.nodeSizeAttribute === nodeSizeAttributeType.CHANNELS
? d.props.length + d.events.length + d.slots.length : d[props.nodeSizeAttribute].length;
return nodeSizeAttributeScale.value(value);
};
const updateNodeSize = () => {
d3.selectAll('.node__circle')
.transition(transitionTime)
.attr('r', (d) => calculateNodeSize(d));
};
const getCirclePositionFactor = (index, nodeSize) => (index / nodeSize)
* (SMALL_CIRCLE_RADIUS / 2) * DISTANCE_BETWEEN_SMALL_CIRCLES;
const updateCommunicationChannelPositions = () => {
const nodeSize = d3.local();
d3.selectAll('.node--selectedDependent')
.each((d, i, currentNodes) => nodeSize.set(currentNodes[i], calculateNodeSize(d)))
.selectAll('.node__channel')
.transition(transitionTime)
.attr('cx', (d, i, currentNodes) => {
const channelCircleRadius = nodeSize.get(currentNodes[i]) + 3;
return channelCircleRadius * Math.cos(getCirclePositionFactor(i, channelCircleRadius) - Math.PI * 0.5);
})
.attr('cy', (d, i, currentNodes) => {
const channelCircleRadius = nodeSize.get(currentNodes[i]) + 3;
return channelCircleRadius * Math.sin(getCirclePositionFactor(i, channelCircleRadius) - Math.PI * 0.5);
});
};
const updateArrowTips = () => {
const LINK_ARROW_SIZE = 4;
d3.selectAll('.arrow')
.transition(transitionTime)
.attr('refX', (d) => {
const targetNode = nodes.find((node) => node.id === d.target.id);
return calculateNodeSize(targetNode) + LINK_ARROW_SIZE;
});
};
const drawCommunicationChannelCircles = () => {
d3.selectAll('.node--selectedDependent')
.append('g')
.selectAll('.node__channel')
.data((data) => {
const dependent = selectedNode?.value.dependents.find((dep) => dep.fullPath === data.fullPath);
return dependent ? dependent[`used${props.selectedChannelType}`] : [];
})
.enter()
.append('circle')
.attr('r', SMALL_CIRCLE_RADIUS)
.attr('class', `node__channel node__channel--${props.selectedChannelType.toLowerCase()}`);
updateCommunicationChannelPositions();
};
const resetChannelSelection = () => {
d3.selectAll('.node--usesProps').classed('node--usesProps', false);
d3.selectAll('.node--usesEvents').classed('node--usesEvents', false);
d3.selectAll('.node--usesSlots').classed('node--usesSlots', false);
d3.selectAll('.node__channel--selected').classed('node__channel--selected', false);
};
const removeNodeChannels = () => {
d3.selectAll('.node__channel').remove();
d3.selectAll('.node__channel--props').remove();
d3.selectAll('.node__channel--slots').remove();
d3.selectAll('.node__channel--events').remove();
};
const resetNodeSelection = () => {
emit('unselected');
d3.selectAll('.node--selected').classed('node--selected', false);
d3.selectAll('.node--selectedDependent').classed('node--selectedDependent', false);
d3.selectAll('.link--selected').classed('link--selected', false);
d3.selectAll('.arrow--selected').classed('arrow--selected', false);
d3.selectAll('.node--greyedOut').classed('node--greyedOut', false);
d3.selectAll('.link--greyedOut').classed('link--greyedOut', false);
removeNodeChannels();
resetChannelSelection();
d3.selectAll('.node').raise();
};
onMounted(() => {
const svg = d3.select(graphRef.value)
.attr('viewBox', [0, 0, props.width, props.height])
.attr('class', 'svg')
.on('click', () => {
resetNodeSelection();
});
svg.transition().duration(500);
const g = svg.append('g');
const layout = cola.d3adaptor(d3)
.size([props.width, props.height])
.nodes(nodes)
.links(links)
.avoidOverlaps(true)
.symmetricDiffLinkLengths(20)
.start(10, 15, 20);
const LINK_ARROW_SIZE = 4;
svg.append('defs').selectAll('.arrow')
.data(links)
.enter()
.append('marker')
.attr('class', 'arrow')
.attr('id', (d) => `arrow-${d.source.index}-${d.target.index}`)
.attr('viewBox', `0 -${LINK_ARROW_SIZE / 2} ${LINK_ARROW_SIZE} ${LINK_ARROW_SIZE}`)
.attr('refX', (d) => {
const targetNode = nodes.find((node) => node.id === d.target.id);
return calculateNodeSize(targetNode) + LINK_ARROW_SIZE;
})
.attr('refY', 0)
.attr('markerWidth', LINK_ARROW_SIZE)
.attr('markerHeight', LINK_ARROW_SIZE)
.attr('orient', 'auto')
.append('path')
.attr('d', `M1,-${(LINK_ARROW_SIZE / 2) - 1}
L${LINK_ARROW_SIZE - 1},0
L1,${(LINK_ARROW_SIZE / 2) - 1}
L1,-${(LINK_ARROW_SIZE / 2) - 1} Z`)
.style('opacity', '1');
const link = g.selectAll('line')
.data(links)
.enter()
.append('line')
.attr('id', (d) => `link-${d.source.index}-${d.target.index}`)
.attr('class', 'link')
.attr('marker-end', (d) => `url(#arrow-${d.source.index}-${d.target.index})`);
const node = g.selectAll('g')
.data(nodes)
.enter()
.append('g')
.attr('id', (d) => `node-${d.index}`)
.attr('class', 'node')
.call(layout.drag)
// eslint-disable-next-line func-names
.on('click', function (data) {
d3.event.stopPropagation();
resetNodeSelection();
selectedNode.value = data;
// highlight selected node + dependents
const dependents = links.filter((l) => l.target.index === data.index);
dependents.forEach((dependent) => {
d3.select(`#link-${dependent.source.index}-${dependent.target.index}`)
.classed('link--selected', true)
.raise();
d3.select(`#arrow-${dependent.source.index}-${dependent.target.index}`)
.classed('arrow--selected', true)
.raise();
d3.select(`#node-${dependent.source.index}`).classed('node--selectedDependent', true);
});
d3.selectAll('.node--selectedDependent').raise();
d3.select(this).classed('node--selected', true).raise();
drawCommunicationChannelCircles();
// grey out everything else
d3.selectAll('.node:not(.node--selected, .node--selectedDependent)').classed('node--greyedOut', true);
d3.selectAll('.link:not(.link--selected').classed('link--greyedOut', true);
emit('selected', data);
});
node.append('circle')
.attr('class', 'node__circle');
updateNodeSize();
const label = node.append('g')
.attr('dy', 2)
.attr('dx', 0)
.attr('class', 'node__label');
label.append('text')
.attr('dy', 2)
.attr('dx', 0)
.attr('class', 'node__labelText')
.text((d) => d.name);
label.insert('rect', '.node__labelText')
.attr('fill', 'white')
// eslint-disable-next-line func-names
.attr('width', function () {
const bbox = d3.select(this.parentNode).select('.node__labelText').node().getBBox();
return bbox.width + 2;
})
// eslint-disable-next-line func-names
.attr('height', function () {
const bbox = d3.select(this.parentNode).select('.node__labelText').node().getBBox();
return bbox.height;
})
// eslint-disable-next-line func-names
.attr('x', function () {
const bbox = d3.select(this.parentNode).select('.node__labelText').node().getBBox();
return bbox.x - 1;
})
// eslint-disable-next-line func-names
.attr('y', function () {
const bbox = d3.select(this.parentNode).select('.node__labelText').node().getBBox();
return bbox.y;
})
.attr('class', 'node__labelBackground');
layout.on('tick', () => {
link
.attr('x1', (d) => d.source.x)
.attr('y1', (d) => d.source.y)
.attr('x2', (d) => d.target.x)
.attr('y2', (d) => d.target.y);
node.attr('transform', (d) => `translate(${d.x},${d.y})`);
});
function zoomed() {
g.attr('transform', d3.event.transform);
}
const zoom = d3.zoom()
.scaleExtent([0.4, 8])
.on('zoom', zoomed);
svg.call(zoom);
});
const highlightChannelUsage = (channel) => {
d3.selectAll('.node--selectedDependent')
.classed(`node--uses${props.selectedChannelType}`, (d) => {
const dependent = selectedNode.value.dependents.find((dep) => dep.fullPath === d.fullPath);
const channels = selectedNode.value[props.selectedChannelType.toLowerCase()];
return dependent[`used${props.selectedChannelType}`].some((prop) => channels[prop].name === channel.name);
});
d3.selectAll(`.node--selectedDependent .node__channel--${props.selectedChannelType.toLowerCase()}`)
.classed(
'node__channel--selected',
(d) => selectedNode.value[props.selectedChannelType.toLowerCase()][d].name === channel.name,
);
};
watch(() => props.nodeSizeAttribute, () => {
updateNodeSize();
updateCommunicationChannelPositions();
updateArrowTips();
});
watch(() => props.searchString, () => {
d3.selectAll('.node')
.classed('node--searchMatches', (d) => props.searchString.length > 0
&& d.name.toLowerCase().includes(props.searchString.toLowerCase()));
});
watch(() => props.selectedChannel, () => {
resetChannelSelection();
if (props.selectedChannel) highlightChannelUsage(props.selectedChannel);
});
watch(() => props.selectedChannelType, () => {
removeNodeChannels();
resetChannelSelection();
drawCommunicationChannelCircles();
});
return {
graphRef,
};
},
});
</script>
<style lang="scss">
.svg {
height: 100%;
width: 100%;
cursor: grab;
}
.node {
text-anchor: middle;
&__circle {
fill: white;
stroke: var(--grey-20);
}
&__label {
cursor: pointer;
}
&__labelText {
font-size: var(--font-size--3xs);
fill: var(--navy-90);
}
&__labelBackground {
fill: white;
rx: var(--border-radius--xs);
}
&__channel {
&--props {
fill: var(--mint-grey);
stroke: var(--mint-50);
}
&--events {
fill: var(--red-grey);
stroke: var(--red-50);
}
&--slots {
fill: var(--purple-grey);
stroke: var(--purple-50);
}
}
&__circle,
&__channel {
stroke-width: 1px;
cursor: pointer;
}
&__circle,
&__channel,
&__labelText,
&__labelBackground {
transition: fill 200ms;
}
&--searchMatches {
.node__labelBackground {
fill: var(--yellow-30);
}
}
&--selected {
.node__circle,
.node__labelBackground {
fill: var(--yellow-30);
}
.node__circle {
stroke: var(--yellow-50);
}
}
&--selectedDependent {
.node__circle {
stroke: var(--grey-50);
}
}
&--usesProps {
.node__circle,
.node__labelBackground {
fill: var(--mint-30);
}
.node__circle {
stroke: var(--mint-50);
}
.node__channel--selected {
fill: var(--mint-50);
stroke: var(--mint-70);
}
}
&--usesSlots {
.node__circle,
.node__labelBackground {
fill: var(--purple-30);
}
.node__circle {
stroke: var(--purple-50);
}
.node__channel--selected {
fill: var(--purple-50);
stroke: var(--purple-70);
}
}
&--usesEvents {
.node__circle,
.node__labelBackground {
fill: var(--red-30);
}
.node__circle {
stroke: var(--red-50);
}
.node__channel--selected {
fill: var(--red-50);
stroke: var(--red-70);
}
}
&--greyedOut {
.node__circle {
stroke: none;
fill: var(--grey-5);
}
.node__labelBackground {
fill: var(--grey-5);
}
.node__labelText {
fill: var(--grey-20);
}
}
}
.link {
stroke: var(--grey-20);
&--selected {
stroke: var(--grey-50);
}
&--greyedOut {
stroke: var(--grey-15);
}
}
.arrow {
stroke: var(--grey-20);
fill: var(--grey-20);
&--selected {
stroke: var(--grey-50);
fill: var(--grey-50);
}
}
</style>
================================================
FILE: packages/app/src/components/MenuCommunication.vue
================================================
<template>
<div class="menuCommunication">
<base-dropdown>
<template #trigger="{ isOpen }">
<base-icon icon-name="node size filter">
<icon-filter />
</base-icon>
{{ nodeSizeFilterLocal.label }}
<base-arrow-icon :is-flipped="isOpen" />
</template>
<div class="menuCommunication__filterForm">
<h4>Component size</h4>
<base-radio-button-group
v-model="nodeSizeFilterLocal"
:options="nodeSizeFilterOptions"
name="nodeSizeFilter"
/>
</div>
</base-dropdown>
<label class="input" for="search">
<base-icon icon-name="search">
<icon-search />
</base-icon>
<input
id="search"
:value="search"
class="menuCommunication__search"
placeholder="Search for a component"
@input="$emit('update:search', $event.target.value)"
/>
<base-icon-button
icon-name="cross"
size="xs"
@click="$emit('update:search', '')"
>
<icon-cross />
</base-icon-button>
</label>
</div>
</template>
<script lang="ts">
import {
defineComponent, ref, watch,
} from 'vue';
import BaseArrowIcon from '@/components/base/BaseArrowIcon.vue';
import BaseDropdown from '@/components/base/BaseDropdown.vue';
import BaseIcon from '@/components/base/BaseIcon.vue';
import BaseIconButton from '@/components/base/BaseIconButton.vue';
import BaseRadioButtonGroup from '@/components/base/BaseRadioButtonGroup.vue';
import IconSearch from '@/components/icons/IconSearch.vue';
import IconCross from '@/components/icons/IconCross.vue';
import IconFilter from '@/components/icons/IconFilter.vue';
import nodeSizeAttributeType from '@/types/nodeSizeAttributeType';
const nodeSizeFilterOptions = [
{
label: 'Props',
value: nodeSizeAttributeType.PROP,
},
{
label: 'Events',
value: nodeSizeAttributeType.EVENT,
},
{
label: 'Slots',
value: nodeSizeAttributeType.SLOT,
},
{
label: 'Props, Events & Slots',
value: nodeSizeAttributeType.CHANNELS,
},
{
label: 'Dependencies',
value: nodeSizeAttributeType.DEPENDENCIES,
},
{
label: 'Dependents',
value: nodeSizeAttributeType.DEPENDENTS,
},
{
label: 'No filter',
value: nodeSizeAttributeType.NONE,
},
];
export default defineComponent({
components: {
BaseArrowIcon,
BaseIcon,
BaseIconButton,
BaseDropdown,
BaseRadioButtonGroup,
IconCross,
IconFilter,
IconSearch,
},
props: {
nodeSizeFilter: {
type: String,
required: true,
},
search: {
type: String,
default: '',
},
},
setup(props, { emit }) {
const nodeSizeFilterLocal = ref(nodeSizeFilterOptions.find((option) => option.value === props.nodeSizeFilter));
watch(nodeSizeFilterLocal, () => {
emit('update:nodeSizeFilter', nodeSizeFilterLocal?.value?.value);
});
return {
nodeSizeAttributeType,
nodeSizeFilterLocal,
nodeSizeFilterOptions,
};
},
});
</script>
<style lang="scss">
.menuCommunication {
padding: var(--spacing--m);
display: flex;
gap: var(--spacing--m);
&__filterForm {
display: flex;
flex-direction: column;
gap: var(--spacing--m);
}
&__search {
min-width: 15rem;
}
}
</style>
================================================
FILE: packages/app/src/components/SidebarCommunication.vue
================================================
<template>
<div class="sidebarCommunication">
<h2>{{ component.name }}</h2>
<p>{{ component.fullPath }}</p>
<base-sub-nav
:items="[
{
to: '/',
name: 'Props',
color: 'mint',
counter: component.props.length,
disabled: component.props.length > 0
},
{
to: '/events',
name: 'Events',
color: 'red',
counter: component.events.length,
disabled: component.events.length > 0
},
{
to: '/slots',
name: 'Slots',
color: 'purple',
counter: component.slots.length,
disabled: component.slots.length > 0
}
]"
/>
<router-view
:component="component"
@channelSelected="selectChannel"
/>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, PropType } from 'vue';
import BaseSubNav from '@/components/base/BaseSubNav.vue';
import { Prop, VueComponent } from '@vuensight/types';
export default defineComponent({
components: {
BaseSubNav,
},
props: {
component: {
type: Object as PropType<VueComponent>,
required: true,
},
},
setup(props, { emit }) {
const selectedChannel = ref<Prop | null>(null);
const selectChannel = (channel: Prop) => {
selectedChannel.value = channel;
emit('channelSelected', selectedChannel.value);
};
return {
selectChannel,
selectedChannel,
};
},
});
</script>
<style lang="scss">
.sidebarCommunication {
display: flex;
flex-direction: column;
gap: var(--spacing--l);
height: 100%;
}
</style>
================================================
FILE: packages/app/src/components/SidebarCommunicationEventsTab.vue
================================================
<template>
<div class="sidebarCommunicationPropsTab">
<card-communication-channel
v-for="event in eventsWithDependents"
:key="event.name"
:channel="event"
:dependents="event.dependents"
:is-selected="event.dependents.length > 0 && selectedChannel && selectedChannel.name === event.name"
color="red"
@click="event.dependents.length > 0 && selectChannel(event)"
/>
</div>
</template>
<script lang="ts">
import {
computed, defineComponent, ref, PropType,
} from 'vue';
import CardCommunicationChannel from '@/components/CardCommunicationChannel.vue';
import { Event, VueComponent } from '@vuensight/types';
export default defineComponent({
components: {
CardCommunicationChannel,
},
props: {
component: {
type: Object as PropType<VueComponent>,
required: true,
},
},
setup(props, { emit }) {
const selectedChannel = ref<Event | null>(null);
const selectChannel = (channel: Event) => {
console.log('event', selectedChannel.value?.name, channel.name);
selectedChannel.value = selectedChannel.value?.name !== channel.name ? channel : null;
emit('channelSelected', selectedChannel.value);
};
const eventsWithDependents = computed(() => props.component.events.map((event, index) => ({
...event,
dependents: props.component.dependents.filter((dependent) => dependent.usedEvents.includes(index)),
})));
return {
eventsWithDependents,
selectChannel,
selectedChannel,
};
},
});
</script>
<style lang="scss">
.sidebarCommunicationPropsTab {
display: flex;
flex-direction: column;
gap: var(--spacing--l);
}
</style>
================================================
FILE: packages/app/src/components/SidebarCommunicationPropsTab.vue
================================================
<template>
<div class="sidebarCommunicationPropsTab">
<card-communication-channel
v-for="prop in propsWithDependents"
:key="prop.name"
:channel="prop"
:dependents="prop.dependents"
:is-selected="prop.dependents.length > 0 && selectedChannel && selectedChannel.name === prop.name"
color="mint"
@click="prop.dependents.length > 0 && selectChannel(prop)"
/>
</div>
</template>
<script lang="ts">
import {
computed, defineComponent, ref, PropType,
} from 'vue';
import CardCommunicationChannel from '@/components/CardCommunicationChannel.vue';
import { Prop, VueComponent } from '@vuensight/types';
export default defineComponent({
components: {
CardCommunicationChannel,
},
props: {
component: {
type: Object as PropType<VueComponent>,
required: true,
},
},
setup(props, { emit }) {
const selectedChannel = ref<Prop | null>(null);
const selectChannel = (channel: Prop) => {
selectedChannel.value = selectedChannel.value?.name !== channel.name ? channel : null;
emit('channelSelected', selectedChannel.value);
};
const propsWithDependents = computed(() => props.component.props.map((prop, index) => ({
...prop,
dependents: props.component.dependents.filter((dependent) => dependent.usedProps.includes(index)),
})));
return {
propsWithDependents,
selectChannel,
selectedChannel,
};
},
});
</script>
<style lang="scss">
.sidebarCommunicationPropsTab {
display: flex;
flex-direction: column;
gap: var(--spacing--l);
}
</style>
================================================
FILE: packages/app/src/components/SidebarCommunicationSlotsTab.vue
================================================
<template>
<div class="sidebarCommunicationSlotsTab">
<card-communication-channel
v-for="slot in slotsWithDependents"
:key="slot.name"
:channel="slot"
:dependents="slot.dependents"
:is-selected="slot.dependents.length > 0 && selectedChannel && selectedChannel.name === slot.name"
color="purple"
@click="slot.dependents.length > 0 && selectChannel(slot)"
/>
</div>
</template>
<script lang="ts">
import {
computed, defineComponent, ref, PropType,
} from 'vue';
import CardCommunicationChannel from '@/components/CardCommunicationChannel.vue';
import { Slot, VueComponent } from '@vuensight/types';
export default defineComponent({
components: {
CardCommunicationChannel,
},
props: {
component: {
type: Object as PropType<VueComponent>,
required: true,
},
},
setup(props, { emit }) {
const selectedChannel = ref<Slot | null>(null);
const selectChannel = (channel: Slot) => {
selectedChannel.value = selectedChannel.value?.name !== channel.name ? channel : null;
emit('channelSelected', selectedChannel.value);
};
const slotsWithDependents = computed(() => props.component.slots.map((slot, index) => ({
...slot,
dependents: props.component.dependents.filter((dependent) => dependent.usedSlots.includes(index)),
})));
return {
slotsWithDependents,
selectChannel,
selectedChannel,
};
},
});
</script>
<style lang="scss">
.sidebarCommunicationSlotsTab {
display: flex;
flex-direction: column;
gap: var(--spacing--l);
}
</style>
================================================
FILE: packages/app/src/components/base/BaseArrowIcon.vue
================================================
<template>
<svg
:class="{ 'baseArrowIcon--flipped': isFlipped }"
class="baseArrowIcon"
viewBox="0 0 15 16"
xmlns="http://www.w3.org/2000/svg"
>
<path
class="baseArrowIcon__arrow"
d="M7.5 12.58a.66.66 0 0 1-.46-.19L1.18 6.53c-.25-.25-.25-.67 0-.92s.67-.25.92
0l5.4 5.4 5.4-5.4c.25-.25.67-.25.92 0s.25.67 0 .92l-5.86 5.86a.66.66 0 0 1-.46.19z"
/>
</svg>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
name: 'BaseArrowIcon',
props: {
isFlipped: {
type: Boolean,
default: false,
},
},
});
</script>
<style lang="scss">
.baseArrowIcon {
display: flex;
height: 1.75rem;
width: 1.75rem;
padding: var(--spacing--s);
cursor: pointer;
transform: rotate(0deg);
background: transparent;
transition: background, transform 200ms ease;
&:hover,
&:focus {
background: var(--grey-10);
border-radius: var(--border-radius--2xl);
}
&__arrow {
fill: var(--grey-50);
stroke: var(--grey-50);
}
&--flipped {
transform: rotate(180deg);
}
}
</style>
================================================
FILE: packages/app/src/components/base/BaseBadge.vue
================================================
<template>
<span
class="baseBadge"
:class="`baseBadge--${color} ${isRound ? 'baseBade--round' : ''}`"
>
<slot />
</span>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
import { Color } from '@/types/color';
export default defineComponent({
name: 'BaseBadge',
props: {
color: {
type: String as PropType<Color>,
},
isRound: {
type: Boolean,
default: true,
},
},
});
</script>
<style lang="scss">
.baseBadge {
background: var(--grey-10);
color: var(--grey-50);
font-size: var(--font-size--s);
line-height: var(--spacing--l);
padding: var(--spacing--2xs) var(--spacing--s);
border-radius: var(--border-radius--s);
&--round {
border-radius: var(--border-radius--l);
}
&--mint {
background: var(--mint-50);
color: white;
}
&--red {
background: var(--red-50);
color: white;
}
&--purple {
background: var(--purple-50);
color: white;
}
&--light-mint {
background: var(--mint-10);
color: var(--mint-70);
}
&--light-red {
background: var(--red-10);
color: var(--red-70);
}
}
</style>
================================================
FILE: packages/app/src/components/base/BaseCard.vue
================================================
<template>
<button class="baseCard">
<span class="baseCard__header" :class="{'baseCard__header--expanded': isExpanded && $slots.body}">
<slot name="header"/>
<button
v-if="$slots.body || $slots.footer"
@click.stop="isExpanded = !isExpanded"
>
<base-arrow-icon
:is-flipped="isExpanded"
/>
</button>
</span>
<transition>
<span v-if="isExpanded && $slots.body" class="baseCard__body">
<slot name="body" />
</span>
</transition>
<transition>
<span v-if="isExpanded && $slots.footer" class="baseCard__footer">
<slot name="footer" />
</span>
</transition>
</button>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
import BaseArrowIcon from '@/components/base/BaseArrowIcon.vue';
export default defineComponent({
name: 'BaseCard',
components: {
BaseArrowIcon,
},
setup() {
const isExpanded = ref(false);
return {
isExpanded,
};
},
});
</script>
<style lang="scss">
.baseCard {
background: white;
border-radius: var(--border-radius--m);
box-shadow: var(--box-shadow--s);
display: flex;
flex-direction: column;
&__header {
display: flex;
justify-content: space-between;
width: 100%;
padding: var(--spacing--m) var(--spacing--l);
&--expanded {
border-bottom: 1px solid var(--grey-10);
}
}
&__body,
&__footer {
padding: var(--spacing--m) var(--spacing--l);
font-size: var(--font-size--m);
}
&__footer {
border-top: 1px solid var(--grey-10);
width: 100%;
}
.v-enter-active,
.v-leave-active {
transition: opacity 0.2s ease;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
}
}
</style>
================================================
FILE: packages/app/src/components/base/BaseCheckIcon.vue
================================================
<template>
<svg
class="baseCheckIcon"
:class="{
[`baseCheckIcon--${color}`]: color,
'baseCheckIcon--selected': isChecked,
'baseCheckIcon--disabled': isDisabled
}"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 12.19 18"
>
<path
class="baseCheckIcon__check"
d="M4.7 12.83a1 1 0 0 1-.71-.3L1.3 9.81c-.39-.4-.38-1.03.01-1.42.39-.38 1.03-.38
1.41.01l1.97 1.99 4.77-4.92a.987.987 0 0 1 1.41-.02c.4.38.41 1.02.02 1.41l-5.48
5.65c-.18.2-.48.29-.71.32z"
/>
</svg>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
import { Color } from '@/types/color';
export default defineComponent({
props: {
isChecked: {
type: Boolean,
default: false,
},
color: {
type: String as PropType<Color>,
default: 'mint',
},
isDisabled: {
type: Boolean,
default: false,
},
},
});
</script>
<style lang="scss">
.baseCheckIcon {
height: 1.5rem;
width: 1.5rem;
border-radius: var(--border-radius--l);
background: var(--grey-10);
&__check {
fill: var(--grey-50);
}
&--disabled .baseCheckIcon__check {
fill: var(--grey-20);
}
&--selected {
.baseCheckIcon__check {
fill: white;
}
}
&--mint.baseCheckIcon--selected {
background: var(--mint-50);
}
&--red.baseCheckIcon--selected {
background: var(--red-50);
}
&--purple.baseCheckIcon--selected {
background: var(--purple-50);
}
}
</style>
================================================
FILE: packages/app/src/components/base/BaseDelimiter.vue
================================================
<template>
<span
:class="{
[`baseDelimiter--${color}`]: color,
}"
class="baseDelimiter"
>
•
</span>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
import { Color } from '@/types/color';
export default defineComponent({
name: 'BaseDelimiter',
props: {
color: {
type: String as PropType<Color>,
},
},
});
</script>
<style lang="scss">
.baseDelimiter {
font-weight: bold;
color: var(--navy-90);
&--mint {
color: var(--mint-50);
}
&--red {
color: var(--red-50);
}
&--purple {
color: var(--purple-50);
}
}
</style>
================================================
FILE: packages/app/src/components/base/BaseDropdown.vue
================================================
<template>
<div
ref="dropdown"
class="baseDropdown"
:class="{'c-appDropdown--open': isOpen}"
>
<button class="input" @click="toggle">
<slot :isOpen="isOpen" name="trigger"/>
</button>
<div
v-if="isOpen"
class="baseDropdown__content"
>
<slot :close="close"/>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
import { onClickOutside } from '@vueuse/core';
export default defineComponent({
setup() {
const isOpen = ref<boolean>(false);
const toggle = () => {
isOpen.value = !isOpen.value;
};
const close = () => {
isOpen.value = false;
};
const dropdown = ref(null);
onClickOutside(dropdown, () => close());
return {
close,
isOpen,
toggle,
dropdown,
};
},
});
</script>
<style lang="scss">
.baseDropdown {
display: flex;
flex-direction: column;
align-items: start;
gap: var(--spacing--m);
&__content {
padding: var(--spacing--m);
position: absolute;
top: calc(var(--spacing--3xl) + var(--spacing--m));
margin-top: var(--spacing--m);
background: white;
border: none;
border-radius: var(--border-radius--s);
box-shadow: var(--box-shadow--s);
}
}
</style>
================================================
FILE: packages/app/src/components/base/BaseIcon.vue
================================================
<template>
<svg :aria-labelledby="iconName"
:class="`baseIcon baseIcon--${size}`"
role="presentation"
viewBox="0 0 18 18"
xmlns="http://www.w3.org/2000/svg"
>
<title :id="iconName" lang="en">{{iconName}} icon</title>
<g :fill="`var(--${iconColor})`">
<slot />
</g>
</svg>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
props: {
iconName: {
type: String,
default: 'box',
},
size: {
type: String,
default: 'm',
},
iconColor: {
type: String,
default: 'grey-30',
},
},
});
</script>
<style lang="scss" scoped>
.baseIcon {
display: inline-block;
vertical-align: baseline;
&--xs {
width: var(--icon--xs);
height: var(--icon--xs);
}
&--s {
width: var(--icon--s);
height: var(--icon--s);
}
&--m {
width: var(--icon--m);
height: var(--icon--m);
}
&--l {
width: var(--icon--l);
height: var(--icon--l);
}
&--xl {
width: var(--icon--xl);
height: var(--icon--xl);
}
&--2xl {
width: var(--icon--2xl);
height: var(--icon--2xl);
}
}
</style>
================================================
FILE: packages/app/src/components/base/BaseIconButton.vue
================================================
<template>
<button class="baseIconButton">
<base-icon :icon-color="iconColor" :icon-name="iconName" :size="size">
<slot />
</base-icon>
</button>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import BaseIcon from '@/components/base/BaseIcon.vue';
export default defineComponent({
props: {
iconName: {
type: String,
default: 'box',
},
size: {
type: String,
default: 'm',
},
iconColor: {
type: String,
default: 'grey-30',
},
},
components: {
BaseIcon,
},
});
</script>
<style lang="scss" scoped>
.baseIconButton {
display: flex;
justify-content: center;
padding: var(--spacing--s);
}
</style>
================================================
FILE: packages/app/src/components/base/BaseList.vue
================================================
<template>
<ul
:class="{
[`baseList--${color}`]: color,
}"
class="baseList"
>
<li
v-for="item in items"
:key="item"
class="baseList__item"
>
<span class="baseList__itemText">{{ item }}</span>
</li>
</ul>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
import { Color } from '@/types/color';
export default defineComponent({
name: 'BaseDelimiter',
props: {
items: {
type: Array,
default: () => [],
},
color: {
type: String as PropType<Color>,
},
},
});
</script>
<style lang="scss">
.baseList {
list-style: none; /* Remove default bullets */
padding-inline-start: var(--spacing--xl);
&__item {
display: flex;
}
&__item::before {
content: "\2022";
font-weight: bold;
color: var(--navy-90);
display: inline-block;
width: 1em;
margin-left: -1em;
}
&--mint .baseList__item::before {
color: var(--mint-50);
}
&--red .baseList__item::before {
color: var(--red-50);
}
&--purple .baseList__item::before {
color: var(--purple-50);
}
&__itemText {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
</style>
================================================
FILE: packages/app/src/components/base/BaseLoadingSpinner.vue
================================================
<template>
<div class="flower-spinner">
<div class="dots-container">
<div class="bigger-dot">
<div class="smaller-dot"></div>
</div>
</div>
</div>
</template>
<style>
.flower-spinner, .flower-spinner * {
box-sizing: border-box;
}
.flower-spinner {
height: 70px;
width: 70px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
transform: scale(0.8);
}
.flower-spinner .dots-container {
height: calc(70px / 7);
width: calc(70px / 7);
}
.flower-spinner .smaller-dot {
background: var(--yellow-50);
height: 100%;
width: 100%;
border-radius: 50%;
animation: flower-spinner-smaller-dot-animation 2.5s 0s infinite both;
}
.flower-spinner .bigger-dot {
background: var(--yellow-50);
height: 100%;
width: 100%;
padding: 10%;
border-radius: 50%;
animation: flower-spinner-bigger-dot-animation 2.5s 0s infinite both;
}
@keyframes flower-spinner-bigger-dot-animation {
0%, 100% {
box-shadow: var(--yellow-50) 0px 0px 0px,
var(--yellow-50) 0px 0px 0px,
var(--yellow-50) 0px 0px 0px,
var(--yellow-50) 0px 0px 0px,
var(--yellow-50) 0px 0px 0px,
var(--yellow-50) 0px 0px 0px,
var(--yellow-50) 0px 0px 0px,
var(--yellow-50) 0px 0px 0px;
}
50% {
transform: rotate(180deg);
}
25%, 75% {
box-shadow: var(--yellow-50) 26px 0px 0px,
var(--yellow-50) -26px 0px 0px,
var(--yellow-50) 0px 26px 0px,
var(--yellow-50) 0px -26px 0px,
var(--yellow-50) 19px -19px 0px,
var(--yellow-50) 19px 19px 0px,
var(--yellow-50) -19px -19px 0px,
var(--yellow-50) -19px 19px 0px;
}
100% {
transform: rotate(360deg);
box-shadow: var(--yellow-50) 0px 0px 0px,
var(--yellow-50) 0px 0px 0px,
var(--yellow-50) 0px 0px 0px,
var(--yellow-50) 0px 0px 0px,
var(--yellow-50) 0px 0px 0px,
var(--yellow-50) 0px 0px 0px,
var(--yellow-50) 0px 0px 0px,
var(--yellow-50) 0px 0px 0px;
}
}
@keyframes flower-spinner-smaller-dot-animation {
0%, 100% {
box-shadow: var(--yellow-50) 0px 0px 0px,
var(--yellow-50) 0px 0px 0px,
var(--yellow-50) 0px 0px 0px,
var(--yellow-50) 0px 0px 0px,
var(--yellow-50) 0px 0px 0px,
var(--yellow-50) 0px 0px 0px,
var(--yellow-50) 0px 0px 0px,
var(--yellow-50) 0px 0px 0px;
}
25%, 75% {
box-shadow: var(--yellow-50) 14px 0px 0px,
var(--yellow-50) -14px 0px 0px,
var(--yellow-50) 0px 14px 0px,
var(--yellow-50) 0px -14px 0px,
var(--yellow-50) 10px -10px 0px,
var(--yellow-50) 10px 10px 0px,
var(--yellow-50) -10px -10px 0px,
var(--yellow-50) -10px 10px 0px;
}
100% {
box-shadow: var(--yellow-50) 0px 0px 0px,
var(--yellow-50) 0px 0px 0px,
var(--yellow-50) 0px 0px 0px,
var(--yellow-50) 0px 0px 0px,
var(--yellow-50) 0px 0px 0px,
var(--yellow-50) 0px 0px 0px,
var(--yellow-50) 0px 0px 0px,
var(--yellow-50) 0px 0px 0px;
}
}
</style>
================================================
FILE: packages/app/src/components/base/BaseRadioButtonGroup.vue
================================================
<template>
<form class="baseRadioButtonGroup">
<label
v-for="option in options"
:key="option.value"
:for="option.value"
class="baseRadioButtonGroup__label"
>
<input
type="radio"
:name="name"
:checked="modelValue.value === option.value"
:value="option.value"
:id="option.value"
class="baseRadioButtonGroup__input"
@change="$emit('update:modelValue', option)"
/>
{{ option.label }}
</label>
</form>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
type RadioButtonItem = {
label: string;
value: string;
}
export default defineComponent({
props: {
modelValue: {
type: Object as PropType<RadioButtonItem>,
required: true,
},
options: {
type: Array as PropType<RadioButtonItem[]>,
required: true,
},
name: {
type: String,
default: 'radio-button',
},
},
});
</script>
<style lang="scss">
.baseRadioButtonGroup {
display: flex;
flex-direction: column;
align-items: start;
gap: var(--spacing--m);
cursor: pointer;
&__input {
accent-color: var(--mint-70);
}
&__label {
width: 100%;
cursor: pointer;
}
}
</style>
================================================
FILE: packages/app/src/components/base/BaseSubNav.vue
================================================
<template>
<nav
class="baseSubNav"
>
<router-link
v-for="item in items"
:key="item.name"
:to="item.to"
:disabled="item.disabled"
:class="`baseSubNav__item baseSubNav__item--${item.color}`"
>
<span class="baseSubNav__itemText">
<base-badge
v-if="item.counter !== undefined"
is-round
:color="item.color"
>
{{ item.counter }}
</base-badge>
{{ item.name }}
</span>
</router-link>
</nav>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
import BaseBadge from '@/components/base/BaseBadge.vue';
type SubNavItem = {
to: string,
name: string,
counter?: number,
disabled?: boolean,
}
export default defineComponent({
components: {
BaseBadge,
},
props: {
items: {
type: Object as () => PropType<SubNavItem[]>,
required: true,
},
},
});
</script>
<style lang="scss">
.baseSubNav {
display: flex;
gap: var(--spacing--s);
text-decoration: none;
border-bottom: 1px solid var(--grey-10);
&__item {
display: flex;
gap: var(--spacing--xs);
color: var(--navy-90);
text-decoration: none;
transition: border-color, background-color, font-weight 200ms ease;
border-bottom: 3px solid transparent;
&:hover {
.baseSubNav__itemText {
background: var(--grey-10);
border-radius: var(--border-radius--s);
}
}
&--mint.router-link-exact-active {
border-color: var(--mint-50);
}
&--red.router-link-exact-active {
border-color: var(--red-50);
}
&--purple.router-link-exact-active {
border-color: var(--purple-50);
}
&:active {
color: var(--navy-90);
}
}
&__itemText {
padding: var(--spacing--m) var(--spacing--s);
}
}
</style>
================================================
FILE: packages/app/src/components/icons/IconCross.vue
================================================
<template>
<!-- eslint-disable-next-line max-len -->
<path d="M 3.601562 1.949219 L 9 7.347656 L 14.398438 1.964844 C 14.613281 1.746094 14.910156 1.625 15.21875 1.636719 C 15.832031 1.675781 16.324219 2.167969 16.363281 2.78125 C 16.367188 3.078125 16.25 3.359375 16.035156 3.566406 L 10.621094 9 L 16.035156 14.433594 C 16.25 14.640625 16.367188 14.921875 16.363281 15.21875 C 16.324219 15.832031 15.832031 16.324219 15.21875 16.363281 C 14.910156 16.375 14.613281 16.253906 14.398438 16.035156 L 9 10.652344 L 3.617188 16.035156 C 3.402344 16.253906 3.105469 16.375 2.796875 16.363281 C 2.171875 16.332031 1.667969 15.828125 1.636719 15.203125 C 1.632812 14.90625 1.75 14.621094 1.964844 14.417969 L 7.378906 9 L 1.949219 3.566406 C 1.742188 3.359375 1.628906 3.074219 1.636719 2.78125 C 1.675781 2.167969 2.167969 1.675781 2.78125 1.636719 C 3.085938 1.621094 3.382812 1.734375 3.601562 1.949219 Z M 3.601562 1.949219 "/>
</template>
================================================
FILE: packages/app/src/components/icons/IconFilter.vue
================================================
<template>
<!-- eslint-disable max-len -->
<path d="M 2.46875 16.507812 C 2.46875 16.828125 2.722656 17.078125 3.042969 17.078125 C 3.363281 17.078125 3.613281 16.828125 3.613281 16.507812 L 3.613281 5.175781 C 4.535156 4.925781 5.21875 4.089844 5.21875 3.082031 C 5.21875 1.898438 4.253906 0.921875 3.054688 0.921875 C 1.855469 0.921875 0.90625 1.898438 0.90625 3.082031 C 0.90625 4.074219 1.578125 4.910156 2.484375 5.164062 L 2.484375 16.507812 Z M 2.023438 3.082031 C 2.023438 2.527344 2.484375 2.066406 3.042969 2.066406 C 3.601562 2.066406 4.058594 2.527344 4.058594 3.082031 C 4.058594 3.640625 3.601562 4.101562 3.042969 4.101562 C 2.484375 4.101562 2.023438 3.640625 2.023438 3.082031 Z M 2.023438 3.082031 "/>
<path d="M 8.984375 0.921875 C 8.664062 0.921875 8.414062 1.171875 8.414062 1.492188 L 8.414062 7.382812 C 7.507812 7.632812 6.835938 8.46875 6.835938 9.460938 C 6.835938 10.449219 7.507812 11.289062 8.414062 11.539062 L 8.414062 16.519531 C 8.414062 16.84375 8.664062 17.09375 8.984375 17.09375 C 9.308594 17.09375 9.558594 16.84375 9.558594 16.519531 L 9.558594 11.539062 C 10.480469 11.289062 11.164062 10.449219 11.164062 9.445312 C 11.164062 8.441406 10.480469 7.605469 9.558594 7.351562 L 9.558594 1.492188 C 9.558594 1.171875 9.308594 0.921875 8.984375 0.921875 Z M 10.03125 9.460938 C 10.03125 10.019531 9.570312 10.480469 9.015625 10.480469 C 8.457031 10.480469 7.996094 10.019531 7.996094 9.460938 C 7.996094 8.902344 8.457031 8.441406 9.015625 8.441406 C 9.570312 8.441406 10.03125 8.886719 10.03125 9.460938 Z M 10.03125 9.460938 "/>
<path d="M 15.503906 12.835938 L 15.503906 1.492188 C 15.503906 1.171875 15.25 0.921875 14.929688 0.921875 C 14.609375 0.921875 14.359375 1.171875 14.359375 1.492188 L 14.359375 12.835938 C 13.4375 13.089844 12.765625 13.925781 12.765625 14.917969 C 12.765625 16.101562 13.730469 17.078125 14.929688 17.078125 C 16.128906 17.078125 17.09375 16.117188 17.09375 14.917969 C 17.105469 13.925781 16.421875 13.089844 15.503906 12.835938 Z M 14.945312 15.933594 C 14.386719 15.933594 13.925781 15.472656 13.925781 14.917969 C 13.925781 14.359375 14.386719 13.898438 14.945312 13.898438 C 15.503906 13.898438 15.960938 14.359375 15.960938 14.917969 C 15.960938 15.472656 15.503906 15.933594 14.945312 15.933594 Z M 14.945312 15.933594 "/>
<!-- eslint-enable max-len -->
</template>
================================================
FILE: packages/app/src/components/icons/IconSearch.vue
================================================
<template>
<!-- eslint-disable-next-line max-len -->
<path d="M 7.199219 13.492188 C 8.734375 13.492188 10.128906 12.949219 11.21875 12.027344 L 16.101562 16.910156 C 16.214844 17.023438 16.351562 17.078125 16.507812 17.078125 C 16.660156 17.078125 16.800781 17.023438 16.910156 16.910156 C 17.136719 16.6875 17.136719 16.324219 16.910156 16.101562 L 12.027344 11.21875 C 12.933594 10.128906 13.492188 8.722656 13.492188 7.199219 C 13.492188 3.726562 10.675781 0.90625 7.199219 0.90625 C 3.738281 0.90625 0.90625 3.738281 0.90625 7.199219 C 0.90625 10.675781 3.738281 13.492188 7.199219 13.492188 Z M 7.199219 2.050781 C 10.046875 2.050781 12.347656 4.367188 12.347656 7.199219 C 12.347656 10.046875 10.046875 12.347656 7.199219 12.347656 C 4.351562 12.347656 2.050781 10.03125 2.050781 7.199219 C 2.050781 4.367188 4.367188 2.050781 7.199219 2.050781 Z M 7.199219 2.050781 "/>
</template>
================================================
FILE: packages/app/src/components/icons/IconSelect.vue
================================================
<template>
<!-- eslint-disable-next-line max-len -->
<path d="M 8.773438 13.5 C 7.574219 13.4375 6.5625 12.976562 5.738281 12.113281 C 4.914062 11.25 4.5 10.210938 4.5 9 C 4.5 7.75 4.9375 6.6875 5.8125 5.8125 C 6.6875 4.9375 7.75 4.5 9 4.5 C 10.210938 4.5 11.25 4.914062 12.113281 5.738281 C 12.976562 6.5625 13.4375 7.582031 13.5 8.792969 L 12.320312 8.417969 C 12.179688 7.617188 11.804688 6.953125 11.195312 6.421875 C 10.582031 5.890625 9.851562 5.625 9 5.625 C 8.0625 5.625 7.265625 5.953125 6.609375 6.609375 C 5.953125 7.265625 5.625 8.0625 5.625 9 C 5.625 9.835938 5.890625 10.566406 6.421875 11.183594 C 6.953125 11.804688 7.617188 12.179688 8.417969 12.320312 Z M 9 16.5 C 7.960938 16.5 6.988281 16.304688 6.074219 15.910156 C 5.164062 15.515625 4.367188 14.980469 3.695312 14.304688 C 3.019531 13.632812 2.484375 12.835938 2.089844 11.925781 C 1.695312 11.011719 1.5 10.039062 1.5 9 C 1.5 7.960938 1.695312 6.988281 2.089844 6.074219 C 2.484375 5.164062 3.019531 4.367188 3.695312 3.695312 C 4.367188 3.019531 5.164062 2.484375 6.074219 2.089844 C 6.988281 1.695312 7.960938 1.5 9 1.5 C 10.039062 1.5 11.011719 1.695312 11.925781 2.089844 C 12.835938 2.484375 13.632812 3.019531 14.304688 3.695312 C 14.980469 4.367188 15.515625 5.164062 15.910156 6.074219 C 16.304688 6.988281 16.5 7.960938 16.5 9 C 16.5 9.113281 16.496094 9.226562 16.492188 9.335938 C 16.484375 9.449219 16.476562 9.5625 16.460938 9.675781 L 15.375 9.335938 L 15.375 9 C 15.375 7.226562 14.757812 5.71875 13.519531 4.480469 C 12.28125 3.242188 10.773438 2.625 9 2.625 C 7.226562 2.625 5.71875 3.242188 4.480469 4.480469 C 3.242188 5.71875 2.625 7.226562 2.625 9 C 2.625 10.773438 3.242188 12.28125 4.480469 13.519531 C 5.71875 14.757812 7.226562 15.375 9 15.375 L 9.335938 15.375 L 9.675781 16.460938 C 9.5625 16.476562 9.449219 16.484375 9.335938 16.492188 C 9.226562 16.496094 9.113281 16.5 9 16.5 Z M 15.394531 16.875 L 12.1875 13.667969 L 11.25 16.5 L 9 9 L 16.5 11.25 L 13.667969 12.1875 L 16.875 15.394531 Z M 15.394531 16.875 "/>
</template>
================================================
FILE: packages/app/src/components/layout/LayoutSplitView.vue
================================================
<template>
<div class="layoutSplitView">
<main class="layoutSplitView__main">
<slot />
</main>
<aside class="layoutSplitView__aside">
<slot name="aside" />
</aside>
</div>
</template>
<script>
import { defineComponent } from 'vue';
export default defineComponent({
name: 'LayoutSplitView',
});
</script>
<style lang="scss">
.layoutSplitView {
display: flex;
flex-direction: row;
height: 100vh;
&__main {
width: 75vw;
overflow-y: hidden;
background-color: var(--grey-10);
}
&__aside {
width: 25vw;
background: white;
padding: var(--spacing--l) var(--spacing--xl);
box-shadow: var(--box-shadow--s);
overflow-y: scroll;
}
}
</style>
================================================
FILE: packages/app/src/composables/fetch.ts
================================================
import { ref, readonly } from 'vue';
export const useFetch = (fetcher: () => Promise<any>) => { // eslint-disable-line @typescript-eslint/no-explicit-any
const data = ref(null);
const isLoading = ref(false);
const isError = ref(false);
const get = async () => {
isLoading.value = true;
data.value = null;
isError.value = false;
try {
data.value = await fetcher();
} catch (e) {
isError.value = true;
console.error(e);
}
isLoading.value = false;
};
return {
data: readonly(data),
isLoading: readonly(isLoading),
isError: readonly(isError),
get,
};
};
export default {
useFetch,
};
================================================
FILE: packages/app/src/main.ts
================================================
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
createApp(App).use(router).mount('#app');
================================================
FILE: packages/app/src/router/index.ts
================================================
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
import PageCommunication from '../views/PageCommunication.vue';
import SidebarCommunicationEventsTab from '../components/SidebarCommunicationEventsTab.vue';
import SidebarCommunicationPropsTab from '../components/SidebarCommunicationPropsTab.vue';
import SidebarCommunicationSlotsTab from '../components/SidebarCommunicationSlotsTab.vue';
const routes: Array<RouteRecordRaw> = [
{
path: '/',
name: 'page-communication',
component: PageCommunication,
children: [
{
path: '',
name: 'Props',
component: SidebarCommunicationPropsTab,
},
{
path: 'events',
name: 'Events',
component: SidebarCommunicationEventsTab,
},
{
path: 'slots',
name: 'Slots',
component: SidebarCommunicationSlotsTab,
},
],
},
];
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes,
});
export default router;
================================================
FILE: packages/app/src/services/parser.ts
================================================
import { VueComponent } from '@vuensight/types';
export const get = ():Promise<VueComponent[]> => fetch('/parse-result')
.then((result) => result.json());
export default {
get,
};
================================================
FILE: packages/app/src/shims-vue.d.ts
================================================
/* eslint-disable */
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
================================================
FILE: packages/app/src/types/color.ts
================================================
export type Color = 'mint' | 'red' | 'purple' | 'light-mint' | 'light-red' | 'light-purple';
================================================
FILE: packages/app/src/types/force.ts
================================================
import { VueComponent } from '@vuensight/types';
export type Link = {
source: string,
target: string,
}
export type ForceLayout = {
nodes: VueComponent[],
links: Link[],
}
================================================
FILE: packages/app/src/types/nodeSizeAttributeType.ts
================================================
const nodeSizeAttributeType = {
PROP: 'props',
EVENT: 'events',
SLOT: 'slots',
CHANNELS: 'channels',
DEPENDENTS: 'dependents',
DEPENDENCIES: 'dependencies',
NONE: 'none',
};
Object.freeze(nodeSizeAttributeType);
export default nodeSizeAttributeType;
================================================
FILE: packages/app/src/views/PageCommunication.vue
================================================
<template>
<layout-split-view>
<div
v-if="isLoading"
class="pageCommunication__loading"
>
<base-loading-spinner />
<p>... analyzing your components</p>
</div>
<div
v-else-if="isError"
class="pageCommunication__error"
>
<h2>Oh no!</h2>
<p>Something went wrong while parsing your project.</p>
</div>
<div
v-else-if="forceGraphData && forceGraphData.nodes.length === 0"
class="pageCommunication__empty"
>
<h2>No components found.</h2>
<p>Sorry, we could not find any Vue components. Did you specify the correct folder?</p>
</div>
<template
v-else-if="forceGraphData && forceGraphData.nodes.length > 0"
>
<menu-communication
v-model:node-size-filter="nodeSizeFilter"
v-model:search="componentSearch"
class="pageCommunication__menu"
/>
<force-graph
:selected-channel="selectedChannel"
:selected-channel-type="selectedChannelType"
:data="forceGraphData"
:node-size-attribute="nodeSizeFilter"
:search-string="componentSearch"
@selected="selectedComponent = $event"
@unselected="selectedComponent = null"
/>
</template>
<template #aside>
<sidebar-communication
v-if="selectedComponent"
:component="selectedComponent"
@channelSelected="selectedChannel = $event"
/>
<div v-else class="pageCommunication__noSelection">
<base-icon
icon-color="yellow-50"
icon-name="select"
size="2xl"
>
<icon-select/>
</base-icon>
<p>Select one of the components to get details about its props, events and slots.</p>
</div>
</template>
</layout-split-view>
</template>
<script lang="ts">
import {
defineComponent,
ComputedRef,
computed,
ref,
} from 'vue';
import { useRoute } from 'vue-router';
import * as parserService from '@/services/parser';
import BaseIcon from '@/components/base/BaseIcon.vue';
import BaseLoadingSpinner from '@/components/base/BaseLoadingSpinner.vue';
import ForceGraph from '@/components/ForceGraph.vue';
import IconSelect from '@/components/icons/IconSelect.vue';
import LayoutSplitView from '@/components/layout/LayoutSplitView.vue';
import MenuCommunication from '@/components/MenuCommunication.vue';
import SidebarCommunication from '@/components/SidebarCommunication.vue';
import { useFetch } from '@/composables/fetch';
import {
VueComponent, Dependency, Prop,
} from '@vuensight/types';
import {
ForceLayout, Link,
} from '@/types/force';
export default defineComponent({
name: 'PageCommunication',
components: {
BaseIcon,
BaseLoadingSpinner,
ForceGraph,
IconSelect,
LayoutSplitView,
MenuCommunication,
SidebarCommunication,
},
setup() {
const {
data,
get: getParserData,
isLoading,
isError,
} = useFetch(parserService.get);
getParserData();
const selectedComponent = ref<VueComponent | null>(null);
const selectedChannel = ref<Prop | null>(null);
const route = useRoute();
const selectedChannelType = computed<string>(
() => (route && typeof route.name === 'string' ? route.name : 'Props'),
);
const nodeSizeFilter = ref<string>('props');
const componentSearch = ref<string>('');
const formatDataForForceLayout = (originalData: VueComponent[]) => {
const nodes: VueComponent[] = [];
const links: Link[] = [];
originalData.forEach((component: VueComponent) => {
nodes.push(component);
component.dependencies.forEach(
(dependency: Dependency) => links.push({
source: component.fullPath,
target: dependency.fullPath,
}),
);
});
return { nodes, links };
};
const forceGraphData: ComputedRef<ForceLayout | null> = computed(() => {
if (data && data.value !== null) {
return formatDataForForceLayout(data.value as unknown as VueComponent[]);
}
return null;
});
return {
componentSearch,
data,
forceGraphData,
isLoading,
isError,
nodeSizeFilter,
selectedChannel,
selectedChannelType,
selectedComponent,
};
},
});
</script>
<style lang="scss">
.pageCommunication {
&__menu {
position: fixed;
top: 0;
left: 0;
}
&__loading,
&__error,
&__empty,
&__noSelection {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin-top: calc(var(--spacing--3xl) * -1);
height: 100%;
color: var(--grey-50);
}
&__noSelection {
width: 75%;
margin-left: auto;
margin-right: auto;
text-align: center;
gap: var(--spacing--s);
}
}
</style>
================================================
FILE: packages/app/tsconfig.json
================================================
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"strict": true,
"jsx": "preserve",
"importHelpers": true,
"moduleResolution": "node",
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"composite": true,
"baseUrl": "./",
"types": [
"webpack-env",
"jest"
],
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
},
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"tests/**/*.ts",
"tests/**/*.tsx"
],
"exclude": [
"node_modules",
"server/**/*"
]
}
================================================
FILE: packages/cli/.eslintrc
================================================
{
"ignorePatterns": ["bin/**/*.js"]
}
================================================
FILE: packages/cli/.gitignore
================================================
/dist
================================================
FILE: packages/cli/.npmignore
================================================
*
!dist/*
!package.json
!readme.md
================================================
FILE: packages/cli/LICENSE.txt
================================================
MIT License
Copyright (c) 2021-2022 Martina Scharrer
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: packages/cli/README.md
================================================
# vuensight 👀
Visualize Vue.js **component relationships** and **communication channels**, i.e. props, events and slots. This tool operates on the
command line and is made for developers. The aim of vuensight is to provide visual insight into the components of a
Vue.js project and to support developers before and during refactoring, e.g. by visually analyzing which prop is used
in which parent component or by highlighting unused components or channels.
An example visualization of vuensight itself:

This tool is built on top of the two awesome packages:
- [dependency-cruiser](https://github.com/sverweij/dependency-cruiser) for building the dependency tree
- [vue-docgen-api](https://github.com/vue-styleguidist/vue-styleguidist/tree/dev/packages/vue-docgen-api) for parsing the Vue files
## Getting started 🚀
### Install
First, install the cli package either locally in the project you want to visualize:
```
npm i -D @vuensight/cli
```
Or globally on your machine if you plan to visualize multiple projects:
```
npm i -g @vuensight/cli
```
### Run
Then run the tool in your project folder:
```
vuensight
```
#### Options
- `--dir` or `-d` (optional): Specify the directory that should be parsed relative from your current working directory, default is `src`
- `--port` or `-p` (optional): Start the application in a different port, default is 4444
- `--webpack-config` or `-wpc` (optional): Specify the path to your webpack-config (from your current working directory). This is particularly important if you use aliases.
- `--ts-config` or `-tsc` (optional): Specify the path to your TypeScript config file (from your current working directory).
An example usage:
```
vuensight --dir resources/js --port 9999 --webpack-config ./webpack-config.json --ts-config ./tsconfig.json
```
## Licencse
[MIT](LICENSE.txt)
================================================
FILE: packages/cli/package.json
================================================
{
"name": "@vuensight/cli",
"version": "0.1.5",
"description": "Command line interface of @vuensight.",
"main": "bin/index.js",
"repository": {
"type": "git",
"url": "git+https://github.com/martinascharrer/vuensight.git"
},
"scripts": {
"build": "rm -rf dist/* && tsc -p tsconfig.pkg.json",
"build:watch": "tsc -w -p tsconfig.pkg.json",
"lint": "eslint --ext ts",
"test": "echo 'no tests yet in cli'",
"prepublish": "npm run build"
},
"bin": {
"vuensight": "dist/index.js"
},
"keywords": [
"cli",
"node.js"
],
"author": "Martina Scharrer",
"license": "(MIT)",
"bugs": {
"url": "https://github.com/martinascharrer/vuensight/issues"
},
"homepage": "https://github.com/martinascharrer/vuensight#readme",
"dependencies": {
"@vuensight/app": "^0.3.1",
"commander": "^9.1.0"
}
}
================================================
FILE: packages/cli/src/index.ts
================================================
#!/usr/bin/env node
import { program } from 'commander';
import startServer from '@vuensight/app';
program
.description('Vue Component Insight CLI')
.option('-d, --dir [dir]', 'specify the directory that should be analyzed (default is src)', 'src')
.option('-p, --port [port]', 'start the application in a different port (default is 4444)')
.option('-wpc, --webpack-config [webpackConfig]', 'path to webpack config file')
.option('-tsc, --ts-config [tsConfig]', 'path to TypeScript config file')
.parse();
const dir = program.opts().dir;
const port = program.opts().port;
const webpackConfig = program.opts().webpackConfig;
const tsConfig = program.opts().tsConfig;
const init = async () => {
try {
await startServer(dir, port, webpackConfig, tsConfig);
} catch (e) {
console.error('Something went wrong parsing the project', e);
}
};
init();
================================================
FILE: packages/cli/tsconfig.pkg.json
================================================
{
"extends": "../../tsconfig.build.json",
"compilerOptions": {
"outDir": "./dist/"
},
"include": [
"./src/*.ts"
],
"paths": {
"@/*": [
"src/*"
]
}
}
================================================
FILE: packages/parser/.eslintrc
================================================
{
"ignorePatterns": ["test/project/*"]
}
================================================
FILE: packages/parser/.gitignore
================================================
/dist
/coverage
================================================
FILE: packages/parser/.npmignore
================================================
*
!dist/**/*.js
dist/tsconfig.pkg.tsbuildinfo
!package.json
!readme.md
================================================
FILE: packages/parser/LICENSE.txt
================================================
MIT License
Copyright (c) 2021-2022 Martina Scharrer
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: packages/parser/README.md
================================================
# @vuensight/parser
> ⚠️ **General information about usage and setup of `vuensight` can be found [here](https://github.com/martinascharrer/vuensight)**
The `parser` extracts information about component dependencies as well as their used props/events/slots of a
given Vue.js project.
## Development
Compile typescript files to `dist` folder
```
npm run build
```
Run eslint
```
npm run lint
```
Run unit tests + coverage
```
npm test
```
Run unit test watcher
```
npm run test:watch
```
## Licencse
[MIT](LICENSE.txt)
================================================
FILE: packages/parser/package.json
================================================
{
"name": "@vuensight/parser",
"version": "0.1.8",
"main": "dist/index.js",
"description": "This parser extracts information regarding the dependencies and their used props, events and slots of a given Vue.js project.",
"scripts": {
"build": "rm -rf dist/* && tsc -p tsconfig.pkg.json",
"build:watch": "tsc -w -p tsconfig.pkg.json",
"test": "jest --coverage",
"test:watch": "jest --watch",
"lint": "eslint --ext ts",
"prepublish": "npm run build"
},
"repository": {
"type": "git",
"url": "git+https://github.com/martinascharrer/vuensight.git"
},
"keywords": [
"parser",
"vue"
],
"author": "Martina Scharrer",
"license": "(MIT)",
"bugs": {
"url": "https://github.com/martinascharrer/vuensight/issues"
},
"homepage": "https://github.com/martinascharrer/vuensight#readme",
"jest": {
"preset": "ts-jest",
"testEnvironment": "node",
"moduleNameMapper": {
"dependency-cruiser/config-utl/extract-ts-config": "dependency-cruiser/src/config-utl/extract-ts-config",
"dependency-cruiser/config-utl/extract-webpack-resolve-config": "dependency-cruiser/src/config-utl/extract-webpack-resolve-config"
}
},
"dependencies": {
"dependency-cruiser": "^12.10.0",
"fs": "^0.0.1-security",
"jsdom": "^21.1.0",
"vue-docgen-api": "^4.60.0"
},
"devDependencies": {
"@types/jsdom": "^21.1.0",
"@vuensight/types": "^0.1.0"
}
}
================================================
FILE: packages/parser/src/index.ts
================================================
import { VueComponent } from '@vuensight/types';
import { findDependencies } from './vue/dependencies';
import { analyzeComponents, analyzeCommunicationChannelUsage } from './vue/analyzer';
export const parse = async (
directory: string,
fileType = 'vue',
webpackConfigPath?: string,
tsConfigPath?: string
): Promise<VueComponent[]> => {
const modules = findDependencies(directory, fileType, webpackConfigPath, tsConfigPath);
if (!modules) return new Array<VueComponent>();
const components: VueComponent[] = await analyzeComponents(modules);
return analyzeCommunicationChannelUsage(components);
};
================================================
FILE: packages/parser/src/utils/files.test.ts
================================================
import { getFileNameFromPath } from './files';
describe('files', () => {
describe('getFileNameFromPath', () => {
it('should extract the filename from the path', () => {
expect(getFileNameFromPath('src/test/asdf/TestFile.vue')).toBe('TestFile.vue');
});
it('should extract the filename from the path', () => {
expect(getFileNameFromPath('src\\test\\asdf\\TestFile.vue')).toBe('TestFile.vue');
});
});
});
================================================
FILE: packages/parser/src/utils/files.ts
================================================
export const getFileNameFromPath = (path: string): string => {
const lastDirectoryIndex = path.lastIndexOf('\\') !== -1 ? path.lastIndexOf('\\') : path.lastIndexOf('/');
return path.substring(lastDirectoryIndex + 1, path.length);
};
================================================
FILE: packages/parser/src/utils/kababize.ts
================================================
export const kebabize = (str: string):string => str.split('')
.map((letter, idx) => (letter.toUpperCase() === letter
? `${idx !== 0 ? '-' : ''}${letter.toLowerCase()}`
: letter)).join('');
export default {
kebabize,
};
================================================
FILE: packages/parser/src/utils/kebabize.test.ts
================================================
import { kebabize } from './kababize';
describe('kebabize', () => {
it('should convert the string to kebab-style', () => {
expect(kebabize('FooBar')).toBe('foo-bar');
});
});
================================================
FILE: packages/parser/src/utils/vue.test.ts
================================================
import {
extractScriptContent,
findTemplate,
getComponentImportName,
getTemplateContent
} from './vue';
const TEST_FILE = '<template>Hello</template><script>const foo = 3;</script><style></style>';
const TEST_FILE_WITHOUT_TEMPLATE = '<script>const foo = 3;</script><style></style>';
const makeTestFileWithScript = (script: string) => `
<template><div>Test File</div></template>
<script>${script}</script>
<style>div { color: red; }</style>
`;
describe('vue', () => {
describe('findTemplate', () => {
it('should find the template string in the file', () => {
expect(findTemplate(TEST_FILE)).toBe('<template>Hello</template>');
});
it('should not find the template string in the file when there is none', () => {
expect(findTemplate(TEST_FILE_WITHOUT_TEMPLATE)).toBe(null);
});
});
describe('getTemplateContent', () => {
it('should return the content of the template without the template tag', () => {
expect(getTemplateContent(TEST_FILE)).toBe('Hello');
});
it('should return null when there is no template', () => {
expect(getTemplateContent(TEST_FILE_WITHOUT_TEMPLATE)).toBe(null);
});
});
describe('extractScriptContent', () => {
it('should extract the script content from the given file', () => {
const script = `const test = 3`;
const testFile = makeTestFileWithScript(script);
expect(extractScriptContent(testFile)).toBe(script);
});
});
describe('getComponentImportName', () => {
it('should find the import name of a component in a js script', () => {
const script = `import Button from '@/components/base/BaseButton';`;
const testFile = makeTestFileWithScript(script);
expect(getComponentImportName(testFile, 'BaseButton')).toBe('Button');
});
it('should not find the import name of a component if it is not imported', () => {
const script = `import Test from '@/components/base/Test';`;
const testFile = makeTestFileWithScript(script);
expect(getComponentImportName(testFile, 'BaseButton')).toBe(null);
});
});
});
================================================
FILE: packages/parser/src/utils/vue.ts
================================================
export const findTemplate = (fileContent: string): string | null => {
const templateBody = fileContent.match(/(?<template><template>[\s\S]*<\/template>)/u);
return templateBody?.groups?.template || null;
};
export const getTemplateContent = (fileContent: string): string | null => {
const template = findTemplate(fileContent);
// remove <template> and </template> tags at the beginning and the end
return template ? template.substring(10, template.length - 11) : null;
};
export const extractScriptContent = (fileContent: string): string => {
const scriptStartString = '<script>';
const scriptStart = fileContent.search(scriptStartString);
const scriptEnd = fileContent.search('</script>');
return fileContent.slice(scriptStart + scriptStartString.length, scriptEnd);
};
export const getComponentImportName = (fileContent: string, fileName: string) => {
const script = extractScriptContent(fileContent);
let name: string | null = null;
script.split(/\r?\n/).forEach((line: string) => {
if (line.includes('import') && line.includes(`/${fileName}`)) name = line.split(' ')[1];
});
return name;
};
================================================
FILE: packages/parser/src/vue/analyzer.ts
================================================
import { readFileSync } from 'fs';
import { normalize } from 'path';
import { IModule } from 'dependency-cruiser';
import { VueComponent } from '@vuensight/types';
import { getFileNameFromPath } from '../utils/files';
import { formatDependencies } from './dependencies';
import { parseComponentFile, getDependentWithUsedChannelsAnalysis } from './communication-channels';
export const findComponentData = (components: VueComponent[], fullPath: string)
: VueComponent | undefined => components.find((component) => component.fullPath === fullPath);
export const findComponentDataByString = (components: VueComponent[], string: string)
: VueComponent | undefined => components.find((component) => component.fullPath.includes(string));
export const analyzeComponents = async (modules: IModule[]): Promise<VueComponent[]> => {
return await Promise.all(modules.map(async (module) => {
const fullPath = normalize(module.source);
const fileName = getFileNameFromPath(fullPath);
const [name, fileType] = fileName.split('.');
let fileContent = '';
try {
fileContent = readFileSync(fullPath, {encoding: 'utf-8'});
} catch (e) {
console.error(e);
}
const dependencies = formatDependencies(module.dependencies);
const parsedComponentData = fileType === 'vue' ? await parseComponentFile(fullPath) : null;
return {
name: parsedComponentData?.name && parsedComponentData?.name !== name ? parsedComponentData?.name : name,
fullPath,
fileContent,
fileName,
fileType,
props: parsedComponentData?.props ?? [],
events: parsedComponentData?.events ?? [],
slots: parsedComponentData?.slots ?? [],
dependencies,
dependents: module.dependents.map(dependent => ({
fullPath: dependent,
name: '',
usedProps: [],
usedSlots: [],
usedEvents: [],
})),
};
}));
};
export const analyzeCommunicationChannelUsage = (components: VueComponent[]): VueComponent[] => {
return components.map((component) => {
const dependents = component.dependents.map((dependent) => {
const dependentData = findComponentData(components, normalize(dependent.fullPath));
if (dependentData && dependentData.fileType === 'vue') {
return getDependentWithUsedChannelsAnalysis(dependentData, component);
}
return dependent;
});
return {
...component,
dependents,
};
});
};
================================================
FILE: packages/parser/src/vue/communication-channels.test.ts
================================================
import { JSDOM } from 'jsdom';
import {
findDependencyInstancesInTemplate,
isPropUsed,
isEventUsed,
isSlotUsed,
getUsedChannels
} from './communication-channels';
const createComponent = (template: string) => {
const fragment = JSDOM.fragment(template);
return fragment.querySelector('ComponentName');
};
describe('parser', () => {
describe('findDependencyInstancesInTemplate', () => {
it('should find a component usage in camel case', function () {
const template = '<div><TestComponent>test</TestComponent></div>';
expect(findDependencyInstancesInTemplate(template, 'TestComponent')).toHaveLength(1);
});
it('should find a component usage in kebab case', function () {
const template = '<div><test-component>test</test-component></div>';
expect(findDependencyInstancesInTemplate(template, 'TestComponent')).toHaveLength(1);
});
it('should find multiple usages of a component', function () {
const template = '<div><test-component>test</test-component><TestComponent>test</TestComponent></div>';
expect(findDependencyInstancesInTemplate(template, 'TestComponent')).toHaveLength(2);
});
it('should find a component usage inside of a template tag', function () {
const template = `
<template>
<template v-if="true"><test-component>test</test-component></template>
</template>
`;
expect(findDependencyInstancesInTemplate(template, 'TestComponent')).toHaveLength(1);
});
});
describe('isPropUsed', () => {
it('should find the prop in kebab syntax', () => {
const prop = { name: 'TestProp', type: { name: 'String' }, default: 'Test', required: false };
const element = createComponent(`<ComponentName test-prop="foo"/>`);
const isUsed = element ? isPropUsed(element, prop) : false;
expect(isUsed).toBe(true);
});
it('should find the prop in kebab syntax when it is bound to data', () => {
const prop = { name: 'TestProp', type: { name: 'String' }, default: 'Test', required: false };
const element = createComponent(`<ComponentName :test-prop="foo"/>`);
const isUsed = element ? isPropUsed(element, prop) : false;
expect(isUsed).toBe(true);
});
it('should find the prop in camel-case syntax', () => {
const prop = { name: 'TestProp', type: { name: 'String' }, default: 'Test', required: false };
const element = createComponent(`<ComponentName TestProp="foo"/>`);
const isUsed = element ? isPropUsed(element, prop) : false;
expect(isUsed).toBe(true);
});
it('should find the prop in camel-case syntax when it is bound to data', () => {
const prop = { name: 'TestProp', type: { name: 'String' }, default: 'Test', required: false };
const element = createComponent(`<ComponentName :TestProp="foo"/>`);
const isUsed = element ? isPropUsed(element, prop) : false;
expect(isUsed).toBe(true);
});
});
describe('isEventUsed', () => {
it('should find the event when the `@` syntax is used', () => {
const event = { name: 'test-event', isSync: false };
const element = createComponent(`<ComponentName @test-event="foo"/>`);
const isUsed = element ? isEventUsed(element, event) : false;
expect(isUsed).toBe(true);
});
it('should find the event when the `v-on:` syntax is used', () => {
const event = { name: 'test-event', isSync: false };
const element = createComponent(`<ComponentName v-on:test-event="foo"/>`);
const isUsed = element ? isEventUsed(element, event) : false;
expect(isUsed).toBe(true);
});
it('should not find the event when it is not used', () => {
const event = { name: 'test-event', isSync: false };
const element = createComponent(`<ComponentName v-on:other-event="foo"/>`);
const isUsed = element ? isEventUsed(element, event) : false;
expect(isUsed).toBe(false);
});
});
describe('isSlotUsed', () => {
it('should find the slot when the `#` syntax is used', () => {
const slot = { name: 'header' };
const element = createComponent(
`<ComponentName @test-event="foo"><template #header>Headline</template></ComponentName>`
);
const isUsed = element ? isSlotUsed(element, slot) : false;
expect(isUsed).toBe(true);
});
it('should find the slot when the `v-slot` syntax is used', () => {
const slot = { name: 'header' };
const element = createComponent(
`<ComponentName @test-event="foo"><template v-slot:header>Headline</template></ComponentName>`
);
const isUsed = element ? isSlotUsed(element, slot) : false;
expect(isUsed).toBe(true);
});
it('should not find the slot when it is not used', () => {
const slot = { name: 'header' };
const element = createComponent(
`<ComponentName @test-event="foo">Headline</ComponentName>`
);
const isUsed = element ? isSlotUsed(element, slot) : true;
expect(isUsed).toBe(false);
});
});
describe('getUsedChannel', () => {
it('should return the indices of the used events when they are used once', () => {
const events = [{ name: 'test-event', isSync: false }, { name: 'test-event2' }, ];
const element = createComponent(`<ComponentName @test-event="foo"/>`);
const usedEvents = element ? getUsedChannels([element], events, isEventUsed) : false;
expect(usedEvents).toStrictEqual([0]);
});
it('should return the index of the used events once when they are used multiple times', () => {
const events = [{ name: 'test-event', isSync: false }, { name: 'TestEventTwo' }, ];
const element = createComponent(
`<ComponentName @TestEventTwo="foo"/>
<ComponentName @TestEventTwo="foo"/>
<ComponentName @TestEventTwo="foo"/>`
);
const usedEvents = element ? getUsedChannels([element], events, isEventUsed) : false;
expect(usedEvents).toStrictEqual([1]);
});
it('should return an empty array when the event is not used', () => {
const events = [{ name: 'test-event', isSync: false }, { name: 'TestEventTwo' }, ];
const element = createComponent(`<ComponentName />`);
const usedEvents = element ? getUsedChannels([element], events, isEventUsed) : false;
expect(usedEvents).toStrictEqual([]);
});
});
});
================================================
FILE: packages/parser/src/vue/communication-channels.ts
================================================
import { parse, PropDescriptor } from 'vue-docgen-api';
import { JSDOM } from 'jsdom';
import { Dependent, Event, Prop, Slot, VueComponent } from '@vuensight/types';
import { getComponentImportName } from '../utils/vue';
import { getTemplateContent } from '../utils/vue';
import { kebabize } from '../utils/kababize';
export const findDependencyInstancesInTemplate = (template: string, name: string): Element[] => {
const templateWithoutTemplateTags = template.replace(/template/g, 'temp-tag');
const fragment = JSDOM.fragment(templateWithoutTemplateTags);
const dependencyUsagesCamelCase = Array.from(fragment.querySelectorAll(name));
const dependencyUsagesKebabCase = Array.from(fragment.querySelectorAll(kebabize(name)));
return [...dependencyUsagesCamelCase, ...dependencyUsagesKebabCase];
};
export const parseComponentFile = async (filePath: string): Promise<Partial<VueComponent> | null> => {
try {
const { displayName: name, props, events, slots } = await parse(filePath);
return { name, props: props && formatProps(props), events, slots };
} catch (e) {
console.error(`Something went wrong while parsing the component: ${filePath}`, e);
}
return null;
};
const formatProps = (props: PropDescriptor[]):Prop[] => {
return props.map((prop) => ({
...prop,
default: prop?.defaultValue?.value,
}));
};
export const isPropUsed = (template: Element, prop: Prop): boolean => {
const propFormats = [prop.name, `:${prop.name}`, `:${kebabize(prop.name)}`, kebabize(prop.name)];
let isUsed = false;
propFormats.forEach((format) => {
if (!isUsed) isUsed = Boolean(template.attributes.getNamedItem(format));
});
return isUsed;
};
export const isEventUsed = (template: Element, event: Event): boolean => {
const eventFormat = [`@${event.name}`, `v-on:${event.name}`];
let isUsed = false;
eventFormat.forEach((format) => (isUsed = isUsed || Boolean(template.attributes.getNamedItem(format))));
return isUsed;
};
export const isSlotUsed = (template: Element, slot: Slot): boolean => {
const slotFormat = [`#${slot.name}`, `v-slot:${slot.name}`];
let isUsed = false;
slotFormat.forEach((format) => (isUsed = isUsed || Boolean(template.innerHTML.includes(format))));
return isUsed;
};
export const getUsedChannels = <Channel>(
dependencyInstances: Element[],
channels: Channel[],
validator: (instance: Element, channel: Channel) => boolean
): number[] => {
const usedChannels = new Set<number>();
channels.forEach((channel, index) => {
dependencyInstances.forEach((dependencyUsage) => validator(dependencyUsage, channel) && usedChannels.add(index));
});
return [...usedChannels];
};
export const getDependentWithUsedChannelsAnalysis = (
{ fullPath: dependentFullPath, name: dependentName, fileContent: dependentFilecontent }: VueComponent,
{ name, props, events, slots }: VueComponent
): Dependent => {
const template = getTemplateContent(dependentFilecontent);
if (!template) return {
fullPath: dependentFullPath,
name: dependentName,
usedProps: [],
usedEvents: [],
usedSlots: []
};
let dependencyInstances = findDependencyInstancesInTemplate(template, name);
if (dependencyInstances.length === 0) {
const importName = getComponentImportName(dependentFilecontent, name);
dependencyInstances = importName ? findDependencyInstancesInTemplate(template, importName) : dependencyInstances;
}
return {
fullPath: dependentFullPath,
name: dependentName,
usedProps: dependencyInstances ? getUsedChannels(dependencyInstances, props, isPropUsed) : [],
usedEvents: dependencyInstances ? getUsedChannels(dependencyInstances, events, isEventUsed) : [],
usedSlots: dependencyInstances ? getUsedChannels(dependencyInstances, slots, isSlotUsed) : []
};
};
================================================
FILE: packages/parser/src/vue/dependencies.ts
================================================
import { normalize } from 'path';
import {
cruise, IDependency, IReporterOutput, IModule
} from 'dependency-cruiser';
// extract features of dependency-cruiser are still experimental and therefore not exported by default.
// See: https://github.com/sverweij/dependency-cruiser/blob/develop/doc/api.md#utility-functions
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import extractTSConfig from 'dependency-cruiser/config-utl/extract-ts-config';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import extractWebpackResolveConfig from 'dependency-cruiser/config-utl/extract-webpack-resolve-config';
import { Dependency } from '@vuensight/types';
export const findDependencies = (
directory = 'src',
fileType?: string,
webpackConfigPath?: string,
tsConfigPath?: string
):IModule[] | null => {
let cruiseResult: IReporterOutput | null = null;
const webpackResolveConfig = webpackConfigPath ? extractWebpackResolveConfig(webpackConfigPath) : null;
const tsConfig = tsConfigPath ? extractTSConfig(tsConfigPath) : null;
try {
cruiseResult = cruise(
[directory],
{
includeOnly: `^.*.(${fileType})$`,
exclude: ['node_modules'],
doNotFollow: {
path: 'node_modules',
dependencyTypes: [
'npm',
'npm-dev',
'npm-optional',
'npm-peer',
'npm-bundled',
'npm-no-pkg',
],
},
forceDeriveDependents: true,
},
webpackResolveConfig,
tsConfig
);
} catch (error) {
console.error('Something went wrong cruising the project ', error);
}
if (cruiseResult && typeof cruiseResult?.output !== 'string') return cruiseResult?.output?.modules;
return null;
};
export const formatDependencies = (dependencies: IDependency[]): Dependency[] => {
return dependencies.map((dependency) => ({
fullPath: normalize(dependency.resolved),
}));
};
================================================
FILE: packages/parser/test/parsing-process.test.ts
================================================
import { parse } from '../src';
import { normalize } from 'path';
describe('parse', () => {
it('should return data about dependencies and communication channels when parsing a Vue project', async () => {
const expectedParseResult = [{
name: 'Child',
fullPath: normalize('test/project/Child.vue'),
fileName: 'Child.vue',
fileType: 'vue',
fileContent: expect.any(String),
events: [{ name: 'selected' }],
props: [{
name: 'title',
type: {
name: 'string'
},
default: "'Hello'",
defaultValue: {
func: false,
value: "'Hello'"
}
}],
slots: [{ name: 'header' }],
dependencies: [],
dependents: [{
fullPath: normalize('test/project/Parent.vue'),
name: "Parent",
usedProps: [0],
usedEvents: [0],
usedSlots: [0],
}],
}, {
name: 'Parent',
fullPath: normalize('test/project/Parent.vue'),
fileName: 'Parent.vue',
fileType: 'vue',
events: [],
props: [],
slots: [],
fileContent: expect.any(String),
dependencies: [{
fullPath: normalize('test/project/Child.vue'),
}],
dependents: [],
}];
const parseResult = await parse('test/project');
expect(parseResult).toEqual(expectedParseResult);
});
});
================================================
FILE: packages/parser/test/project/Child.vue
================================================
<template>
<div>
<slot name="header" />
Test {{ title }}
<button @click="$emit('selected')">Select</button>
</div>
</template>
<script>
export default {
name: 'Child',
props: {
title: {
default: 'Hello',
type: String
}
}
};
</script>
<style scoped>
</style>
================================================
FILE: packages/parser/test/project/Parent.vue
================================================
<template>
<div>
<Child title="test" @selected="select">
<template #header>Important Information</template>
</Child>
</div>
</template>
<script>
import Child from './Child';
export default {
name: 'Parent',
components: { Child },
methods: {
select: () => {},
}
};
</script>
<style scoped>
</style>
================================================
FILE: packages/parser/tsconfig.pkg.json
================================================
{
"extends": "../../tsconfig.build.json",
"compilerOptions": {
"outDir": "./dist/"
},
"include": [
"./src/*.ts"
]
}
================================================
FILE: packages/types/index.d.ts
================================================
export interface Mixin {
name: string
path: string
}
export type Prop = {
name: string,
type?: { name: string; func?: boolean | undefined; } | undefined,
required?: boolean,
default?: string,
mixin?: Mixin,
}
export type Event = {
name: string,
isSync?: boolean,
mixin?: Mixin,
}
export type Slot = {
name: string,
}
export type Dependency = {
fullPath: string,
}
export type Dependent = {
fullPath: string,
name: string,
usedProps: number[], // indexOf used prop
usedEvents: number[], // indexOf used event
usedSlots: number[], // indexOf used event
}
export type VueComponent = {
name: string,
fullPath: string,
fileName: string,
fileType: string,
fileContent: string,
props: Prop[],
events: Event[],
slots: Slot[],
dependencies: Dependency[],
dependents: Dependent[],
}
================================================
FILE: packages/types/package.json
================================================
{
"name": "@vuensight/types",
"version": "0.1.0",
"description": "The shared types package for @vuensight.",
"main": "index.d.ts",
"scripts": {
"build": "echo 'no build for types package'",
"test": "echo 'no tests for types package'"
},
"repository": {
"type": "git",
"url": "git+https://github.com/martinascharrer/vuensight.git"
},
"keywords": [
"Vue.js",
"types",
"typescript"
],
"author": "Martina Scharrer",
"license": "(MIT)",
"bugs": {
"url": "https://github.com/martinascharrer/vuensight/issues"
},
"homepage": "https://github.com/martinascharrer/vuensight#readme"
}
================================================
FILE: tsconfig.build.json
================================================
{
"exclude": [
"**/*.test.ts",
"**/*.stub.ts",
"node_modules",
"dist"
],
"compilerOptions": {
"incremental": true,
"target": "es2019",
"module": "commonjs",
"declaration": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "node",
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"paths": {
"@vuensight/*": ["./packages/*/"]
}
},
}
================================================
FILE: tsconfig.json
================================================
{
"extends": "./tsconfig.build.json",
"compilerOptions": {
"baseUrl": "./"
},
"include": [
"./packages/*/**.ts"
]
}
gitextract_z_w9sqhw/ ├── .editorconfig ├── .eslintrc ├── .github/ │ └── workflows/ │ └── node.js.yml ├── .gitignore ├── .nvmrc ├── LICENSE.txt ├── README.md ├── package.json ├── packages/ │ ├── app/ │ │ ├── .browserslistrc │ │ ├── .editorconfig │ │ ├── .eslintrc.js │ │ ├── .gitignore │ │ ├── .npmignore │ │ ├── LICENSE.txt │ │ ├── README.md │ │ ├── babel.config.js │ │ ├── jest.config.js │ │ ├── package.json │ │ ├── public/ │ │ │ └── index.html │ │ ├── server/ │ │ │ ├── index.ts │ │ │ └── tsconfig.pkg.json │ │ ├── src/ │ │ │ ├── App.vue │ │ │ ├── assets/ │ │ │ │ └── css/ │ │ │ │ ├── border-radius.css │ │ │ │ ├── box-shadow.css │ │ │ │ ├── color.css │ │ │ │ ├── font.css │ │ │ │ ├── icon.css │ │ │ │ └── spacing.css │ │ │ ├── components/ │ │ │ │ ├── CardCommunicationChannel.test.ts │ │ │ │ ├── CardCommunicationChannel.vue │ │ │ │ ├── ForceGraph.vue │ │ │ │ ├── MenuCommunication.vue │ │ │ │ ├── SidebarCommunication.vue │ │ │ │ ├── SidebarCommunicationEventsTab.vue │ │ │ │ ├── SidebarCommunicationPropsTab.vue │ │ │ │ ├── SidebarCommunicationSlotsTab.vue │ │ │ │ ├── base/ │ │ │ │ │ ├── BaseArrowIcon.vue │ │ │ │ │ ├── BaseBadge.vue │ │ │ │ │ ├── BaseCard.vue │ │ │ │ │ ├── BaseCheckIcon.vue │ │ │ │ │ ├── BaseDelimiter.vue │ │ │ │ │ ├── BaseDropdown.vue │ │ │ │ │ ├── BaseIcon.vue │ │ │ │ │ ├── BaseIconButton.vue │ │ │ │ │ ├── BaseList.vue │ │ │ │ │ ├── BaseLoadingSpinner.vue │ │ │ │ │ ├── BaseRadioButtonGroup.vue │ │ │ │ │ └── BaseSubNav.vue │ │ │ │ ├── icons/ │ │ │ │ │ ├── IconCross.vue │ │ │ │ │ ├── IconFilter.vue │ │ │ │ │ ├── IconSearch.vue │ │ │ │ │ └── IconSelect.vue │ │ │ │ └── layout/ │ │ │ │ └── LayoutSplitView.vue │ │ │ ├── composables/ │ │ │ │ └── fetch.ts │ │ │ ├── main.ts │ │ │ ├── router/ │ │ │ │ └── index.ts │ │ │ ├── services/ │ │ │ │ └── parser.ts │ │ │ ├── shims-vue.d.ts │ │ │ ├── types/ │ │ │ │ ├── color.ts │ │ │ │ ├── force.ts │ │ │ │ └── nodeSizeAttributeType.ts │ │ │ └── views/ │ │ │ └── PageCommunication.vue │ │ └── tsconfig.json │ ├── cli/ │ │ ├── .eslintrc │ │ ├── .gitignore │ │ ├── .npmignore │ │ ├── LICENSE.txt │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ └── index.ts │ │ └── tsconfig.pkg.json │ ├── parser/ │ │ ├── .eslintrc │ │ ├── .gitignore │ │ ├── .npmignore │ │ ├── LICENSE.txt │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── index.ts │ │ │ ├── utils/ │ │ │ │ ├── files.test.ts │ │ │ │ ├── files.ts │ │ │ │ ├── kababize.ts │ │ │ │ ├── kebabize.test.ts │ │ │ │ ├── vue.test.ts │ │ │ │ └── vue.ts │ │ │ └── vue/ │ │ │ ├── analyzer.ts │ │ │ ├── communication-channels.test.ts │ │ │ ├── communication-channels.ts │ │ │ └── dependencies.ts │ │ ├── test/ │ │ │ ├── parsing-process.test.ts │ │ │ └── project/ │ │ │ ├── Child.vue │ │ │ └── Parent.vue │ │ └── tsconfig.pkg.json │ └── types/ │ ├── index.d.ts │ └── package.json ├── tsconfig.build.json └── tsconfig.json
SYMBOL INDEX (12 symbols across 4 files)
FILE: packages/app/src/types/color.ts
type Color (line 1) | type Color = 'mint' | 'red' | 'purple' | 'light-mint' | 'light-red' | 'l...
FILE: packages/app/src/types/force.ts
type Link (line 3) | type Link = {
type ForceLayout (line 8) | type ForceLayout = {
FILE: packages/parser/src/utils/vue.test.ts
constant TEST_FILE (line 8) | const TEST_FILE = '<template>Hello</template><script>const foo = 3;</scr...
constant TEST_FILE_WITHOUT_TEMPLATE (line 9) | const TEST_FILE_WITHOUT_TEMPLATE = '<script>const foo = 3;</script><styl...
FILE: packages/types/index.d.ts
type Mixin (line 1) | interface Mixin {
type Prop (line 6) | type Prop = {
type Event (line 14) | type Event = {
type Slot (line 20) | type Slot = {
type Dependency (line 24) | type Dependency = {
type Dependent (line 28) | type Dependent = {
type VueComponent (line 36) | type VueComponent = {
Condensed preview — 96 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (125K chars).
[
{
"path": ".editorconfig",
"chars": 160,
"preview": "[*.{js,jsx,ts,tsx,vue}]\nindent_style = space\nindent_size = 4\nend_of_line = lf\ntrim_trailing_whitespace = true\ninsert_fin"
},
{
"path": ".eslintrc",
"chars": 381,
"preview": "{\n \"root\": true,\n \"parser\": \"@typescript-eslint/parser\",\n \"plugins\": [\n \"@typescript-eslint\"\n ],\n \"ignorePattern"
},
{
"path": ".github/workflows/node.js.yml",
"chars": 525,
"preview": "name: Push (lint + unit tests)\n\non: [push]\n\njobs:\n build:\n runs-on: ubuntu-latest\n\n strategy:\n matrix:\n "
},
{
"path": ".gitignore",
"chars": 95,
"preview": "node_modules\n\n# Editor directories and files\n.idea\n.vscode\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"
},
{
"path": ".nvmrc",
"chars": 5,
"preview": "18.6\n"
},
{
"path": "LICENSE.txt",
"chars": 1077,
"preview": "MIT License\n\nCopyright (c) 2021-2022 Martina Scharrer\n\nPermission is hereby granted, free of charge, to any person obtai"
},
{
"path": "README.md",
"chars": 2985,
"preview": "# vuensight 👀\nVisualize Vue.js **component relationships** and **communication channels**, i.e. props, events and slots."
},
{
"path": "package.json",
"chars": 1146,
"preview": "{\n \"name\": \"vuensight\",\n \"description\": \"`vuensight` is a cli tool for parsing and visualizing Vue.js projects. The ul"
},
{
"path": "packages/app/.browserslistrc",
"chars": 30,
"preview": "> 1%\nlast 2 versions\nnot dead\n"
},
{
"path": "packages/app/.editorconfig",
"chars": 160,
"preview": "[*.{js,jsx,ts,tsx,vue}]\nindent_style = space\nindent_size = 2\nend_of_line = lf\ntrim_trailing_whitespace = true\ninsert_fin"
},
{
"path": "packages/app/.eslintrc.js",
"chars": 665,
"preview": "module.exports = {\n root: true,\n env: {\n node: true,\n },\n extends: [\n 'plugin:vue/vue3-essential',\n '@vue/a"
},
{
"path": "packages/app/.gitignore",
"chars": 251,
"preview": ".DS_Store\nnode_modules\n\n# local env files\n.env.local\n.env.*.local\n\n# Log files\nnpm-debug.log*\nyarn-debug.log*\nyarn-error"
},
{
"path": "packages/app/.npmignore",
"chars": 101,
"preview": "*\n\n!dist/app/**/*\ndist/app/**/*.js.map\n!dist/server/*.js\n!dist/server/*.d.ts\n!package.json\n!readme.md"
},
{
"path": "packages/app/LICENSE.txt",
"chars": 1077,
"preview": "MIT License\n\nCopyright (c) 2021-2022 Martina Scharrer\n\nPermission is hereby granted, free of charge, to any person obtai"
},
{
"path": "packages/app/README.md",
"chars": 731,
"preview": "# @vuensight/app\n> ⚠️ **General information about usage and setup of `vuensight` can be found [here](https://github.com/"
},
{
"path": "packages/app/babel.config.js",
"chars": 76,
"preview": "module.exports = {\n presets: [\n '@vue/cli-plugin-babel/preset',\n ],\n};\n"
},
{
"path": "packages/app/jest.config.js",
"chars": 277,
"preview": "module.exports = {\n preset: '@vue/cli-plugin-unit-jest/presets/typescript-and-babel',\n transform: {\n '^.+\\\\.vue$': "
},
{
"path": "packages/app/package.json",
"chars": 1893,
"preview": "{\n \"name\": \"@vuensight/app\",\n \"version\": \"0.3.3\",\n \"main\": \"dist/server/index.js\",\n \"description\": \"The front-end of"
},
{
"path": "packages/app/public/index.html",
"chars": 611,
"preview": "<!DOCTYPE html>\n<html lang=\"\">\n <head>\n <meta charset=\"utf-8\">\n <meta http-equiv=\"X-UA-Compatible\" content=\"IE=ed"
},
{
"path": "packages/app/server/index.ts",
"chars": 700,
"preview": "import history from 'connect-history-api-fallback';\nimport express from 'express';\nimport { join } from 'path';\n\nimport "
},
{
"path": "packages/app/server/tsconfig.pkg.json",
"chars": 143,
"preview": "{\n \"extends\": \"../../../tsconfig.build.json\",\n \"compilerOptions\": {\n \"outDir\": \"../dist/server\"\n },\n \"include\": ["
},
{
"path": "packages/app/src/App.vue",
"chars": 1471,
"preview": "<template>\n <router-view/>\n</template>\n\n<style lang=\"scss\">\n@import url('https://fonts.googleapis.com/css2?family=Noto+"
},
{
"path": "packages/app/src/assets/css/border-radius.css",
"chars": 191,
"preview": ":root {\n --border-radius--xs: 2px;\n --border-radius--s: 5px;\n --border-radius--m: 10px;\n --border-radius--l:"
},
{
"path": "packages/app/src/assets/css/box-shadow.css",
"chars": 161,
"preview": ":root {\n --box-shadow--xs: 0px 0px 2px rgba(0,0,0,0.2);\n --box-shadow--s: 0px 0px 4px rgba(0,0,0,0.23);\n --box-"
},
{
"path": "packages/app/src/assets/css/color.css",
"chars": 602,
"preview": ":root {\n --mint-grey: #CBE5DE;\n --mint-10: #CDF6EB;\n --mint-30: #A0E2CF;\n --mint-50: #61c799;\n --mint-70:"
},
{
"path": "packages/app/src/assets/css/font.css",
"chars": 319,
"preview": ":root {\n font-size: 16px;\n\n --font-size--3xs: 0.35em;\n --font-size--2xs: 0.5em;\n --font-size--xs: 0.75em;\n "
},
{
"path": "packages/app/src/assets/css/icon.css",
"chars": 150,
"preview": ":root {\n --icon--xs: 0.75rem;\n --icon--s: 1rem;\n --icon--m: 1.25rem;\n --icon--l: 1.5rem;\n --icon--xl: 1.7"
},
{
"path": "packages/app/src/assets/css/spacing.css",
"chars": 278,
"preview": ":root {\n --spacing--2xs: 0.125rem;\n --spacing--xs: 0.25rem;\n --spacing--s: 0.5rem;\n --spacing--m: 0.75rem;\n "
},
{
"path": "packages/app/src/components/CardCommunicationChannel.test.ts",
"chars": 896,
"preview": "// eslint-disable-next-line import/no-extraneous-dependencies\nimport { mount, MountingOptions } from '@vue/test-utils';\n"
},
{
"path": "packages/app/src/components/CardCommunicationChannel.vue",
"chars": 3633,
"preview": "<template>\n <base-card\n class=\"cardCommunicationChannel\"\n :class=\"{\n [`cardCommunicationChan"
},
{
"path": "packages/app/src/components/ForceGraph.vue",
"chars": 14955,
"preview": "<template>\n <svg ref=\"graphRef\" class=\"forceGraph\"></svg>\n</template>\n\n<script>\nimport {\n defineComponent, ref, onMoun"
},
{
"path": "packages/app/src/components/MenuCommunication.vue",
"chars": 3693,
"preview": "<template>\n <div class=\"menuCommunication\">\n <base-dropdown>\n <template #trigger=\"{ isOpen "
},
{
"path": "packages/app/src/components/SidebarCommunication.vue",
"chars": 1846,
"preview": "<template>\n <div class=\"sidebarCommunication\">\n <h2>{{ component.name }}</h2>\n <p>{{ component.fullPath }}</p"
},
{
"path": "packages/app/src/components/SidebarCommunicationEventsTab.vue",
"chars": 1721,
"preview": "<template>\n <div class=\"sidebarCommunicationPropsTab\">\n <card-communication-channel\n v-for=\"event in even"
},
{
"path": "packages/app/src/components/SidebarCommunicationPropsTab.vue",
"chars": 1633,
"preview": "<template>\n <div class=\"sidebarCommunicationPropsTab\">\n <card-communication-channel\n v-for=\"prop in props"
},
{
"path": "packages/app/src/components/SidebarCommunicationSlotsTab.vue",
"chars": 1635,
"preview": "<template>\n <div class=\"sidebarCommunicationSlotsTab\">\n <card-communication-channel\n v-for=\"slot in slots"
},
{
"path": "packages/app/src/components/base/BaseArrowIcon.vue",
"chars": 1212,
"preview": "<template>\n <svg\n :class=\"{ 'baseArrowIcon--flipped': isFlipped }\"\n class=\"baseArrowIcon\"\n viewB"
},
{
"path": "packages/app/src/components/base/BaseBadge.vue",
"chars": 1252,
"preview": "<template>\n <span\n class=\"baseBadge\"\n :class=\"`baseBadge--${color} ${isRound ? 'baseBade--round' : ''}`"
},
{
"path": "packages/app/src/components/base/BaseCard.vue",
"chars": 1985,
"preview": "<template>\n <button class=\"baseCard\">\n <span class=\"baseCard__header\" :class=\"{'baseCard__header--expanded': i"
},
{
"path": "packages/app/src/components/base/BaseCheckIcon.vue",
"chars": 1641,
"preview": "<template>\n <svg\n class=\"baseCheckIcon\"\n :class=\"{\n [`baseCheckIcon--${color}`]: color,\n "
},
{
"path": "packages/app/src/components/base/BaseDelimiter.vue",
"chars": 686,
"preview": "<template>\n <span\n :class=\"{\n [`baseDelimiter--${color}`]: color,\n }\"\n class=\"baseDel"
},
{
"path": "packages/app/src/components/base/BaseDropdown.vue",
"chars": 1386,
"preview": "<template>\n <div\n ref=\"dropdown\"\n class=\"baseDropdown\"\n :class=\"{'c-appDropdown--open': isOpen}\""
},
{
"path": "packages/app/src/components/base/BaseIcon.vue",
"chars": 1291,
"preview": "<template>\n <svg :aria-labelledby=\"iconName\"\n :class=\"`baseIcon baseIcon--${size}`\"\n role=\"presentati"
},
{
"path": "packages/app/src/components/base/BaseIconButton.vue",
"chars": 739,
"preview": "<template>\n <button class=\"baseIconButton\">\n <base-icon :icon-color=\"iconColor\" :icon-name=\"iconName\" :size=\"s"
},
{
"path": "packages/app/src/components/base/BaseList.vue",
"chars": 1376,
"preview": "<template>\n <ul\n :class=\"{\n [`baseList--${color}`]: color,\n }\"\n class=\"baseList\"\n "
},
{
"path": "packages/app/src/components/base/BaseLoadingSpinner.vue",
"chars": 3249,
"preview": "<template>\n <div class=\"flower-spinner\">\n <div class=\"dots-container\">\n <div class=\"bigger-dot\">\n "
},
{
"path": "packages/app/src/components/base/BaseRadioButtonGroup.vue",
"chars": 1399,
"preview": "<template>\n <form class=\"baseRadioButtonGroup\">\n <label\n v-for=\"option in options\"\n :key"
},
{
"path": "packages/app/src/components/base/BaseSubNav.vue",
"chars": 2130,
"preview": "<template>\n <nav\n class=\"baseSubNav\"\n >\n <router-link\n v-for=\"item in items\"\n "
},
{
"path": "packages/app/src/components/icons/IconCross.vue",
"chars": 946,
"preview": "<template>\n <!-- eslint-disable-next-line max-len -->\n <path d=\"M 3.601562 1.949219 L 9 7.347656 L 14.398438 1.964"
},
{
"path": "packages/app/src/components/icons/IconFilter.vue",
"chars": 2361,
"preview": "<template>\n <!-- eslint-disable max-len -->\n <path d=\"M 2.46875 16.507812 C 2.46875 16.828125 2.722656 17.078125 3"
},
{
"path": "packages/app/src/components/icons/IconSearch.vue",
"chars": 898,
"preview": "<template>\n <!-- eslint-disable-next-line max-len -->\n <path d=\"M 7.199219 13.492188 C 8.734375 13.492188 10.12890"
},
{
"path": "packages/app/src/components/icons/IconSelect.vue",
"chars": 2038,
"preview": "<template>\n <!-- eslint-disable-next-line max-len -->\n <path d=\"M 8.773438 13.5 C 7.574219 13.4375 6.5625 12.97656"
},
{
"path": "packages/app/src/components/layout/LayoutSplitView.vue",
"chars": 793,
"preview": "<template>\n <div class=\"layoutSplitView\">\n <main class=\"layoutSplitView__main\">\n <slot />\n <"
},
{
"path": "packages/app/src/composables/fetch.ts",
"chars": 662,
"preview": "import { ref, readonly } from 'vue';\n\nexport const useFetch = (fetcher: () => Promise<any>) => { // eslint-disable-line "
},
{
"path": "packages/app/src/main.ts",
"chars": 136,
"preview": "import { createApp } from 'vue';\nimport App from './App.vue';\nimport router from './router';\n\ncreateApp(App).use(router)"
},
{
"path": "packages/app/src/router/index.ts",
"chars": 1027,
"preview": "import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';\nimport PageCommunication from '../views/Pag"
},
{
"path": "packages/app/src/services/parser.ts",
"chars": 186,
"preview": "import { VueComponent } from '@vuensight/types';\n\nexport const get = ():Promise<VueComponent[]> => fetch('/parse-result'"
},
{
"path": "packages/app/src/shims-vue.d.ts",
"chars": 167,
"preview": "/* eslint-disable */\ndeclare module '*.vue' {\n import type { DefineComponent } from 'vue'\n const component: DefineComp"
},
{
"path": "packages/app/src/types/color.ts",
"chars": 93,
"preview": "export type Color = 'mint' | 'red' | 'purple' | 'light-mint' | 'light-red' | 'light-purple';\n"
},
{
"path": "packages/app/src/types/force.ts",
"chars": 190,
"preview": "import { VueComponent } from '@vuensight/types';\n\nexport type Link = {\n source: string,\n target: string,\n}\n\nexport"
},
{
"path": "packages/app/src/types/nodeSizeAttributeType.ts",
"chars": 265,
"preview": "const nodeSizeAttributeType = {\n PROP: 'props',\n EVENT: 'events',\n SLOT: 'slots',\n CHANNELS: 'channels',\n DEPENDENT"
},
{
"path": "packages/app/src/views/PageCommunication.vue",
"chars": 5265,
"preview": "<template>\n <layout-split-view>\n <div\n v-if=\"isLoading\"\n class=\"pageCommunication__loadi"
},
{
"path": "packages/app/tsconfig.json",
"chars": 730,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"esnext\",\n \"module\": \"esnext\",\n \"strict\": true,\n \"jsx\": \"preserve\",\n "
},
{
"path": "packages/cli/.eslintrc",
"chars": 40,
"preview": "{\n \"ignorePatterns\": [\"bin/**/*.js\"]\n}\n"
},
{
"path": "packages/cli/.gitignore",
"chars": 6,
"preview": "/dist\n"
},
{
"path": "packages/cli/.npmignore",
"chars": 34,
"preview": "*\n!dist/*\n!package.json\n!readme.md"
},
{
"path": "packages/cli/LICENSE.txt",
"chars": 1077,
"preview": "MIT License\n\nCopyright (c) 2021-2022 Martina Scharrer\n\nPermission is hereby granted, free of charge, to any person obtai"
},
{
"path": "packages/cli/README.md",
"chars": 1878,
"preview": "# vuensight 👀\nVisualize Vue.js **component relationships** and **communication channels**, i.e. props, events and slots."
},
{
"path": "packages/cli/package.json",
"chars": 864,
"preview": "{\n \"name\": \"@vuensight/cli\",\n \"version\": \"0.1.5\",\n \"description\": \"Command line interface of @vuensight.\",\n \"main\": "
},
{
"path": "packages/cli/src/index.ts",
"chars": 889,
"preview": "#!/usr/bin/env node\n\nimport { program } from 'commander';\nimport startServer from '@vuensight/app';\n\nprogram\n .descri"
},
{
"path": "packages/cli/tsconfig.pkg.json",
"chars": 185,
"preview": "{\n \"extends\": \"../../tsconfig.build.json\",\n \"compilerOptions\": {\n \"outDir\": \"./dist/\"\n },\n \"include\": [\n \"./sr"
},
{
"path": "packages/parser/.eslintrc",
"chars": 43,
"preview": "{\n \"ignorePatterns\": [\"test/project/*\"]\n}\n"
},
{
"path": "packages/parser/.gitignore",
"chars": 16,
"preview": "/dist\n/coverage\n"
},
{
"path": "packages/parser/.npmignore",
"chars": 70,
"preview": "*\n!dist/**/*.js\ndist/tsconfig.pkg.tsbuildinfo\n!package.json\n!readme.md"
},
{
"path": "packages/parser/LICENSE.txt",
"chars": 1077,
"preview": "MIT License\n\nCopyright (c) 2021-2022 Martina Scharrer\n\nPermission is hereby granted, free of charge, to any person obtai"
},
{
"path": "packages/parser/README.md",
"chars": 522,
"preview": "# @vuensight/parser\n> ⚠️ **General information about usage and setup of `vuensight` can be found [here](https://github.c"
},
{
"path": "packages/parser/package.json",
"chars": 1439,
"preview": "{\n \"name\": \"@vuensight/parser\",\n \"version\": \"0.1.8\",\n \"main\": \"dist/index.js\",\n \"description\": \"This parser extracts"
},
{
"path": "packages/parser/src/index.ts",
"chars": 627,
"preview": "import { VueComponent } from '@vuensight/types';\n\nimport { findDependencies } from './vue/dependencies';\nimport { analy"
},
{
"path": "packages/parser/src/utils/files.test.ts",
"chars": 438,
"preview": "import { getFileNameFromPath } from './files';\n\ndescribe('files', () => {\n describe('getFileNameFromPath', () => {\n "
},
{
"path": "packages/parser/src/utils/files.ts",
"chars": 237,
"preview": "export const getFileNameFromPath = (path: string): string => {\n const lastDirectoryIndex = path.lastIndexOf('\\\\') !== -"
},
{
"path": "packages/parser/src/utils/kababize.ts",
"chars": 232,
"preview": "export const kebabize = (str: string):string => str.split('')\n .map((letter, idx) => (letter.toUpperCase() === letter\n "
},
{
"path": "packages/parser/src/utils/kebabize.test.ts",
"chars": 184,
"preview": "import { kebabize } from './kababize';\n\ndescribe('kebabize', () => {\n it('should convert the string to kebab-style', ()"
},
{
"path": "packages/parser/src/utils/vue.test.ts",
"chars": 2093,
"preview": "import {\n extractScriptContent,\n findTemplate,\n getComponentImportName,\n getTemplateContent\n} from './vue';\n\nconst T"
},
{
"path": "packages/parser/src/utils/vue.ts",
"chars": 1131,
"preview": "export const findTemplate = (fileContent: string): string | null => {\n const templateBody = fileContent.match(/(?<templ"
},
{
"path": "packages/parser/src/vue/analyzer.ts",
"chars": 2457,
"preview": "import { readFileSync } from 'fs';\nimport { normalize } from 'path';\nimport { IModule } from 'dependency-cruiser';\n\nimpo"
},
{
"path": "packages/parser/src/vue/communication-channels.test.ts",
"chars": 7028,
"preview": "import { JSDOM } from 'jsdom';\n\nimport {\n findDependencyInstancesInTemplate,\n isPropUsed,\n isEventUsed,\n isS"
},
{
"path": "packages/parser/src/vue/communication-channels.ts",
"chars": 3809,
"preview": "import { parse, PropDescriptor } from 'vue-docgen-api';\nimport { JSDOM } from 'jsdom';\nimport { Dependent, Event, Prop, "
},
{
"path": "packages/parser/src/vue/dependencies.ts",
"chars": 2045,
"preview": "import { normalize } from 'path';\nimport {\n cruise, IDependency, IReporterOutput, IModule\n} from 'dependency-cruiser';\n"
},
{
"path": "packages/parser/test/parsing-process.test.ts",
"chars": 1655,
"preview": "import { parse } from '../src';\nimport { normalize } from 'path';\n\ndescribe('parse', () => {\n it('should return data "
},
{
"path": "packages/parser/test/project/Child.vue",
"chars": 344,
"preview": "<template>\n <div>\n <slot name=\"header\" />\n Test {{ title }}\n <button @click=\"$emit('selected')\">"
},
{
"path": "packages/parser/test/project/Parent.vue",
"chars": 360,
"preview": "<template>\n <div>\n <Child title=\"test\" @selected=\"select\">\n <template #header>Important Information"
},
{
"path": "packages/parser/tsconfig.pkg.json",
"chars": 134,
"preview": "{\n \"extends\": \"../../tsconfig.build.json\",\n \"compilerOptions\": {\n \"outDir\": \"./dist/\"\n },\n \"include\": [\n \"./sr"
},
{
"path": "packages/types/index.d.ts",
"chars": 833,
"preview": "export interface Mixin {\n name: string\n path: string\n}\n\nexport type Prop = {\n name: string,\n type?: { name: string; "
},
{
"path": "packages/types/package.json",
"chars": 636,
"preview": "{\n \"name\": \"@vuensight/types\",\n \"version\": \"0.1.0\",\n \"description\": \"The shared types package for @vuensight.\",\n \"ma"
},
{
"path": "tsconfig.build.json",
"chars": 459,
"preview": "{\n \"exclude\": [\n \"**/*.test.ts\",\n \"**/*.stub.ts\",\n \"node_modules\",\n \"dist\"\n ],\n \"compilerOptions\": {\n "
},
{
"path": "tsconfig.json",
"chars": 133,
"preview": "{\n \"extends\": \"./tsconfig.build.json\",\n \"compilerOptions\": {\n \"baseUrl\": \"./\"\n },\n \"include\": [\n \"./packages/*"
}
]
About this extraction
This page contains the full source code of the martinascharrer/vuensight GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 96 files (110.7 KB), approximately 33.5k tokens, and a symbol index with 12 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.