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 @@
+
+
+
+
+
Hello Vue!
+ Test VSCode Message
+
+ (message = e.target.value)">
+ Please enter a message
+
+
+
+
+
+
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 `