mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
[ENG-126] Migrate to Vite (#1876)
* Initial webview vite migration * Make vite work * Fix test running * Enable HMR, disable vite chunking * Silence type checking errors * Vite doesn't use browserslist * get rid of breaking css flag * add doc to getHMRHtmlContent * Make it work * Changeset * Add IS_DEV to env definitions * Update tasks to include HMR * Update CSP image rules * prettier * reintroduce IS_DEV in env * add new deps to pkg lock --------- Co-authored-by: Dennis Bartlett <bartlett.dc.1@gmail.com>
This commit is contained in:
parent
4506eb39a6
commit
916660a390
31 changed files with 2056 additions and 16267 deletions
5
.changeset/lazy-insects-report.md
Normal file
5
.changeset/lazy-insects-report.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Migrate webview from CRA to Vite
|
||||
3
.github/dependabot.yml
vendored
3
.github/dependabot.yml
vendored
|
|
@ -28,10 +28,7 @@ updates:
|
|||
patterns:
|
||||
- "*"
|
||||
ignore:
|
||||
# Ignore CRA and related packages that often have false positives
|
||||
- dependency-name: "react-scripts"
|
||||
- dependency-name: "@testing-library/*"
|
||||
- dependency-name: "web-vitals"
|
||||
- dependency-name: "*"
|
||||
update-types:
|
||||
- "version-update:semver-major"
|
||||
|
|
|
|||
58
.vscode/tasks.json
vendored
58
.vscode/tasks.json
vendored
|
|
@ -5,7 +5,7 @@
|
|||
"tasks": [
|
||||
{
|
||||
"label": "watch",
|
||||
"dependsOn": ["npm: build:webview", "npm: watch:tsc", "npm: watch:esbuild"],
|
||||
"dependsOn": ["npm: build:webview", "npm: dev:webview", "npm: watch:tsc", "npm: watch:esbuild"],
|
||||
"presentation": {
|
||||
"reveal": "never"
|
||||
},
|
||||
|
|
@ -23,7 +23,42 @@
|
|||
"label": "npm: build:webview",
|
||||
"presentation": {
|
||||
"group": "watch",
|
||||
"reveal": "never"
|
||||
"reveal": "never",
|
||||
"close": true
|
||||
},
|
||||
"options": {
|
||||
"env": {
|
||||
"IS_DEV": "true"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "dev:webview",
|
||||
"group": "build",
|
||||
"problemMatcher": [
|
||||
{
|
||||
"pattern": [
|
||||
{
|
||||
"regexp": ".",
|
||||
"file": 1,
|
||||
"location": 2,
|
||||
"message": 3
|
||||
}
|
||||
],
|
||||
"background": {
|
||||
"activeOnStart": true,
|
||||
"beginsPattern": ".",
|
||||
"endsPattern": "."
|
||||
}
|
||||
}
|
||||
],
|
||||
"isBackground": true,
|
||||
"label": "npm: dev:webview",
|
||||
"presentation": {
|
||||
"group": "watch",
|
||||
"reveal": "never",
|
||||
"close": true
|
||||
},
|
||||
"options": {
|
||||
"env": {
|
||||
|
|
@ -40,7 +75,8 @@
|
|||
"label": "npm: watch:esbuild",
|
||||
"presentation": {
|
||||
"group": "watch",
|
||||
"reveal": "never"
|
||||
"reveal": "never",
|
||||
"close": true
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -52,7 +88,8 @@
|
|||
"label": "npm: watch:tsc",
|
||||
"presentation": {
|
||||
"group": "watch",
|
||||
"reveal": "never"
|
||||
"reveal": "never",
|
||||
"close": true
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -70,6 +107,19 @@
|
|||
"label": "tasks: watch-tests",
|
||||
"dependsOn": ["npm: watch", "npm: watch-tests"],
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "stop",
|
||||
"command": "echo ${input:terminate}",
|
||||
"type": "shell"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"id": "terminate",
|
||||
"type": "command",
|
||||
"command": "workbench.action.tasks.terminate",
|
||||
"args": "terminateAll"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ demo.gif
|
|||
# Ignore all webview-ui files except the build directory (https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/frameworks/hello-world-react-cra/.vscodeignore)
|
||||
webview-ui/src/**
|
||||
webview-ui/public/**
|
||||
webview-ui/scripts/**
|
||||
webview-ui/index.html
|
||||
webview-ui/README.md
|
||||
webview-ui/package.json
|
||||
|
|
|
|||
|
|
@ -206,7 +206,7 @@
|
|||
"format:fix": "prettier . --write",
|
||||
"test": "vscode-test",
|
||||
"install:all": "npm install && cd webview-ui && npm install",
|
||||
"start:webview": "cd webview-ui && npm run start",
|
||||
"dev:webview": "cd webview-ui && npm run dev",
|
||||
"build:webview": "cd webview-ui && npm run build",
|
||||
"test:webview": "cd webview-ui && npm run test",
|
||||
"publish:marketplace": "vsce publish && ovsx publish",
|
||||
|
|
|
|||
|
|
@ -178,11 +178,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
return findLast(Array.from(this.activeInstances), (instance) => instance.view?.visible === true)
|
||||
}
|
||||
|
||||
resolveWebviewView(
|
||||
webviewView: vscode.WebviewView | vscode.WebviewPanel,
|
||||
//context: vscode.WebviewViewResolveContext<unknown>, used to recreate a deallocated webview, but we don't need this since we use retainContextWhenHidden
|
||||
//token: vscode.CancellationToken
|
||||
): void | Thenable<void> {
|
||||
async resolveWebviewView(webviewView: vscode.WebviewView | vscode.WebviewPanel) {
|
||||
this.outputChannel.appendLine("Resolving webview view")
|
||||
this.view = webviewView
|
||||
|
||||
|
|
@ -191,7 +187,11 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
enableScripts: true,
|
||||
localResourceRoots: [this.context.extensionUri],
|
||||
}
|
||||
webviewView.webview.html = this.getHtmlContent(webviewView.webview)
|
||||
|
||||
webviewView.webview.html =
|
||||
this.context.extensionMode === vscode.ExtensionMode.Development
|
||||
? await this.getHMRHtmlContent(webviewView.webview)
|
||||
: this.getHtmlContent(webviewView.webview)
|
||||
|
||||
// Sets up an event listener to listen for messages passed from the webview view context
|
||||
// and executes code based on the message that is received
|
||||
|
|
@ -342,9 +342,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
// then convert it to a uri we can use in the webview.
|
||||
|
||||
// The CSS file from the React build output
|
||||
const stylesUri = getUri(webview, this.context.extensionUri, ["webview-ui", "build", "static", "css", "main.css"])
|
||||
const stylesUri = getUri(webview, this.context.extensionUri, ["webview-ui", "build", "assets", "index.css"])
|
||||
// The JS file from the React build output
|
||||
const scriptUri = getUri(webview, this.context.extensionUri, ["webview-ui", "build", "static", "js", "main.js"])
|
||||
const scriptUri = getUri(webview, this.context.extensionUri, ["webview-ui", "build", "assets", "index.js"])
|
||||
|
||||
// The codicon font from the React build output
|
||||
// https://github.com/microsoft/vscode-extension-samples/blob/main/webview-codicons-sample/src/extension.ts
|
||||
|
|
@ -389,18 +389,92 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
<meta name="theme-color" content="#000000">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; font-src ${webview.cspSource}; style-src ${webview.cspSource} 'unsafe-inline'; img-src ${webview.cspSource} https: data:; script-src 'nonce-${nonce}';">
|
||||
<link rel="stylesheet" type="text/css" href="${stylesUri}">
|
||||
<link href="${codiconsUri}" rel="stylesheet" />
|
||||
<link href="${codiconsUri}" rel="stylesheet" />
|
||||
<title>Cline</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<script nonce="${nonce}" src="${scriptUri}"></script>
|
||||
<script type="module" nonce="${nonce}" src="${scriptUri}"></script>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
}
|
||||
|
||||
/**
|
||||
* Connects to the local Vite dev server to allow HMR, with fallback to the bundled assets
|
||||
*
|
||||
* @param webview A reference to the extension webview
|
||||
* @returns A template string literal containing the HTML that should be
|
||||
* rendered within the webview panel
|
||||
*/
|
||||
private async getHMRHtmlContent(webview: vscode.Webview): Promise<string> {
|
||||
const localPort = 25463
|
||||
const localServerUrl = `localhost:${localPort}`
|
||||
|
||||
// Check if local dev server is running.
|
||||
try {
|
||||
await axios.get(`http://${localServerUrl}`)
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(
|
||||
"Cline: Local webview dev server is not running, HMR will not work. Please run 'npm run dev:webview' before launching the extension to enable HMR. Using bundled assets.",
|
||||
)
|
||||
|
||||
return this.getHtmlContent(webview)
|
||||
}
|
||||
|
||||
const nonce = getNonce()
|
||||
const stylesUri = getUri(webview, this.context.extensionUri, ["webview-ui", "build", "assets", "index.css"])
|
||||
const codiconsUri = getUri(webview, this.context.extensionUri, [
|
||||
"node_modules",
|
||||
"@vscode",
|
||||
"codicons",
|
||||
"dist",
|
||||
"codicon.css",
|
||||
])
|
||||
|
||||
const scriptEntrypoint = "src/main.tsx"
|
||||
const scriptUri = `http://${localServerUrl}/${scriptEntrypoint}`
|
||||
|
||||
const reactRefresh = /*html*/ `
|
||||
<script nonce="${nonce}" type="module">
|
||||
import RefreshRuntime from "http://${localServerUrl}/@react-refresh"
|
||||
RefreshRuntime.injectIntoGlobalHook(window)
|
||||
window.$RefreshReg$ = () => {}
|
||||
window.$RefreshSig$ = () => (type) => type
|
||||
window.__vite_plugin_react_preamble_installed__ = true
|
||||
</script>
|
||||
`
|
||||
|
||||
const csp = [
|
||||
"default-src 'none'",
|
||||
`font-src ${webview.cspSource}`,
|
||||
`style-src ${webview.cspSource} 'unsafe-inline' https://* http://${localServerUrl} http://0.0.0.0:${localPort}`,
|
||||
`img-src ${webview.cspSource} https: data:`,
|
||||
`script-src 'unsafe-eval' https://* http://${localServerUrl} http://0.0.0.0:${localPort} 'nonce-${nonce}'`,
|
||||
`connect-src https://* ws://${localServerUrl} ws://0.0.0.0:${localPort} http://${localServerUrl} http://0.0.0.0:${localPort}`,
|
||||
]
|
||||
|
||||
return /*html*/ `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
|
||||
<meta http-equiv="Content-Security-Policy" content="${csp.join("; ")}">
|
||||
<link rel="stylesheet" type="text/css" href="${stylesUri}">
|
||||
<link href="${codiconsUri}" rel="stylesheet" />
|
||||
<title>Roo Code</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
${reactRefresh}
|
||||
<script type="module" src="${scriptUri}"></script>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up an event listener to listen for messages passed from the webview context and
|
||||
* executes code based on the message that is received.
|
||||
|
|
|
|||
49
webview-ui/.gitignore
vendored
49
webview-ui/.gitignore
vendored
|
|
@ -1,23 +1,32 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
build
|
||||
*.local
|
||||
coverage
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
!.env.test
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
|
|
|||
26
webview-ui/eslint.config.js
Normal file
26
webview-ui/eslint.config.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import js from "@eslint/js"
|
||||
import globals from "globals"
|
||||
import reactHooks from "eslint-plugin-react-hooks"
|
||||
import reactRefresh from "eslint-plugin-react-refresh"
|
||||
import tseslint from "typescript-eslint"
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ["build"] },
|
||||
{
|
||||
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||
files: ["**/*.{ts,tsx}"],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
plugins: {
|
||||
"react-hooks": reactHooks,
|
||||
"react-refresh": reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
},
|
||||
},
|
||||
)
|
||||
12
webview-ui/index.html
Normal file
12
webview-ui/index.html
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Cline Webview</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
// "Official" jest workaround for mocking window.matchMedia()
|
||||
// https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom
|
||||
|
||||
Object.defineProperty(window, "matchMedia", {
|
||||
writable: true,
|
||||
value: vi.fn().mockImplementation((query) => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: vi.fn(), // Deprecated
|
||||
removeListener: vi.fn(), // Deprecated
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
dispatchEvent: vi.fn(),
|
||||
})),
|
||||
})
|
||||
17487
webview-ui/package-lock.json
generated
17487
webview-ui/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,7 +1,16 @@
|
|||
{
|
||||
"name": "webview-ui",
|
||||
"version": "0.1.0",
|
||||
"version": "0.3.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint .",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest dev"
|
||||
},
|
||||
"dependencies": {
|
||||
"@floating-ui/react": "^0.27.4",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
|
|
@ -16,54 +25,31 @@
|
|||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-remark": "^2.1.0",
|
||||
"react-scripts": "^5.0.1",
|
||||
"react-textarea-autosize": "^8.5.3",
|
||||
"react-use": "^17.5.1",
|
||||
"react-virtuoso": "^4.7.13",
|
||||
"rehype-highlight": "^7.0.0",
|
||||
"rewire": "^7.0.0",
|
||||
"styled-components": "^6.1.13",
|
||||
"typescript": "^5.7.3",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"overrides": {
|
||||
"typescript": "^5.7.3"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "node ./scripts/build-react-no-split.js",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest dev",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
"react-textarea-autosize": "^8.5.7",
|
||||
"react-use": "^17.6.0",
|
||||
"react-virtuoso": "^4.12.3",
|
||||
"rehype-highlight": "^7.0.1",
|
||||
"styled-components": "^6.1.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^15.0.6",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/node": "^20.x",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@eslint/js": "^9.17.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.2.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^22.13.4",
|
||||
"@types/react": "^18.3.18",
|
||||
"@types/react-dom": "^18.3.5",
|
||||
"@types/vscode-webview": "^1.57.5",
|
||||
"jsdom": "^25.0.1",
|
||||
"vitest": "^2.1.9"
|
||||
"@vitejs/plugin-react-swc": "^3.5.0",
|
||||
"eslint": "^9.17.0",
|
||||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.16",
|
||||
"globals": "^15.14.0",
|
||||
"jsdom": "^26.0.0",
|
||||
"typescript": "^5.7.3",
|
||||
"typescript-eslint": "^8.18.2",
|
||||
"vite": "^6.1.1",
|
||||
"vitest": "^3.0.5"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,38 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="description" content="Web site created using create-react-app" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
--></body>
|
||||
</html>
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
|
|
@ -1,134 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* A script that overrides some of the create-react-app build script configurations
|
||||
* in order to disable code splitting/chunking and rename the output build files so
|
||||
* they have no hash. (Reference: https://mtm.dev/disable-code-splitting-create-react-app).
|
||||
*
|
||||
* This is crucial for getting React webview code to run because VS Code expects a
|
||||
* single (consistently named) JavaScript and CSS file when configuring webviews.
|
||||
*/
|
||||
|
||||
const rewire = require("rewire")
|
||||
const defaults = rewire("react-scripts/scripts/build.js")
|
||||
const config = defaults.__get__("config")
|
||||
const webpack = require("webpack")
|
||||
|
||||
/* Modifying Webpack Configuration for 'shared' dir
|
||||
This section uses Rewire to modify Create React App's webpack configuration without ejecting. Rewire allows us to inject and alter the internal build scripts of CRA at runtime. This allows us to maintain a flexible project structure that keeps shared code outside the webview-ui/src directory, while still adhering to CRA's security model that typically restricts imports to within src/.
|
||||
1. Uses the ModuleScopePlugin to whitelist files from the shared directory, allowing them to be imported despite being outside src/. (see: https://stackoverflow.com/questions/44114436/the-create-react-app-imports-restriction-outside-of-src-directory/58321458#58321458)
|
||||
2. Modifies the TypeScript rule to include the shared directory in compilation. This essentially transpiles and includes the ts files in shared dir in the output main.js file.
|
||||
Before, we would just import types from shared dir and specifying include (and alias to have cleaner paths) in tsconfig.json was enough. But now that we are creating values (i.e. models in api.ts) to import into the react app, we must also include these files in the webpack resolution.
|
||||
- Imports from the shared directory must use full paths relative to the src directory, without file extensions.
|
||||
- Example: import { someFunction } from '../../src/shared/utils/helpers'
|
||||
*/
|
||||
const ModuleScopePlugin = require("react-dev-utils/ModuleScopePlugin")
|
||||
const path = require("path")
|
||||
const fs = require("fs")
|
||||
// Get all files in the shared directory
|
||||
const sharedDir = path.resolve(__dirname, "..", "..", "src", "shared")
|
||||
|
||||
function getAllFiles(dir) {
|
||||
let files = []
|
||||
fs.readdirSync(dir).forEach((file) => {
|
||||
const filePath = path.join(dir, file)
|
||||
if (fs.statSync(filePath).isDirectory()) {
|
||||
files = files.concat(getAllFiles(filePath))
|
||||
} else {
|
||||
// Skip test files
|
||||
if (!file.endsWith(".test.ts")) {
|
||||
const withoutExtension = path.join(dir, path.parse(file).name)
|
||||
files.push(withoutExtension)
|
||||
}
|
||||
}
|
||||
})
|
||||
return files
|
||||
}
|
||||
const sharedFiles = getAllFiles(sharedDir)
|
||||
// config.resolve.plugins = config.resolve.plugins.filter((plugin) => !(plugin instanceof ModuleScopePlugin))
|
||||
// Instead of excluding the whole ModuleScopePlugin, we just whitelist specific files that can be imported from outside src.
|
||||
config.resolve.plugins.forEach((plugin) => {
|
||||
if (plugin instanceof ModuleScopePlugin) {
|
||||
console.log("Whitelisting shared files: ", sharedFiles)
|
||||
sharedFiles.forEach((file) => plugin.allowedFiles.add(file))
|
||||
}
|
||||
})
|
||||
/*
|
||||
Webpack configuration
|
||||
|
||||
Webpack is a module bundler for JavaScript applications. It processes your project files, resolving dependencies and generating a deployable production build.
|
||||
The webpack config is an object that tells webpack how to process and bundle your code. It defines entry points, output settings, and how to handle different file types.
|
||||
This config.module section of the webpack config deals with how different file types (modules) should be treated.
|
||||
config.module.rules:
|
||||
Rules define how module files should be processed. Each rule can:
|
||||
- Specify which files to process (test)
|
||||
When webpack "processes" a file, it performs several operations:
|
||||
1. Reads the file
|
||||
2. Parses its content and analyzes dependencies
|
||||
3. Applies transformations (e.g., converting TypeScript to JavaScript)
|
||||
4. Potentially modifies the code (e.g., applying polyfills)
|
||||
5. Includes the processed file in the final bundle
|
||||
By specifying which files to process, we're telling webpack which files should go through this pipeline and be included in our application bundle. Files that aren't processed are ignored by webpack.
|
||||
In our case, we're ensuring that TypeScript files in our shared directory are processed, allowing us to use them in our application.
|
||||
- Define which folders to include or exclude
|
||||
- Set which loaders to use for transformation
|
||||
A loader transforms certain types of files into valid modules that webpack can process. For example, the TypeScript loader converts .ts files into JavaScript that webpack can understand.
|
||||
By modifying these rules, we can change how webpack processes different files in our project, allowing us to include files from outside the standard src directory.
|
||||
|
||||
Why we need to modify the webpack config
|
||||
|
||||
Create React App (CRA) is designed to only process files within the src directory for security reasons. (CRA limits processing to the src directory to prevent accidental inclusion of sensitive files, reduce the attack surface, and ensure predictable builds, enhancing overall project security and consistency. Therefore it's essential that if you do include files outside src, you do so explicitly.)
|
||||
To use files from the shared directory, we need to:
|
||||
1. Modify ModuleScopePlugin to allow imports from the shared directory.
|
||||
2. Update the TypeScript loader rule to process TypeScript files from the shared directory.
|
||||
These changes tell webpack it's okay to import from the shared directory and ensure that TypeScript files in this directory are properly converted to JavaScript.
|
||||
|
||||
Modify webpack configuration to process TypeScript files from shared directory
|
||||
|
||||
This code modifies the webpack configuration to allow processing of TypeScript files from our shared directory, which is outside the standard src folder.
|
||||
1. config.module.rules[1]: In Create React App's webpack config, the second rule (index 1) typically contains the rules for processing JavaScript and TypeScript files.
|
||||
2. .oneOf: This array contains a list of loaders, and webpack will use the first matching loader for each file. We iterate through these to find the TypeScript loader.
|
||||
3. We check each rule to see if it applies to TypeScript files by looking for 'ts|tsx' in the test regex.
|
||||
4. When we find the TypeScript rule, we add our shared directory to its 'include' array. This tells webpack to also process TypeScript files from the shared directory.
|
||||
Note: This code assumes a specific structure in the CRA webpack config. If CRA updates its config structure in future versions, this code might need to be adjusted.
|
||||
*/
|
||||
config.module.rules[1].oneOf.forEach((rule) => {
|
||||
if (rule.test && rule.test.toString().includes("ts|tsx")) {
|
||||
// rule.include is path to src by default, but we can update rule.include to be an array as it matches an expected schema by react-scripts
|
||||
rule.include = [rule.include, sharedDir].filter(Boolean)
|
||||
}
|
||||
})
|
||||
|
||||
// Force all code into a single bundle for VS Code webview compatibility.
|
||||
// This is necessary for:
|
||||
// 1. Mermaid.js to work properly (prevents async chunk loading)
|
||||
// 2. Consistent CSP nonce handling (single bundle = single nonce)
|
||||
config.optimization = {
|
||||
...config.optimization,
|
||||
splitChunks: {
|
||||
cacheGroups: {
|
||||
default: false,
|
||||
},
|
||||
name: "main", // Forces all chunks (dynamic import() calls, for example those used by Mermaid) into one bundle - this is what actually prevents code splitting
|
||||
},
|
||||
runtimeChunk: false,
|
||||
}
|
||||
|
||||
// Ensure all chunks are named 'main' to match our CSP nonce setup
|
||||
config.output = {
|
||||
...config.output,
|
||||
filename: "static/js/[name].js",
|
||||
}
|
||||
|
||||
// Adjust build environment variables for dev/debug builds.
|
||||
config.plugins[4] = new webpack.DefinePlugin({
|
||||
"process.env": {
|
||||
...config.plugins[4].definitions["process.env"],
|
||||
NODE_ENV: JSON.stringify(process.env.IS_DEV ? "development" : "production"),
|
||||
IS_DEV: JSON.stringify(process.env.IS_DEV),
|
||||
},
|
||||
})
|
||||
|
||||
// Rename main.{hash}.css to main.css
|
||||
config.plugins[5].options.filename = "static/css/[name].css"
|
||||
config.plugins[5].options.moduleFilename = () => "static/css/main.css"
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
import "@testing-library/jest-dom"
|
||||
import "./matchMedia"
|
||||
|
|
@ -76,53 +76,56 @@ const Markdown = memo(({ markdown }: { markdown?: string }) => {
|
|||
)
|
||||
})
|
||||
|
||||
const ChatRow = memo((props: ChatRowProps) => {
|
||||
const { isLast, onHeightChange, message, lastModifiedMessage } = props
|
||||
// Store the previous height to compare with the current height
|
||||
// This allows us to detect changes without causing re-renders
|
||||
const prevHeightRef = useRef(0)
|
||||
const ChatRow = memo(
|
||||
(props: ChatRowProps) => {
|
||||
const { isLast, onHeightChange, message, lastModifiedMessage } = props
|
||||
// Store the previous height to compare with the current height
|
||||
// This allows us to detect changes without causing re-renders
|
||||
const prevHeightRef = useRef(0)
|
||||
|
||||
// NOTE: for tools that are interrupted and not responded to (approved or rejected) there won't be a checkpoint hash
|
||||
let shouldShowCheckpoints =
|
||||
message.lastCheckpointHash != null &&
|
||||
(message.say === "tool" ||
|
||||
message.ask === "tool" ||
|
||||
message.say === "command" ||
|
||||
message.ask === "command" ||
|
||||
// message.say === "completion_result" ||
|
||||
// message.ask === "completion_result" ||
|
||||
message.say === "use_mcp_server" ||
|
||||
message.ask === "use_mcp_server")
|
||||
// NOTE: for tools that are interrupted and not responded to (approved or rejected) there won't be a checkpoint hash
|
||||
let shouldShowCheckpoints =
|
||||
message.lastCheckpointHash != null &&
|
||||
(message.say === "tool" ||
|
||||
message.ask === "tool" ||
|
||||
message.say === "command" ||
|
||||
message.ask === "command" ||
|
||||
// message.say === "completion_result" ||
|
||||
// message.ask === "completion_result" ||
|
||||
message.say === "use_mcp_server" ||
|
||||
message.ask === "use_mcp_server")
|
||||
|
||||
if (shouldShowCheckpoints && isLast) {
|
||||
shouldShowCheckpoints = lastModifiedMessage?.ask === "resume_completed_task" || lastModifiedMessage?.ask === "resume_task"
|
||||
}
|
||||
|
||||
const [chatrow, { height }] = useSize(
|
||||
<ChatRowContainer>
|
||||
<ChatRowContent {...props} />
|
||||
{shouldShowCheckpoints && <CheckpointOverlay messageTs={message.ts} />}
|
||||
</ChatRowContainer>,
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
// used for partials command output etc.
|
||||
// NOTE: it's important we don't distinguish between partial or complete here since our scroll effects in chatview need to handle height change during partial -> complete
|
||||
const isInitialRender = prevHeightRef.current === 0 // prevents scrolling when new element is added since we already scroll for that
|
||||
// height starts off at Infinity
|
||||
if (isLast && height !== 0 && height !== Infinity && height !== prevHeightRef.current) {
|
||||
if (!isInitialRender) {
|
||||
onHeightChange(height > prevHeightRef.current)
|
||||
}
|
||||
prevHeightRef.current = height
|
||||
if (shouldShowCheckpoints && isLast) {
|
||||
shouldShowCheckpoints =
|
||||
lastModifiedMessage?.ask === "resume_completed_task" || lastModifiedMessage?.ask === "resume_task"
|
||||
}
|
||||
}, [height, isLast, onHeightChange, message])
|
||||
|
||||
// we cannot return null as virtuoso does not support it so we use a separate visibleMessages array to filter out messages that should not be rendered
|
||||
return chatrow
|
||||
},
|
||||
// memo does shallow comparison of props, so we need to do deep comparison of arrays/objects whose properties might change
|
||||
deepEqual)
|
||||
const [chatrow, { height }] = useSize(
|
||||
<ChatRowContainer>
|
||||
<ChatRowContent {...props} />
|
||||
{shouldShowCheckpoints && <CheckpointOverlay messageTs={message.ts} />}
|
||||
</ChatRowContainer>,
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
// used for partials command output etc.
|
||||
// NOTE: it's important we don't distinguish between partial or complete here since our scroll effects in chatview need to handle height change during partial -> complete
|
||||
const isInitialRender = prevHeightRef.current === 0 // prevents scrolling when new element is added since we already scroll for that
|
||||
// height starts off at Infinity
|
||||
if (isLast && height !== 0 && height !== Infinity && height !== prevHeightRef.current) {
|
||||
if (!isInitialRender) {
|
||||
onHeightChange(height > prevHeightRef.current)
|
||||
}
|
||||
prevHeightRef.current = height
|
||||
}
|
||||
}, [height, isLast, onHeightChange, message])
|
||||
|
||||
// we cannot return null as virtuoso does not support it so we use a separate visibleMessages array to filter out messages that should not be rendered
|
||||
return chatrow
|
||||
},
|
||||
// memo does shallow comparison of props, so we need to do deep comparison of arrays/objects whose properties might change
|
||||
deepEqual,
|
||||
)
|
||||
|
||||
export default ChatRow
|
||||
|
||||
|
|
@ -137,7 +140,7 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
|
|||
}
|
||||
return [undefined, undefined, undefined]
|
||||
}, [message.text, message.say])
|
||||
|
||||
|
||||
// when resuming task last won't be api_req_failed but a resume_task message so api_req_started will show loading spinner. that's why we just remove the last api_req_started that failed without streaming anything
|
||||
const apiRequestFailedMessage =
|
||||
isLast && lastModifiedMessage?.ask === "api_req_failed" // if request is retried then the latest message is a api_req_retried
|
||||
|
|
@ -817,7 +820,7 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
|
|||
// marginBottom: 15,
|
||||
cursor: "pointer",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
|
||||
|
||||
fontStyle: "italic",
|
||||
overflow: "hidden",
|
||||
}}>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useEffect, useState } from "react"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import DOMPurify from 'dompurify';
|
||||
import DOMPurify from "dompurify"
|
||||
|
||||
interface OpenGraphData {
|
||||
title?: string
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { vscode } from "../../utils/vscode"
|
|||
import LinkPreview from "./LinkPreview"
|
||||
import styled from "styled-components"
|
||||
import { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
|
||||
import DOMPurify from 'dompurify';
|
||||
import DOMPurify from "dompurify"
|
||||
|
||||
// We'll use the backend isImageUrl function for HEAD requests
|
||||
// This is a client-side fallback for data URLs and obvious image extensions
|
||||
|
|
|
|||
|
|
@ -1,18 +0,0 @@
|
|||
import React from "react"
|
||||
import ReactDOM from "react-dom/client"
|
||||
import "./index.css"
|
||||
import App from "./App"
|
||||
import reportWebVitals from "./reportWebVitals"
|
||||
import "../../node_modules/@vscode/codicons/dist/codicon.css"
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement)
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
)
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
// to log results (for example: reportWebVitals(console.log))
|
||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||
reportWebVitals()
|
||||
12
webview-ui/src/main.tsx
Normal file
12
webview-ui/src/main.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { StrictMode } from "react"
|
||||
import { createRoot } from "react-dom/client"
|
||||
|
||||
import "./index.css"
|
||||
import App from "./App.tsx"
|
||||
import "../../node_modules/@vscode/codicons/dist/codicon.css"
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
1
webview-ui/src/react-app-env.d.ts
vendored
1
webview-ui/src/react-app-env.d.ts
vendored
|
|
@ -1 +0,0 @@
|
|||
/// <reference types="react-scripts" />
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
import { ReportHandler } from "web-vitals"
|
||||
|
||||
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry)
|
||||
getFID(onPerfEntry)
|
||||
getFCP(onPerfEntry)
|
||||
getLCP(onPerfEntry)
|
||||
getTTFB(onPerfEntry)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default reportWebVitals
|
||||
|
|
@ -1,5 +1,19 @@
|
|||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import "@testing-library/jest-dom"
|
||||
import { vi } from "vitest"
|
||||
|
||||
// "Official" jest workaround for mocking window.matchMedia()
|
||||
// https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom
|
||||
|
||||
Object.defineProperty(window, "matchMedia", {
|
||||
writable: true,
|
||||
value: vi.fn().mockImplementation((query) => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: vi.fn(), // Deprecated
|
||||
removeListener: vi.fn(), // Deprecated
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
dispatchEvent: vi.fn(),
|
||||
})),
|
||||
})
|
||||
|
|
|
|||
1
webview-ui/src/vite-env.d.ts
vendored
Normal file
1
webview-ui/src/vite-env.d.ts
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/// <reference types="vite/client" />
|
||||
25
webview-ui/tsconfig.app.json
Normal file
25
webview-ui/tsconfig.app.json
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["src/**/__tests__/**"]
|
||||
}
|
||||
|
|
@ -1,21 +1,4 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": ["src", "../src/shared"],
|
||||
"exclude": ["src/**/*.spec.ts", "setupTests.js", "matchMedia.js"]
|
||||
"files": [],
|
||||
"references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
|
|
|
|||
24
webview-ui/tsconfig.node.json
Normal file
24
webview-ui/tsconfig.node.json
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
import { defineConfig } from "vitest/config"
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
environment: "jsdom",
|
||||
globals: true,
|
||||
setupFiles: ["./setupTests.js"],
|
||||
},
|
||||
})
|
||||
43
webview-ui/vite.config.ts
Normal file
43
webview-ui/vite.config.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
/// <reference types="vitest/config" />
|
||||
|
||||
import { defineConfig } from "vite"
|
||||
import react from "@vitejs/plugin-react-swc"
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
test: {
|
||||
environment: "jsdom",
|
||||
globals: true,
|
||||
setupFiles: ["./src/setupTests.ts"],
|
||||
},
|
||||
build: {
|
||||
outDir: "build",
|
||||
rollupOptions: {
|
||||
output: {
|
||||
inlineDynamicImports: true,
|
||||
entryFileNames: `assets/[name].js`,
|
||||
chunkFileNames: `assets/[name].js`,
|
||||
assetFileNames: `assets/[name].[ext]`,
|
||||
},
|
||||
},
|
||||
chunkSizeWarningLimit: 100000,
|
||||
},
|
||||
server: {
|
||||
port: 25463,
|
||||
hmr: {
|
||||
host: "localhost",
|
||||
protocol: "ws",
|
||||
},
|
||||
cors: {
|
||||
origin: "*",
|
||||
methods: "*",
|
||||
allowedHeaders: "*",
|
||||
},
|
||||
},
|
||||
define: {
|
||||
"process.env": {
|
||||
NODE_ENV: JSON.stringify(process.env.IS_DEV ? "development" : "production"),
|
||||
IS_DEV: JSON.stringify(process.env.IS_DEV),
|
||||
},
|
||||
},
|
||||
})
|
||||
Loading…
Add table
Reference in a new issue