web/frpc: upgrade vue and element-plus (#3322)

This commit is contained in:
fatedier 2023-02-20 23:52:55 +08:00 committed by GitHub
parent 24f0b3afa5
commit fe8374e99b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 2910 additions and 5801 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +1,16 @@
<!doctype html> <html lang=en> <head> <meta charset=utf-8> <title>frp client admin UI</title> <link rel="shortcut icon" href="favicon.ico"></head> <body> <div id=app></div> <script type="text/javascript" src="manifest.js?5d5774096cf5c1b4d5af"></script><script type="text/javascript" src="vendor.js?dc42700731a508d39009"></script></body> </html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>frp client admin UI</title>
<script type="module" crossorigin src="./index-fec891f3.js"></script>
<link rel="stylesheet" href="./index-26827c97.css">
</head>
<body>
<div id="app"></div>
</body>
</html>

View File

@ -1 +0,0 @@
!function(e){function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}var r=window.webpackJsonp;window.webpackJsonp=function(t,c,u){for(var i,a,f,l=0,s=[];l<t.length;l++)a=t[l],o[a]&&s.push(o[a][0]),o[a]=0;for(i in c)Object.prototype.hasOwnProperty.call(c,i)&&(e[i]=c[i]);for(r&&r(t,c,u);s.length;)s.shift()();if(u)for(l=0;l<u.length;l++)f=n(n.s=u[l]);return f};var t={},o={1:0};n.e=function(e){function r(){i.onerror=i.onload=null,clearTimeout(a);var n=o[e];0!==n&&(n&&n[1](new Error("Loading chunk "+e+" failed.")),o[e]=void 0)}var t=o[e];if(0===t)return new Promise(function(e){e()});if(t)return t[2];var c=new Promise(function(n,r){t=o[e]=[n,r]});t[2]=c;var u=document.getElementsByTagName("head")[0],i=document.createElement("script");i.type="text/javascript",i.charset="utf-8",i.async=!0,i.timeout=12e4,n.nc&&i.setAttribute("nonce",n.nc),i.src=n.p+""+e+".js?"+{0:"dc42700731a508d39009"}[e];var a=setTimeout(r,12e4);return i.onerror=i.onload=r,u.appendChild(i),c},n.m=e,n.c=t,n.i=function(e){return e},n.d=function(e,r,t){n.o(e,r)||Object.defineProperty(e,r,{configurable:!1,enumerable:!0,get:t})},n.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(r,"a",r),r},n.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},n.p="",n.oe=function(e){throw console.error(e),e}}([]);

File diff suppressed because one or more lines are too long

View File

@ -1,14 +0,0 @@
{
"presets": [
["es2015", { "modules": false }]
],
"plugins": [
[
"component",
{
"libraryName": "element-ui",
"styleLibraryName": "theme-chalk"
}
]
]
}

30
web/frpc/.eslintrc.cjs Normal file
View File

@ -0,0 +1,30 @@
/* eslint-env node */
require("@rushstack/eslint-patch/modern-module-resolution");
module.exports = {
root: true,
extends: [
"plugin:vue/vue3-essential",
"eslint:recommended",
"@vue/eslint-config-typescript",
"@vue/eslint-config-prettier",
],
parserOptions: {
ecmaVersion: "latest",
},
rules: {
"@typescript-eslint/no-unused-vars": [
"warn",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
},
],
"vue/multi-word-component-names": [
"error",
{
ignores: ["Overview"],
},
],
},
};

30
web/frpc/.gitignore vendored
View File

@ -1,6 +1,28 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
node_modules/
dist/
npm-debug.log
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.vscode/settings.json
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@ -0,0 +1 @@
{}

View File

@ -1,6 +1,13 @@
.PHONY: dist build
.PHONY: dist build preview lint
build:
@npm run build
dev:
@npm run dev
preview:
@npm run preview
lint:
@npm run lint

25
web/frpc/README.md Normal file
View File

@ -0,0 +1,25 @@
# frpc-dashboard
## Project Setup
```sh
yarn install
```
### Compile and Hot-Reload for Development
```sh
make dev
```
### Type-Check, Compile and Minify for Production
```sh
make build
```
### Lint with [ESLint](https://eslint.org/)
```sh
make lint
```

8
web/frpc/auto-imports.d.ts vendored Normal file
View File

@ -0,0 +1,8 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// Generated by unplugin-auto-import
export {}
declare global {
}

25
web/frpc/components.d.ts vendored Normal file
View File

