diff --git a/README.md b/README.md index 78ef4dc..7fef584 100644 --- a/README.md +++ b/README.md @@ -4,17 +4,18 @@ **English** | [中文](./README.zh_CN.md) -> The [vite](https://cn.vitejs.dev/) plugin for [vscode extension](https://code.visualstudio.com/api), supports `esm` and `cjs`. +> Use `vue`/`react` to develop [vscode extension webview](https://code.visualstudio.com/api/references/vscode-api#WebviewPanel), supporting `esm` and `cjs`. -Inject [@tomjs/vscode-extension-webview](https://github.com/tomjs/vscode-extension-webview) into vscode extension code and web client code, so that webview can support HMR during the development stage. +In development mode, inject the code of [@tomjs/vscode-extension-webview](https://github.com/tomjs/vscode-extension-webview) into `vscode extension code` and `web page code`, use To support `HMR`; during production build, the final generated `index.html` code is injected into `vscode extension code` to reduce the workload. ## Features -- Fast build `extension` with [tsup](https://github.com/egoist/tsup) -- Little configuration, focus on business +- Use [tsup](https://github.com/egoist/tsup) to quickly build `extension code` +- Simple configuration, focus on business - Support `esm` and `cjs` - Support webview `HMR` -- Support `vue` and `react` and other [frameworks](https://vitejs.dev/guide/#trying-vite-online) supported by `vite` +- Support [Multi-Page App](https://vitejs.dev/guide/build.html#multi-page-app) +- Supports `vue` and `react` and other [frameworks](https://cn.vitejs.dev/guide/#trying-vite-online) supported by `vite` ## Install @@ -75,39 +76,11 @@ const panel = window.createWebviewPanel('showHelloWorld', 'Hello World', ViewCol enableScripts: true, localResourceRoots: [Uri.joinPath(extensionUri, 'dist/webview')], }); -``` - -```ts -private _getWebviewContent(webview: Webview, extensionUri: Uri) { - // The CSS file from the Vue build output - const stylesUri = getUri(webview, extensionUri, ['dist', 'webview', 'assets', 'index.css']); - // The JS file from the Vue build output - const scriptUri = getUri(webview, extensionUri, ['dist', 'webview', 'assets', 'index.js']); - - const nonce = uuid(); - - if (process.env.VITE_DEV_SERVER_URL) { - return __getWebviewHtml__(process.env.VITE_DEV_SERVER_URL); - } - // Tip: Install the es6-string-html VS Code extension to enable code highlighting below - return /*html*/ ` - - - - - - - - - Hello World - - -
- - - `; - } +// Vite development mode and production mode inject different webview codes to reduce development work +panel.webview.html = process.env.VITE_DEV_SERVER_URL + ? __getWebviewHtml__(process.env.VITE_DEV_SERVER_URL) + : __getWebviewHtml__(webview, context); ``` - `package.json` @@ -157,6 +130,70 @@ export default defineConfig({ }); ``` +### Multi-page application + +See [vue-import](./examples/vue-import) example + +- `vite.config.ts` + +```ts +import path from 'node:path'; +import vscode from '@tomjs/vite-plugin-vscode'; + +export default defineConfig({ + build: { + plugins: [vscode()] + rollupOptions: { + // https://cn.vitejs.dev/guide/build.html#multi-page-app + input: [path.resolve(__dirname, 'index.html'), path.resolve(__dirname, 'index2.html')], + // You can also customize the name + // input:{ + // 'index': path.resolve(__dirname, 'index.html'), + // 'index2': path.resolve(__dirname, 'index2.html'), + // } + }, + }, +}); +``` + +- page one + +```ts +process.env.VITE_DEV_SERVER_URL + ? __getWebviewHtml__(process.env.VITE_DEV_SERVER_URL) + : __getWebviewHtml__(webview, context); +``` + +- page two + +```ts +process.env.VITE_DEV_SERVER_URL + ? __getWebviewHtml__(`${process.env.VITE_DEV_SERVER_URL}/index2.html`) + : __getWebviewHtml__(webview, context, 'index2'); +``` + +**getWebviewHtml** Description + +```ts +/** + * `[vite serve]` Gets the html of webview in development mode. + * @param options serverUrl: The url of the vite dev server. + */ +function __getWebviewHtml__(options?: string | { serverUrl: string }): string; + +/** + * `[vite serve]` Gets the html of webview in production mode. + * @param webview The WebviewPanel instance of the extension. + * @param context The ExtensionContext instance of the extension. + * @param inputName vite build.rollupOptions.input name. Default is `index`. + */ +function __getWebviewHtml__( + webview: Webview, + context: ExtensionContext, + inputName?: string, +): string; +``` + ## Documentation - [API Documentation](https://paka.dev/npm/@tomjs/vite-plugin-vscode) provided by [paka.dev](https://paka.dev). @@ -206,15 +243,15 @@ Based on [Options](https://paka.dev/npm/tsup) of [tsup](https://tsup.egoist.dev/ - `development` mode -| Variable | Description | -| --------------------- | ------------------------------- | -| `VITE_DEV_SERVER_URL` | The url of the vite dev server. | +| Variable | Description | +| --------------------- | ------------------------------ | +| `VITE_DEV_SERVER_URL` | The url of the vite dev server | - `production` mode -| Variable | Description | -| --- | --- | -| `VITE_DIST_FILES` | All js files in the dist directory, excluding index.js. It's to be a json string. | +| Variable | Description | +| ------------------- | ----------------------------- | +| `VITE_WEBVIEW_DIST` | vite webview page output path | ## Debug @@ -232,7 +269,15 @@ Run `Debug Extension` through `vscode` to debug. For debugging tools, refer to [ "request": "launch", "args": ["--extensionDevelopmentPath=${workspaceFolder}"], "outFiles": ["${workspaceFolder}/dist/extension/*.js"], - "preLaunchTask": "${defaultBuildTask}" + "preLaunchTask": "npm: dev" + }, + { + "name": "Preview Extension", + "type": "extensionHost", + "request": "launch", + "args": ["--extensionDevelopmentPath=${workspaceFolder}"], + "outFiles": ["${workspaceFolder}/dist/extension/*.js"], + "preLaunchTask": "npm: build" } ] } @@ -272,6 +317,15 @@ Run `Debug Extension` through `vscode` to debug. For debugging tools, refer to [ "kind": "build", "isDefault": true } + }, + { + "type": "npm", + "script": "build", + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": [] } ] } @@ -288,6 +342,6 @@ pnpm build Open the [examples](./examples) directory, there are `vue` and `react` examples. -- [react](./examples/react): simple react example. -- [vue](./examples/vue): simple vue example. -- [vue-import](./examples/vue-import): dynamic import() example. +- [react](./examples/react): Simple react example. +- [vue](./examples/vue): Simple vue example. +- [vue-import](./examples/vue-import): Dynamic import() and multi-page examples. diff --git a/README.zh_CN.md b/README.zh_CN.md index 9bf43a2..4a10e7e 100644 --- a/README.zh_CN.md +++ b/README.zh_CN.md @@ -4,17 +4,18 @@ [English](./README.md) | **中文** -> [vscode extension](https://code.visualstudio.com/api) 的 [vite](https://cn.vitejs.dev/) 插件,支持 `esm` 和 `cjs`。 +> 用 `vue`/`react` 来开发 [vscode extension webview](https://code.visualstudio.com/api/references/vscode-api#WebviewPanel) ,支持 `esm` 和 `cjs`。 -给 vscode 扩展代码和 web 客户端代码中注入 [@tomjs/vscode-extension-webview](https://github.com/tomjs/vscode-extension-webview),可以让 webview 在开发阶段支持 `HMR` +在开发模式时,给 `vscode 扩展代码` 和 `web 页面代码`中注入 [@tomjs/vscode-extension-webview](https://github.com/tomjs/vscode-extension-webview) 的代码,用来支持 `HMR`;生产构建时,将最终生成的`index.html` 代码注入到 `vscode 扩展代码` 中,减少工作量。 ## 特性 -- 使用 [tsup](https://github.com/egoist/tsup) 快速构建 `main` 和 `preload` +- 使用 [tsup](https://github.com/egoist/tsup) 快速构建 `扩展代码` - 配置简单,专注业务 - 支持 `esm`和 `cjs` - 支持 webview `HMR` -- 支持 `vue` 和 `react` 等其他 `vite` 支持的[框架](https://cn.vitejs.dev/guide/#trying-vite-online) +- 支持[多页面应用](https://cn.vitejs.dev/guide/build.html#multi-page-app) +- 支持 `vue` 、`react` 等其他 `vite` 支持的[框架](https://cn.vitejs.dev/guide/#trying-vite-online) ## 安装 @@ -75,39 +76,15 @@ const panel = window.createWebviewPanel('showHelloWorld', 'Hello World', ViewCol enableScripts: true, localResourceRoots: [Uri.joinPath(extensionUri, 'dist/webview')], }); -``` - -```ts -private _getWebviewContent(webview: Webview, extensionUri: Uri) { - // The CSS file from the Vue build output - const stylesUri = getUri(webview, extensionUri, ['dist/webview/assets/index.css']); - // The JS file from the Vue build output - const scriptUri = getUri(webview, extensionUri, ['dist/webview/assets/index.js']); - const nonce = uuid(); - - if (process.env.VITE_DEV_SERVER_URL) { - return __getWebviewHtml__(process.env.VITE_DEV_SERVER_URL); - } +// vite 开发模式和生产模式注入不同的webview代码,减少开发工作 +function getHtml(webview: Webview, context: ExtensionContext) { + process.env.VITE_DEV_SERVER_URL + ? __getWebviewHtml__(process.env.VITE_DEV_SERVER_URL) + : __getWebviewHtml__(webview, context); +} - // Tip: Install the es6-string-html VS Code extension to enable code highlighting below - return /*html*/ ` - - - - - - - - - Hello World - - -
- - - `; - } +panel.webview.html = getHtml(webview, context); ``` - `package.json` @@ -157,6 +134,70 @@ export default defineConfig({ }); ``` +### **getWebviewHtml** + +可查看 [vue-import](./examples/vue-import) 示例 + +- `vite.config.ts` + +```ts +import path from 'node:path'; +import vscode from '@tomjs/vite-plugin-vscode'; + +export default defineConfig({ + build: { + plugins: [vscode()] + rollupOptions: { + // https://cn.vitejs.dev/guide/build.html#multi-page-app + input: [path.resolve(__dirname, 'index.html'), path.resolve(__dirname, 'index2.html')], + // 也可自定义名称 + // input:{ + // 'index': path.resolve(__dirname, 'index.html'), + // 'index2': path.resolve(__dirname, 'index2.html'), + // } + }, + }, +}); +``` + +- 页面一 + +```ts +process.env.VITE_DEV_SERVER_URL + ? __getWebviewHtml__(process.env.VITE_DEV_SERVER_URL) + : __getWebviewHtml__(webview, context); +``` + +- 页面二 + +```ts +process.env.VITE_DEV_SERVER_URL + ? __getWebviewHtml__(`${process.env.VITE_DEV_SERVER_URL}/index2.html`) + : __getWebviewHtml__(webview, context, 'index2'); +``` + +**getWebviewHtml** 说明 + +```ts +/** + * `[vite serve]` 在开发模式获取webview的html + * @param options serverUrl: vite开发服务器的url + */ +function __getWebviewHtml__(options?: string | { serverUrl: string }): string; + +/** + * `[vite serve]` 在生产模式获取webview的html + * @param webview 扩展的 Webview 实例 + * @param context 扩展的 ExtensionContext 实例 + * @param inputName vite build.rollupOptions.input 设置的名称. 默认 `index`. + */ +function __getWebviewHtml__( + webview: Webview, + context: ExtensionContext, + inputName?: string, +): string; +``` + ## 文档 - [paka.dev](https://paka.dev) 提供的 [API文档](https://paka.dev/npm/@tomjs/vite-plugin-vscode). @@ -205,15 +246,15 @@ export default defineConfig({ - `development` 模式 -| 变量 | 描述 | -| --------------------- | --------------------- | -| `VITE_DEV_SERVER_URL` | vite开发服务器的url。 | +| 变量 | 描述 | +| --------------------- | ------------------- | +| `VITE_DEV_SERVER_URL` | vite开发服务器的url | - `production` 模式 -| 变量 | 描述 | -| ----------------- | --------------------------------------------------------------- | -| `VITE_DIST_FILES` | dist目录下的所有js文件,不包括index.js。 它是一个 json 字符串。 | +| 变量 | 描述 | +| ------------------- | ------------------------- | +| `VITE_WEBVIEW_DIST` | vite webview 页面输出路径 | ## Debug @@ -231,7 +272,15 @@ export default defineConfig({ "request": "launch", "args": ["--extensionDevelopmentPath=${workspaceFolder}"], "outFiles": ["${workspaceFolder}/dist/extension/*.js"], - "preLaunchTask": "${defaultBuildTask}" + "preLaunchTask": "npm: dev" + }, + { + "name": "Preview Extension", + "type": "extensionHost", + "request": "launch", + "args": ["--extensionDevelopmentPath=${workspaceFolder}"], + "outFiles": ["${workspaceFolder}/dist/extension/*.js"], + "preLaunchTask": "npm: build" } ] } @@ -271,6 +320,15 @@ export default defineConfig({ "kind": "build", "isDefault": true } + }, + { + "type": "npm", + "script": "build", + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": [] } ] } @@ -289,4 +347,4 @@ pnpm build - [react](./examples/react):简单的 react 示例。 - [vue](./examples/vue):简单的 vue 示例。 -- [vue-import](./examples/vue-import):动态 import() 示例。 +- [vue-import](./examples/vue-import):动态 import() 和多页面示例。 diff --git a/env.d.ts b/env.d.ts index 242da4e..6100e64 100644 --- a/env.d.ts +++ b/env.d.ts @@ -1,5 +1,6 @@ -export {}; // Make this a module - +import { ExtensionContext, Webview } from 'vscode'; +// Make this a module +export {}; declare global { /** * fix code hint @@ -19,13 +20,25 @@ declare global { /** * `[vite build]` All js files in the dist directory, excluding index.js. It's to be a json string. */ - VITE_DIST_FILES?: string; + VITE_WEBVIEW_DIST?: string; } } /** - * `[vite serve]` Get the html of the development webview. + * `[vite serve]` Gets the html of webview in development mode. * @param options serverUrl: The url of the vite dev server. */ function __getWebviewHtml__(options?: string | { serverUrl: string }): string; + + /** + * `[vite serve]` Gets the html of webview in production mode. + * @param webview The Webview instance of the extension. + * @param context The ExtensionContext instance of the extension. + * @param inputName vite build.rollupOptions.input name. Default is `index`. + */ + function __getWebviewHtml__( + webview: Webview, + context: ExtensionContext, + inputName?: string, + ): string; } diff --git a/examples/react/.vscode/launch.json b/examples/react/.vscode/launch.json index 319030c..d2342f8 100644 --- a/examples/react/.vscode/launch.json +++ b/examples/react/.vscode/launch.json @@ -15,7 +15,7 @@ "outFiles": [ "${workspaceFolder}/dist/extension/*.js" ], - "preLaunchTask": "${defaultBuildTask}" + "preLaunchTask": "npm: dev" }, { "name": "Preview Extension", diff --git a/examples/react/extension/index.ts b/examples/react/extension/index.ts index 506e42f..eb788f6 100644 --- a/examples/react/extension/index.ts +++ b/examples/react/extension/index.ts @@ -1,14 +1,13 @@ import { commands, ExtensionContext } from 'vscode'; -import { MainPanel } from './panels/MainPanel'; +import { MainPanel } from './views/panel'; export function activate(context: ExtensionContext) { - // Create the show hello world command - const showHelloWorldCommand = commands.registerCommand('hello-world.showHelloWorld', async () => { - MainPanel.render(context.extensionUri); - }); - // Add command to the extension context - context.subscriptions.push(showHelloWorldCommand); + context.subscriptions.push( + commands.registerCommand('hello-world.showHelloWorld', async () => { + MainPanel.render(context); + }), + ); } export function deactivate() {} diff --git a/examples/react/extension/panels/MainPanel.ts b/examples/react/extension/panels/MainPanel.ts deleted file mode 100644 index ed57c60..0000000 --- a/examples/react/extension/panels/MainPanel.ts +++ /dev/null @@ -1,212 +0,0 @@ -import { Disposable, Uri, ViewColumn, Webview, WebviewPanel, window } from 'vscode'; -// import __getWebviewHtml__ from '@tomjs/vscode-extension-webview'; - -function uuid() { - let text = ''; - const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - for (let i = 0; i < 32; i++) { - text += possible.charAt(Math.floor(Math.random() * possible.length)); - } - return text; -} - -function getUri(webview: Webview, extensionUri: Uri, pathList: string[]) { - return webview.asWebviewUri(Uri.joinPath(extensionUri, ...pathList)); -} - -/** - * This class manages the state and behavior of HelloWorld webview panels. - * - * It contains all the data and methods for: - * - * - Creating and rendering HelloWorld webview panels - * - Properly cleaning up and disposing of webview resources when the panel is closed - * - Setting the HTML (and by proxy CSS/JavaScript) content of the webview panel - * - Setting message listeners so data can be passed between the webview and extension - */ -export class MainPanel { - public static currentPanel: MainPanel | undefined; - private readonly _panel: WebviewPanel; - private _disposables: Disposable[] = []; - - /** - * The MainPanel class private constructor (called only from the render method). - * - * @param panel A reference to the webview panel - * @param extensionUri The URI of the directory containing the extension - */ - private constructor(panel: WebviewPanel, extensionUri: Uri) { - this._panel = panel; - - // Set an event listener to listen for when the panel is disposed (i.e. when the user closes - // the panel or when the panel is closed programmatically) - this._panel.onDidDispose(() => this.dispose(), null, this._disposables); - - // Set the HTML content for the webview panel - this._panel.webview.html = this._getWebviewContent(this._panel.webview, extensionUri); - - // Set an event listener to listen for messages passed from the webview context - this._setWebviewMessageListener(this._panel.webview); - } - - /** - * Renders the current webview panel if it exists otherwise a new webview panel - * will be created and displayed. - * - * @param extensionUri The URI of the directory containing the extension. - */ - public static render(extensionUri: Uri) { - if (MainPanel.currentPanel) { - // If the webview panel already exists reveal it - MainPanel.currentPanel._panel.reveal(ViewColumn.One); - } else { - // If a webview panel does not already exist create and show a new one - const panel = window.createWebviewPanel( - // Panel view type - 'showHelloWorld', - // Panel title - 'Hello World', - // The editor column the panel should be displayed in - ViewColumn.One, - // Extra panel configurations - { - // Enable JavaScript in the webview - enableScripts: true, - // Restrict the webview to only load resources from the `dist/webview` directories - localResourceRoots: [Uri.joinPath(extensionUri, 'dist/webview')], - }, - ); - - MainPanel.currentPanel = new MainPanel(panel, extensionUri); - } - } - - /** - * Cleans up and disposes of webview resources when the webview panel is closed. - */ - public dispose() { - MainPanel.currentPanel = undefined; - - // Dispose of the current webview panel - this._panel.dispose(); - - // Dispose of all disposables (i.e. commands) for the current webview panel - while (this._disposables.length) { - const disposable = this._disposables.pop(); - if (disposable) { - disposable.dispose(); - } - } - } - - /** - * Defines and returns the HTML that should be rendered within the webview panel. - * - * @remarks This is also the place where references to the Vue webview build files - * are created and inserted into the webview HTML. - * - * @param webview A reference to the extension webview - * @param extensionUri The URI of the directory containing the extension - * @returns A template string literal containing the HTML that should be - * rendered within the webview panel - */ - private _getWebviewContent(webview: Webview, extensionUri: Uri) { - console.log('extensionUri:', extensionUri); - // The CSS file from the Vue build output - const stylesUri = getUri(webview, extensionUri, ['dist/webview/assets/index.css']); - const scriptUri = getUri(webview, extensionUri, ['dist/webview/assets/index.js']); - // The JS file from the Vue build output - // const scriptUri = getUri(webview, extensionUri, ['dist', 'webview', 'assets', 'index.js']); - console.log( - 'scriptUri:', - getUri(webview, extensionUri, ['dist', 'webview', 'assets', 'index.js']), - ); - - const baseUri = getUri(webview, extensionUri, ['dist/webview']); - console.log('baseUri:', baseUri, baseUri.toString()); - - const nonce = uuid(); - - console.log('VITE_DEV_SERVER_URL:', process.env.VITE_DEV_SERVER_URL); - - if (process.env.VITE_DEV_SERVER_URL) { - return __getWebviewHtml__(process.env.VITE_DEV_SERVER_URL); - } - - // const jsFiles = [ - // 'dist/webview/assets/batchSamplersUniformGroup.js', - // 'dist/webview/assets/browserAll.js', - // 'dist/webview/assets/CanvasPool.js', - // 'dist/webview/assets/localUniformBit.js', - // 'dist/webview/assets/SharedSystems.js', - // 'dist/webview/assets/WebGLRenderer.js', - // 'dist/webview/assets/WebGPURenderer.js', - // 'dist/webview/assets/webworkerAll.js', - // ]; - - const jsDistFiles = process.env.VITE_DIST_FILES; - let jsFiles = []; - try { - if (jsDistFiles) { - jsFiles = JSON.parse(jsDistFiles || ''); - } - } catch {} - - const injectScripts = jsFiles - .map( - s => - ``, - ) - .join('\n'); - - // Tip: Install the es6-string-html VS Code extension to enable code highlighting below - return /*html*/ ` - - - - - - - - ${injectScripts} - - - Hello World - - -
- - - `; - } - - /** - * Sets up an event listener to listen for messages passed from the webview context and - * executes code based on the message that is recieved. - * - * @param webview A reference to the extension webview - * @param context A reference to the extension context - */ - private _setWebviewMessageListener(webview: Webview) { - webview.onDidReceiveMessage( - (message: any) => { - const command = message.command; - const text = message.text; - console.log(`command: ${command}`); - - switch (command) { - case 'hello': - // Code that should run in response to the hello message command - window.showInformationMessage(text); - return; - // Add more switch case statements here as more webview message commands - // are created within the webview context (i.e. inside media/main.js) - } - }, - undefined, - this._disposables, - ); - } -} diff --git a/examples/react/extension/views/helper.ts b/examples/react/extension/views/helper.ts new file mode 100644 index 0000000..78447b4 --- /dev/null +++ b/examples/react/extension/views/helper.ts @@ -0,0 +1,35 @@ +import { Disposable, ExtensionContext, Webview, window } from 'vscode'; + +export function uuid() { + let text = ''; + const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + for (let i = 0; i < 32; i++) { + text += possible.charAt(Math.floor(Math.random() * possible.length)); + } + return text; +} + +export class WebviewHelper { + public static setupHtml(webview: Webview, context: ExtensionContext) { + return process.env.VITE_DEV_SERVER_URL + ? __getWebviewHtml__(process.env.VITE_DEV_SERVER_URL) + : __getWebviewHtml__(webview, context); + } + + public static setupWebviewHooks(webview: Webview, disposables: Disposable[]) { + webview.onDidReceiveMessage( + (message: any) => { + const command = message.command; + const text = message.text; + console.log(`command: ${command}`); + switch (command) { + case 'hello': + window.showInformationMessage(text); + return; + } + }, + undefined, + disposables, + ); + } +} diff --git a/examples/react/extension/views/panel.ts b/examples/react/extension/views/panel.ts new file mode 100644 index 0000000..eee8e68 --- /dev/null +++ b/examples/react/extension/views/panel.ts @@ -0,0 +1,47 @@ +import { Disposable, ExtensionContext, ViewColumn, WebviewPanel, window } from 'vscode'; +import { WebviewHelper } from './helper'; + +export class MainPanel { + public static currentPanel: MainPanel | undefined; + private readonly _panel: WebviewPanel; + private _disposables: Disposable[] = []; + + private constructor(panel: WebviewPanel, context: ExtensionContext) { + this._panel = panel; + + this._panel.onDidDispose(() => this.dispose(), null, this._disposables); + this._panel.webview.html = WebviewHelper.setupHtml(this._panel.webview, context); + + WebviewHelper.setupWebviewHooks(this._panel.webview, this._disposables); + } + + public static render(context: ExtensionContext) { + if (MainPanel.currentPanel) { + MainPanel.currentPanel._panel.reveal(ViewColumn.One); + } else { + const panel = window.createWebviewPanel('showHelloWorld', 'Hello World', ViewColumn.One, { + enableScripts: true, + }); + + MainPanel.currentPanel = new MainPanel(panel, context); + } + } + + /** + * Cleans up and disposes of webview resources when the webview panel is closed. + */ + public dispose() { + MainPanel.currentPanel = undefined; + + // Dispose of the current webview panel + this._panel.dispose(); + + // Dispose of all disposables (i.e. commands) for the current webview panel + while (this._disposables.length) { + const disposable = this._disposables.pop(); + if (disposable) { + disposable.dispose(); + } + } + } +} diff --git a/examples/react/index.html b/examples/react/index.html index 68e8204..2e4277b 100644 --- a/examples/react/index.html +++ b/examples/react/index.html @@ -2,7 +2,6 @@ - Vite + React + TS diff --git a/examples/vue-import/.vscode/launch.json b/examples/vue-import/.vscode/launch.json index 319030c..d2342f8 100644 --- a/examples/vue-import/.vscode/launch.json +++ b/examples/vue-import/.vscode/launch.json @@ -15,7 +15,7 @@ "outFiles": [ "${workspaceFolder}/dist/extension/*.js" ], - "preLaunchTask": "${defaultBuildTask}" + "preLaunchTask": "npm: dev" }, { "name": "Preview Extension", diff --git a/examples/vue-import/extension/index.ts b/examples/vue-import/extension/index.ts index 506e42f..85587f1 100644 --- a/examples/vue-import/extension/index.ts +++ b/examples/vue-import/extension/index.ts @@ -1,14 +1,18 @@ import { commands, ExtensionContext } from 'vscode'; -import { MainPanel } from './panels/MainPanel'; +import { MainPanel, MainPanel2 } from './views'; export function activate(context: ExtensionContext) { - // Create the show hello world command - const showHelloWorldCommand = commands.registerCommand('hello-world.showHelloWorld', async () => { - MainPanel.render(context.extensionUri); - }); - // Add command to the extension context - context.subscriptions.push(showHelloWorldCommand); + context.subscriptions.push( + commands.registerCommand('hello-world.showPage1', async () => { + MainPanel.render(context); + }), + ); + context.subscriptions.push( + commands.registerCommand('hello-world.showPage2', async () => { + MainPanel2.render(context); + }), + ); } export function deactivate() {} diff --git a/examples/vue-import/extension/panels/MainPanel.ts b/examples/vue-import/extension/panels/MainPanel.ts deleted file mode 100644 index ed57c60..0000000 --- a/examples/vue-import/extension/panels/MainPanel.ts +++ /dev/null @@ -1,212 +0,0 @@ -import { Disposable, Uri, ViewColumn, Webview, WebviewPanel, window } from 'vscode'; -// import __getWebviewHtml__ from '@tomjs/vscode-extension-webview'; - -function uuid() { - let text = ''; - const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - for (let i = 0; i < 32; i++) { - text += possible.charAt(Math.floor(Math.random() * possible.length)); - } - return text; -} - -function getUri(webview: Webview, extensionUri: Uri, pathList: string[]) { - return webview.asWebviewUri(Uri.joinPath(extensionUri, ...pathList)); -} - -/** - * This class manages the state and behavior of HelloWorld webview panels. - * - * It contains all the data and methods for: - * - * - Creating and rendering HelloWorld webview panels - * - Properly cleaning up and disposing of webview resources when the panel is closed - * - Setting the HTML (and by proxy CSS/JavaScript) content of the webview panel - * - Setting message listeners so data can be passed between the webview and extension - */ -export class MainPanel { - public static currentPanel: MainPanel | undefined; - private readonly _panel: WebviewPanel; - private _disposables: Disposable[] = []; - - /** - * The MainPanel class private constructor (called only from the render method). - * - * @param panel A reference to the webview panel - * @param extensionUri The URI of the directory containing the extension - */ - private constructor(panel: WebviewPanel, extensionUri: Uri) { - this._panel = panel; - - // Set an event listener to listen for when the panel is disposed (i.e. when the user closes - // the panel or when the panel is closed programmatically) - this._panel.onDidDispose(() => this.dispose(), null, this._disposables); - - // Set the HTML content for the webview panel - this._panel.webview.html = this._getWebviewContent(this._panel.webview, extensionUri); - - // Set an event listener to listen for messages passed from the webview context - this._setWebviewMessageListener(this._panel.webview); - } - - /** - * Renders the current webview panel if it exists otherwise a new webview panel - * will be created and displayed. - * - * @param extensionUri The URI of the directory containing the extension. - */ - public static render(extensionUri: Uri) { - if (MainPanel.currentPanel) { - // If the webview panel already exists reveal it - MainPanel.currentPanel._panel.reveal(ViewColumn.One); - } else { - // If a webview panel does not already exist create and show a new one - const panel = window.createWebviewPanel( - // Panel view type - 'showHelloWorld', - // Panel title - 'Hello World', - // The editor column the panel should be displayed in - ViewColumn.One, - // Extra panel configurations - { - // Enable JavaScript in the webview - enableScripts: true, - // Restrict the webview to only load resources from the `dist/webview` directories - localResourceRoots: [Uri.joinPath(extensionUri, 'dist/webview')], - }, - ); - - MainPanel.currentPanel = new MainPanel(panel, extensionUri); - } - } - - /** - * Cleans up and disposes of webview resources when the webview panel is closed. - */ - public dispose() { - MainPanel.currentPanel = undefined; - - // Dispose of the current webview panel - this._panel.dispose(); - - // Dispose of all disposables (i.e. commands) for the current webview panel - while (this._disposables.length) { - const disposable = this._disposables.pop(); - if (disposable) { - disposable.dispose(); - } - } - } - - /** - * Defines and returns the HTML that should be rendered within the webview panel. - * - * @remarks This is also the place where references to the Vue webview build files - * are created and inserted into the webview HTML. - * - * @param webview A reference to the extension webview - * @param extensionUri The URI of the directory containing the extension - * @returns A template string literal containing the HTML that should be - * rendered within the webview panel - */ - private _getWebviewContent(webview: Webview, extensionUri: Uri) { - console.log('extensionUri:', extensionUri); - // The CSS file from the Vue build output - const stylesUri = getUri(webview, extensionUri, ['dist/webview/assets/index.css']); - const scriptUri = getUri(webview, extensionUri, ['dist/webview/assets/index.js']); - // The JS file from the Vue build output - // const scriptUri = getUri(webview, extensionUri, ['dist', 'webview', 'assets', 'index.js']); - console.log( - 'scriptUri:', - getUri(webview, extensionUri, ['dist', 'webview', 'assets', 'index.js']), - ); - - const baseUri = getUri(webview, extensionUri, ['dist/webview']); - console.log('baseUri:', baseUri, baseUri.toString()); - - const nonce = uuid(); - - console.log('VITE_DEV_SERVER_URL:', process.env.VITE_DEV_SERVER_URL); - - if (process.env.VITE_DEV_SERVER_URL) { - return __getWebviewHtml__(process.env.VITE_DEV_SERVER_URL); - } - - // const jsFiles = [ - // 'dist/webview/assets/batchSamplersUniformGroup.js', - // 'dist/webview/assets/browserAll.js', - // 'dist/webview/assets/CanvasPool.js', - // 'dist/webview/assets/localUniformBit.js', - // 'dist/webview/assets/SharedSystems.js', - // 'dist/webview/assets/WebGLRenderer.js', - // 'dist/webview/assets/WebGPURenderer.js', - // 'dist/webview/assets/webworkerAll.js', - // ]; - - const jsDistFiles = process.env.VITE_DIST_FILES; - let jsFiles = []; - try { - if (jsDistFiles) { - jsFiles = JSON.parse(jsDistFiles || ''); - } - } catch {} - - const injectScripts = jsFiles - .map( - s => - ``, - ) - .join('\n'); - - // Tip: Install the es6-string-html VS Code extension to enable code highlighting below - return /*html*/ ` - - - - - - - - ${injectScripts} - - - Hello World - - -
- - - `; - } - - /** - * Sets up an event listener to listen for messages passed from the webview context and - * executes code based on the message that is recieved. - * - * @param webview A reference to the extension webview - * @param context A reference to the extension context - */ - private _setWebviewMessageListener(webview: Webview) { - webview.onDidReceiveMessage( - (message: any) => { - const command = message.command; - const text = message.text; - console.log(`command: ${command}`); - - switch (command) { - case 'hello': - // Code that should run in response to the hello message command - window.showInformationMessage(text); - return; - // Add more switch case statements here as more webview message commands - // are created within the webview context (i.e. inside media/main.js) - } - }, - undefined, - this._disposables, - ); - } -} diff --git a/examples/vue-import/extension/views/helper.ts b/examples/vue-import/extension/views/helper.ts new file mode 100644 index 0000000..66fa47f --- /dev/null +++ b/examples/vue-import/extension/views/helper.ts @@ -0,0 +1,43 @@ +import { Disposable, ExtensionContext, Webview, window } from 'vscode'; + +export function uuid() { + let text = ''; + const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + for (let i = 0; i < 32; i++) { + text += possible.charAt(Math.floor(Math.random() * possible.length)); + } + return text; +} + +export class WebviewHelper { + public static setupHtml(webview: Webview, context: ExtensionContext) { + console.log(process.env.NODE_ENV, process.env.VITE_WEBVIEW_DIST); + return process.env.VITE_DEV_SERVER_URL + ? __getWebviewHtml__(process.env.VITE_DEV_SERVER_URL) + : __getWebviewHtml__(webview, context); + } + + public static setupHtml2(webview: Webview, context: ExtensionContext) { + console.log(process.env.NODE_ENV, process.env.VITE_WEBVIEW_DIST); + return process.env.VITE_DEV_SERVER_URL + ? __getWebviewHtml__(process.env.VITE_DEV_SERVER_URL) + : __getWebviewHtml__(webview, context, 'index2'); + } + + public static setupWebviewHooks(webview: Webview, disposables: Disposable[]) { + webview.onDidReceiveMessage( + (message: any) => { + const command = message.command; + const text = message.text; + console.log(`command: ${command}`); + switch (command) { + case 'hello': + window.showInformationMessage(text); + return; + } + }, + undefined, + disposables, + ); + } +} diff --git a/examples/vue-import/extension/views/index.ts b/examples/vue-import/extension/views/index.ts new file mode 100644 index 0000000..9749f93 --- /dev/null +++ b/examples/vue-import/extension/views/index.ts @@ -0,0 +1,2 @@ +export * from './panel'; +export * from './panel2'; diff --git a/examples/vue-import/extension/views/panel.ts b/examples/vue-import/extension/views/panel.ts new file mode 100644 index 0000000..b6fd176 --- /dev/null +++ b/examples/vue-import/extension/views/panel.ts @@ -0,0 +1,47 @@ +import { Disposable, ExtensionContext, ViewColumn, WebviewPanel, window } from 'vscode'; +import { WebviewHelper } from './helper'; + +export class MainPanel { + public static currentPanel: MainPanel | undefined; + private readonly _panel: WebviewPanel; + private _disposables: Disposable[] = []; + + private constructor(panel: WebviewPanel, context: ExtensionContext) { + this._panel = panel; + + this._panel.onDidDispose(() => this.dispose(), null, this._disposables); + this._panel.webview.html = WebviewHelper.setupHtml(this._panel.webview, context); + + WebviewHelper.setupWebviewHooks(this._panel.webview, this._disposables); + } + + public static render(context: ExtensionContext) { + if (MainPanel.currentPanel) { + MainPanel.currentPanel._panel.reveal(ViewColumn.One); + } else { + const panel = window.createWebviewPanel('showPage1', 'Hello Page1', ViewColumn.One, { + enableScripts: true, + }); + + MainPanel.currentPanel = new MainPanel(panel, context); + } + } + + /** + * Cleans up and disposes of webview resources when the webview panel is closed. + */ + public dispose() { + MainPanel.currentPanel = undefined; + + // Dispose of the current webview panel + this._panel.dispose(); + + // Dispose of all disposables (i.e. commands) for the current webview panel + while (this._disposables.length) { + const disposable = this._disposables.pop(); + if (disposable) { + disposable.dispose(); + } + } + } +} diff --git a/examples/vue-import/extension/views/panel2.ts b/examples/vue-import/extension/views/panel2.ts new file mode 100644 index 0000000..de37571 --- /dev/null +++ b/examples/vue-import/extension/views/panel2.ts @@ -0,0 +1,47 @@ +import { Disposable, ExtensionContext, ViewColumn, WebviewPanel, window } from 'vscode'; +import { WebviewHelper } from './helper'; + +export class MainPanel2 { + public static currentPanel: MainPanel2 | undefined; + private readonly _panel: WebviewPanel; + private _disposables: Disposable[] = []; + + private constructor(panel: WebviewPanel, context: ExtensionContext) { + this._panel = panel; + + this._panel.onDidDispose(() => this.dispose(), null, this._disposables); + this._panel.webview.html = WebviewHelper.setupHtml2(this._panel.webview, context); + + WebviewHelper.setupWebviewHooks(this._panel.webview, this._disposables); + } + + public static render(context: ExtensionContext) { + if (MainPanel2.currentPanel) { + MainPanel2.currentPanel._panel.reveal(ViewColumn.One); + } else { + const panel = window.createWebviewPanel('showPage2', 'Hello Page2', ViewColumn.One, { + enableScripts: true, + }); + + MainPanel2.currentPanel = new MainPanel2(panel, context); + } + } + + /** + * Cleans up and disposes of webview resources when the webview panel is closed. + */ + public dispose() { + MainPanel2.currentPanel = undefined; + + // Dispose of the current webview panel + this._panel.dispose(); + + // Dispose of all disposables (i.e. commands) for the current webview panel + while (this._disposables.length) { + const disposable = this._disposables.pop(); + if (disposable) { + disposable.dispose(); + } + } + } +} diff --git a/examples/vue-import/index.html b/examples/vue-import/index.html index d1586f8..9630b52 100644 --- a/examples/vue-import/index.html +++ b/examples/vue-import/index.html @@ -2,9 +2,8 @@ - - Vite + Vue + TS + page 1 diff --git a/examples/vue-import/index2.html b/examples/vue-import/index2.html new file mode 100644 index 0000000..6b54d2d --- /dev/null +++ b/examples/vue-import/index2.html @@ -0,0 +1,13 @@ + + + + + + page 2 + + + +
+ + + diff --git a/examples/vue-import/package.json b/examples/vue-import/package.json index ab63812..c798ed3 100644 --- a/examples/vue-import/package.json +++ b/examples/vue-import/package.json @@ -1,20 +1,22 @@ { - "name": "template-vue", + "name": "template-vue-import", "version": "0.0.0", "description": "vite + vue", "engines": { "node": ">=18", - "vscode": "^1.56.0" + "vscode": "^1.75.0" }, "main": "dist/extension/index.js", - "activationEvents": [ - "onCommand:hello-world.showHelloWorld" - ], + "activationEvents": [], "contributes": { "commands": [ { - "command": "hello-world.showHelloWorld", - "title": "Hello World: Show" + "command": "hello-world.showPage1", + "title": "Hello World: Show Page 1" + }, + { + "command": "hello-world.showPage2", + "title": "Hello World: Show Page 2" } ] }, @@ -25,14 +27,14 @@ }, "dependencies": { "@vscode/webview-ui-toolkit": "^1.4.0", - "pixi.js": "8.0.0-rc", + "pixi.js": "8.0.0-rc.7", "vue": "^3.4.3" }, "devDependencies": { "@tomjs/vite-plugin-vscode": "workspace:^", "@tomjs/vscode-extension-webview": "^1.2.0", "@types/node": "^18.19.4", - "@types/vscode": "^1.85.0", + "@types/vscode": "^1.75.0", "@types/vscode-webview": "^1.57.4", "@vitejs/plugin-vue": "^4.6.2", "cross-env": "^7.0.3", diff --git a/examples/vue-import/src/App2.vue b/examples/vue-import/src/App2.vue new file mode 100644 index 0000000..a5ed0fc --- /dev/null +++ b/examples/vue-import/src/App2.vue @@ -0,0 +1,38 @@ + + + + + diff --git a/examples/vue-import/src/main2.ts b/examples/vue-import/src/main2.ts new file mode 100644 index 0000000..41fee91 --- /dev/null +++ b/examples/vue-import/src/main2.ts @@ -0,0 +1,4 @@ +import { createApp } from 'vue'; +import App from './App2.vue'; + +createApp(App).mount('#app'); diff --git a/examples/vue-import/vite.config.ts b/examples/vue-import/vite.config.ts index cc0e3a8..a40fcf4 100644 --- a/examples/vue-import/vite.config.ts +++ b/examples/vue-import/vite.config.ts @@ -1,3 +1,4 @@ +import path from 'node:path'; import { defineConfig } from 'vite'; import vscode from '@tomjs/vite-plugin-vscode'; import vue from '@vitejs/plugin-vue'; @@ -20,5 +21,16 @@ export default defineConfig({ ], build: { minify: false, + rollupOptions: { + input: [path.resolve(__dirname, 'index.html'), path.resolve(__dirname, 'index2.html')], + output: { + // https://rollupjs.org/configuration-options/#output-manualchunks + manualChunks: id => { + if (id.includes('pixi.js')) { + return 'pixi'; + } + }, + }, + }, }, }); diff --git a/examples/vue/.vscode/launch.json b/examples/vue/.vscode/launch.json index 319030c..d2342f8 100644 --- a/examples/vue/.vscode/launch.json +++ b/examples/vue/.vscode/launch.json @@ -15,7 +15,7 @@ "outFiles": [ "${workspaceFolder}/dist/extension/*.js" ], - "preLaunchTask": "${defaultBuildTask}" + "preLaunchTask": "npm: dev" }, { "name": "Preview Extension", diff --git a/examples/vue/extension/index.ts b/examples/vue/extension/index.ts index 506e42f..eb788f6 100644 --- a/examples/vue/extension/index.ts +++ b/examples/vue/extension/index.ts @@ -1,14 +1,13 @@ import { commands, ExtensionContext } from 'vscode'; -import { MainPanel } from './panels/MainPanel'; +import { MainPanel } from './views/panel'; export function activate(context: ExtensionContext) { - // Create the show hello world command - const showHelloWorldCommand = commands.registerCommand('hello-world.showHelloWorld', async () => { - MainPanel.render(context.extensionUri); - }); - // Add command to the extension context - context.subscriptions.push(showHelloWorldCommand); + context.subscriptions.push( + commands.registerCommand('hello-world.showHelloWorld', async () => { + MainPanel.render(context); + }), + ); } export function deactivate() {} diff --git a/examples/vue/extension/panels/MainPanel.ts b/examples/vue/extension/panels/MainPanel.ts deleted file mode 100644 index ed57c60..0000000 --- a/examples/vue/extension/panels/MainPanel.ts +++ /dev/null @@ -1,212 +0,0 @@ -import { Disposable, Uri, ViewColumn, Webview, WebviewPanel, window } from 'vscode'; -// import __getWebviewHtml__ from '@tomjs/vscode-extension-webview'; - -function uuid() { - let text = ''; - const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - for (let i = 0; i < 32; i++) { - text += possible.charAt(Math.floor(Math.random() * possible.length)); - } - return text; -} - -function getUri(webview: Webview, extensionUri: Uri, pathList: string[]) { - return webview.asWebviewUri(Uri.joinPath(extensionUri, ...pathList)); -} - -/** - * This class manages the state and behavior of HelloWorld webview panels. - * - * It contains all the data and methods for: - * - * - Creating and rendering HelloWorld webview panels - * - Properly cleaning up and disposing of webview resources when the panel is closed - * - Setting the HTML (and by proxy CSS/JavaScript) content of the webview panel - * - Setting message listeners so data can be passed between the webview and extension - */ -export class MainPanel { - public static currentPanel: MainPanel | undefined; - private readonly _panel: WebviewPanel; - private _disposables: Disposable[] = []; - - /** - * The MainPanel class private constructor (called only from the render method). - * - * @param panel A reference to the webview panel - * @param extensionUri The URI of the directory containing the extension - */ - private constructor(panel: WebviewPanel, extensionUri: Uri) { - this._panel = panel; - - // Set an event listener to listen for when the panel is disposed (i.e. when the user closes - // the panel or when the panel is closed programmatically) - this._panel.onDidDispose(() => this.dispose(), null, this._disposables); - - // Set the HTML content for the webview panel - this._panel.webview.html = this._getWebviewContent(this._panel.webview, extensionUri); - - // Set an event listener to listen for messages passed from the webview context - this._setWebviewMessageListener(this._panel.webview); - } - - /** - * Renders the current webview panel if it exists otherwise a new webview panel - * will be created and displayed. - * - * @param extensionUri The URI of the directory containing the extension. - */ - public static render(extensionUri: Uri) { - if (MainPanel.currentPanel) { - // If the webview panel already exists reveal it - MainPanel.currentPanel._panel.reveal(ViewColumn.One); - } else { - // If a webview panel does not already exist create and show a new one - const panel = window.createWebviewPanel( - // Panel view type - 'showHelloWorld', - // Panel title - 'Hello World', - // The editor column the panel should be displayed in - ViewColumn.One, - // Extra panel configurations - { - // Enable JavaScript in the webview - enableScripts: true, - // Restrict the webview to only load resources from the `dist/webview` directories - localResourceRoots: [Uri.joinPath(extensionUri, 'dist/webview')], - }, - ); - - MainPanel.currentPanel = new MainPanel(panel, extensionUri); - } - } - - /** - * Cleans up and disposes of webview resources when the webview panel is closed. - */ - public dispose() { - MainPanel.currentPanel = undefined; - - // Dispose of the current webview panel - this._panel.dispose(); - - // Dispose of all disposables (i.e. commands) for the current webview panel - while (this._disposables.length) { - const disposable = this._disposables.pop(); - if (disposable) { - disposable.dispose(); - } - } - } - - /** - * Defines and returns the HTML that should be rendered within the webview panel. - * - * @remarks This is also the place where references to the Vue webview build files - * are created and inserted into the webview HTML. - * - * @param webview A reference to the extension webview - * @param extensionUri The URI of the directory containing the extension - * @returns A template string literal containing the HTML that should be - * rendered within the webview panel - */ - private _getWebviewContent(webview: Webview, extensionUri: Uri) { - console.log('extensionUri:', extensionUri); - // The CSS file from the Vue build output - const stylesUri = getUri(webview, extensionUri, ['dist/webview/assets/index.css']); - const scriptUri = getUri(webview, extensionUri, ['dist/webview/assets/index.js']); - // The JS file from the Vue build output - // const scriptUri = getUri(webview, extensionUri, ['dist', 'webview', 'assets', 'index.js']); - console.log( - 'scriptUri:', - getUri(webview, extensionUri, ['dist', 'webview', 'assets', 'index.js']), - ); - - const baseUri = getUri(webview, extensionUri, ['dist/webview']); - console.log('baseUri:', baseUri, baseUri.toString()); - - const nonce = uuid(); - - console.log('VITE_DEV_SERVER_URL:', process.env.VITE_DEV_SERVER_URL); - - if (process.env.VITE_DEV_SERVER_URL) { - return __getWebviewHtml__(process.env.VITE_DEV_SERVER_URL); - } - - // const jsFiles = [ - // 'dist/webview/assets/batchSamplersUniformGroup.js', - // 'dist/webview/assets/browserAll.js', - // 'dist/webview/assets/CanvasPool.js', - // 'dist/webview/assets/localUniformBit.js', - // 'dist/webview/assets/SharedSystems.js', - // 'dist/webview/assets/WebGLRenderer.js', - // 'dist/webview/assets/WebGPURenderer.js', - // 'dist/webview/assets/webworkerAll.js', - // ]; - - const jsDistFiles = process.env.VITE_DIST_FILES; - let jsFiles = []; - try { - if (jsDistFiles) { - jsFiles = JSON.parse(jsDistFiles || ''); - } - } catch {} - - const injectScripts = jsFiles - .map( - s => - ``, - ) - .join('\n'); - - // Tip: Install the es6-string-html VS Code extension to enable code highlighting below - return /*html*/ ` - - - - - - - - ${injectScripts} - - - Hello World - - -
- - - `; - } - - /** - * Sets up an event listener to listen for messages passed from the webview context and - * executes code based on the message that is recieved. - * - * @param webview A reference to the extension webview - * @param context A reference to the extension context - */ - private _setWebviewMessageListener(webview: Webview) { - webview.onDidReceiveMessage( - (message: any) => { - const command = message.command; - const text = message.text; - console.log(`command: ${command}`); - - switch (command) { - case 'hello': - // Code that should run in response to the hello message command - window.showInformationMessage(text); - return; - // Add more switch case statements here as more webview message commands - // are created within the webview context (i.e. inside media/main.js) - } - }, - undefined, - this._disposables, - ); - } -} diff --git a/examples/vue/extension/views/helper.ts b/examples/vue/extension/views/helper.ts new file mode 100644 index 0000000..da57125 --- /dev/null +++ b/examples/vue/extension/views/helper.ts @@ -0,0 +1,36 @@ +import { Disposable, ExtensionContext, Webview, window } from 'vscode'; + +export function uuid() { + let text = ''; + const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + for (let i = 0; i < 32; i++) { + text += possible.charAt(Math.floor(Math.random() * possible.length)); + } + return text; +} + +export class WebviewHelper { + public static setupHtml(webview: Webview, context: ExtensionContext) { + if (process.env.VITE_DEV_SERVER_URL) { + return __getWebviewHtml__(process.env.VITE_DEV_SERVER_URL); + } + return __getWebviewHtml__(webview, context); + } + + public static setupWebviewHooks(webview: Webview, disposables: Disposable[]) { + webview.onDidReceiveMessage( + (message: any) => { + const command = message.command; + const text = message.text; + console.log(`command: ${command}`); + switch (command) { + case 'hello': + window.showInformationMessage(text); + return; + } + }, + undefined, + disposables, + ); + } +} diff --git a/examples/vue/extension/views/panel.ts b/examples/vue/extension/views/panel.ts new file mode 100644 index 0000000..eee8e68 --- /dev/null +++ b/examples/vue/extension/views/panel.ts @@ -0,0 +1,47 @@ +import { Disposable, ExtensionContext, ViewColumn, WebviewPanel, window } from 'vscode'; +import { WebviewHelper } from './helper'; + +export class MainPanel { + public static currentPanel: MainPanel | undefined; + private readonly _panel: WebviewPanel; + private _disposables: Disposable[] = []; + + private constructor(panel: WebviewPanel, context: ExtensionContext) { + this._panel = panel; + + this._panel.onDidDispose(() => this.dispose(), null, this._disposables); + this._panel.webview.html = WebviewHelper.setupHtml(this._panel.webview, context); + + WebviewHelper.setupWebviewHooks(this._panel.webview, this._disposables); + } + + public static render(context: ExtensionContext) { + if (MainPanel.currentPanel) { + MainPanel.currentPanel._panel.reveal(ViewColumn.One); + } else { + const panel = window.createWebviewPanel('showHelloWorld', 'Hello World', ViewColumn.One, { + enableScripts: true, + }); + + MainPanel.currentPanel = new MainPanel(panel, context); + } + } + + /** + * Cleans up and disposes of webview resources when the webview panel is closed. + */ + public dispose() { + MainPanel.currentPanel = undefined; + + // Dispose of the current webview panel + this._panel.dispose(); + + // Dispose of all disposables (i.e. commands) for the current webview panel + while (this._disposables.length) { + const disposable = this._disposables.pop(); + if (disposable) { + disposable.dispose(); + } + } + } +} diff --git a/examples/vue/index.html b/examples/vue/index.html index d1586f8..ecc3a45 100644 --- a/examples/vue/index.html +++ b/examples/vue/index.html @@ -2,7 +2,6 @@ - Vite + Vue + TS diff --git a/package.json b/package.json index dc5d373..585c414 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@tomjs/vite-plugin-vscode", "version": "1.5.0", - "description": "The vite plugin for vscode extension, supports esm and cjs.", + "description": "Use vue/react to develop 'vscode extension webview', supporting esm/cjs", "keywords": [ "vite", "plugin", @@ -57,6 +57,7 @@ "kolorist": "^1.8.0", "lodash.clonedeep": "^4.5.0", "lodash.merge": "^4.6.2", + "node-html-parser": "^6.1.12", "tsup": "7.2.0" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0408c6e..c1b7733 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: lodash.merge: specifier: ^4.6.2 version: 4.6.2 + node-html-parser: + specifier: ^6.1.12 + version: 6.1.12 tsup: specifier: 7.2.0 version: 7.2.0(postcss@8.4.32)(typescript@5.3.3) @@ -183,8 +186,8 @@ importers: specifier: ^1.4.0 version: 1.4.0(react@18.2.0) pixi.js: - specifier: 8.0.0-rc - version: 8.0.0-rc + specifier: 8.0.0-rc.7 + version: 8.0.0-rc.7 vue: specifier: ^3.4.3 version: 3.4.3(typescript@5.3.3) @@ -199,7 +202,7 @@ importers: specifier: ^18.19.4 version: 18.19.4 '@types/vscode': - specifier: ^1.85.0 + specifier: ^1.73.0 version: 1.85.0 '@types/vscode-webview': specifier: ^1.57.4 @@ -1401,8 +1404,8 @@ packages: '@types/responselike': 1.0.3 dev: true - /@types/css-font-loading-module@0.0.8: - resolution: {integrity: sha512-PdJeLlCJj/ShOA+c0dXdZ/e1P0Cdjhip+dRBtPaigOqwKd0DiFx3NeO6T2E7AQ5JszSR3dub3YkQjc2hcQyxSw==} + /@types/css-font-loading-module@0.0.12: + resolution: {integrity: sha512-x2tZZYkSxXqWvTDgveSynfjq/T2HyiZHXb00j/+gy19yp70PHCizM48XFdjBCWH7eHBD0R5i/pw9yMBP/BH5uA==} dev: false /@types/earcut@2.1.4: @@ -2087,7 +2090,6 @@ packages: /boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} - dev: true /boxen@7.1.1: resolution: {integrity: sha512-2hCgjEmP8YLWQ130n2FerGv7rYpfBmnmp9Uy2Le1vge6X3gZIfSmEzP5QTDElFxcvVcXlEn8Aq6MU/PZygIOog==} @@ -2583,6 +2585,16 @@ packages: resolution: {integrity: sha512-nXutswsivIEBOrPo/OZw2KQjFPLvtg68aovJf6Kqrm3L6FmTvvFPaeDrk83hh0+pRJGuP3PeKJwMS0E6DFipdQ==} dev: true + /css-select@5.1.0: + resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} + dependencies: + boolbase: 1.0.0 + css-what: 6.1.0 + domhandler: 5.0.3 + domutils: 3.1.0 + nth-check: 2.1.1 + dev: false + /css-tree@2.3.1: resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} @@ -2591,6 +2603,11 @@ packages: source-map-js: 1.0.2 dev: true + /css-what@6.1.0: + resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} + engines: {node: '>= 6'} + dev: false + /cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -2760,18 +2777,15 @@ packages: domelementtype: 2.3.0 domhandler: 5.0.3 entities: 4.5.0 - dev: true /domelementtype@2.3.0: resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} - dev: true /domhandler@5.0.3: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} dependencies: domelementtype: 2.3.0 - dev: true /domutils@3.1.0: resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} @@ -2779,7 +2793,6 @@ packages: dom-serializer: 2.0.0 domelementtype: 2.3.0 domhandler: 5.0.3 - dev: true /dot-prop@5.3.0: resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} @@ -3867,7 +3880,6 @@ packages: /he@1.2.0: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true - dev: true /hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} @@ -5046,6 +5058,13 @@ packages: resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} dev: true + /node-html-parser@6.1.12: + resolution: {integrity: sha512-/bT/Ncmv+fbMGX96XG9g05vFt43m/+SYKIs9oAemQVYyVcZmDAI2Xq/SbNcpOA35eF0Zk2av3Ksf+Xk8Vt8abA==} + dependencies: + css-select: 5.1.0 + he: 1.2.0 + dev: false + /normalize-package-data@2.5.0: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} dependencies: @@ -5187,7 +5206,6 @@ packages: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} dependencies: boolbase: 1.0.0 - dev: true /number-is-nan@1.0.1: resolution: {integrity: sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==} @@ -5570,11 +5588,11 @@ packages: engines: {node: '>= 6'} dev: false - /pixi.js@8.0.0-rc: - resolution: {integrity: sha512-AW1TWj+TfQ6XbAiJD6cC0Y5wGQ3qXkKacxulnuKg9BkR2Cl40jF94nW37D63i2ZUwqw4Pz3+vmbpzTPky41C/w==} + /pixi.js@8.0.0-rc.7: + resolution: {integrity: sha512-JtLikYYCvelWPhfAk0vDEM/DKKumhmmtvmSZvufvVEa/1owR8kS2lGqzFFNKk1VsMb4GsqZ0bw2WcJEnTEb2AA==} dependencies: '@pixi/colord': 2.9.6 - '@types/css-font-loading-module': 0.0.8 + '@types/css-font-loading-module': 0.0.12 '@types/earcut': 2.1.4 '@webgpu/types': 0.1.40 '@xmldom/xmldom': 0.8.10 diff --git a/src/constants.ts b/src/constants.ts index 0eb3963..b6e2d29 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,4 +1,5 @@ export const PLUGIN_NAME = '@tomjs:vscode'; +export const ORG_NAME = '@tomjs'; export const PACKAGE_NAME = '@tomjs/vite-plugin-vscode'; export const WEBVIEW_PACKAGE_NAME = '@tomjs/vscode-extension-webview'; export const WEBVIEW_METHOD_NAME = '__getWebviewHtml__'; diff --git a/src/index.ts b/src/index.ts index e30bb43..8052f94 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,10 +5,11 @@ import path from 'node:path'; import { cwd } from 'node:process'; import cloneDeep from 'lodash.clonedeep'; import merge from 'lodash.merge'; +import { parse as htmlParser } from 'node-html-parser'; import { build as tsupBuild, type Options as TsupOptions } from 'tsup'; -import { WEBVIEW_METHOD_NAME, WEBVIEW_PACKAGE_NAME } from './constants'; +import { PACKAGE_NAME, WEBVIEW_METHOD_NAME, WEBVIEW_PACKAGE_NAME } from './constants'; import { createLogger } from './logger'; -import { readJson, resolveServerUrl } from './utils'; +import { emptyPath, readJson, resolveServerUrl } from './utils'; const isDev = process.env.NODE_ENV === 'development'; const logger = createLogger(); @@ -87,12 +88,73 @@ function preMergeOptions(options?: PluginOptions): PluginOptions { return opts; } -function readAllFiles(dir: string): string[] { - return fs.readdirSync(dir).reduce((files, file) => { - const name = path.join(dir, file); - const isDir = fs.statSync(name).isDirectory(); - return isDir ? [...files, ...readAllFiles(name)] : [...files, name]; - }, [] as string[]); +const prodCachePkgName = `${PACKAGE_NAME}-inject`; +function genProdWebviewCode(cache: Record) { + const prodCacheFolder = path.join(cwd(), 'node_modules', prodCachePkgName); + emptyPath(prodCacheFolder); + const destFile = path.join(prodCacheFolder, 'index.ts'); + + function handleHtmlCode(html) { + const root = htmlParser(html); + const head = root.querySelector('head')!; + if (!head) { + root?.insertAdjacentHTML('beforeend', ''); + } + + head.insertAdjacentHTML( + 'afterbegin', + `\n`, + ); + + const tags = { + script: 'src', + link: 'href', + }; + + Object.keys(tags).forEach(tag => { + const elements = root.querySelectorAll(tag); + elements.forEach(element => { + const attr = element.getAttribute(tags[tag]); + if (attr) { + element.setAttribute(tags[tag], `{{baseUri}}${attr}`); + } + + element.setAttribute('nonce', '{{nonce}}'); + }); + }); + + return root.toString(); + } + + const cacheCode = /* js */ `const htmlCode = { + ${Object.keys(cache) + .map(s => `${s}: \`${handleHtmlCode(cache[s])}\`,`) + .join('\n')} + };`; + + const code = /* js */ `import { ExtensionContext, Uri, Webview } from 'vscode'; + +${cacheCode} + +function uuid() { + let text = ''; + const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + for (let i = 0; i < 32; i++) { + text += possible.charAt(Math.floor(Math.random() * possible.length)); + } + return text; +} + +export default function getWebviewHtml(webview: Webview, context: ExtensionContext, inputName?:string){ + const nonce = uuid(); + const baseUri = webview.asWebviewUri(Uri.joinPath(context.extensionUri, (process.env.VITE_WEBVIEW_DIST || 'dist'))); + const html = htmlCode[inputName || 'index'] || ''; + return html.replaceAll('{{cspSource}}',webview.cspSource).replaceAll('{{nonce}}', nonce).replaceAll('{{baseUri}}', baseUri); +} + `; + fs.writeFileSync(destFile, code, { encoding: 'utf8' }); + + return destFile.replaceAll('\\', '/'); } export function useVSCodePlugin(options?: PluginOptions): Plugin[] { @@ -150,6 +212,8 @@ export function useVSCodePlugin(options?: PluginOptions): Plugin[] { } let buildConfig: ResolvedConfig; + // multiple entry index.html + const prodHtmlCache: Record = {}; return [ { @@ -224,30 +288,69 @@ export function useVSCodePlugin(options?: PluginOptions): Plugin[] { { name: '@tomjs:vscode', apply: 'build', + enforce: 'post', config(config) { return handleConfig(config); }, configResolved(config) { buildConfig = config; }, + transformIndexHtml(html, ctx) { + if (!opts.webview) { + return html; + } + + prodHtmlCache[ctx.chunk?.name as string] = html; + return html; + }, closeBundle() { - // merge file - const { outDir } = buildConfig.build; - const cwd = process.cwd(); - const allFiles = readAllFiles(outDir) - .filter(file => file.endsWith('.js') && !file.endsWith('index.js')) - .map(s => s.replace(cwd, '').replaceAll('\\', '/').substring(1)); + let webviewPath: string; + if (opts.webview) { + webviewPath = genProdWebviewCode(prodHtmlCache); + } + + let outDir = buildConfig.build.outDir.replace(cwd(), '').replaceAll('\\', '/'); + if (outDir.startsWith('/')) { + outDir = outDir.substring(1); + } + const env = { + NODE_ENV: buildConfig.mode || 'production', + VITE_WEBVIEW_DIST: outDir, + }; + + logger.info('extension build start'); const { onSuccess: _onSuccess, ...tsupOptions } = opts.extension || {}; tsupBuild( merge(tsupOptions, { - env: { - VITE_DIST_FILES: JSON.stringify(allFiles), - }, + env, + silent: true, + esbuildPlugins: !opts.webview + ? [] + : [ + { + name: '@tomjs:vscode:inject', + setup(build) { + build.onLoad({ filter: /\.ts$/ }, async args => { + const file = fs.readFileSync(args.path, 'utf-8'); + if (file.includes(`${opts.webview}(`)) { + return { + contents: `import ${opts.webview} from \`${webviewPath}\`;\n` + file, + loader: 'ts', + }; + } + + return {}; + }); + }, + }, + ], async onSuccess() { if (typeof _onSuccess === 'function') { await _onSuccess(); } + + logger.info('extension build success'); }, }) as TsupOptions, ); diff --git a/src/types.ts b/src/types.ts index 7839078..d584f47 100644 --- a/src/types.ts +++ b/src/types.ts @@ -47,19 +47,23 @@ export interface PluginOptions { /** * Inject [@tomjs/vscode-extension-webview](https://github.com/tomjs/vscode-extension-webview) into vscode extension code and web client code, so that webview can support HMR during the development stage. * - * * extension: Inject `import getDevWebviewHtml from '@tomjs/vscode-extension-webview';` above the file that calls the `getDevWebviewHtml` method - * * web: Add `