|
| 1 | +#!/usr/bin/env node |
| 2 | +// Build levelcode-ai for the browser: ONE file the web extension host can evaluate, plus the |
| 3 | +// files the manifest points at. The desktop extension stays plain JS with no build step; this |
| 4 | +// bundler only exists for the browser edition and writes nothing into extensions/. |
| 5 | +// |
| 6 | +// node web/ai-extension/build.mjs --out <dir> --vscode <Code-OSS checkout> [--minify] |
| 7 | +// |
| 8 | +// Output (<dir>/levelcode-ai): package.json (+ "browser"), extension.web.js, media/, README.md |
| 9 | +import fs from 'node:fs'; |
| 10 | +import path from 'node:path'; |
| 11 | +import { createRequire } from 'node:module'; |
| 12 | +import { fileURLToPath } from 'node:url'; |
| 13 | + |
| 14 | +const HERE = path.dirname(fileURLToPath(import.meta.url)); |
| 15 | +const REPO = path.resolve(HERE, '..', '..'); |
| 16 | +const SRC = path.join(REPO, 'extensions', 'levelcode-ai'); |
| 17 | + |
| 18 | +const args = Object.fromEntries(process.argv.slice(2).reduce((a, v, i, all) => { |
| 19 | + if (v.startsWith('--')) { a.push([v.slice(2), all[i + 1] && !all[i + 1].startsWith('--') ? all[i + 1] : 'true']); } |
| 20 | + return a; |
| 21 | +}, [])); |
| 22 | +const outRoot = path.resolve(args.out || path.join(REPO, 'dist-web', 'extensions')); |
| 23 | +const vscodeDir = path.resolve(args.vscode || path.join(REPO, 'vscode')); |
| 24 | +const minify = args.minify === 'true'; |
| 25 | + |
| 26 | +/** Find esbuild and the `buffer` package in the Code-OSS checkout, which already depends on both. */ |
| 27 | +function locate(name, bases) { |
| 28 | + for (const b of bases) { |
| 29 | + try { return createRequire(path.join(b, 'noop.js')).resolve(name); } catch { /* next */ } |
| 30 | + } |
| 31 | + return null; |
| 32 | +} |
| 33 | +const esbuildEntry = locate('esbuild', [path.join(vscodeDir, 'build'), path.join(vscodeDir, 'extensions'), vscodeDir, REPO]); |
| 34 | +if (!esbuildEntry) { console.error('esbuild not found under ' + vscodeDir + ' (run scripts/bootstrap.sh first)'); process.exit(2); } |
| 35 | +const esbuild = await import(esbuildEntry); |
| 36 | +const nodePaths = [path.join(vscodeDir, 'node_modules'), path.join(vscodeDir, 'build', 'node_modules')].filter((p) => fs.existsSync(p)); |
| 37 | +if (!nodePaths.some((p) => fs.existsSync(path.join(p, 'buffer')))) { console.error('the "buffer" package is missing from ' + vscodeDir + '/node_modules'); process.exit(2); } |
| 38 | + |
| 39 | +const outDir = path.join(outRoot, 'levelcode-ai'); |
| 40 | +fs.rmSync(outDir, { recursive: true, force: true }); |
| 41 | +fs.mkdirSync(outDir, { recursive: true }); |
| 42 | + |
| 43 | +/* ----- the extension's own files, embedded so synchronous reads keep working ------------------ */ |
| 44 | +const TEXT = new Set(['.html', '.svg', '.md', '.json', '.css', '.js', '.txt']); |
| 45 | +function collect(rel, filter) { |
| 46 | + const out = {}; |
| 47 | + const walk = (dir) => { |
| 48 | + for (const e of fs.readdirSync(path.join(SRC, dir), { withFileTypes: true })) { |
| 49 | + const r = path.posix.join(dir, e.name); |
| 50 | + if (e.isDirectory()) { walk(r); continue; } |
| 51 | + if (filter && !filter(r)) { continue; } |
| 52 | + const buf = fs.readFileSync(path.join(SRC, r)); |
| 53 | + out[r] = TEXT.has(path.extname(r)) ? buf.toString('utf8') : { base64: buf.toString('base64') }; |
| 54 | + } |
| 55 | + }; |
| 56 | + if (fs.existsSync(path.join(SRC, rel))) { walk(rel); } |
| 57 | + return out; |
| 58 | +} |
| 59 | +const assets = Object.assign({}, |
| 60 | + collect('media'), |
| 61 | + collect('skills'), |
| 62 | + collect('diagram', (r) => r.endsWith('.js')), |
| 63 | +); |
| 64 | +const generated = path.join(HERE, 'assets.generated.js'); |
| 65 | +fs.writeFileSync(generated, '// GENERATED by build.mjs — do not edit, do not commit.\nmodule.exports = ' + JSON.stringify(assets) + ';\n'); |
| 66 | + |
| 67 | +/* ----- __dirname has no meaning in a bundle: point it at the mounted copy -------------------- */ |
| 68 | +const dirnamePlugin = { |
| 69 | + name: 'levelcode-dirname', |
| 70 | + setup(b) { |
| 71 | + b.onLoad({ filter: /extensions[\\/]levelcode-ai[\\/].*\.js$/ }, (a) => { |
| 72 | + let text = fs.readFileSync(a.path, 'utf8'); |
| 73 | + if (!/\b__dirname\b/.test(text)) { return null; } |
| 74 | + const rel = path.relative(SRC, path.dirname(a.path)).split(path.sep).join('/'); |
| 75 | + text = text.replace(/\b__dirname\b/g, `(globalThis.__lcExtPath + ${JSON.stringify(rel ? '/' + rel : '')})`); |
| 76 | + return { contents: text, loader: 'js' }; |
| 77 | + }); |
| 78 | + }, |
| 79 | +}; |
| 80 | + |
| 81 | +const shim = (n) => path.join(HERE, 'shims', n + '.js'); |
| 82 | +await esbuild.build({ |
| 83 | + entryPoints: [path.join(HERE, 'entry.js')], |
| 84 | + outfile: path.join(outDir, 'extension.web.js'), |
| 85 | + bundle: true, |
| 86 | + platform: 'browser', |
| 87 | + format: 'cjs', |
| 88 | + target: 'es2022', |
| 89 | + external: ['vscode'], |
| 90 | + alias: { |
| 91 | + fs: shim('fs'), 'node:fs': shim('fs'), |
| 92 | + path: shim('path'), 'node:path': shim('path'), |
| 93 | + os: shim('os'), 'node:os': shim('os'), |
| 94 | + crypto: shim('crypto'), 'node:crypto': shim('crypto'), |
| 95 | + child_process: shim('child_process'), 'node:child_process': shim('child_process'), |
| 96 | + }, |
| 97 | + inject: [path.join(HERE, 'inject.js')], |
| 98 | + define: { global: 'globalThis' }, |
| 99 | + nodePaths, |
| 100 | + plugins: [dirnamePlugin], |
| 101 | + minify, |
| 102 | + sourcemap: minify ? false : 'inline', |
| 103 | + legalComments: 'none', |
| 104 | + logLevel: 'info', |
| 105 | +}); |
| 106 | +fs.rmSync(generated, { force: true }); |
| 107 | + |
| 108 | +/* ----- manifest + the files it refers to ---------------------------------------------------- */ |
| 109 | +const pkg = JSON.parse(fs.readFileSync(path.join(SRC, 'package.json'), 'utf8')); |
| 110 | +pkg.browser = './extension.web.js'; |
| 111 | +fs.writeFileSync(path.join(outDir, 'package.json'), JSON.stringify(pkg, null, '\t') + '\n'); |
| 112 | +for (const f of ['README.md', 'package.nls.json']) { if (fs.existsSync(path.join(SRC, f))) { fs.copyFileSync(path.join(SRC, f), path.join(outDir, f)); } } |
| 113 | +fs.cpSync(path.join(SRC, 'media'), path.join(outDir, 'media'), { recursive: true }); |
| 114 | + |
| 115 | +const size = fs.statSync(path.join(outDir, 'extension.web.js')).size; |
| 116 | +console.log(`levelcode-ai (browser) -> ${outDir} extension.web.js ${(size / 1024).toFixed(0)} KB, ${Object.keys(assets).length} embedded files`); |
0 commit comments