@ -0,0 +1,25 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
import '@vue/runtime-core'
export {}
declare module '@vue/runtime-core' {
export interface GlobalComponents {
ClientConfigure: typeof import('./src/components/ClientConfigure.vue')['default']
ElButton: typeof import('element-plus/es')['ElButton']
ElCol: typeof import('element-plus/es')['ElCol']
ElInput: typeof import('element-plus/es')['ElInput']
ElMenu: typeof import('element-plus/es')['ElMenu']
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
ElRow: typeof import('element-plus/es')['ElRow']
ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
Overview: typeof import('./src/components/Overview.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
}
}

1
web/frpc/env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@ -8,8 +8,7 @@
<body>
<div id="app"></div>
<!--<script src="https://code.jquery.com/jquery-3.2.0.min.js"></script>-->
<!--<script src="//cdn.bootcss.com/echarts/3.4.0/echarts.min.js"></script>-->
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@ -1,46 +1,35 @@
{
"name": "frpc-web",
"description": "An admin web ui for frp client.",
"author": "fatedier",
"name": "-frpc-dashboard",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "webpack-dev-server -d --inline --hot --env.dev",
"build": "rimraf dist && webpack -p --progress --hide-modules"
"dev": "vite",
"build": "run-p type-check build-only",
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --noEmit",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
},
"dependencies": {
"element-ui": "^2.5.3",
"vue": "^2.5.22",
"vue-resource": "^1.5.1",
"vue-router": "^3.0.2",
"whatwg-fetch": "^3.0.0"
},
"engines": {
"node": ">=6"
"element-plus": "^2.2.28",
"vue": "^3.2.45",
"vue-router": "^4.1.6"
},
"devDependencies": {
"autoprefixer": "^9.4.7",
"babel-core": "^6.26.3",
"babel-eslint": "^10.0.1",
"babel-loader": "^7.1.5",
"babel-plugin-component": "^1.1.1",
"babel-preset-es2015": "^6.24.1",
"css-loader": "^2.1.0",
"eslint": "^5.12.1",
"eslint-config-enough": "^0.3.4",
"eslint-loader": "^2.1.1",
"file-loader": "^3.0.1",
"html-loader": "^0.5.5",
"html-webpack-plugin": "^2.24.1",
"less": "^3.9.0",
"less-loader": "^4.1.0",
"postcss-loader": "^3.0.0",
"rimraf": "^2.6.3",
"style-loader": "^0.23.1",
"url-loader": "^1.1.2",
"vue-loader": "^15.6.2",
"vue-template-compiler": "^2.5.22",
"webpack": "^2.7.0",
"webpack-cli": "^3.2.1",
"webpack-dev-server": "^3.1.14"
"@rushstack/eslint-patch": "^1.1.4",
"@types/node": "^18.11.12",
"@vitejs/plugin-vue": "^4.0.0",
"@vue/eslint-config-prettier": "^7.0.0",
"@vue/eslint-config-typescript": "^11.0.0",
"@vue/tsconfig": "^0.1.3",
"eslint": "^8.22.0",
"eslint-plugin-vue": "^9.3.0",
"npm-run-all": "^4.1.5",
"prettier": "^2.7.1",
"typescript": "~4.7.4",
"unplugin-auto-import": "^0.14.3",
"unplugin-vue-components": "^0.24.0",
"vite": "^4.0.0",
"vue-tsc": "^1.0.12"
}
}

View File

@ -1,5 +0,0 @@
module.exports = {
plugins: [
require('autoprefixer')()
]
}

View File

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@ -8,7 +8,13 @@
<section>
<el-row>
<el-col id="side-nav" :xs="24" :md="4">
<el-menu default-active="1" mode="vertical" theme="light" router="false" @select="handleSelect">
<el-menu
default-active="1"
mode="vertical"
theme="light"
router="false"
@select="handleSelect"
>
<el-menu-item index="/">Overview</el-menu-item>
<el-menu-item index="/configure">Configure</el-menu-item>
<el-menu-item index="">Help</el-menu-item>
@ -26,16 +32,12 @@
</div>
</template>
<script>
export default {
methods: {
handleSelect(key, path) {
if (key == '') {
window.open("https://github.com/fatedier/frp")
}
}
}
<script setup lang="ts">
const handleSelect = (key: string) => {
if (key == "") {
window.open("https://github.com/fatedier/frp");
}
};
</script>
<style>
@ -51,7 +53,7 @@
}
.header-color {
background: #58B7FF;
background: #58b7ff;
}
#content {

View File

