15. Build and packaging

Relative imports in src/ carry .js extensions (required by Node16 resolution), so the ESM output loads in the browser without a bundler.

15.1 Two TypeScript configurations#

Config Compiles Settings Output
tsconfig.json src/**, dev/**, test/** module: Node16, target: ES2022, lib: ["ES2022"], types: ["node", "mocha"], strict, rootDir: "." out/ (out/src/extension.js, out/src/cli/main.js, out/dev/server.js, out/test/**)
tsconfig.webview.json src/core, src/text, src/layout, src/render, src/commands module: ES2022, target: ES2022, lib: ["ES2022", "DOM"], types: [], strict, declaration: true (JSDoc in media/ checks against the .d.ts) media/lib/ (ignored by git)

Relative imports in src/ carry .js extensions (required by Node16 resolution), so the ESM output loads in the browser without a bundler. The same source therefore compiles twice; the second compile is also the compile-time check that the webview-safe layers use no Node API.

Bundles for Node (ADR-9). tsc output in out/ is what tests run against, but what ships is bundled: scripts/bundle.mjs runs esbuild over out/src/extension.js and out/src/cli/main.js and writes dist/extension.js and dist/ud.js (platform node, format ESM, vscode and the .wasm files external, sourcemaps kept out of the package). Bundling takes the CLI from several hundred module loads to one file, and the host from a slow activation to a fast one; both then opt into Node’s compile cache (module.enableCompileCache() into the extension’s global storage folder, available from Node 22.1), which removes most of the parse cost on the second start. The webview is not bundled: media/ stays plain modules under the page contract, and media/lib/ stays the unbundled tsc output it imports.

package.json changes from the scaffold: name: "softforge-ui-designer", publisher: "SoftForge", main: "./dist/extension.js", engines.vscode: "^1.108.0" (kept), engines.node: ">=22", @types/node ^22 (the CLI’s minimum), and vsce replaced by @vscode/vsce (the release workflow already uses it).

15.2 Scripts#

Script Does
npm run compile tsc -p . and tsc -p tsconfig.webview.json, then scripts/bundle.mjs (esbuild, §15.1), then scripts/copy-wasm.mjs, then scripts/gen-contributions.mjs --check
npm run watch both compilers in watch mode; the host and CLI run from out/ unbundled while developing
npm run dev compile, then node out/dev/server.js --file sample/app.design --port 4173
npm run lint ESLint and scripts/check-layers.mjs
npm run check:webview the page contract checker
npm run check:licences scripts/check-licences.mjs: every production dependency and vendored file against the allowlist, and THIRD-PARTY-NOTICES regenerated and diffed (ADR-11)
npm run test:unit, test:integration, test §14
npm run golden:update, bench §7.5, §12
npm run gen regenerates contributions, typings, coverage table, Unicode tables
npm run package vsce package (runs vscode:prepublish = compile), then scripts/check-vsix-size.mjs (≤ 25 MB, §12)

15.3 Generated files#

VS Code reads contributions statically from package.json, but they come from the registry and the tool table. scripts/gen-contributions.mjs writes contributes.commands (ud.<id>, UI Designer: <Title>, with when clauses for an active design editor), contributes.languageModelTools, and the ud.* settings defaults that commands own. --check (in compile and CI) fails if package.json is out of date. The same script writes ud-plugin-api.d.ts and the coverage table (§8.5).

15.4 WASM and font assets#

Asset From Licence (to verify, ADR-11) Shipped in Loaded by
harfbuzz.wasm harfbuzzjs (dev dependency, only the .wasm is copied) MIT (“Old MIT”) media/wasm/ text engine in host, CLI, webview
icu_segmenter.wasm our own build of ICU4X icu_segmenter with baked data at a pinned CLDR/Unicode version (scripts/build-icu4x.sh, the output committed with its hash) Unicode-3.0 media/wasm/ text engine in host, CLI, webview
resvg.wasm @resvg/resvg-wasm MPL-2.0 (the resvg crate; the npm wrapper says MPL-2.0 too) — verify that shipping the unmodified .wasm needs no more than the notice media/wasm/ RasterRenderer in host and CLI
QuickJS quickjs-emscripten sync release variant (runtime dependency, its .wasm and glue) MIT (QuickJS itself MIT) dist/ (bundled glue) + its .wasm src/plugin-api in the script worker and CLI
JPG encoder mozjpeg compiled to WASM, from the @jsquash/jpeg package (encoder only) BSD-3-Clause / IJG for mozjpeg, Apache-2.0 for the wrapper media/wasm/ RasterRenderer
WebP encoder libwebp compiled to WASM, from the @jsquash/webp package (encoder only) BSD-3-Clause for libwebp, Apache-2.0 for the wrapper media/wasm/ RasterRenderer
Fonts Inter, Roboto, Noto Sans, JetBrains Mono, Source Serif, with their licences OFL-1.1 (Roboto: Apache-2.0) media/fonts/ text engine

The two image encoders are named here so the licence inventory is complete; they are candidates until npm run check:licences has seen their files (another MIT/BSD WASM build of the same libraries is an acceptable swap). scripts/copy-wasm.mjs copies and records each file’s hash in media/wasm/VERSIONS. The CLI and the host are bundled (§15.1), so the package carries dist/, media/ and the .wasm files and no node_modules; .vscodeignore excludes src/**, out/**, **/*.ts, maps, test/, dev/, scripts/ and docs/. What is left is what the 25 MB budget (§12) is spent on: fonts (~6 MB), the three WASM modules plus encoders (~8 MB), icons and code.

15.5 The ud shim#

On activation the extension writes bin/ud (POSIX shell) and bin/ud.cmd (Windows) into its global storage folder. They run the bundled dist/ud.js with VS Code’s own runtime and ELECTRON_RUN_AS_NODE=1, so no separate Node is needed (and the one-file bundle is what keeps the headless budget of §12 reachable under Electron’s slower start-up, ADR-9). When ud.cli.addToTerminalPath is on (default), that folder is prepended to PATH in new integrated terminals through context.environmentVariableCollection. The installed skills and the .mcp.json entry written by ud mcp install name the shim by its full path, because Claude Code may run outside VS Code’s terminals. The licence ticket (§10.6) is written into the same folder.