Skip to content

Commit efef7ab

Browse files
Merge pull request #255 from olasunkanmi-SE/finallyyyy
feat: Bundle extension and React webview using esbuild
2 parents 9a404f1 + c855236 commit efef7ab

File tree

4 files changed

+234
-25
lines changed

4 files changed

+234
-25
lines changed

.vscodeignore

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,30 @@
1+
# Exclude everything by default
2+
*
3+
4+
# Include only what you need
5+
!dist/**
6+
!images/**
7+
!README.md
8+
!LICENSE
9+
10+
!package.json
11+
!node_modules/jsdom/lib/jsdom/living/xhr/xhr-sync-worker.js
12+
13+
# Exclude all source, config, and dev files
14+
src/**
15+
test/**
16+
docs/**
17+
webviewUi/**
18+
node_modules/**
119
.vscode/**
220
.vscode-test/**
3-
src/**
4-
.gitignore
5-
.yarnrc
6-
.github
7-
vsc-extension-quickstart.md
8-
**/tsconfig.json
9-
**/.eslintrc.json
10-
**/*.map
11-
**/*.ts
21+
.github/
22+
*.log
23+
*.ts
24+
*.tsx
25+
*.js.map
26+
*.ts.map
27+
*.eslintrc.json
28+
tsconfig*.json
29+
yarn.lock
30+
package-lock.json