@ -0,0 +1,102 @@
<template>
<div>
<el-row id="head">
<el-button type="primary" @click="fetchData">Refresh</el-button>
<el-button type="primary" @click="uploadConfig">Upload</el-button>
</el-row>
<el-input
type="textarea"
autosize
v-model="textarea"
placeholder="frpc configrue file, can not be empty..."
></el-input>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
let textarea = ref("");
const fetchData = () => {
fetch("/api/config", { credentials: "include" })
.then((res) => {
return res.text();
})
.then((text) => {
textarea.value = text;
})
.catch(() => {
ElMessage({
showClose: true,
message: "Get configure content from frpc failed!",
type: "warning",
});
});
};
const uploadConfig = () => {
ElMessageBox.confirm(
"This operation will upload your frpc configure file content and hot reload it, do you want to continue?",
"Notice",
{
confirmButtonText: "Yes",
cancelButtonText: "No",
type: "warning",
}
)
.then(() => {
if (textarea.value == "") {
ElMessage({
message: "Configure content can not be empty!",
type: "warning",
});
return;
}
fetch("/api/config", {
credentials: "include",
method: "PUT",
body: textarea.value,
})
.then(() => {
fetch("/api/reload", { credentials: "include" })
.then(() => {
ElMessage({
type: "success",
message: "Success",
});
})
.catch((err) => {
ElMessage({
showClose: true,
message: "Reload frpc configure file error, " + err,
type: "warning",
});
});
})
.catch(() => {
ElMessage({
showClose: true,
message: "Put config to frpc and hot reload failed!",
type: "warning",
});
});
})
.catch(() => {
ElMessage({
message: "Canceled",
type: "info",
});
});
};
fetchData();
</script>
<style>
#head {
margin-bottom: 30px;
}
</style>

View File

@ -1,93 +0,0 @@
<template>
<div>
<el-row id="head">
<el-button type="primary" @click="fetchData">Refresh</el-button>
<el-button type="primary" @click="uploadConfig">Upload</el-button>
</el-row>
<el-input type="textarea" autosize v-model="textarea" placeholder="frpc configrue file, can not be empty..."></el-input>
</div>
</template>
<script>
export default {
data() {
return {
textarea: ''
}
},
created() {
this.fetchData()
},
watch: {
'$route': 'fetchData'
},
methods: {
fetchData() {
fetch('/api/config', {credentials: 'include'})
.then(res => {
return res.text()
}).then(text => {
this.textarea= text
}).catch( err => {
this.$message({
showClose: true,
message: 'Get configure content from frpc failed!',
type: 'warning'
})
})
},
uploadConfig() {
this.$confirm('This operation will upload your frpc configure file content and hot reload it, do you want to continue?', 'Notice', {
confirmButtonText: 'Yes',
cancelButtonText: 'No',
type: 'warning'
}).then(() => {
if (this.textarea == "") {
this.$message({
type: 'warning',
message: 'Configure content can not be empty!'
})
return
}
fetch('/api/config', {
credentials: 'include',
method: 'PUT',
body: this.textarea,
}).then(() => {
fetch('/api/reload', {credentials: 'include'})
.then(() => {
this.$message({
type: 'success',
message: 'Success'
})
}).catch(err => {
this.$message({
showClose: true,
message: 'Reload frpc configure file error, ' + err,
type: 'warning'
})
})
}).catch(err => {
this.$message({
showClose: true,
message: 'Put config to frpc and hot reload failed!',
type: 'warning'
})
})
}).catch(() => {
this.$message({
type: 'info',
message: 'Canceled'
})
})
}
}
}
</script>
<style>
#head {
margin-bottom: 30px;
}
</style>

View File

