diff --git a/.vscode/launch.json b/.vscode/launch.json index a5baa676..87a7054c 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -17,6 +17,21 @@ "DEBUGTELEMETRY": "v" } }, + { + "name": "Launch Extension + web view", + "type": "extensionHost", + "request": "launch", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}" + ], + "outFiles": [ + "${workspaceFolder}/dist/**/*.{js,mjs,cjs}", + ], + "preLaunchTask": "Watch: ESBuild with Web View", + "env": { + "DEBUGTELEMETRY": "v" + } + }, { "name": "Launch Extension + Docker", "type": "extensionHost", diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 7ddcc1d4..fd97314b 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -40,5 +40,21 @@ }, "isBackground": true, }, + { + "label": "Watch: ESBuild with Web View", + "type": "shell", + "command": "npx", + "args": [ + "concurrently", + "\"npm:build:esbuild -- --watch\"", + "\"npm:build:webview -- --watch\"" + ], + "isBackground": true, + "problemMatcher": "$esbuild-watch", + "presentation": { + "reveal": "always", + "focus": true + } + } ] } diff --git a/esbuild.views.mjs b/esbuild.views.mjs index 3fdb9d02..e1055b0b 100644 --- a/esbuild.views.mjs +++ b/esbuild.views.mjs @@ -5,14 +5,13 @@ import { isAutoDebug, isAutoWatch } from '@microsoft/vscode-azext-eng/esbuild'; import esbuild from 'esbuild'; -import copy from 'esbuild-plugin-copy'; import path from 'path'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -const outdir = './dist'; +const outdir = path.resolve(__dirname, 'dist'); const commonConfig = { entryPoints: { @@ -20,8 +19,8 @@ const commonConfig = { }, bundle: true, - outdir: './dist', //check if this is correct (may be diff now since we are using esbuild) - format: 'cjs', + outdir: outdir, + format: 'esm', platform: 'browser', //todo: remove (platform is auto browser if not specified) target: 'es2022', sourcemap: isAutoWatch, @@ -29,76 +28,49 @@ const commonConfig = { metafile: isAutoDebug, splitting: false, - inject: [path.resolve(__dirname, 'react-shim.js')], //todo: gotta create this react-shim.js + inject: [path.resolve(__dirname, 'react-shim.js')], - // todo: may need to check these loader: { '.ts': 'ts', '.tsx': 'tsx', '.css': 'css', '.scss': 'css', - '.sass': 'css', - '.ttf': 'file', + '.ttf': 'dataurl', + '.woff': 'dataurl', + '.woff2': 'dataurl', }, plugins: [ - // todo do we need this? Added since esbuild does not support sass so we need to add the plugin but not sure if sass is needed { name: 'sass', setup(build) { - import('sass').then((sass) => { - build.onLoad({ filter: /\.s[ac]ss$/ }, async (args) => { - const result = sass.compile(args.path); - return { - contents: result.css, - loader: 'css', - }; - }); + build.onLoad({ filter: /\.s[ac]ss$/ }, async (args) => { + const sass = await import('sass'); + const result = sass.compile(args.path); + return { + contents: result.css, + loader: 'css', + }; }); }, }, - - // todo: make sure these file paths are still correct. They may be different now with using esbuild - copy({ - assets: { - from: [ - path.resolve(__dirname, 'node_modules/@vscode/codicons/dist/codicon.css'), - path.resolve(__dirname, 'node_modules/@vscode/codicons/dist/codicon.ttf'), - ], - to: [ - path.resolve(outdir, 'icons/codicon.css'), - path.resolve(outdir, 'icons/codicon.ttf'), - ], - }, - }), ], - logLevel: 'info' }; +const ctx = await esbuild.context({ + ...commonConfig, + outdir, +}); + +// Always do an initial rebuild to ensure views.js exists +await ctx.rebuild(); + if (isAutoWatch) { - esbuild - .serve( - { - servedir: outdir, // todo esbuild only serves one directory so in order to copy the static then we would need to copy over into dist/static (this is done in webpack) - port: 8080, - host: '127.0.0.1', - headers: { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS', - 'Access-Control-Allow-Headers': - 'X-Requested-With, content-type, Authorization', - }, - }, - { - ...commonConfig, - publicPath: '/', - } - ) - .then(() => { - console.log('Dev server running at http://127.0.0.1:8080'); - }) - .catch(() => process.exit(1)); + await ctx.watch(); + console.log('Watching webview bundle...'); + await new Promise(() => { }); } else { - esbuild.build(commonConfig).catch(() => process.exit(1)); + await ctx.rebuild(); + await ctx.dispose(); } diff --git a/src/webviews/ConfirmationView.tsx b/src/webviews/ConfirmationView.tsx index a3ec5633..cad8795a 100644 --- a/src/webviews/ConfirmationView.tsx +++ b/src/webviews/ConfirmationView.tsx @@ -6,7 +6,7 @@ import { Button, createTableColumn, DataGrid, DataGridBody, DataGridCell, DataGridRow, TableCellLayout, Tooltip, type OnSelectionChangeData, type SelectionItemId, type TableColumnDefinition } from '@fluentui/react-components'; import * as React from 'react'; import { useContext, useState, type JSX } from 'react'; -import './confirmationView.scss'; +import './styles/confirmationView.scss'; import { type ConfirmationViewControllerType } from './ConfirmationViewController'; import { useConfiguration } from './useConfiguration'; import { ConfirmationViewCommands } from './webviewConstants'; @@ -53,18 +53,18 @@ export const ConfirmationView = (): JSX.Element => { value: value, commandName: commandName }); - } + }; const cancelClicked = () => { vscodeApi.postMessage({ command: ConfirmationViewCommands.Cancel - }) + }); }; const onChange = (_ev: React.MouseEvent, data: OnSelectionChangeData) => { setName(data.selectedItems.size > 0 ? 'Edit' : 'Confirm'); setSelectedItems(data.selectedItems.size > 0 ? createNewSetWithItem(data.selectedItems, false) : createNewSetWithItem(data.selectedItems, true)); - } + }; const columnSizingOptions = { name: { @@ -75,7 +75,7 @@ export const ConfirmationView = (): JSX.Element => { defaultWidth: 275, minWidth: 275, } - } + }; const columns: TableColumnDefinition[] = [ createTableColumn({ @@ -105,7 +105,7 @@ export const ConfirmationView = (): JSX.Element => { ; }, }) - ] + ]; const DataGridComponent: React.FC<{ context: ConfirmationViewControllerType, @@ -158,4 +158,4 @@ export const ConfirmationView = (): JSX.Element => { ); -} +}; diff --git a/src/webviews/LoadingView.tsx b/src/webviews/LoadingView.tsx index 8cf8505a..c7d98ebf 100644 --- a/src/webviews/LoadingView.tsx +++ b/src/webviews/LoadingView.tsx @@ -4,10 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import { Spinner } from "@fluentui/react-components"; -import './loadingView.scss'; +import './styles/loadingView.scss'; export const LoadingView = () =>
-
+ ; diff --git a/src/webviews/extension-server/WebviewBaseController.ts b/src/webviews/extension-server/WebviewBaseController.ts index e82f6b4e..9b426cd4 100644 --- a/src/webviews/extension-server/WebviewBaseController.ts +++ b/src/webviews/extension-server/WebviewBaseController.ts @@ -3,14 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -/* eslint-disable @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-call */ import { randomBytes } from 'crypto'; import * as path from 'path'; import * as vscode from 'vscode'; import { ext } from "../../extensionVariables"; -const DEV_SERVER_HOST = 'http://localhost:8080'; - /** * WebviewBaseController is a class that manages a vscode.Webview and provides * a way to communicate with it. It provides a way to register request handlers and reducers @@ -44,41 +41,24 @@ export abstract class WebviewBaseController implements vscode.Dis } protected getDocumentTemplate(webview?: vscode.Webview) { - const isProduction = ext.context.extensionMode === vscode.ExtensionMode.Production; const nonce = randomBytes(16).toString('base64'); const filename = 'views.js'; const uri = (...parts: string[]) => webview?.asWebviewUri(vscode.Uri.file(path.join(ext.context.extensionPath, ...parts))).toString(true); - const srcUri = isProduction ? uri('dist', filename) : `${DEV_SERVER_HOST}/${filename}`; - - const codiconsUri = (...parts: string[]) => webview?.asWebviewUri(vscode.Uri.file(path.join(ext.context.extensionPath, ...parts))).toString(true); - const codiconsSrcUri = isProduction ? codiconsUri('dist', 'icons', 'codicon.css') : codiconsUri('node_modules', '@vscode', 'codicons', 'dist', 'codicon.css'); - console.log(`codiconsSrcUri: ${codiconsSrcUri}`); - console.log(`srcUri: ${srcUri}`); - - const csp = ( - isProduction - ? [ - `form-action 'none';`, - `default-src ${webview?.cspSource};`, - `script-src ${webview?.cspSource} 'nonce-${nonce}';`, - `style-src ${webview?.cspSource} vscode-resource: 'unsafe-inline';`, - `img-src ${webview?.cspSource} data: vscode-resource:;`, - `connect-src ${webview?.cspSource} ws:;`, - `font-src ${webview?.cspSource};`, - `worker-src ${webview?.cspSource} blob:;`, - ] - : [ - `form-action 'none';`, - `default-src ${webview?.cspSource} ${DEV_SERVER_HOST};`, - `script-src ${webview?.cspSource} ${DEV_SERVER_HOST} 'nonce-${nonce}';`, - `style-src ${webview?.cspSource} ${DEV_SERVER_HOST} vscode-resource: 'unsafe-inline';`, - `img-src ${webview?.cspSource} ${DEV_SERVER_HOST} data: vscode-resource:;`, - `connect-src ${webview?.cspSource} ${DEV_SERVER_HOST} ws:;`, - `font-src ${webview?.cspSource} ${DEV_SERVER_HOST};`, - `worker-src ${webview?.cspSource} ${DEV_SERVER_HOST} blob:;`, - ] - ).join(' '); + const srcUri = uri('dist', filename); + const cssUri = uri('dist', 'views.css'); + + const csp = [ + `form-action 'none';`, + `default-src ${webview?.cspSource};`, + `script-src ${webview?.cspSource} 'nonce-${nonce}';`, + `style-src ${webview?.cspSource} vscode-resource: 'unsafe-inline';`, + `img-src ${webview?.cspSource} data: vscode-resource:;`, + `connect-src ${webview?.cspSource} ws:;`, + `font-src ${webview?.cspSource} data:;`, + `worker-src ${webview?.cspSource} blob:;`, + ] + .join(' '); /** * Note to code maintainers: @@ -91,7 +71,7 @@ export abstract class WebviewBaseController implements vscode.Dis - + @@ -123,7 +103,6 @@ export abstract class WebviewBaseController implements vscode.Dis */ public dispose() { this._onDisposed.fire(); - // eslint-disable-next-line @typescript-eslint/no-unsafe-return this._disposables.forEach((d) => d.dispose()); this._isDisposed = true; } diff --git a/src/webviews/index.tsx b/src/webviews/index.tsx index a21c1919..a869052b 100644 --- a/src/webviews/index.tsx +++ b/src/webviews/index.tsx @@ -2,9 +2,9 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import './styles/global.scss'; import * as React from 'react'; -// eslint-disable-next-line import/no-internal-modules import { createRoot } from 'react-dom/client'; import { type WebviewApi } from 'vscode-webview'; import { DynamicThemeProvider } from './theme/DynamicThemeProvider'; diff --git a/src/webviews/confirmationView.scss b/src/webviews/styles/confirmationView.scss similarity index 100% rename from src/webviews/confirmationView.scss rename to src/webviews/styles/confirmationView.scss diff --git a/src/webviews/styles/global.scss b/src/webviews/styles/global.scss new file mode 100644 index 00000000..cf9dd8a9 --- /dev/null +++ b/src/webviews/styles/global.scss @@ -0,0 +1,10 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +@import '@vscode/codicons/dist/codicon.css'; + +.codicon, +.codicon::before { + font-family: codicon !important; +} diff --git a/src/webviews/loadingView.scss b/src/webviews/styles/loadingView.scss similarity index 100% rename from src/webviews/loadingView.scss rename to src/webviews/styles/loadingView.scss