|
| 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 | +}); |
0 commit comments