@ -3,13 +3,37 @@
<el-row>
<el-col :md="24">
<div>
<el-table :data="status" stripe style="width: 100%" :default-sort="{prop: 'type', order: 'ascending'}">
<el-table
:data="status"
stripe
style="width: 100%"
:default-sort="{ prop: 'type', order: 'ascending' }"
>
<el-table-column prop="name" label="name"></el-table-column>
<el-table-column prop="type" label="type" width="150"></el-table-column>
<el-table-column prop="local_addr" label="local address" width="200"></el-table-column>
<el-table-column prop="plugin" label="plugin" width="200"></el-table-column>
<el-table-column prop="remote_addr" label="remote address"></el-table-column>
<el-table-column prop="status" label="status" width="150"></el-table-column>
<el-table-column
prop="type"
label="type"
width="150"
></el-table-column>
<el-table-column
prop="local_addr"
label="local address"
width="200"
></el-table-column>
<el-table-column
prop="plugin"
label="plugin"
width="200"
></el-table-column>
<el-table-column
prop="remote_addr"
label="remote address"
></el-table-column>
<el-table-column
prop="status"
label="status"
width="150"
></el-table-column>
<el-table-column prop="err" label="info"></el-table-column>
</el-table>
</div>
@ -18,58 +42,50 @@
</div>
</template>
<script>
export default {
data() {
return {
status: new Array(),
}
},
created() {
this.fetchData()
},
watch: {
'$route': 'fetchData'
},
methods: {
fetchData() {
fetch('/api/status', {credentials: 'include'})
.then(res => {
return res.json()
}).then(json => {
this.status = new Array()
<script setup lang="ts">
import { ref } from "vue";
import { ElMessage } from "element-plus";
let status = ref<any[]>([]);
const fetchData = () => {
fetch("/api/status", { credentials: "include" })
.then((res) => {
return res.json();
})
.then((json) => {
status.value = new Array();
for (let s of json.tcp) {
this.status.push(s)
status.value.push(s);
}
for (let s of json.udp) {
this.status.push(s)
status.value.push(s);
}
for (let s of json.http) {
this.status.push(s)
status.value.push(s);
}
for (let s of json.https) {
this.status.push(s)
status.value.push(s);
}
for (let s of json.stcp) {
this.status.push(s)
status.value.push(s);
}
for (let s of json.sudp) {
this.status.push(s)
status.value.push(s);
}
for (let s of json.xtcp) {
this.status.push(s)
status.value.push(s);
}
}).catch( err => {
this.$message({
})
.catch(() => {
ElMessage({
showClose: true,
message: 'Get status info from frpc failed!',
type: 'warning'
})
})
}
}
}
message: "Get status info from frpc failed!",
type: "warning",
});
});
};
fetchData();
</script>
<style>
</style>
<style></style>

View File

@ -1,52 +0,0 @@
import Vue from 'vue'
// import ElementUI from 'element-ui'
import {
Button,
Form,
FormItem,
Row,
Col,
Table,
TableColumn,
Menu,
MenuItem,
MessageBox,
Message,
Input
} from 'element-ui'
import lang from 'element-ui/lib/locale/lang/en'
import locale from 'element-ui/lib/locale'
import 'element-ui/lib/theme-chalk/index.css'
import './utils/less/custom.less'
import App from './App.vue'
import router from './router'
import 'whatwg-fetch'
locale.use(lang)
Vue.use(Button)
Vue.use(Form)
Vue.use(FormItem)
Vue.use(Row)
Vue.use(Col)
Vue.use(Table)
Vue.use(TableColumn)
Vue.use(Menu)
Vue.use(MenuItem)
Vue.use(Input)
Vue.prototype.$msgbox = MessageBox;
Vue.prototype.$confirm = MessageBox.confirm
Vue.prototype.$message = Message
//Vue.use(ElementUI)
Vue.config.productionTip = false
new Vue({
el: '#app',
router,
template: '<App/>',
components: { App }
})

12
web/frpc/src/main.ts Normal file
View File

@ -0,0 +1,12 @@
import { createApp } from "vue";
import "element-plus/dist/index.css";
import App from "./App.vue";
import router from "./router";
// import './assets/custom.css'
const app = createApp(App);
app.use(router);
app.mount("#app");

View File

@ -1,18 +0,0 @@
import Vue from 'vue'
import Router from 'vue-router'
import Overview from '../components/Overview.vue'
import Configure from '../components/Configure.vue'
Vue.use(Router)
export default new Router({
routes: [{
path: '/',
name: 'Overview',
component: Overview
},{
path: '/configure',
name: 'Configure',
component: Configure,
}]
})

View File

@ -0,0 +1,21 @@
import { createRouter, createWebHashHistory } from "vue-router";
import Overview from "../components/Overview.vue";
import ClientConfigure from "../components/ClientConfigure.vue";
const router = createRouter({
history: createWebHashHistory(),
routes: [
{
path: "/",
name: "Overview",
component: Overview,
},
{
path: "/configure",
name: "ClientConfigure",
component: ClientConfigure,
},
],
});
export default router;

View File

@ -1,22 +0,0 @@
@color: red;
.el-form-item {
span {
margin-left: 15px;
}
}
.demo-table-expand {
font-size: 0;
label {
width: 90px;
color: #99a9bf;
}
.el-form-item {
margin-right: 0;
margin-bottom: 0;
width: 50%;
}
}

View File

