Skip to content

Commit bb5ae1d

Browse files
feat: support component preview for Vite + Vue 3 (#1476)
1 parent 936ed48 commit bb5ae1d

File tree

5 files changed

+163
-286
lines changed

5 files changed

+163
-286
lines changed

extensions/vscode-vue-language-features/package.json

+12-3
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,16 @@
5555
"editor.quickSuggestions": true
5656
}
5757
},
58+
"views": {
59+
"explorer": [
60+
{
61+
"id": "vueComponentPreview",
62+
"name": "Vue Component Preview",
63+
"type": "webview",
64+
"when": "volar.foundViteDir"
65+
}
66+
]
67+
},
5868
"jsonValidation": [
5969
{
6070
"fileMatch": "tsconfig.json",
@@ -407,12 +417,12 @@
407417
},
408418
"volar.preview.backgroundColor": {
409419
"type": "string",
410-
"default": "#fff",
420+
"default": "transparent",
411421
"description": "Component preview background color."
412422
},
413423
"volar.preview.transparentGrid": {
414424
"type": "boolean",
415-
"default": true,
425+
"default": false,
416426
"description": "Component preview background style."
417427
},
418428
"volar.splitEditors.layout.left": {
@@ -794,7 +804,6 @@
794804
"@volar/preview": "0.37.9",
795805
"@volar/shared": "0.37.9",
796806
"@volar/vue-language-server": "0.37.9",
797-
"@vue/compiler-dom": "^3.2.37",
798807
"@vue/compiler-sfc": "^3.2.37",
799808
"@vue/reactivity": "^3.2.37",
800809
"esbuild": "latest",

extensions/vscode-vue-language-features/src/features/preview.ts

+70-91
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import * as vscode from 'vscode';
2-
import { compile, NodeTypes } from '@vue/compiler-dom';
32
import * as path from 'path';
43
import * as fs from '../utils/fs';
54
import * as shared from '@volar/shared';
@@ -58,6 +57,59 @@ export async function register(context: vscode.ExtensionContext) {
5857

5958
const sfcs = new WeakMap<vscode.TextDocument, { version: number, sfc: SFCParseResult; }>();
6059

60+
class VueComponentPreview implements vscode.WebviewViewProvider {
61+
62+
public resolveWebviewView(
63+
webviewView: vscode.WebviewView,
64+
_context: vscode.WebviewViewResolveContext,
65+
_token: vscode.CancellationToken,
66+
) {
67+
webviewView.webview.options = {
68+
enableScripts: true,
69+
};
70+
updateWebView();
71+
72+
vscode.window.onDidChangeActiveTextEditor(updateWebView);
73+
vscode.workspace.onDidChangeConfiguration(updateWebView);
74+
vscode.workspace.onDidSaveTextDocument(updateWebView);
75+
76+
async function updateWebView() {
77+
78+
if (!webviewView.visible)
79+
return;
80+
81+
if (vscode.window.activeTextEditor?.document.languageId !== 'vue')
82+
return;
83+
84+
const fileName = vscode.window.activeTextEditor.document.fileName;
85+
let terminal = vscode.window.terminals.find(terminal => terminal.name.startsWith('volar-preview:'));
86+
let port: number;
87+
88+
if (terminal) {
89+
port = Number(terminal.name.split(':')[1]);
90+
}
91+
else {
92+
93+
const configFile = await getConfigFile(fileName, 'vite');
94+
if (!configFile)
95+
return;
96+
97+
const configDir = path.dirname(configFile);
98+
const server = await startPreviewServer(configDir, 'vite');
99+
terminal = server.terminal;
100+
port = server.port;
101+
}
102+
103+
const bgPath = vscode.Uri.file(path.join(context.extensionPath, 'images', 'preview-bg.png'));
104+
const bgSrc = webviewView.webview.asWebviewUri(bgPath);
105+
const url = `http://localhost:${port}/__preview#${fileName}`;
106+
107+
webviewView.webview.html = '';
108+
webviewView.webview.html = getWebviewContent(url, undefined, bgSrc.toString());
109+
}
110+
}
111+
}
112+
61113
class FinderPanelSerializer implements vscode.WebviewPanelSerializer {
62114
async deserializeWebviewPanel(panel: vscode.WebviewPanel, state: PreviewState) {
63115

@@ -66,7 +118,7 @@ export async function register(context: vscode.ExtensionContext) {
66118
return; // don't create server because maybe user closed it intentionally
67119
}
68120

69-
const port = await openPreview(PreviewType.Webview, state.fileName, '', state.mode, panel);
121+
const port = await openPreview(PreviewType.Webview, state.fileName, state.mode, panel);
70122

71123
panel.webview.html = getWebviewContent(`http://localhost:${port}`, state);
72124
}
@@ -83,15 +135,19 @@ export async function register(context: vscode.ExtensionContext) {
83135
return; // don't create server because maybe user closed it intentionally
84136
}
85137

86-
const port = await openPreview(PreviewType.ComponentPreview, editor.document.fileName, editor.document.getText(), state.mode, panel);
138+
const port = await openPreview(PreviewType.ComponentPreview, editor.document.fileName, state.mode, panel);
87139

88140
if (port !== undefined) {
89-
const previewQuery = createQuery(editor.document);
90-
updatePreviewPanel(panel, state.fileName, previewQuery, port, state.mode);
141+
updatePreviewPanel(panel, state.fileName, port, state.mode);
91142
}
92143
}
93144
}
94145

146+
vscode.window.registerWebviewViewProvider(
147+
'vueComponentPreview',
148+
new VueComponentPreview(),
149+
);
150+
95151
context.subscriptions.push(vscode.commands.registerCommand('volar.action.vite', async () => {
96152

97153
const editor = vscode.window.activeTextEditor;
@@ -118,7 +174,7 @@ export async function register(context: vscode.ExtensionContext) {
118174
if (select === undefined)
119175
return; // cancel
120176

121-
openPreview(select as PreviewType, editor.document.fileName, editor.document.getText(), 'vite');
177+
openPreview(select as PreviewType, editor.document.fileName, 'vite');
122178
}));
123179
context.subscriptions.push(vscode.commands.registerCommand('volar.action.nuxt', async () => {
124180

@@ -141,7 +197,7 @@ export async function register(context: vscode.ExtensionContext) {
141197
if (select === undefined)
142198
return; // cancel
143199

144-
openPreview(select as PreviewType, editor.document.fileName, editor.document.getText(), 'nuxt');
200+
openPreview(select as PreviewType, editor.document.fileName, 'nuxt');
145201
}));
146202
context.subscriptions.push(vscode.commands.registerCommand('volar.action.selectElement', () => {
147203
const panel = [...panels].find(panel => panel.active);
@@ -260,7 +316,7 @@ export async function register(context: vscode.ExtensionContext) {
260316
}
261317
}
262318

263-
async function openPreview(previewType: PreviewType, fileName: string, fileText: string, mode: 'vite' | 'nuxt', _panel?: vscode.WebviewPanel) {
319+
async function openPreview(previewType: PreviewType, fileName: string, mode: 'vite' | 'nuxt', _panel?: vscode.WebviewPanel) {
264320

265321
const configFile = await getConfigFile(fileName, mode);
266322
if (!configFile)
@@ -329,42 +385,14 @@ export async function register(context: vscode.ExtensionContext) {
329385
}
330386
else if (previewType === PreviewType.ComponentPreview) {
331387

332-
// const disposable_1 = vscode.window.onDidChangeActiveTextEditor(async e => {
333-
// if (e && e.document.languageId === 'vue' && e.document.fileName !== lastPreviewFile) {
334-
// _panel.dispose();
335-
// vscode.commands.executeCommand('volar.action.preview');
336-
337-
// // TODO: not working
338-
// // const newQuery = createQuery(e.document.getText());
339-
// // const url = `http://localhost:${port}/__preview${newQuery}#${e.document.fileName}`;
340-
// // previewPanel?.webview.postMessage({ sender: 'volar', command: 'updateUrl', data: url });
341-
342-
// // lastPreviewFile = e.document.fileName;
343-
// // lastPreviewQuery = newQuery;
344-
// }
345-
// });
346-
let previewQuery = createQuery({
347-
getText: () => fileText,
348-
fileName,
349-
version: -1,
350-
} as vscode.TextDocument);
351-
352-
panelContext.push(vscode.workspace.onDidChangeTextDocument(e => {
353-
if (e.document.fileName === fileName) {
354-
const newPreviewQuery = createQuery(e.document);
355-
if (newPreviewQuery !== previewQuery) {
356-
const url = `http://localhost:${port}/__preview${newPreviewQuery}#${e.document.fileName}`;
357-
panel.webview.postMessage({ sender: 'volar', command: 'updateUrl', data: url });
358-
359-
previewQuery = newPreviewQuery;
360-
}
361-
}
388+
panelContext.push(vscode.workspace.onDidSaveTextDocument(e => {
389+
vscode.commands.executeCommand('workbench.action.webview.reloadWebviewAction');
362390
}));
363391
panelContext.push(vscode.workspace.onDidChangeConfiguration(() => {
364-
updatePreviewPanel(panel, fileName, previewQuery, port, mode);
392+
updatePreviewPanel(panel, fileName, port, mode);
365393
}));
366394

367-
updatePreviewPanel(panel, fileName, previewQuery, port, mode);
395+
updatePreviewPanel(panel, fileName, port, mode);
368396
}
369397

370398
return port;
@@ -475,59 +503,10 @@ export async function register(context: vscode.ExtensionContext) {
475503
return configFile;
476504
}
477505

478-
function createQuery(document: vscode.TextDocument) {
479-
480-
const sfc = getSfc(document);
481-
let query = '';
482-
let fileName = document.fileName;
483-
484-
for (const customBlock of sfc.descriptor.customBlocks) {
485-
if (customBlock.type === 'preview') {
486-
const previewTagStart = document.getText().substring(0, customBlock.loc.start.offset).lastIndexOf('<preview');
487-
const previewTag = document.getText().substring(previewTagStart, customBlock.loc.start.offset);
488-
const previewGen = compile(previewTag + '</preview>').ast;
489-
const props: Record<string, string> = {};
490-
for (const previewNode of previewGen.children) {
491-
if (previewNode.type === NodeTypes.ELEMENT) {
492-
for (const prop of previewNode.props) {
493-
if (prop.type === NodeTypes.ATTRIBUTE) {
494-
if (prop.value) {
495-
props[prop.name] = JSON.stringify(prop.value.content);
496-
}
497-
else {
498-
props[prop.name] = JSON.stringify(true);
499-
}
500-
}
501-
else if (prop.type === NodeTypes.DIRECTIVE) {
502-
if (prop.arg?.type === NodeTypes.SIMPLE_EXPRESSION && prop.exp?.type == NodeTypes.SIMPLE_EXPRESSION) {
503-
props[prop.arg.content] = prop.exp.content;
504-
}
505-
}
506-
}
507-
}
508-
}
509-
const keys = Object.keys(props);
510-
for (let i = 0; i < keys.length; i++) {
511-
query += i === 0 ? '?' : '&';
512-
const key = keys[i];
513-
const value = props[key];
514-
query += key;
515-
query += '=';
516-
query += encodeURIComponent(value);
517-
}
518-
}
519-
else if (customBlock.type === 'preview-target' && typeof customBlock.attrs.path === 'string') {
520-
fileName = path.resolve(path.dirname(fileName), customBlock.attrs.path);
521-
}
522-
}
523-
524-
return query;
525-
}
526-
527-
function updatePreviewPanel(previewPanel: vscode.WebviewPanel, fileName: string, query: string, port: number, mode: 'vite' | 'nuxt') {
506+
function updatePreviewPanel(previewPanel: vscode.WebviewPanel, fileName: string, port: number, mode: 'vite' | 'nuxt') {
528507
const bgPath = vscode.Uri.file(path.join(context.extensionPath, 'images', 'preview-bg.png'));
529508
const bgSrc = previewPanel.webview.asWebviewUri(bgPath);
530-
const url = `http://localhost:${port}/__preview${query}#${fileName}`;
509+
const url = `http://localhost:${port}/__preview#${fileName}`;
531510
previewPanel.title = 'Preview ' + path.basename(fileName);
532511
previewPanel.webview.html = getWebviewContent(url, { fileName, mode }, bgSrc.toString());
533512
}

packages/preview/bin/nuxi/plugin.ts

-54
Original file line numberDiff line numberDiff line change
@@ -320,58 +320,4 @@ export default app => {
320320
}
321321
}
322322
}
323-
324-
// function installPreview() {
325-
// if (location.pathname === '/__preview') {
326-
// const preview = defineComponent({
327-
// setup() {
328-
// window.addEventListener('message', event => {
329-
// if (event.data?.command === 'updateUrl') {
330-
// url.value = new URL(event.data.data);
331-
// _file.value = url.value.hash.slice(1);
332-
// }
333-
// });
334-
// const url = ref(new URL(location.href));
335-
// const _file = ref(url.value.hash.slice(1));
336-
// const file = computed(() => {
337-
// // fix windows path for vite
338-
// let path = _file.value.replace(/\\/g, '/');
339-
// if (path.indexOf(':') >= 0) {
340-
// path = path.split(':')[1];
341-
// }
342-
// return path;
343-
// });
344-
// const target = computed(() => defineAsyncComponent(() => import(file.value))); // TODO: responsive not working
345-
// const props = computed(() => {
346-
// const _props: Record<string, any> = {};
347-
// url.value.searchParams.forEach((value, key) => {
348-
// eval('_props[key] = ' + value);
349-
// });
350-
// return _props;
351-
// });
352-
// return () => h(Suspense, undefined, [
353-
// h(target.value, props.value)
354-
// ]);
355-
// },
356-
// });
357-
// // TODO: fix preview not working if preview component is root component
358-
// (app._component as any).setup = preview.setup;
359-
360-
// app.config.warnHandler = (msg) => {
361-
// window.parent.postMessage({
362-
// command: 'warn',
363-
// data: msg,
364-
// }, '*');
365-
// console.warn(msg);
366-
// };
367-
// app.config.errorHandler = (msg) => {
368-
// window.parent.postMessage({
369-
// command: 'error',
370-
// data: msg,
371-
// }, '*');
372-
// console.error(msg);
373-
// };
374-
// // TODO: post emit
375-
// }
376-
// }
377323
};

0 commit comments

Comments
 (0)