esbuild.js

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
// esbuild.js - Bundles VS Code extension and React webview for production
2+
const esbuild = require("esbuild");
3+
const fs = require("fs");
4+
const path = require("path");
5+
6+
const production = process.argv.includes("--production");
7+
const watch = process.argv.includes("--watch");
8+
9+
// Ensure dist directory exists
10+
if (!fs.existsSync("dist")) {
11+
fs.mkdirSync("dist", { recursive: true });
12+
}
13+
14+
// Plugin to handle asset copying
15+
const copyAssetsPlugin = {
16+
name: "copy-assets",
17+
setup(build) {
18+
build.onEnd(async () => {
19+
// Copy built webview assets to dist/webview/assets
20+
const builtAssetsDir = path.join(__dirname, "webviewUi", "dist", "assets");
21+
const destDir = path.join(__dirname, "dist", "webview", "assets");
22+
if (fs.existsSync(builtAssetsDir)) {
23+
if (!fs.existsSync(destDir)) {
24+
fs.mkdirSync(destDir, { recursive: true });
25+
}
26+
fs.readdirSync(builtAssetsDir).forEach((file) => {
27+
fs.copyFileSync(path.join(builtAssetsDir, file), path.join(destDir, file));
28+
});
29+
}
30+
});
31+
},
32+
};
33+
34+
// List of Node.js built-in modules to mark as external
35+
const nodeBuiltins = [
36+
"assert",
37+
"buffer",
38+
"child_process",
39+
"cluster",
40+
"crypto",
41+
"dgram",
42+
"dns",
43+
"domain",
44+
"events",
45+
"fs",
46+
"http",
47+
"https",
48+
"net",
49+
"os",
50+
"path",
51+
// "punycode", // removed so it is bundled
52+
"querystring",
53+
"readline",
54+
"stream",
55+
"string_decoder",
56+
"tls",
57+
"tty",
58+
"url",
59+
"util",
60+
"v8",
61+
"vm",
62+
"zlib",
63+
"worker_threads",
64+
];
65+
66+
const nodeModulesPlugin = {
67+
name: "node-modules",
68+
setup(build) {
69+
// Remove punycode from externals so it is bundled
70+
const filteredBuiltins = nodeBuiltins.filter((m) => m !== "punycode");
71+
build.onResolve({ filter: new RegExp(`^(${filteredBuiltins.join("|")})$`) }, () => ({ external: true }));
72+
build.onResolve({ filter: new RegExp(`^(${filteredBuiltins.join("|")})/`) }, () => ({ external: true }));
73+
build.onResolve({ filter: /better-sqlite3|electron/ }, () => ({ external: true })); // jsdom removed
74+
},
75+
};
76+
77+
const treeShakingPlugin = {
78+
name: "tree-shaking",
79+
setup(build) {
80+
build.onResolve({ filter: /.*/ }, (args) => {
81+
if (args.kind === "import-statement") {
82+
return { sideEffects: false };
83+
}
84+
});
85+
},
86+
};
87+
88+
const reactPlugin = {
89+
name: "react-handling",
90+
setup(build) {
91+
build.onLoad({ filter: /\.[jt]sx$/ }, async (args) => {
92+
const source = await fs.promises.readFile(args.path, "utf8");
93+
return {
94+
contents: `import * as React from 'react';\n${source}`,
95+
loader: args.path.endsWith("tsx") ? "tsx" : "jsx",
96+
};
97+
});
98+
},
99+
};
100+
101+
async function main() {
102+
// Extension bundle
103+
const mainCtx = await esbuild.context({
104+
entryPoints: ["src/extension.ts"],
105+
bundle: true,
106+
external: [
107+
"vscode",
108+
"better-sqlite3",
109+
"electron",
110+
"./node_modules/jsdom/lib/jsdom/living/xhr/xhr-sync-worker.js",
111+
// 'punycode' intentionally NOT external, so it is bundled
112+
],
113+
format: "cjs",
114+
target: "node16",
115+
platform: "node",
116+
minify: production,
117+
sourcemap: !production,
118+
outfile: "dist/extension.js",
119+
metafile: true,
120+
logLevel: "info",
121+
plugins: [nodeModulesPlugin, treeShakingPlugin],
122+
});
123+
124+
// Webview bundle
125+
const webviewCtx = await esbuild.context({
126+
entryPoints: ["webviewUi/src/main.tsx"],
127+
bundle: true,
128+
minify: production,
129+
sourcemap: !production,
130+
format: "esm",
131+
platform: "browser",
132+
target: "es2020",
133+
outdir: "dist/webview",
134+
splitting: true,
135+
chunkNames: "chunks/[name]-[hash]",
136+
assetNames: "assets/[name]-[hash]",
137+
loader: {
138+
".tsx": "tsx",
139+
".ts": "ts",
140+
".png": "dataurl",
141+
".svg": "dataurl",
142+
".css": "css",
143+
},
144+
plugins: [
145+
reactPlugin,
146+
{
147+
name: "css-module",
148+
setup(build) {
149+
build.onLoad({ filter: /\.css$/ }, async (args) => {
150+
const css = fs.readFileSync(args.path, "utf8");
151+
const scopedCss = css.replace(/(\.[a-zA-Z][a-zA-Z0-9-_]*)/g, `$1-${Date.now()}`);
152+
return { loader: "css", contents: scopedCss };
153+
});
154+
},
155+
},
156+
copyAssetsPlugin,
157+
treeShakingPlugin,
158+
],
159+
define: {
160+
"process.env.NODE_ENV": production ? '"production"' : '"development"',
161+
global: "window",
162+
},
163+
metafile: true,
164+
});
165+
166+
try {
167+
if (watch) {
168+
console.log("👀 Watching for changes...");
169+
await mainCtx.watch();
170+
await webviewCtx.watch();
171+
} else {
172+
console.log("🚀 Building...");
173+
const startTime = Date.now();
174+
const [mainResult, webviewResult] = await Promise.all([mainCtx.rebuild(), webviewCtx.rebuild()]);
175+
const duration = Date.now() - startTime;
176+
console.log(`\n✨ Build completed in ${duration}ms`);
177+
if (production) {
178+
const mainSize = fs.statSync("dist/extension.js").size / 1024;
179+
const webviewSize = fs
180+
.readdirSync("dist/webview")
181+
.filter((f) => f.endsWith(".js"))
182+
.reduce((acc, file) => acc + fs.statSync(path.join("dist/webview", file)).size / 1024, 0);
183+
console.log("\n📦 Bundle sizes:");
184+
console.log(` Extension: ${mainSize.toFixed(2)}KB`);
185+
console.log(` Webview: ${webviewSize.toFixed(2)}KB`);
186+
}
187+
await mainCtx.dispose();
188+
await webviewCtx.dispose();
189+
}
190+
} catch (error) {
191+
console.error("\n❌ Build failed:");
192+
console.error(error);
193+
process.exit(1);
194+
}
195+
}
196+
197+
main().catch((e) => {
198+
console.error(e);
199+
process.exit(1);
200+
});

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
},
99
"publisher": "fiatinnovations",
1010
"description": "CodeBuddy is a Visual Studio Code extension that enhances developer productivity through AI-powered code assistance. It provides intelligent code review, refactoring suggestions, optimization tips, and interactive chat capabilities powered by multiple AI models including Gemini, Groq, Anthropic, and Deepseek.",
11-
"version": "2.1.0",
11+
"version": "3.2.6",
1212
"engines": {
1313
"vscode": "^1.78.0"
1414
},
@@ -40,7 +40,7 @@
4040
"productivity"
4141
],
4242
"homepage": "https://github.com/olasunkanmi-SE/codebuddy#readme",
43-
"main": "./out/extension.js",
43+
"main": "./dist/extension.js",
4444
"contributes": {
4545
"views": {
4646
"codeBuddy-view-container": [
@@ -288,6 +288,7 @@
288288
]
289289
},
290290
"scripts": {
291+
"package": "node esbuild.js --production",
291292
"build": "npm run compile && npm run format && npm run build:webview",
292293
"dev:webview": "cd webviewUi && npm run dev",
293294
"build:webview": "cd webviewUi && npm run build",

src/webview/chat_html.ts

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@ import { Uri, Webview } from "vscode";
66
// and ensure script integrity when using Content Security Policy (CSP)
77
function getNonce() {
88
let text = "";
9-
const possible =
10-
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
9+
const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
1110
for (let i = 0; i < 32; i++) {
1211
text += possible.charAt(Math.floor(Math.random() * possible.length));
1312
}
@@ -17,18 +16,8 @@ function getNonce() {
1716
const nonce = getNonce();
1817

1918
export const chartComponent = (webview: Webview, extensionUri: Uri) => {
20-
const stylesUri = getUri(webview, extensionUri, [
21-
"webviewUi",
22-
"dist",
23-
"assets",
24-
"index.css",
25-
]);
26-
const scriptUri = getUri(webview, extensionUri, [
27-
"webviewUi",
28-
"dist",
29-
"assets",
30-
"index.js",
31-
]);
19+
const stylesUri = getUri(webview, extensionUri, ["dist", "webview", "assets", "index.css"]);
20+
const scriptUri = getUri(webview, extensionUri, ["dist", "webview", "assets", "index.js"]);
3221
return `
3322
<html lang="en">
3423
<head>

0 commit comments

Comments
 (0)