├── README.md
├── .github
└── FUNDING.yml
├── pnpm-workspace.yaml
├── .gitignore
├── packages
├── vscode
│ ├── .vscodeignore
│ ├── images
│ │ ├── preview-bg.png
│ │ ├── nuxt-logo.svg
│ │ └── vite-logo.svg
│ ├── tsconfig.build.json
│ ├── src
│ │ ├── utils
│ │ │ ├── fs.ts
│ │ │ ├── http.ts
│ │ │ └── quickPick.ts
│ │ └── extension.ts
│ ├── LICENSE
│ ├── scripts
│ │ └── build.js
│ └── package.json
└── core
│ ├── tsconfig.build.json
│ ├── package.json
│ ├── LICENSE
│ ├── bin
│ ├── nuxi.js
│ ├── nuxi
│ │ ├── configExtraContent.ts
│ │ └── plugin.ts
│ ├── vite.js
│ └── vite
│ │ └── config.ts
│ └── src
│ └── index.ts
├── tsconfig.build.json
├── lerna.json
├── .vscode
└── launch.json
├── tsconfig.json
├── package.json
└── LICENSE
/README.md:
--------------------------------------------------------------------------------
1 | # Vue Preview
2 |
3 | TODO
4 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: johnsoncodehk
2 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - 'packages/*'
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | out
2 | dist
3 | node_modules
4 | *.tsbuildinfo
5 | *.vsix
6 |
--------------------------------------------------------------------------------
/packages/vscode/.vscodeignore:
--------------------------------------------------------------------------------
1 | out
2 | src
3 | scripts
4 | tsconfig.build.json
5 | tsconfig.build.tsbuildinfo
6 |
--------------------------------------------------------------------------------
/packages/vscode/images/preview-bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johnsoncodehk/vue-preview/HEAD/packages/vscode/images/preview-bg.png
--------------------------------------------------------------------------------
/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [],
3 | "include": [],
4 | "references": [
5 | // Extensions
6 | {
7 | "path": "./packages/vscode/tsconfig.build.json"
8 | }
9 | ]
10 | }
--------------------------------------------------------------------------------
/lerna.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://raw.githubusercontent.com/lerna-lite/lerna-lite/main/packages/cli/schemas/lerna-schema.json",
3 | "npmClient": "pnpm",
4 | "packages": [
5 | "packages/*"
6 | ],
7 | "version": "0.1.0"
8 | }
9 |
--------------------------------------------------------------------------------
/packages/core/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "noEmit": false,
5 | "outDir": "out",
6 | "rootDir": "src",
7 | },
8 | "include": [
9 | "src"
10 | ],
11 | "exclude": [
12 | "node_modules",
13 | ".vscode-test"
14 | ],
15 | }
--------------------------------------------------------------------------------
/packages/vscode/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "noEmit": false,
5 | "outDir": "out",
6 | "rootDir": "src",
7 | },
8 | "include": [
9 | "src",
10 | ],
11 | "references": [
12 | {
13 | "path": "../core/tsconfig.build.json"
14 | }
15 | ]
16 | }
--------------------------------------------------------------------------------
/packages/vscode/src/utils/fs.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode';
2 |
3 | export async function readFile(uri: vscode.Uri) {
4 | const data = await vscode.workspace.fs.readFile(uri);
5 | return new TextDecoder('utf8').decode(data);
6 | }
7 |
8 | export async function exists(uri: vscode.Uri) {
9 | try {
10 | await vscode.workspace.fs.stat(uri);
11 | return true;
12 | }
13 | catch {
14 | return false;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/packages/vscode/images/nuxt-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/packages/core/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@vue-preview/core",
3 | "version": "0.1.0",
4 | "license": "MIT",
5 | "files": [
6 | "bin",
7 | "out/**/*.js",
8 | "out/**/*.d.ts"
9 | ],
10 | "repository": {
11 | "type": "git",
12 | "url": "https://github.com/johnsoncodehk/vue-preview.git",
13 | "directory": "packages/core"
14 | },
15 | "main": "out/index.js",
16 | "devDependencies": {
17 | "@types/ws": "^8.5.4"
18 | },
19 | "dependencies": {
20 | "ws": "^8.12.0"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/packages/vscode/src/utils/http.ts:
--------------------------------------------------------------------------------
1 | import * as http from 'http';
2 |
3 | export function isLocalHostPortUsing(port: number) {
4 | return new Promise(resolve => {
5 | http.get(`http://localhost:${port}/`, {
6 | headers: {
7 | accept: "*/*", // if not set, always get 404 from vite server
8 | },
9 | }, res => {
10 | resolve(res.statusCode === 200);
11 | }).on('error', () => resolve(false)).end();
12 | });
13 | }
14 |
15 | export async function getLocalHostAvailablePort(port: number) {
16 | if (await isLocalHostPortUsing(port)) {
17 | port++;
18 | }
19 | return port;
20 | }
21 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | // A launch configuration that compiles the extension and then opens it inside a new window
2 | {
3 | "version": "0.2.0",
4 | "configurations": [
5 | {
6 | "name": "Launch Extension",
7 | "type": "extensionHost",
8 | "request": "launch",
9 | "runtimeExecutable": "${execPath}",
10 | "args": [
11 | "--disable-extensions",
12 | "--extensionDevelopmentPath=${workspaceRoot}/packages/vscode"
13 | ],
14 | "outFiles": [
15 | "${workspaceRoot}/*/*/out/**/*.js"
16 | ],
17 | "preLaunchTask": {
18 | "type": "npm",
19 | "script": "watch"
20 | }
21 | },
22 | ],
23 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2016",
4 | "lib": [
5 | "WebWorker",
6 | "ES2021",
7 | ],
8 | "module": "commonjs",
9 | "moduleResolution": "node",
10 | "sourceMap": true,
11 | "composite": true,
12 | "declaration": true,
13 | "strict": true,
14 | "alwaysStrict": false,
15 | "noImplicitUseStrict": true,
16 | "resolveJsonModule": true,
17 | "skipLibCheck": true,
18 | "noUnusedLocals": true,
19 | "noUnusedParameters": true,
20 | "baseUrl": "./",
21 | "paths": {
22 | "@vue-preview/core": [
23 | "packages/core/src"
24 | ],
25 | },
26 | "noEmit": true,
27 | },
28 | "include": [
29 | "*/*/src",
30 | ],
31 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "scripts": {
4 | "build": "tsc -b tsconfig.build.json",
5 | "watch": "npm run build && (npm run watch:base & npm run watch:vscode)",
6 | "watch:base": "tsc -b tsconfig.build.json -w",
7 | "watch:vscode": "cd ./packages/vscode && npm run watch",
8 | "prerelease": "npm run build",
9 | "release": "npm run release:base && npm run release:vscode",
10 | "release:base": "lerna publish --exact --force-publish --yes --sync-workspace-lock",
11 | "release:vscode": "cd ./packages/vscode && npm run release"
12 | },
13 | "devDependencies": {
14 | "@types/node": "latest",
15 | "typescript": "latest"
16 | },
17 | "optionalDependencies": {
18 | "@lerna-lite/cli": "latest"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023-present Johnson Chu
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/packages/core/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023-present Johnson Chu
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/packages/vscode/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023-present Johnson Chu
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/packages/vscode/scripts/build.js:
--------------------------------------------------------------------------------
1 | require('esbuild').build({
2 | entryPoints: {
3 | extension: './out/extension.js',
4 | },
5 | bundle: true,
6 | metafile: process.argv.includes('--metafile'),
7 | outdir: './dist',
8 | external: [
9 | 'vscode',
10 | ],
11 | format: 'cjs',
12 | platform: 'node',
13 | tsconfig: '../../tsconfig.build.json',
14 | define: { 'process.env.NODE_ENV': '"production"' },
15 | minify: process.argv.includes('--minify'),
16 | watch: process.argv.includes('--watch'),
17 | plugins: [
18 | {
19 | name: 'umd2esm',
20 | setup(build) {
21 | build.onResolve({ filter: /^(vscode-.*|estree-walker|jsonc-parser)/ }, args => {
22 | const pathUmdMay = require.resolve(args.path, { paths: [args.resolveDir] })
23 | // Call twice the replace is to solve the problem of the path in Windows
24 | const pathEsm = pathUmdMay.replace('/umd/', '/esm/').replace('\\umd\\', '\\esm\\')
25 | return { path: pathEsm }
26 | })
27 | },
28 | },
29 | require('esbuild-plugin-copy').copy({
30 | resolveFrom: 'cwd',
31 | assets: {
32 | from: ['./node_modules/@volar/preview/bin/**/*'],
33 | to: ['./dist/bin'],
34 | },
35 | keepStructure: true,
36 | }),
37 | ],
38 | }).catch(() => process.exit(1))
39 |
--------------------------------------------------------------------------------
/packages/core/bin/nuxi.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const path = require('path');
4 | const fs = require('fs');
5 | const readFileSync = fs.readFileSync;
6 |
7 | const workspace = process.cwd();
8 | const nuxiBinPath = require.resolve('nuxi/cli', { paths: [workspace] });
9 | const jsConfigPath = path.resolve(workspace, 'nuxt.config.js');
10 | const tsConfigPath = path.resolve(workspace, 'nuxt.config.ts');
11 |
12 | fs.readFileSync = (...args) => {
13 | if (args[0] === jsConfigPath || args[0] === tsConfigPath) {
14 | const configExtraContent = readFileSync(path.resolve(__dirname, 'nuxi', 'configExtraContent.ts'), { encoding: 'utf8' });
15 | return readFileSync(...args) + configExtraContent;
16 | }
17 | return readFileSync(...args);
18 | };
19 |
20 | createNuxtPlugin();
21 |
22 | import('file://' + nuxiBinPath);
23 |
24 | function createNuxtPlugin() {
25 |
26 | if (!fs.existsSync(path.resolve(workspace, 'node_modules', '.vue-preview'))) {
27 | fs.mkdirSync(path.resolve(workspace, 'node_modules', '.vue-preview'));
28 | }
29 |
30 | const proxyConfigPath = path.resolve(workspace, 'node_modules', '.vue-preview', 'nuxt.plugin.ts');
31 | const pluginContent = fs.readFileSync(path.resolve(__dirname, 'nuxi', 'plugin.ts'), { encoding: 'utf8' });
32 |
33 | fs.writeFileSync(proxyConfigPath, pluginContent);
34 | }
35 |
--------------------------------------------------------------------------------
/packages/vscode/src/utils/quickPick.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode';
2 |
3 | export function quickPick(groups: T | T[], placeholder?: string) {
4 | return new Promise(resolve => {
5 | const quickPick = vscode.window.createQuickPick();
6 | const items: vscode.QuickPickItem[] = [];
7 | for (const group of Array.isArray(groups) ? groups : [groups]) {
8 | const groupItems = Object.values(group);
9 | if (groupItems.length) {
10 | if (items.length) {
11 | items.push({ label: '', kind: vscode.QuickPickItemKind.Separator });
12 | }
13 | for (const item of groupItems) {
14 | if (item) {
15 | items.push(item);
16 | }
17 | }
18 | }
19 | }
20 | quickPick.items = items;
21 | quickPick.placeholder = placeholder;
22 | quickPick.onDidChangeSelection(selection => {
23 | if (selection[0]) {
24 | for (const options of Array.isArray(groups) ? groups : [groups]) {
25 | for (let key in options) {
26 | const option = options[key];
27 | if (selection[0] === option) {
28 | resolve(key);
29 | quickPick.hide();
30 | break;
31 | }
32 | }
33 | }
34 | }
35 | });
36 | quickPick.onDidHide(() => {
37 | quickPick.dispose();
38 | resolve(undefined);
39 | });
40 | quickPick.show();
41 | });
42 | }
43 |
--------------------------------------------------------------------------------
/packages/vscode/images/vite-logo.svg:
--------------------------------------------------------------------------------
1 |
16 |
--------------------------------------------------------------------------------
/packages/core/bin/nuxi/configExtraContent.ts:
--------------------------------------------------------------------------------
1 |
2 | if (!module.exports.default)
3 | module.exports.default = {};
4 |
5 | if (!module.exports.default.vue)
6 | module.exports.default.vue = {};
7 |
8 | if (!module.exports.default.vue.compilerOptions)
9 | module.exports.default.vue.compilerOptions = {};
10 |
11 | if (!module.exports.default.vue.compilerOptions.nodeTransforms)
12 | module.exports.default.vue.compilerOptions.nodeTransforms = [];
13 |
14 | module.exports.default.vue.compilerOptions.nodeTransforms.push(
15 | (node, ctx) => {
16 | if (node.type === 1) {
17 | const start = node.loc.start.offset;
18 | const end = node.loc.end.offset;
19 | addEvent(node, 'pointerenter', `$event ? $volar.highlight($event.target, $.type.__file, [${start},${end}]) : undefined`);
20 | addEvent(node, 'pointerleave', '$event ? $volar.unHighlight($event.target) : undefined');
21 | addEvent(node, 'vnode-mounted', `$event ? $volar.vnodeMounted($event.el, $.type.__file, [${start},${end}]) : undefined`);
22 | addEvent(node, 'vnode-unmounted', '$event ? $volar.vnodeUnmounted($event.el) : undefined');
23 | }
24 | }
25 | );
26 |
27 | if (!module.exports.default.plugins)
28 | module.exports.default.plugins = [];
29 |
30 | module.exports.default.plugins.push({ src: './node_modules/.vue-preview/nuxt.plugin.ts', ssr: false });
31 |
32 | function addEvent(node, name: string, exp: string) {
33 | node.props.push({
34 | type: 7,
35 | name: 'on',
36 | exp: {
37 | type: 4,
38 | content: exp,
39 | isStatic: false,
40 | constType: 0,
41 | loc: node.loc,
42 | },
43 | arg: {
44 | type: 4,
45 | content: name,
46 | isStatic: true,
47 | constType: 3,
48 | loc: node.loc,
49 | },
50 | modifiers: [],
51 | loc: node.loc,
52 | });
53 | }
54 |
--------------------------------------------------------------------------------
/packages/core/src/index.ts:
--------------------------------------------------------------------------------
1 | import * as WebSocket from 'ws';
2 |
3 | export function createPreviewConnection(options: {
4 | onGotoCode: (fileName: string, range: [number, number], cancelToken: { readonly isCancelled: boolean; }) => void,
5 | getFileHref: (fileName: string, range: [number, number]) => string,
6 | }) {
7 |
8 | const wsList: WebSocket.WebSocket[] = [];
9 | let wss: WebSocket.Server | undefined;
10 | let goToTemplateReq = 0;
11 |
12 | wss = new WebSocket.Server({ port: 56789 });
13 | wss.on('connection', ws => {
14 |
15 | wsList.push(ws);
16 |
17 | ws.on('message', msg => {
18 |
19 | const message = JSON.parse(msg.toString());
20 |
21 | if (message.command === 'goToTemplate') {
22 |
23 | const req = ++goToTemplateReq;
24 | const data = message.data as {
25 | fileName: string,
26 | range: [number, number],
27 | };
28 | const token = {
29 | get isCancelled() {
30 | return req !== goToTemplateReq;
31 | }
32 | };
33 |
34 | options.onGotoCode(data.fileName, data.range, token);
35 | }
36 |
37 | if (message.command === 'requestOpenFile') {
38 |
39 | const data = message.data as {
40 | fileName: string,
41 | range: [number, number],
42 | };
43 | const url = options.getFileHref(data.fileName, data.range);
44 |
45 | ws.send(JSON.stringify({
46 | command: 'openFile',
47 | data: url,
48 | }));
49 | }
50 | });
51 | });
52 |
53 | return {
54 | stop,
55 | highlight,
56 | unhighlight,
57 | };
58 |
59 | function stop() {
60 | wss?.close();
61 | wsList.length = 0;
62 | }
63 |
64 | function highlight(fileName: string, ranges: { start: number, end: number; }[], isDirty: boolean) {
65 | const msg = {
66 | command: 'highlightSelections',
67 | data: {
68 | fileName,
69 | ranges,
70 | isDirty,
71 | },
72 | };
73 | for (const ws of wsList) {
74 | ws.send(JSON.stringify(msg));
75 | }
76 | }
77 |
78 | function unhighlight() {
79 | const msg = {
80 | command: 'highlightSelections',
81 | data: undefined,
82 | };
83 | for (const ws of wsList) {
84 | ws.send(JSON.stringify(msg));
85 | }
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/packages/core/bin/vite.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const path = require('path');
4 | const fs = require('fs');
5 |
6 | const workspace = process.cwd();
7 | const vitePkgPath = require.resolve('vite/package.json', { paths: [workspace] });
8 | const viteDir = path.dirname(vitePkgPath);
9 | const viteBinPath = require.resolve('./bin/vite.js', { paths: [viteDir] });
10 | const viteVersion = require(vitePkgPath).version;
11 |
12 | try {
13 | const vuePluginPath = require.resolve('@vitejs/plugin-vue', { paths: [workspace] });
14 | const viteExtraCode = `
15 | function __proxyExport(rawOptions = {}) {
16 |
17 | if (!rawOptions)
18 | rawOptions = {};
19 |
20 | if (!rawOptions.template)
21 | rawOptions.template = {};
22 |
23 | if (!rawOptions.template.compilerOptions)
24 | rawOptions.template.compilerOptions = {};
25 |
26 | if (!rawOptions.template.compilerOptions.nodeTransforms)
27 | rawOptions.template.compilerOptions.nodeTransforms = [];
28 |
29 | rawOptions.template.compilerOptions.nodeTransforms.push((node, ctx) => {
30 | if (node.type === 1) {
31 | const start = node.loc.start.offset;
32 | const end = node.loc.end.offset;
33 | addEvent(node, 'pointerenter', \`$event ? $volar.highlight($event.target, $.type.__file, [\${start},\${end}]) : undefined\`);
34 | addEvent(node, 'pointerleave', '$event ? $volar.unHighlight($event.target) : undefined');
35 | addEvent(node, 'vnode-mounted', \`$event ? $volar.vnodeMounted($event.el, $.type.__file, [\${start},\${end}]) : undefined\`);
36 | addEvent(node, 'vnode-unmounted', '$event ? $volar.vnodeUnmounted($event.el) : undefined');
37 | }
38 | });
39 |
40 | return __originalExport(rawOptions);
41 |
42 | function addEvent(node, name, exp) {
43 | node.props.push({
44 | type: 7,
45 | name: 'on',
46 | exp: {
47 | type: 4,
48 | content: exp,
49 | isStatic: false,
50 | constType: 0,
51 | loc: node.loc,
52 | },
53 | arg: {
54 | type: 4,
55 | content: name,
56 | isStatic: true,
57 | constType: 3,
58 | loc: node.loc,
59 | },
60 | modifiers: [],
61 | loc: node.loc,
62 | });
63 | }
64 | }
65 |
66 | const __originalExport = module.exports;
67 | module.exports = __proxyExport;
68 | `;
69 |
70 | const readFileSync = fs.readFileSync;
71 | fs.readFileSync = (...args) => {
72 | if (args[0] === vuePluginPath) {
73 | return readFileSync(...args) + viteExtraCode;
74 | }
75 | return readFileSync(...args);
76 | };
77 | } catch (e) {
78 | console.warn('Volar: @vitejs/plugin-vue not found, skip vite extra code. (vue@2 only)')
79 | }
80 |
81 | createViteConfig();
82 |
83 | if (Number(viteVersion.split('.')[0]) >= 3) {
84 | import('file://' + viteBinPath);
85 | }
86 | else {
87 | require(viteBinPath);
88 | }
89 |
90 | function createViteConfig() {
91 | let proxyConfigContent = fs.readFileSync(path.resolve(__dirname, 'vite', 'config.ts'), { encoding: 'utf8' });
92 | proxyConfigContent = proxyConfigContent.replace('{CONFIG_PATH}', JSON.stringify(path.resolve(workspace, 'vite.config')));
93 |
94 | if (!fs.existsSync(path.resolve(workspace, 'node_modules', '.vue-preview'))) {
95 | fs.mkdirSync(path.resolve(workspace, 'node_modules', '.vue-preview'));
96 | }
97 |
98 | const proxyConfigPath = path.resolve(workspace, 'node_modules', '.vue-preview', 'vite.config.ts');
99 | fs.writeFileSync(proxyConfigPath, proxyConfigContent);
100 |
101 | process.argv.push('--config', proxyConfigPath);
102 | }
103 |
--------------------------------------------------------------------------------
/packages/vscode/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "name": "vscode-vue-preview",
4 | "version": "0.0.2",
5 | "repository": {
6 | "type": "git",
7 | "url": "https://github.com/johnsoncodehk/vue-preview.git",
8 | "directory": "packages/vscode"
9 | },
10 | "sponsor": {
11 | "url": "https://github.com/sponsors/johnsoncodehk"
12 | },
13 | "displayName": "Vue and Nuxt Preview (from Volar)",
14 | "description": "Component and App Preview For Vue and Nuxt",
15 | "author": "johnsoncodehk",
16 | "publisher": "johnsoncodehk",
17 | "engines": {
18 | "vscode": "^1.67.0"
19 | },
20 | "activationEvents": [
21 | "onLanguage:vue",
22 | "onWebviewPanel:preview"
23 | ],
24 | "main": "./dist/extension.js",
25 | "contributes": {
26 | "views": {
27 | "explorer": [
28 | {
29 | "id": "vueComponentPreview",
30 | "name": "Vue Component Preview",
31 | "type": "webview",
32 | "when": "vue-preview.foundViteDir || vue-preview.foundNuxtDir"
33 | }
34 | ]
35 | },
36 | "languages": [
37 | {
38 | "id": "vue",
39 | "extensions": [
40 | ".vue"
41 | ]
42 | }
43 | ],
44 | "configuration": {
45 | "type": "object",
46 | "title": "Vue Preview",
47 | "properties": {
48 | "vue-preview.script.vite": {
49 | "type": "string",
50 | "default": "node {VITE_BIN} --port={PORT}"
51 | },
52 | "vue-preview.script.nuxi": {
53 | "type": "string",
54 | "default": "node {NUXI_BIN} dev --port {PORT}"
55 | },
56 | "vue-preview.port": {
57 | "type": "number",
58 | "default": 3333,
59 | "description": "Default port for component preview server."
60 | },
61 | "vue-preview.root": {
62 | "type": "string",
63 | "default": ".",
64 | "description": "Component preview root directory. (For Nuxt, it should be \"./src\" by default.)"
65 | },
66 | "vue-preview.backgroundColor": {
67 | "type": "string",
68 | "default": "#fff",
69 | "description": "Component preview background color."
70 | },
71 | "vue-preview.transparentGrid": {
72 | "type": "boolean",
73 | "default": false,
74 | "description": "Component preview background style."
75 | }
76 | }
77 | },
78 | "commands": [
79 | {
80 | "command": "vue-preview.action.vite",
81 | "title": "Experimental Features for Vite",
82 | "category": "Vue Preview",
83 | "icon": "images/vite-logo.svg"
84 | },
85 | {
86 | "command": "vue-preview.action.nuxt",
87 | "title": "Experimental Features for Nuxt",
88 | "category": "Vue Preview",
89 | "icon": "images/nuxt-logo.svg"
90 | }
91 | ],
92 | "menus": {
93 | "commandPalette": [
94 | {
95 | "command": "vue-preview.action.vite",
96 | "when": "editorLangId == vue"
97 | },
98 | {
99 | "command": "vue-preview.action.nuxt",
100 | "when": "editorLangId == vue"
101 | }
102 | ],
103 | "editor/title": [
104 | {
105 | "command": "vue-preview.action.vite",
106 | "when": "editorLangId == vue && vue-preview.foundViteDir",
107 | "group": "navigation"
108 | },
109 | {
110 | "command": "vue-preview.action.nuxt",
111 | "when": "editorLangId == vue && vue-preview.foundNuxtDir",
112 | "group": "navigation"
113 | }
114 | ]
115 | }
116 | },
117 | "scripts": {
118 | "prebuild": "cd ../.. && npm run build",
119 | "build": "node scripts/build",
120 | "watch": "npm run build -- --watch",
121 | "prepack": "npm run prebuild && npm run build -- --minify",
122 | "pack": "npm run prepack && vsce package",
123 | "release": "npm run prepack && vsce publish"
124 | },
125 | "devDependencies": {
126 | "@types/vscode": "1.67.0",
127 | "@vue-preview/core": "0.1.0",
128 | "esbuild": "0.15.18",
129 | "esbuild-plugin-copy": "latest",
130 | "typesafe-path": "^0.2.2",
131 | "vscode-html-languageservice": "^5.0.4",
132 | "vsce": "latest"
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/packages/core/bin/nuxi/plugin.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | // remove defineNuxtPlugin() to fixed https://github.com/yaegassy/coc-volar-tools/pull/3#issuecomment-1109155402
3 | export default app => {
4 |
5 | if (process.server)
6 | return;
7 |
8 | const ws = new WebSocket('ws://localhost:56789');
9 | const finderApis = installGoToCode();
10 | const highlightApis = installSelectionHighlight();
11 |
12 | return {
13 | provide: {
14 | volar: {
15 | ...finderApis,
16 | ...highlightApis,
17 | },
18 | },
19 | };
20 |
21 | function installSelectionHighlight() {
22 |
23 | let selection: {
24 | fileName: string,
25 | ranges: {
26 | start,
27 | end,
28 | }[],
29 | isDirty: boolean,
30 | } | undefined;
31 | let updateTimeout: number | undefined;
32 | const nodes = new Map();
36 | const cursorInOverlays = new Map();
37 | const rangeCoverOverlays = new Map();
38 | const cursorInResizeObserver = new ResizeObserver(scheduleUpdate);
39 | const rangeCoverResizeObserver = new ResizeObserver(scheduleUpdate);
40 |
41 | window.addEventListener('scroll', scheduleUpdate);
42 |
43 | ws.addEventListener('message', event => {
44 | const data = JSON.parse(event.data);
45 | if (data?.command === 'highlightSelections') {
46 | selection = data.data;
47 | updateHighlights();
48 | }
49 | });
50 |
51 | return {
52 | vnodeMounted,
53 | vnodeUnmounted,
54 | };
55 |
56 | function vnodeMounted(node: unknown, fileName: string, range: [number, number]) {
57 | if (node instanceof Element) {
58 | nodes.set(node, {
59 | fileName,
60 | range,
61 | });
62 | scheduleUpdate();
63 | }
64 | }
65 | function vnodeUnmounted(node: unknown) {
66 | if (node instanceof Element) {
67 | nodes.delete(node);
68 | scheduleUpdate();
69 | }
70 | }
71 | function scheduleUpdate() {
72 | if (updateTimeout === undefined) {
73 | updateTimeout = setTimeout(() => {
74 | updateHighlights();
75 | updateTimeout = undefined;
76 | }, 0);
77 | }
78 | }
79 | function updateHighlights() {
80 |
81 | if (selection?.isDirty) {
82 | for (const [_, overlay] of cursorInOverlays) {
83 | overlay.style.opacity = '0.5';
84 | }
85 | for (const [_, overlay] of rangeCoverOverlays) {
86 | overlay.style.opacity = '0.5';
87 | }
88 | return;
89 | }
90 | else {
91 | for (const [_, overlay] of cursorInOverlays) {
92 | overlay.style.opacity = '1';
93 | }
94 | for (const [_, overlay] of rangeCoverOverlays) {
95 | overlay.style.opacity = '1';
96 | }
97 | }
98 |
99 | const cursorIn = new Set();
100 | const rangeConver = new Set();
101 |
102 | if (selection) {
103 | for (const range of selection.ranges) {
104 | for (const [el, loc] of nodes) {
105 | if (loc.fileName.replace(/\\/g, '/').toLowerCase() === selection.fileName.replace(/\\/g, '/').toLowerCase()) {
106 | if (range.start <= loc.range[0] && range.end >= loc.range[1]) {
107 | rangeConver.add(el);
108 | }
109 | else if (
110 | range.start >= loc.range[0] && range.start <= loc.range[1]
111 | || range.end >= loc.range[0] && range.end <= loc.range[1]
112 | ) {
113 | cursorIn.add(el);
114 | }
115 | }
116 | }
117 | }
118 | }
119 |
120 | for (const [el, overlay] of [...cursorInOverlays]) {
121 | if (!cursorIn.has(el)) {
122 | overlay.remove();
123 | cursorInOverlays.delete(el);
124 | cursorInResizeObserver.disconnect(el);
125 | }
126 | }
127 | for (const [el, overlay] of [...rangeCoverOverlays]) {
128 | if (!rangeConver.has(el)) {
129 | overlay.remove();
130 | rangeCoverOverlays.delete(el);
131 | rangeCoverResizeObserver.disconnect(el);
132 | }
133 | }
134 |
135 | for (const el of cursorIn) {
136 | let overlay = cursorInOverlays.get(el);
137 | if (!overlay) {
138 | overlay = createCursorInOverlay();
139 | cursorInOverlays.set(el, overlay);
140 | cursorInResizeObserver.observe(el);
141 | }
142 | const rect = el.getBoundingClientRect();
143 | overlay.style.width = ~~rect.width + 'px';
144 | overlay.style.height = ~~rect.height + 'px';
145 | overlay.style.top = ~~rect.top + 'px';
146 | overlay.style.left = ~~rect.left + 'px';
147 | }
148 | for (const el of rangeConver) {
149 | let overlay = rangeCoverOverlays.get(el);
150 | if (!overlay) {
151 | overlay = createRangeCoverOverlay();
152 | rangeCoverOverlays.set(el, overlay);
153 | rangeCoverResizeObserver.observe(el);
154 | }
155 | const rect = el.getBoundingClientRect();
156 | overlay.style.width = ~~rect.width + 'px';
157 | overlay.style.height = ~~rect.height + 'px';
158 | overlay.style.top = ~~rect.top + 'px';
159 | overlay.style.left = ~~rect.left + 'px';
160 | }
161 | }
162 | function createCursorInOverlay() {
163 | const overlay = document.createElement('div');
164 | overlay.style.position = 'fixed';
165 | overlay.style.zIndex = '99999999999999';
166 | overlay.style.pointerEvents = 'none';
167 | overlay.style.borderRadius = '3px';
168 | overlay.style.borderStyle = 'dashed';
169 | overlay.style.borderColor = 'rgb(196, 105, 183)';
170 | overlay.style.borderWidth = '1px';
171 | overlay.style.boxSizing = 'border-box';
172 | document.body.appendChild(overlay);
173 | return overlay;
174 | }
175 | function createRangeCoverOverlay() {
176 | const overlay = createCursorInOverlay();
177 | overlay.style.backgroundColor = 'rgba(196, 105, 183, 0.1)';
178 | return overlay;
179 | }
180 | }
181 |
182 | function installGoToCode() {
183 |
184 | window.addEventListener('scroll', updateOverlay);
185 | window.addEventListener('pointerdown', event => {
186 | disable(true);
187 | });
188 | window.addEventListener('keydown', event => {
189 | if (event.key === 'Alt') {
190 | enable();
191 | }
192 | });
193 | window.addEventListener('keyup', event => {
194 | if (event.key === 'Alt') {
195 | disable(false);
196 | }
197 | });
198 |
199 | ws.addEventListener('message', event => {
200 | const data = JSON.parse(event.data);
201 | if (data?.command === 'openFile') {
202 | window.open(data.data);
203 | }
204 | });
205 |
206 | const overlay = createOverlay();
207 | const clickMask = createClickMask();
208 |
209 | let highlightNodes: [Element, string, [number, number]][] = [];
210 | let enabled = false;
211 | let lastCodeLoc: any | undefined;
212 |
213 | return {
214 | highlight,
215 | unHighlight,
216 | };
217 |
218 | function enable() {
219 | enabled = true;
220 | clickMask.style.pointerEvents = 'none';
221 | document.body.appendChild(clickMask);
222 | updateOverlay();
223 | }
224 | function disable(openEditor: boolean) {
225 | if (enabled) {
226 | enabled = false;
227 | clickMask.style.pointerEvents = '';
228 | highlightNodes = [];
229 | updateOverlay();
230 | if (lastCodeLoc) {
231 | ws.send(JSON.stringify(lastCodeLoc));
232 | if (openEditor) {
233 | ws.send(JSON.stringify({
234 | command: 'requestOpenFile',
235 | data: lastCodeLoc.data,
236 | }));
237 | }
238 | lastCodeLoc = undefined;
239 | }
240 | }
241 | }
242 | function goToTemplate(fileName: string, range: [number, number]) {
243 | if (!enabled) return;
244 | lastCodeLoc = {
245 | command: 'goToTemplate',
246 | data: {
247 | fileName,
248 | range,
249 | },
250 | };
251 | ws.send(JSON.stringify(lastCodeLoc));
252 | }
253 | function highlight(node: unknown, fileName: string, range: [number, number]) {
254 | if (node instanceof Element) {
255 | highlightNodes.push([node, fileName, range]);
256 | }
257 | updateOverlay();
258 | }
259 | function unHighlight(node: Element) {
260 | highlightNodes = highlightNodes.filter(hNode => hNode[0] !== node);
261 | updateOverlay();
262 | }
263 | function createOverlay() {
264 | const overlay = document.createElement('div');
265 | overlay.style.backgroundColor = 'rgba(65, 184, 131, 0.35)';
266 | overlay.style.position = 'fixed';
267 | overlay.style.zIndex = '99999999999999';
268 | overlay.style.pointerEvents = 'none';
269 | overlay.style.display = 'flex';
270 | overlay.style.alignItems = 'center';
271 | overlay.style.justifyContent = 'center';
272 | overlay.style.borderRadius = '3px';
273 | return overlay;
274 | }
275 | function createClickMask() {
276 | const overlay = document.createElement('div');
277 | overlay.style.position = 'fixed';
278 | overlay.style.zIndex = '99999999999999';
279 | overlay.style.pointerEvents = 'none';
280 | overlay.style.display = 'flex';
281 | overlay.style.left = '0';
282 | overlay.style.right = '0';
283 | overlay.style.top = '0';
284 | overlay.style.bottom = '0';
285 | overlay.addEventListener('pointerup', () => {
286 | if (overlay.parentNode) {
287 | overlay.parentNode?.removeChild(overlay);
288 | }
289 | });
290 | return overlay;
291 | }
292 | function updateOverlay() {
293 | if (enabled && highlightNodes.length) {
294 | document.body.appendChild(overlay);
295 | const highlight = highlightNodes[highlightNodes.length - 1];
296 | const highlightNode = highlight[0];
297 | const rect = highlightNode.getBoundingClientRect();
298 | overlay.style.width = ~~rect.width + 'px';
299 | overlay.style.height = ~~rect.height + 'px';
300 | overlay.style.top = ~~rect.top + 'px';
301 | overlay.style.left = ~~rect.left + 'px';
302 | goToTemplate(highlight[1], highlight[2]);
303 | }
304 | else if (overlay.parentNode) {
305 | overlay.parentNode.removeChild(overlay);
306 | }
307 | }
308 | }
309 | };
310 |
--------------------------------------------------------------------------------
/packages/core/bin/vite/config.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | import config from {CONFIG_PATH};
3 |
4 | if (!config.plugins)
5 | config.plugins = [];
6 |
7 | const installCode = `
8 | function __createAppProxy(...args) {
9 |
10 | const app = createApp(...args);
11 |
12 | const ws = new WebSocket('ws://localhost:56789');
13 | const finderApis = installGoToCode();
14 | const highlightApis = installSelectionHighlight();
15 |
16 | app.config.globalProperties.$volar = {
17 | ...finderApis,
18 | ...highlightApis,
19 | };
20 |
21 | return app;
22 |
23 | function installSelectionHighlight() {
24 |
25 | let selection;
26 | let updateTimeout;
27 | const nodes = new Map();
28 | const cursorInOverlays = new Map();
29 | const rangeCoverOverlays = new Map();
30 | const cursorInResizeObserver = new ResizeObserver(scheduleUpdate);
31 | const rangeCoverResizeObserver = new ResizeObserver(scheduleUpdate);
32 |
33 | window.addEventListener('scroll', scheduleUpdate);
34 |
35 | ws.addEventListener('message', event => {
36 | const data = JSON.parse(event.data);
37 | if (data?.command === 'highlightSelections') {
38 | selection = data.data;
39 | updateHighlights();
40 | }
41 | });
42 |
43 | return {
44 | vnodeMounted,
45 | vnodeUnmounted,
46 | };
47 |
48 | function vnodeMounted(node, fileName, range) {
49 | if (node instanceof Element) {
50 | nodes.set(node, {
51 | fileName,
52 | range,
53 | });
54 | scheduleUpdate();
55 | }
56 | }
57 | function vnodeUnmounted(node) {
58 | if (node instanceof Element) {
59 | nodes.delete(node);
60 | scheduleUpdate();
61 | }
62 | }
63 | function scheduleUpdate() {
64 | if (updateTimeout === undefined) {
65 | updateTimeout = setTimeout(() => {
66 | updateHighlights();
67 | updateTimeout = undefined;
68 | }, 0);
69 | }
70 | }
71 | function updateHighlights() {
72 |
73 | if (selection?.isDirty) {
74 | for (const [_, overlay] of cursorInOverlays) {
75 | overlay.style.opacity = '0.5';
76 | }
77 | for (const [_, overlay] of rangeCoverOverlays) {
78 | overlay.style.opacity = '0.5';
79 | }
80 | return;
81 | }
82 | else {
83 | for (const [_, overlay] of cursorInOverlays) {
84 | overlay.style.opacity = '1';
85 | }
86 | for (const [_, overlay] of rangeCoverOverlays) {
87 | overlay.style.opacity = '1';
88 | }
89 | }
90 |
91 | const cursorIn = new Set();
92 | const rangeConver = new Set();
93 |
94 | if (selection) {
95 | for (const range of selection.ranges) {
96 | for (const [el, loc] of nodes) {
97 | if (loc.fileName === selection.fileName) {
98 | if (range.start <= loc.range[0] && range.end >= loc.range[1]) {
99 | rangeConver.add(el);
100 | }
101 | else if (
102 | range.start >= loc.range[0] && range.start <= loc.range[1]
103 | || range.end >= loc.range[0] && range.end <= loc.range[1]
104 | ) {
105 | cursorIn.add(el);
106 | }
107 | }
108 | }
109 | }
110 | }
111 |
112 | for (const [el, overlay] of [...cursorInOverlays]) {
113 | if (!cursorIn.has(el)) {
114 | overlay.remove();
115 | cursorInOverlays.delete(el);
116 | cursorInResizeObserver.disconnect(el);
117 | }
118 | }
119 | for (const [el, overlay] of [...rangeCoverOverlays]) {
120 | if (!rangeConver.has(el)) {
121 | overlay.remove();
122 | rangeCoverOverlays.delete(el);
123 | rangeCoverResizeObserver.disconnect(el);
124 | }
125 | }
126 |
127 | for (const el of cursorIn) {
128 | let overlay = cursorInOverlays.get(el);
129 | if (!overlay) {
130 | overlay = createCursorInOverlay();
131 | cursorInOverlays.set(el, overlay);
132 | cursorInResizeObserver.observe(el);
133 | }
134 | const rect = el.getBoundingClientRect();
135 | overlay.style.width = ~~rect.width + 'px';
136 | overlay.style.height = ~~rect.height + 'px';
137 | overlay.style.top = ~~rect.top + 'px';
138 | overlay.style.left = ~~rect.left + 'px';
139 | }
140 | for (const el of rangeConver) {
141 | let overlay = rangeCoverOverlays.get(el);
142 | if (!overlay) {
143 | overlay = createRangeCoverOverlay();
144 | rangeCoverOverlays.set(el, overlay);
145 | rangeCoverResizeObserver.observe(el);
146 | }
147 | const rect = el.getBoundingClientRect();
148 | overlay.style.width = ~~rect.width + 'px';
149 | overlay.style.height = ~~rect.height + 'px';
150 | overlay.style.top = ~~rect.top + 'px';
151 | overlay.style.left = ~~rect.left + 'px';
152 | }
153 | }
154 | function createCursorInOverlay() {
155 | const overlay = document.createElement('div');
156 | overlay.style.position = 'fixed';
157 | overlay.style.zIndex = '99999999999999';
158 | overlay.style.pointerEvents = 'none';
159 | overlay.style.borderRadius = '3px';
160 | overlay.style.borderStyle = 'dashed';
161 | overlay.style.borderColor = 'rgb(196, 105, 183)';
162 | overlay.style.borderWidth = '1px';
163 | overlay.style.boxSizing = 'border-box';
164 | document.body.appendChild(overlay);
165 | return overlay;
166 | }
167 | function createRangeCoverOverlay() {
168 | const overlay = createCursorInOverlay();
169 | overlay.style.backgroundColor = 'rgba(196, 105, 183, 0.1)';
170 | return overlay;
171 | }
172 | }
173 | function installGoToCode() {
174 | window.addEventListener('scroll', updateOverlay);
175 | window.addEventListener('pointerdown', function (ev) {
176 | disable(true);
177 | });
178 | window.addEventListener('keydown', event => {
179 | if (event.key === 'Alt') {
180 | enable();
181 | }
182 | });
183 | window.addEventListener('keyup', event => {
184 | if (event.key === 'Alt') {
185 | disable(false);
186 | }
187 | });
188 |
189 | ws.addEventListener('message', event => {
190 | const data = JSON.parse(event.data);
191 | if (data?.command === 'openFile') {
192 | window.open(data.data);
193 | }
194 | });
195 |
196 | var overlay = createOverlay();
197 | var clickMask = createClickMask();
198 | var highlightNodes = [];
199 | var enabled = false;
200 | var lastCodeLoc;
201 |
202 | return {
203 | highlight,
204 | unHighlight,
205 | };
206 |
207 | function enable() {
208 | enabled = true;
209 | clickMask.style.pointerEvents = 'none';
210 | document.body.appendChild(clickMask);
211 | updateOverlay();
212 | }
213 | function disable(openEditor) {
214 | if (enabled) {
215 | enabled = false;
216 | clickMask.style.pointerEvents = '';
217 | highlightNodes = [];
218 | updateOverlay();
219 | if (lastCodeLoc) {
220 | ws.send(JSON.stringify(lastCodeLoc));
221 | if (openEditor) {
222 | ws.send(JSON.stringify({
223 | command: 'requestOpenFile',
224 | data: lastCodeLoc.data,
225 | }));
226 | }
227 | lastCodeLoc = undefined;
228 | }
229 | }
230 | }
231 | function goToTemplate(fileName, range) {
232 | if (!enabled)
233 | return;
234 | lastCodeLoc = {
235 | command: 'goToTemplate',
236 | data: {
237 | fileName: fileName,
238 | range,
239 | },
240 | };
241 | ws.send(JSON.stringify(lastCodeLoc));
242 | }
243 | function highlight(node, fileName, range) {
244 | if (node instanceof Element) {
245 | highlightNodes.push([node, fileName, range]);
246 | }
247 | updateOverlay();
248 | }
249 | function unHighlight(node) {
250 | highlightNodes = highlightNodes.filter(function (hNode) { return hNode[0] !== node; });
251 | updateOverlay();
252 | }
253 | function createOverlay() {
254 | var overlay = document.createElement('div');
255 | overlay.style.backgroundColor = 'rgba(65, 184, 131, 0.35)';
256 | overlay.style.position = 'fixed';
257 | overlay.style.zIndex = '99999999999999';
258 | overlay.style.pointerEvents = 'none';
259 | overlay.style.display = 'flex';
260 | overlay.style.alignItems = 'center';
261 | overlay.style.justifyContent = 'center';
262 | overlay.style.borderRadius = '3px';
263 | return overlay;
264 | }
265 | function createClickMask() {
266 | var overlay = document.createElement('div');
267 | overlay.style.position = 'fixed';
268 | overlay.style.zIndex = '99999999999999';
269 | overlay.style.pointerEvents = 'none';
270 | overlay.style.display = 'flex';
271 | overlay.style.left = '0';
272 | overlay.style.right = '0';
273 | overlay.style.top = '0';
274 | overlay.style.bottom = '0';
275 | overlay.addEventListener('pointerup', function () {
276 | var _a;
277 | if (overlay.parentNode) {
278 | (_a = overlay.parentNode) === null || _a === void 0 ? void 0 : _a.removeChild(overlay);
279 | }
280 | });
281 | return overlay;
282 | }
283 | function updateOverlay() {
284 | if (enabled && highlightNodes.length) {
285 | document.body.appendChild(overlay);
286 | var highlight_1 = highlightNodes[highlightNodes.length - 1];
287 | var highlightNode = highlight_1[0];
288 | var rect = highlightNode.getBoundingClientRect();
289 | overlay.style.width = ~~rect.width + 'px';
290 | overlay.style.height = ~~rect.height + 'px';
291 | overlay.style.top = ~~rect.top + 'px';
292 | overlay.style.left = ~~rect.left + 'px';
293 | goToTemplate(highlight_1[1], highlight_1[2]);
294 | }
295 | else if (overlay.parentNode) {
296 | overlay.parentNode.removeChild(overlay);
297 | }
298 | }
299 | }
300 | }
301 | `;
302 |
303 | config.plugins.push({
304 | name: '__volar_preview',
305 | transform(this, code, id, options?) {
306 | const createAppText = 'createApp,';
307 | if (id.indexOf('/vue.js?') >= 0 && code.indexOf(createAppText) >= 0 && code.indexOf('__createAppProxy') === -1) {
308 | const createAppOffset = code.lastIndexOf(createAppText);
309 | code =
310 | code.substring(0, createAppOffset)
311 | + '__createAppProxy as createApp,'
312 | + code.substring(createAppOffset + createAppText.length)
313 | + `${installCode}`;
314 | }
315 | return code;
316 | },
317 | });
318 |
319 | export default config;
320 |
--------------------------------------------------------------------------------
/packages/vscode/src/extension.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode';
2 | import * as path from 'typesafe-path';
3 | import * as posix from 'typesafe-path/posix';
4 | import * as fs from './utils/fs';
5 | import * as preview from '@vue-preview/core';
6 | import { getLocalHostAvailablePort } from './utils/http';
7 | import * as html from 'vscode-html-languageservice';
8 | import { quickPick } from './utils/quickPick';
9 |
10 | const htmlLs = html.getLanguageService();
11 |
12 | const enum PreviewType {
13 | Webview = 'vue-preview-webview',
14 | ExternalBrowser = 'vue-preview-start-server',
15 | ExternalBrowser_Component = 'vue-preview-component-preview',
16 | }
17 |
18 | export async function activate(context: vscode.ExtensionContext) {
19 |
20 | let _loadingPanel: vscode.WebviewPanel | undefined;
21 | let avoidUpdateOnDidChangeActiveTextEditor = false;
22 | let updateComponentPreview: Function | undefined;
23 |
24 | const statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, -1);
25 | statusBar.command = 'vue-preview.previewMenu';
26 | statusBar.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground');
27 | context.subscriptions.push(statusBar);
28 |
29 | let connection: ReturnType | undefined;
30 | let highlightDomElements = true;
31 |
32 | const previewTerminal = vscode.window.terminals.find(terminal => terminal.name.startsWith('vue-preview:'));
33 | if (previewTerminal) {
34 | connection = preview.createPreviewConnection({
35 | onGotoCode: handleGoToCode,
36 | getFileHref: (fileName) => {
37 | avoidUpdateOnDidChangeActiveTextEditor = false;
38 | updateComponentPreview?.();
39 | return 'vscode://files:/' + fileName;
40 | },
41 | });
42 | statusBar.text = 'Preview Port: ' + previewTerminal.name.split(':')[2];
43 | statusBar.show();
44 | }
45 | vscode.window.onDidOpenTerminal(e => {
46 | if (e.name.startsWith('vue-preview:')) {
47 | connection = preview.createPreviewConnection({
48 | onGotoCode: handleGoToCode,
49 | getFileHref: (fileName) => {
50 | avoidUpdateOnDidChangeActiveTextEditor = false;
51 | updateComponentPreview?.();
52 | return 'vscode://files:/' + fileName;
53 | },
54 | });
55 | statusBar.text = 'Preview Port: ' + e.name.split(':')[2];
56 | statusBar.show();
57 | }
58 | });
59 | vscode.window.onDidCloseTerminal(e => {
60 | if (e.name.startsWith('vue-preview:')) {
61 | connection?.stop();
62 | connection = undefined;
63 | statusBar.hide();
64 | }
65 | });
66 |
67 | const templateOffsets = new WeakMap();
68 |
69 | class VueComponentPreview implements vscode.WebviewViewProvider {
70 |
71 | public resolveWebviewView(
72 | webviewView: vscode.WebviewView,
73 | _context: vscode.WebviewViewResolveContext,
74 | _token: vscode.CancellationToken,
75 | ) {
76 |
77 | let lastPreviewDocument: vscode.TextDocument | undefined;
78 | let updating: Promise | undefined;
79 |
80 | webviewView.webview.options = {
81 | enableScripts: true,
82 | };
83 | updateWebView(true);
84 | updateComponentPreview = updateWebView;
85 |
86 | vscode.window.onDidChangeActiveTextEditor(() => {
87 | if (avoidUpdateOnDidChangeActiveTextEditor)
88 | return;
89 | if (!vscode.window.activeTextEditor || lastPreviewDocument === vscode.window.activeTextEditor.document)
90 | return;
91 | updateWebView(false);
92 | });
93 | vscode.workspace.onDidChangeTextDocument(() => updateWebView(false));
94 | vscode.workspace.onDidChangeConfiguration(() => updateWebView(true));
95 |
96 | webviewView.onDidChangeVisibility(() => updateWebView(false));
97 |
98 | function updateWebView(refresh: boolean) {
99 | if (updating) {
100 | updating = updating.then(() => updateWebViewWorker(refresh));
101 | }
102 | else {
103 | updating = updateWebViewWorker(refresh);
104 | }
105 | }
106 |
107 | async function updateWebViewWorker(refresh: boolean) {
108 |
109 | if (!webviewView.visible)
110 | return;
111 |
112 | if (vscode.window.activeTextEditor?.document.languageId === 'vue')
113 | lastPreviewDocument = vscode.window.activeTextEditor.document;
114 |
115 | if (!lastPreviewDocument)
116 | return;
117 |
118 | const fileName = lastPreviewDocument.fileName as path.OsPath;
119 | let terminal = vscode.window.terminals.find(terminal => terminal.name.startsWith('vue-preview:'));
120 | let port: number;
121 | let configFile = await getConfigFile(fileName, 'vite');
122 | let previewMode: 'vite' | 'nuxt' = 'vite';
123 |
124 | if (!configFile) {
125 | configFile = await getConfigFile(fileName, 'nuxt');
126 | previewMode = 'nuxt';
127 | }
128 | if (!configFile)
129 | return;
130 |
131 | if (terminal) {
132 | port = Number(terminal.name.split(':')[2]);
133 | }
134 | else {
135 | const configDir = path.dirname(configFile);
136 | const server = await startPreviewServer(configDir, previewMode);
137 | terminal = server.terminal;
138 | port = server.port;
139 | }
140 |
141 | const root = vscode.workspace.getConfiguration('vue-preview').get('root')!;
142 | const relativePath = path.relative(path.resolve(path.dirname(configFile), root), fileName).replace(/\\/g, '/') as path.PosixPath;
143 | let url = `http://localhost:${port}/${posix.join('__preview' as path.PosixPath, relativePath)}#`;
144 |
145 | if (lastPreviewDocument.isDirty) {
146 | url += btoa(lastPreviewDocument.getText());
147 | }
148 |
149 | if (refresh) {
150 |
151 | const bgPath = vscode.Uri.file(path.join(context.extensionPath as path.OsPath, 'images/preview-bg.png' as path.PosixPath));
152 | const bgSrc = webviewView.webview.asWebviewUri(bgPath);
153 |
154 | webviewView.webview.html = '';
155 | webviewView.webview.html = getWebviewContent(url, bgSrc.toString());
156 | }
157 | else {
158 | webviewView.webview.postMessage({
159 | sender: 'volar',
160 | command: 'updateUrl',
161 | data: url,
162 | });
163 | }
164 | }
165 | }
166 | }
167 |
168 | vscode.window.registerWebviewViewProvider(
169 | 'vueComponentPreview',
170 | new VueComponentPreview(),
171 | );
172 |
173 | context.subscriptions.push(vscode.commands.registerCommand('vue-preview.previewMenu', async () => {
174 |
175 | const baseOptions: Record = {};
176 | const urlOptions: Record = {};
177 | const highlight: Record = {};
178 |
179 | baseOptions['kill'] = { label: 'Kill Preview Server' };
180 | baseOptions['browser'] = { label: 'Open in Browser' };
181 | highlight['highlight-on'] = { label: (highlightDomElements ? '• ' : '') + 'Highlight DOM Elements' };
182 | highlight['highlight-off'] = { label: (!highlightDomElements ? '• ' : '') + `Don't Highlight DOM Elements` };
183 |
184 | const key = await quickPick([baseOptions, urlOptions, highlight]);
185 |
186 | if (key === 'kill') {
187 | for (const terminal of vscode.window.terminals) {
188 | if (terminal.name.startsWith('vue-preview:')) {
189 | terminal.dispose();
190 | }
191 | }
192 | }
193 | if (key === 'browser') {
194 | vscode.env.openExternal(vscode.Uri.parse('http://localhost:' + statusBar.text.split(':')[1].trim()));
195 | }
196 | if (key === 'highlight-on') {
197 | highlightDomElements = true;
198 | if (vscode.window.activeTextEditor) {
199 | updateSelectionHighlights(vscode.window.activeTextEditor);
200 | }
201 | }
202 | if (key === 'highlight-off') {
203 | highlightDomElements = false;
204 | if (vscode.window.activeTextEditor) {
205 | updateSelectionHighlights(vscode.window.activeTextEditor);
206 | }
207 | }
208 | }));
209 | context.subscriptions.push(vscode.commands.registerCommand('vue-preview.action.vite', async () => {
210 |
211 | const editor = vscode.window.activeTextEditor;
212 | if (!editor)
213 | return;
214 |
215 | const viteConfigFile = await getConfigFile(editor.document.fileName as path.OsPath, 'vite');
216 | const select = await quickPick({
217 | [PreviewType.Webview]: {
218 | label: 'Preview Vite App',
219 | detail: vscode.workspace.rootPath && viteConfigFile ? path.relative(vscode.workspace.rootPath as path.OsPath, viteConfigFile) : viteConfigFile,
220 | description: 'Press `Alt` to use go to code feature',
221 | },
222 | [PreviewType.ExternalBrowser]: {
223 | label: 'Preview Vite App in External Browser',
224 | detail: vscode.workspace.rootPath && viteConfigFile ? path.relative(vscode.workspace.rootPath as path.OsPath, viteConfigFile) : viteConfigFile,
225 | description: 'Press `Alt` to use go to code feature',
226 | },
227 | [PreviewType.ExternalBrowser_Component]: {
228 | label: `Preview Component in External Browser`,
229 | detail: vscode.workspace.rootPath ? path.relative(vscode.workspace.rootPath as path.OsPath, editor.document.fileName as path.OsPath) : editor.document.fileName,
230 | },
231 | });
232 | if (select === undefined)
233 | return; // cancel
234 |
235 | openPreview(select as PreviewType, editor.document.fileName as path.OsPath, 'vite');
236 | }));
237 | context.subscriptions.push(vscode.commands.registerCommand('vue-preview.action.nuxt', async () => {
238 |
239 | const editor = vscode.window.activeTextEditor;
240 | if (!editor)
241 | return;
242 |
243 | const viteConfigFile = await getConfigFile(editor.document.fileName as path.OsPath, 'nuxt');
244 | const root = vscode.workspace.getConfiguration('vue-preview').get('root')!;
245 | const select = await quickPick({
246 | [PreviewType.Webview]: {
247 | label: 'Preview Nuxt App',
248 | detail: vscode.workspace.rootPath && viteConfigFile ? path.relative(vscode.workspace.rootPath as path.OsPath, viteConfigFile) : viteConfigFile,
249 | },
250 | [PreviewType.ExternalBrowser]: {
251 | label: 'Preview Nuxt App in External Browser',
252 | detail: vscode.workspace.rootPath && viteConfigFile ? path.relative(vscode.workspace.rootPath as path.OsPath, viteConfigFile) : viteConfigFile,
253 | description: 'Press `Alt` to use go to code in Browser',
254 | },
255 | [PreviewType.ExternalBrowser_Component]: {
256 | label: `Preview Component in External Browser`,
257 | detail: vscode.workspace.rootPath ? path.relative(path.resolve(vscode.workspace.rootPath as path.OsPath, root), editor.document.fileName as path.OsPath) : editor.document.fileName,
258 | },
259 | });
260 | if (select === undefined)
261 | return; // cancel
262 |
263 | openPreview(select as PreviewType, editor.document.fileName as path.OsPath, 'nuxt');
264 | }));
265 | context.subscriptions.push(vscode.window.onDidChangeTextEditorSelection(e => {
266 | updateSelectionHighlights(e.textEditor);
267 | }));
268 | context.subscriptions.push(vscode.workspace.onDidChangeTextDocument((e) => {
269 | if (vscode.window.activeTextEditor?.document === e.document) {
270 | updateSelectionHighlights(vscode.window.activeTextEditor);
271 | }
272 | }));
273 | context.subscriptions.push(vscode.workspace.onDidSaveTextDocument((document) => {
274 | if (vscode.window.activeTextEditor?.document === document) {
275 | updateSelectionHighlights(vscode.window.activeTextEditor);
276 | }
277 | }));
278 |
279 | context.subscriptions.push(vscode.window.onDidChangeActiveTextEditor(updatePreviewIconStatus));
280 |
281 | updatePreviewIconStatus();
282 |
283 | function getTemplateOffset(document: vscode.TextDocument) {
284 | let cache = templateOffsets.get(document);
285 | if (!cache || cache.version !== document.version) {
286 | templateOffsets.delete(document);
287 | const htmlDocument = htmlLs.parseHTMLDocument(html.TextDocument.create(document.uri.toString(), document.languageId, document.version, document.getText()));
288 | const template = htmlDocument.roots.find(node => node.tag === 'template');
289 | if (template?.startTagEnd !== undefined) {
290 | templateOffsets.set(document, {
291 | version: document.version,
292 | offset: template.startTagEnd,
293 | });
294 | }
295 | }
296 | return templateOffsets.get(document)?.offset ?? 0;
297 | }
298 |
299 | async function updatePreviewIconStatus() {
300 | if (vscode.window.activeTextEditor?.document.languageId === 'vue') {
301 |
302 | const viteConfigFile = await getConfigFile(vscode.window.activeTextEditor.document.fileName as path.OsPath, 'vite');
303 | const nuxtConfigFile = await getConfigFile(vscode.window.activeTextEditor.document.fileName as path.OsPath, 'nuxt');
304 |
305 | vscode.commands.executeCommand('setContext', 'vue-preview.foundViteDir', viteConfigFile !== undefined);
306 | vscode.commands.executeCommand('setContext', 'vue-preview.foundNuxtDir', nuxtConfigFile !== undefined);
307 | }
308 | }
309 |
310 | async function updateSelectionHighlights(textEditor: vscode.TextEditor) {
311 | if (connection && textEditor.document.languageId === 'vue' && highlightDomElements) {
312 | const offset = await getTemplateOffset(textEditor.document);
313 | connection.highlight(
314 | textEditor.document.fileName,
315 | textEditor.selections.map(selection => ({
316 | start: textEditor.document.offsetAt(selection.start) - offset,
317 | end: textEditor.document.offsetAt(selection.end) - offset,
318 | })),
319 | textEditor.document.isDirty,
320 | );
321 | }
322 | else {
323 | connection?.unhighlight();
324 | }
325 | }
326 |
327 | async function openPreview(previewType: PreviewType, fileName: path.OsPath, mode: 'vite' | 'nuxt', _panel?: vscode.WebviewPanel) {
328 |
329 | const configFile = await getConfigFile(fileName, mode);
330 | if (!configFile)
331 | return;
332 |
333 | let terminal = vscode.window.terminals.find(terminal => terminal.name.startsWith('vue-preview:'));
334 | let port: number;
335 |
336 | if (terminal) {
337 | port = Number(terminal.name.split(':')[2]);
338 | }
339 | else {
340 | const configDir = path.dirname(configFile);
341 | const server = await startPreviewServer(configDir, mode);
342 | terminal = server.terminal;
343 | port = server.port;
344 | }
345 |
346 | const loadingPanel = _panel ?? vscode.window.createWebviewPanel(
347 | previewType,
348 | 'Preview ' + path.relative((vscode.workspace.rootPath ?? '') as path.OsPath, configFile),
349 | vscode.ViewColumn.Beside,
350 | {
351 | retainContextWhenHidden: true,
352 | enableScripts: true,
353 | enableFindWidget: true,
354 | },
355 | );
356 |
357 | const panelContext: vscode.Disposable[] = [];
358 |
359 | loadingPanel.onDidDispose(() => {
360 | for (const disposable of panelContext) {
361 | disposable.dispose();
362 | }
363 | });
364 |
365 | panelContext.push(loadingPanel.webview.onDidReceiveMessage(webviewEventHandler));
366 |
367 | terminal.show();
368 | _loadingPanel = loadingPanel;
369 |
370 | if (previewType === PreviewType.ExternalBrowser) {
371 | loadingPanel.webview.html = getWebviewContent(`http://localhost:${port}`, undefined, 'openExternal');
372 | }
373 | else if (previewType === PreviewType.ExternalBrowser_Component) {
374 | const root = vscode.workspace.getConfiguration('vue-preview').get('root')!;
375 | const relativePath = path.relative(path.resolve(path.dirname(configFile), root), fileName).replace(/\\/g, '/') as path.PosixPath;
376 | loadingPanel.webview.html = getWebviewContent(`http://localhost:${port}/${posix.join('__preview' as path.PosixPath, relativePath)}`, undefined, 'openExternal');
377 | }
378 | else if (previewType === PreviewType.Webview) {
379 | loadingPanel.webview.html = getWebviewContent(`http://localhost:${port}`, undefined, 'openSimpleBrowser');
380 | }
381 |
382 | return port;
383 |
384 | async function webviewEventHandler(message: any) {
385 | switch (message.command) {
386 | case 'openUrl': {
387 | const { url, external } = message.data;
388 | if (external) {
389 | vscode.env.openExternal(vscode.Uri.parse(url));
390 | }
391 | else {
392 | vscode.commands.executeCommand('simpleBrowser.api.open', url, { preserveFocus: true, viewColumn: _loadingPanel?.viewColumn ?? vscode.ViewColumn.Beside });
393 | }
394 | _loadingPanel?.dispose();
395 | break;
396 | }
397 | case 'log': {
398 | const text = message.data;
399 | vscode.window.showInformationMessage(text);
400 | break;
401 | }
402 | case 'warn': {
403 | const text = message.data;
404 | vscode.window.showWarningMessage(text);
405 | break;
406 | }
407 | case 'error': {
408 | const text = message.data;
409 | vscode.window.showErrorMessage(text);
410 | break;
411 | }
412 | }
413 | }
414 | }
415 |
416 | async function handleGoToCode(fileName: string, range: [number, number], cancelToken: { readonly isCancelled: boolean; }) {
417 |
418 | avoidUpdateOnDidChangeActiveTextEditor = true;
419 |
420 | const doc = await vscode.workspace.openTextDocument(fileName);
421 |
422 | if (cancelToken.isCancelled)
423 | return;
424 |
425 | const offset = await getTemplateOffset(doc);
426 | const start = doc.positionAt(range[0] + offset);
427 | const end = doc.positionAt(range[1] + offset);
428 | await vscode.window.showTextDocument(doc, vscode.ViewColumn.One);
429 |
430 | if (cancelToken.isCancelled)
431 | return;
432 |
433 | const editor = vscode.window.activeTextEditor;
434 | if (editor) {
435 | editor.selection = new vscode.Selection(start, end);
436 | editor.revealRange(new vscode.Range(start, end));
437 | }
438 | }
439 |
440 | async function startPreviewServer(viteDir: string, type: 'vite' | 'nuxt') {
441 |
442 | const port = await getLocalHostAvailablePort(vscode.workspace.getConfiguration('vue-preview').get('port')!);
443 | let script = await vscode.workspace.getConfiguration('vue-preview').get('script.' + (type === 'nuxt' ? 'nuxi' : 'vite')) ?? '';
444 |
445 | if (script.indexOf('{VITE_BIN}') >= 0) {
446 | script = script.replace('{VITE_BIN}', JSON.stringify(require.resolve('./dist/bin/vite', { paths: [context.extensionPath] })));
447 | }
448 | if (script.indexOf('{NUXI_BIN}') >= 0) {
449 | script = script.replace('{NUXI_BIN}', JSON.stringify(require.resolve('./dist/bin/nuxi', { paths: [context.extensionPath] })));
450 | }
451 | if (script.indexOf('{PORT}') >= 0) {
452 | script = script.replace('{PORT}', port.toString());
453 | }
454 |
455 | const terminal = vscode.window.createTerminal({
456 | name: 'vue-preview:' + type + ':' + port,
457 | isTransient: true,
458 | });
459 | terminal.sendText(`cd ${JSON.stringify(viteDir)}`);
460 | terminal.sendText(script);
461 |
462 | return {
463 | port,
464 | terminal,
465 | };
466 | }
467 |
468 | async function getConfigFile(fileName: path.OsPath, mode: 'vite' | 'nuxt') {
469 | let dir = path.dirname(fileName);
470 | let configFile: path.OsPath | undefined;
471 | while (true) {
472 | const configTs = path.join(dir, mode + '.config.ts' as path.PosixPath);
473 | const configJs = path.join(dir, mode + '.config.js' as path.PosixPath);
474 | if (await fs.exists(vscode.Uri.file(configTs))) {
475 | configFile = configTs;
476 | break;
477 | }
478 | if (await fs.exists(vscode.Uri.file(configJs))) {
479 | configFile = configJs;
480 | break;
481 | }
482 | const upperDir = path.dirname(dir);
483 | if (upperDir === dir) {
484 | break;
485 | }
486 | dir = upperDir;
487 | }
488 | return configFile;
489 | }
490 |
491 | function getWebviewContent(url: string, bg?: string, onLoadEvent?: 'openExternal' | 'openSimpleBrowser') {
492 |
493 | const configs = vscode.workspace.getConfiguration('vue-preview');
494 |
495 | let html = `
496 |
503 |
504 |
566 |
567 |
568 |
598 |
599 |
621 |
622 | `;
623 |
624 | return html;
625 | }
626 | }
627 |
--------------------------------------------------------------------------------