Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support component preview for Vite + Vue 3 #1476

Merged
merged 3 commits into from Jun 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
15 changes: 12 additions & 3 deletions extensions/vscode-vue-language-features/package.json
Expand Up @@ -55,6 +55,16 @@
"editor.quickSuggestions": true
}
},
"views": {
"explorer": [
{
"id": "vueComponentPreview",
"name": "Vue Component Preview",
"type": "webview",
"when": "volar.foundViteDir"
}
]
},
"jsonValidation": [
{
"fileMatch": "tsconfig.json",
Expand Down Expand Up @@ -407,12 +417,12 @@
},
"volar.preview.backgroundColor": {
"type": "string",
"default": "#fff",
"default": "transparent",
"description": "Component preview background color."
},
"volar.preview.transparentGrid": {
"type": "boolean",
"default": true,
"default": false,
"description": "Component preview background style."
},
"volar.splitEditors.layout.left": {
Expand Down Expand Up @@ -794,7 +804,6 @@
"@volar/preview": "0.37.9",
"@volar/shared": "0.37.9",
"@volar/vue-language-server": "0.37.9",
"@vue/compiler-dom": "^3.2.37",
"@vue/compiler-sfc": "^3.2.37",
"@vue/reactivity": "^3.2.37",
"esbuild": "latest",
Expand Down
161 changes: 70 additions & 91 deletions extensions/vscode-vue-language-features/src/features/preview.ts
@@ -1,5 +1,4 @@
import * as vscode from 'vscode';
import { compile, NodeTypes } from '@vue/compiler-dom';
import * as path from 'path';
import * as fs from '../utils/fs';
import * as shared from '@volar/shared';
Expand Down Expand Up @@ -58,6 +57,59 @@ export async function register(context: vscode.ExtensionContext) {

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

class VueComponentPreview implements vscode.WebviewViewProvider {

public resolveWebviewView(
webviewView: vscode.WebviewView,
_context: vscode.WebviewViewResolveContext,
_token: vscode.CancellationToken,
) {
webviewView.webview.options = {
enableScripts: true,
};
updateWebView();

vscode.window.onDidChangeActiveTextEditor(updateWebView);
vscode.workspace.onDidChangeConfiguration(updateWebView);
vscode.workspace.onDidSaveTextDocument(updateWebView);

async function updateWebView() {

if (!webviewView.visible)
return;

if (vscode.window.activeTextEditor?.document.languageId !== 'vue')
return;

const fileName = vscode.window.activeTextEditor.document.fileName;
let terminal = vscode.window.terminals.find(terminal => terminal.name.startsWith('volar-preview:'));
let port: number;

if (terminal) {
port = Number(terminal.name.split(':')[1]);
}
else {

const configFile = await getConfigFile(fileName, 'vite');
if (!configFile)
return;

const configDir = path.dirname(configFile);
const server = await startPreviewServer(configDir, 'vite');
terminal = server.terminal;
port = server.port;
}

const bgPath = vscode.Uri.file(path.join(context.extensionPath, 'images', 'preview-bg.png'));
const bgSrc = webviewView.webview.asWebviewUri(bgPath);
const url = `http://localhost:${port}/__preview#${fileName}`;

webviewView.webview.html = '';
webviewView.webview.html = getWebviewContent(url, undefined, bgSrc.toString());
}
}
}

class FinderPanelSerializer implements vscode.WebviewPanelSerializer {
async deserializeWebviewPanel(panel: vscode.WebviewPanel, state: PreviewState) {

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

const port = await openPreview(PreviewType.Webview, state.fileName, '', state.mode, panel);
const port = await openPreview(PreviewType.Webview, state.fileName, state.mode, panel);

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

const port = await openPreview(PreviewType.ComponentPreview, editor.document.fileName, editor.document.getText(), state.mode, panel);
const port = await openPreview(PreviewType.ComponentPreview, editor.document.fileName, state.mode, panel);

if (port !== undefined) {
const previewQuery = createQuery(editor.document);
updatePreviewPanel(panel, state.fileName, previewQuery, port, state.mode);
updatePreviewPanel(panel, state.fileName, port, state.mode);
}
}
}

vscode.window.registerWebviewViewProvider(
'vueComponentPreview',
new VueComponentPreview(),
);

context.subscriptions.push(vscode.commands.registerCommand('volar.action.vite', async () => {

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

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

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

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

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

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

// const disposable_1 = vscode.window.onDidChangeActiveTextEditor(async e => {
// if (e && e.document.languageId === 'vue' && e.document.fileName !== lastPreviewFile) {
// _panel.dispose();
// vscode.commands.executeCommand('volar.action.preview');

// // TODO: not working
// // const newQuery = createQuery(e.document.getText());
// // const url = `http://localhost:${port}/__preview${newQuery}#${e.document.fileName}`;
// // previewPanel?.webview.postMessage({ sender: 'volar', command: 'updateUrl', data: url });

// // lastPreviewFile = e.document.fileName;
// // lastPreviewQuery = newQuery;
// }
// });
let previewQuery = createQuery({
getText: () => fileText,
fileName,
version: -1,
} as vscode.TextDocument);

panelContext.push(vscode.workspace.onDidChangeTextDocument(e => {
if (e.document.fileName === fileName) {
const newPreviewQuery = createQuery(e.document);
if (newPreviewQuery !== previewQuery) {
const url = `http://localhost:${port}/__preview${newPreviewQuery}#${e.document.fileName}`;
panel.webview.postMessage({ sender: 'volar', command: 'updateUrl', data: url });

previewQuery = newPreviewQuery;
}
}
panelContext.push(vscode.workspace.onDidSaveTextDocument(e => {
vscode.commands.executeCommand('workbench.action.webview.reloadWebviewAction');
}));
panelContext.push(vscode.workspace.onDidChangeConfiguration(() => {
updatePreviewPanel(panel, fileName, previewQuery, port, mode);
updatePreviewPanel(panel, fileName, port, mode);
}));

updatePreviewPanel(panel, fileName, previewQuery, port, mode);
updatePreviewPanel(panel, fileName, port, mode);
}

return port;
Expand Down Expand Up @@ -475,59 +503,10 @@ export async function register(context: vscode.ExtensionContext) {
return configFile;
}

function createQuery(document: vscode.TextDocument) {

const sfc = getSfc(document);
let query = '';
let fileName = document.fileName;

for (const customBlock of sfc.descriptor.customBlocks) {
if (customBlock.type === 'preview') {
const previewTagStart = document.getText().substring(0, customBlock.loc.start.offset).lastIndexOf('<preview');
const previewTag = document.getText().substring(previewTagStart, customBlock.loc.start.offset);
const previewGen = compile(previewTag + '</preview>').ast;
const props: Record<string, string> = {};
for (const previewNode of previewGen.children) {
if (previewNode.type === NodeTypes.ELEMENT) {
for (const prop of previewNode.props) {
if (prop.type === NodeTypes.ATTRIBUTE) {
if (prop.value) {
props[prop.name] = JSON.stringify(prop.value.content);
}
else {
props[prop.name] = JSON.stringify(true);
}
}
else if (prop.type === NodeTypes.DIRECTIVE) {
if (prop.arg?.type === NodeTypes.SIMPLE_EXPRESSION && prop.exp?.type == NodeTypes.SIMPLE_EXPRESSION) {
props[prop.arg.content] = prop.exp.content;
}
}
}
}
}
const keys = Object.keys(props);
for (let i = 0; i < keys.length; i++) {
query += i === 0 ? '?' : '&';
const key = keys[i];
const value = props[key];
query += key;
query += '=';
query += encodeURIComponent(value);
}
}
else if (customBlock.type === 'preview-target' && typeof customBlock.attrs.path === 'string') {
fileName = path.resolve(path.dirname(fileName), customBlock.attrs.path);
}
}

return query;
}

function updatePreviewPanel(previewPanel: vscode.WebviewPanel, fileName: string, query: string, port: number, mode: 'vite' | 'nuxt') {
function updatePreviewPanel(previewPanel: vscode.WebviewPanel, fileName: string, port: number, mode: 'vite' | 'nuxt') {
const bgPath = vscode.Uri.file(path.join(context.extensionPath, 'images', 'preview-bg.png'));
const bgSrc = previewPanel.webview.asWebviewUri(bgPath);
const url = `http://localhost:${port}/__preview${query}#${fileName}`;
const url = `http://localhost:${port}/__preview#${fileName}`;
previewPanel.title = 'Preview ' + path.basename(fileName);
previewPanel.webview.html = getWebviewContent(url, { fileName, mode }, bgSrc.toString());
}
Expand Down
54 changes: 0 additions & 54 deletions packages/preview/bin/nuxi/plugin.ts
Expand Up @@ -320,58 +320,4 @@ export default app => {
}
}
}

// function installPreview() {
// if (location.pathname === '/__preview') {
// const preview = defineComponent({
// setup() {
// window.addEventListener('message', event => {
// if (event.data?.command === 'updateUrl') {
// url.value = new URL(event.data.data);
// _file.value = url.value.hash.slice(1);
// }
// });
// const url = ref(new URL(location.href));
// const _file = ref(url.value.hash.slice(1));
// const file = computed(() => {
// // fix windows path for vite
// let path = _file.value.replace(/\\/g, '/');
// if (path.indexOf(':') >= 0) {
// path = path.split(':')[1];
// }
// return path;
// });
// const target = computed(() => defineAsyncComponent(() => import(file.value))); // TODO: responsive not working
// const props = computed(() => {
// const _props: Record<string, any> = {};
// url.value.searchParams.forEach((value, key) => {
// eval('_props[key] = ' + value);
// });
// return _props;
// });
// return () => h(Suspense, undefined, [
// h(target.value, props.value)
// ]);
// },
// });
// // TODO: fix preview not working if preview component is root component
// (app._component as any).setup = preview.setup;

// app.config.warnHandler = (msg) => {
// window.parent.postMessage({
// command: 'warn',
// data: msg,
// }, '*');
// console.warn(msg);
// };
// app.config.errorHandler = (msg) => {
// window.parent.postMessage({
// command: 'error',
// data: msg,
// }, '*');
// console.error(msg);
// };
// // TODO: post emit
// }
// }
};