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.