@ -1,13 +0,0 @@
class ProxyStatus {
constructor(status) {
this.name = status.name
this.type = status.type
this.status = status.status
this.err = status.err
this.local_addr = status.local_addr
this.plugin = status.plugin
this.remote_addr = status.remote_addr
}
}
export {ProxyStatus}

View File

@ -0,0 +1,8 @@
{
"extends": "@vue/tsconfig/tsconfig.node.json",
"include": ["vite.config.*", "vitest.config.*", "cypress.config.*", "playwright.config.*"],
"compilerOptions": {
"composite": true,
"types": ["node"]
}
}

16
web/frpc/tsconfig.json Normal file
View File

@ -0,0 +1,16 @@
{
"extends": "@vue/tsconfig/tsconfig.web.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"references": [
{
"path": "./tsconfig.config.json"
}
]
}

29
web/frpc/vite.config.ts Normal file
View File

@ -0,0 +1,29 @@
import { fileURLToPath, URL } from "node:url";
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import AutoImport from "unplugin-auto-import/vite";
import Components from "unplugin-vue-components/vite";
import { ElementPlusResolver } from "unplugin-vue-components/resolvers";
// https://vitejs.dev/config/
export default defineConfig({
base: "",
plugins: [
vue(),
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
},
},
build: {
assetsDir: "",
},
});

View File

@ -1,107 +0,0 @@
const path = require('path')
var webpack = require('webpack')
var HtmlWebpackPlugin = require('html-webpack-plugin')
var VueLoaderPlugin = require('vue-loader/lib/plugin')
var url = require('url')
var publicPath = ''
module.exports = (options = {}) => ({
entry: {
vendor: './src/main'
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: options.dev ? '[name].js' : '[name].js?[chunkhash]',
chunkFilename: '[id].js?[chunkhash]',
publicPath: options.dev ? '/assets/' : publicPath
},
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
'vue$': 'vue/dist/vue.esm.js',
'@': path.resolve(__dirname, 'src'),
}
},
module: {
rules: [{
test: /\.vue$/,
loader: 'vue-loader'
}, {
test: /\.js$/,
use: ['babel-loader'],
exclude: /node_modules/
}, {
test: /\.html$/,
use: [{
loader: 'html-loader',
options: {
root: path.resolve(__dirname, 'src'),
attrs: ['img:src', 'link:href']
}
}]
}, {
test: /\.less$/,
loader: 'style-loader!css-loader!postcss-loader!less-loader'
}, {
test: /\.css$/,
use: ['style-loader', 'css-loader', 'postcss-loader']
}, {
test: /favicon\.png$/,
use: [{
loader: 'file-loader',
options: {
name: '[name].[ext]?[hash]'
}
}]
}, {
test: /\.(png|jpg|jpeg|gif|eot|ttf|woff|woff2|svg|svgz)(\?.+)?$/,
exclude: /favicon\.png$/,
use: [{
loader: 'url-loader',
options: {
limit: 10000
}
}]
}]
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
names: ['vendor', 'manifest']
}),
new HtmlWebpackPlugin({
favicon: 'src/assets/favicon.ico',
template: 'src/index.html'
}),
new webpack.NormalModuleReplacementPlugin(/element-ui[\/\\]lib[\/\\]locale[\/\\]lang[\/\\]zh-CN/, 'element-ui/lib/locale/lang/en'),
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: '"production"'
}
}),
new webpack.optimize.UglifyJsPlugin({
sourceMap: false,
comments: false,
compress: {
warnings: false
}
}),
new VueLoaderPlugin()
],
devServer: {
host: '127.0.0.1',
port: 8010,
proxy: {
'/api/': {
target: 'http://127.0.0.1:8080',
changeOrigin: true,
pathRewrite: {
'^/api': ''
}
}
},
historyApiFallback: {
index: url.parse(options.dev ? '/assets/' : publicPath).pathname
}
}//,
//devtool: options.dev ? '#eval-source-map' : '#source-map'
})

File diff suppressed because it is too large Load Diff

View File

@ -1,46 +1,25 @@
# frps-dashboard
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
## Type Support for `.vue` Imports in TS
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
1. Disable the built-in TypeScript Extension
1) Run `Extensions: Show Built-in Extensions` from VSCode's command palette
2) Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
## Customize configuration
See [Vite Configuration Reference](https://vitejs.dev/config/).
## Project Setup
```sh
npm install
yarn install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
make dev
```
### Type-Check, Compile and Minify for Production
```sh
npm run build
make build
```
### Lint with [ESLint](https://eslint.org/)
```sh
npm run lint
make lint
```