-
Notifications
You must be signed in to change notification settings - Fork 37
Add React webview system with pnpm workspaces #761
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,8 @@ import prettierConfig from "eslint-config-prettier"; | |
| import { createTypeScriptImportResolver } from "eslint-import-resolver-typescript"; | ||
| import { flatConfigs as importXFlatConfigs } from "eslint-plugin-import-x"; | ||
| import packageJson from "eslint-plugin-package-json"; | ||
| import reactPlugin from "eslint-plugin-react"; | ||
| import reactHooksPlugin from "eslint-plugin-react-hooks"; | ||
| import globals from "globals"; | ||
|
|
||
| export default defineConfig( | ||
|
|
@@ -15,21 +17,23 @@ export default defineConfig( | |
| ignores: [ | ||
| "out/**", | ||
| "dist/**", | ||
| "packages/*/dist/**", | ||
| "**/*.d.ts", | ||
| "vitest.config.ts", | ||
| "**/vite.config*.ts", | ||
| ".vscode-test/**", | ||
| ], | ||
| }, | ||
|
|
||
| // Base ESLint recommended rules (for JS/TS files only) | ||
| // Base ESLint recommended rules (for JS/TS/TSX files only) | ||
| { | ||
| files: ["**/*.ts", "**/*.js", "**/*.mjs"], | ||
| files: ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.mjs"], | ||
| ...eslint.configs.recommended, | ||
| }, | ||
|
|
||
| // TypeScript configuration with type-checked rules | ||
| { | ||
| files: ["**/*.ts"], | ||
| files: ["**/*.ts", "**/*.tsx"], | ||
| extends: [ | ||
| ...tseslint.configs.recommendedTypeChecked, | ||
| ...tseslint.configs.stylisticTypeChecked, | ||
|
|
@@ -64,7 +68,7 @@ export default defineConfig( | |
| ], | ||
| "@typescript-eslint/no-unused-vars": [ | ||
| "error", | ||
| { varsIgnorePattern: "^_" }, | ||
| { varsIgnorePattern: "^_", argsIgnorePattern: "^_" }, | ||
| ], | ||
| "@typescript-eslint/array-type": ["error", { default: "array-simple" }], | ||
| "@typescript-eslint/prefer-nullish-coalescing": [ | ||
|
|
@@ -160,6 +164,37 @@ export default defineConfig( | |
| }, | ||
| }, | ||
|
|
||
| // Webview packages - browser globals | ||
| { | ||
| files: ["packages/*/src/**/*.ts", "packages/*/src/**/*.tsx"], | ||
| languageOptions: { | ||
| globals: { | ||
| ...globals.browser, | ||
| }, | ||
| }, | ||
| }, | ||
|
|
||
| // TSX files - React rules | ||
| { | ||
| files: ["**/*.tsx"], | ||
| plugins: { | ||
| react: reactPlugin, | ||
| "react-hooks": reactHooksPlugin, | ||
| }, | ||
| settings: { | ||
| react: { | ||
| version: "detect", | ||
| }, | ||
| }, | ||
| rules: { | ||
| // Only add React-specific rules, TS rules already applied via **/*.ts config above | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| ...reactPlugin.configs.recommended.rules, | ||
| ...reactPlugin.configs["jsx-runtime"].rules, // React 17+ JSX transform | ||
| ...reactHooksPlugin.configs.recommended.rules, | ||
| "react/prop-types": "off", // Using TypeScript | ||
| }, | ||
| }, | ||
|
|
||
| // Package.json linting | ||
| packageJson.configs.recommended, | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,8 +18,10 @@ | |
| "type": "commonjs", | ||
| "main": "./dist/extension.js", | ||
| "scripts": { | ||
| "build": "tsc --noEmit && node esbuild.mjs", | ||
| "build:production": "tsc --noEmit && node esbuild.mjs --production", | ||
| "build": "pnpm build:webviews && tsc --noEmit && node esbuild.mjs", | ||
| "build:production": "NODE_ENV=production pnpm build:webviews && tsc --noEmit && node esbuild.mjs --production", | ||
| "build:webviews": "pnpm -r --filter \"./packages/*\" build", | ||
| "dev:webviews": "pnpm -r --filter \"./packages/*\" --parallel dev", | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For parity should this be |
||
| "fmt": "prettier --write --cache --cache-strategy content .", | ||
| "fmt:check": "prettier --check --cache --cache-strategy content .", | ||
| "lint": "eslint --cache --cache-strategy content .", | ||
|
|
@@ -194,6 +196,13 @@ | |
| "visibility": "visible", | ||
| "icon": "media/logo-white.svg", | ||
| "when": "coder.authenticated && coder.isOwner" | ||
| }, | ||
| { | ||
| "type": "webview", | ||
| "id": "coder.tasksPanel", | ||
| "name": "Tasks", | ||
| "icon": "media/logo-white.svg", | ||
| "when": "coder.authenticated && coder.devMode" | ||
| } | ||
| ] | ||
| }, | ||
|
|
@@ -202,6 +211,11 @@ | |
| "view": "myWorkspaces", | ||
| "contents": "Coder is a platform that provisions remote development environments. \n[Login](command:coder.login)", | ||
| "when": "!coder.authenticated && coder.loaded" | ||
| }, | ||
| { | ||
| "view": "coder.tasksPanel", | ||
| "contents": "[Login](command:coder.login) to view tasks.", | ||
| "when": "!coder.authenticated && coder.loaded" | ||
| } | ||
| ], | ||
| "commands": [ | ||
|
|
@@ -445,6 +459,7 @@ | |
| "@types/ws": "^8.18.1", | ||
| "@typescript-eslint/eslint-plugin": "^8.53.0", | ||
| "@typescript-eslint/parser": "^8.53.1", | ||
| "@vitejs/plugin-react-swc": "^3.8.0", | ||
| "@vitest/coverage-v8": "^4.0.16", | ||
| "@vscode/test-cli": "^0.0.12", | ||
| "@vscode/test-electron": "^2.5.2", | ||
|
|
@@ -459,13 +474,16 @@ | |
| "eslint-import-resolver-typescript": "^4.4.4", | ||
| "eslint-plugin-import-x": "^4.16.1", | ||
| "eslint-plugin-package-json": "^0.88.1", | ||
| "eslint-plugin-react": "^7.37.0", | ||
| "eslint-plugin-react-hooks": "^5.0.0", | ||
| "globals": "^17.0.0", | ||
| "jsonc-eslint-parser": "^2.4.2", | ||
| "memfs": "^4.56.4", | ||
| "prettier": "^3.7.4", | ||
| "typescript": "^5.9.3", | ||
| "typescript-eslint": "^8.53.1", | ||
| "utf-8-validate": "^6.0.6", | ||
| "vite": "^6.0.0", | ||
| "vitest": "^4.0.16" | ||
| }, | ||
| "extensionPack": [ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| // Types exposed to the extension (react/ subpath is excluded). | ||
| export type { WebviewMessage } from "./src/index"; | ||
|
Comment on lines
+1
to
+2
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. noice |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| { | ||
| "name": "@coder/shared", | ||
| "version": "1.0.0", | ||
| "description": "Shared types and utilities for Coder webviews", | ||
| "private": true, | ||
| "type": "module", | ||
| "exports": { | ||
| ".": { | ||
| "types": "./src/index.ts", | ||
| "default": "./src/index.ts" | ||
| }, | ||
| "./react": { | ||
| "types": "./src/react/index.ts", | ||
| "default": "./src/react/index.ts" | ||
| } | ||
| }, | ||
| "peerDependencies": { | ||
| "react": "^19.0.0" | ||
| }, | ||
| "peerDependenciesMeta": { | ||
| "react": { | ||
| "optional": true | ||
| } | ||
| }, | ||
| "devDependencies": { | ||
| "@types/react": "^19.0.0", | ||
| "@types/vscode-webview": "^1.57.5", | ||
| "react": "^19.0.0", | ||
| "typescript": "^5.7.3" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| // Message passing types - simple generic interface | ||
| export interface WebviewMessage<T = unknown> { | ||
| type: string; | ||
| data?: T; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| import type { WebviewApi } from "vscode-webview"; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bit of a nit, but this is in Also instead of calling this |
||
|
|
||
| import type { WebviewMessage } from "../index"; | ||
|
|
||
| // Singleton - acquireVsCodeApi can only be called once | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we expand on this a little, say something like trying to acquire the API more than once will throw an error. |
||
| let vscodeApi: WebviewApi<unknown> | undefined; | ||
|
|
||
| declare function acquireVsCodeApi(): WebviewApi<unknown>; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we throw a comment on here saying VS Code provides this inside webviews?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually do we need it? I removed it and still get completion and docs for |
||
|
|
||
| export function getVsCodeApi(): WebviewApi<unknown> { | ||
| vscodeApi ??= acquireVsCodeApi(); | ||
| return vscodeApi; | ||
| } | ||
|
|
||
| export function postMessage(message: WebviewMessage): void { | ||
| getVsCodeApi().postMessage(message); | ||
| } | ||
|
|
||
| export function getState<T>(): T | undefined { | ||
| return getVsCodeApi().getState() as T | undefined; | ||
| } | ||
|
|
||
| export function setState<T>(state: T): void { | ||
| getVsCodeApi().setState(state); | ||
| } | ||
|
Comment on lines
+15
to
+25
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have not seen how this is used yet but do we need the wrappers? Seems like callers could grab the API via Not a big deal either way. If we keep these wrappers though there is probably no need to export |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| import { useCallback, useEffect, useState } from "react"; | ||
|
|
||
| import { getState, setState } from "./api"; | ||
|
|
||
| import type { WebviewMessage } from "../index"; | ||
|
|
||
| /** | ||
| * Hook to listen for messages from the extension | ||
| */ | ||
| export function useMessage<T = unknown>( | ||
| handler: (message: WebviewMessage<T>) => void, | ||
| ): void { | ||
| useEffect((): (() => void) => { | ||
| const listener = (event: MessageEvent<WebviewMessage<T>>): void => { | ||
| handler(event.data); | ||
| }; | ||
| window.addEventListener("message", listener); | ||
| return (): void => { | ||
| window.removeEventListener("message", listener); | ||
| }; | ||
| }, [handler]); | ||
| } | ||
|
|
||
| /** | ||
| * Hook to manage webview state with VS Code's state API | ||
| */ | ||
| export function useVsCodeState<T>(initialState: T): [T, (state: T) => void] { | ||
| const [state, setLocalState] = useState<T>((): T => { | ||
| const saved = getState<T>(); | ||
| return saved ?? initialState; | ||
| }); | ||
|
|
||
| const setVsCodeState = useCallback((newState: T): void => { | ||
| setLocalState(newState); | ||
| setState(newState); | ||
| }, []); | ||
|
|
||
| return [state, setVsCodeState]; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| export { getVsCodeApi, postMessage, getState, setState } from "./api"; | ||
| export { useMessage, useVsCodeState } from "./hooks"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| { | ||
| "extends": "./tsconfig.webview.json", | ||
| "compilerOptions": { | ||
| "composite": true, | ||
| "declaration": true, | ||
| "noEmit": false, | ||
| "outDir": "dist", | ||
| "rootDir": "src" | ||
| }, | ||
| "include": ["src"] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| { | ||
| "extends": "@tsconfig/node20/tsconfig.json", | ||
| "compilerOptions": { | ||
| "lib": ["ES2023", "DOM", "DOM.Iterable"], | ||
| "module": "ESNext", | ||
| "moduleResolution": "bundler", | ||
| "jsx": "react-jsx", | ||
| "noEmit": true, | ||
| "noFallthroughCasesInSwitch": true, | ||
| "noImplicitOverride": true, | ||
| "noImplicitReturns": true | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| import react from "@vitejs/plugin-react-swc"; | ||
| import { resolve } from "node:path"; | ||
| import { defineConfig, type UserConfig } from "vite"; | ||
|
|
||
| /** | ||
| * Create a Vite config for a webview package | ||
| * @param webviewName - Name of the webview (used for output path) | ||
| * @param dirname - __dirname of the calling config file | ||
| */ | ||
| export function createWebviewConfig( | ||
| webviewName: string, | ||
| dirname: string, | ||
| ): UserConfig { | ||
| const production = process.env.NODE_ENV === "production"; | ||
|
|
||
| return defineConfig({ | ||
| plugins: [react()], | ||
| build: { | ||
| outDir: resolve(dirname, `../../dist/webviews/${webviewName}`), | ||
| emptyOutDir: true, | ||
| // Target modern browsers (VS Code webview uses Chromium from Electron) | ||
| target: "esnext", | ||
| // Skip gzip size calculation for faster builds | ||
| reportCompressedSize: false, | ||
| rollupOptions: { | ||
| output: { | ||
| entryFileNames: "index.js", | ||
| assetFileNames: "index.[ext]", | ||
| }, | ||
| }, | ||
| // No sourcemaps in production (not needed in packaged extension) | ||
| sourcemap: !production, | ||
|
Comment on lines
+31
to
+32
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It could be nice to have sourcemaps for stack traces. |
||
| }, | ||
| resolve: { | ||
| alias: { | ||
| "@coder/shared": resolve(dirname, "../shared/src"), | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there some common safe scheme people use for internal packages like these? I worry about the Although personally I would like to just see the direct imports in the code instead of aliases lol. |
||
| }, | ||
| }, | ||
| }); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah bummer is there no way to watch everything with one command?