Tooling

Packaging

unframework build --package writes one package you can publish, with each framework's components in that framework's own package format, a subpath per framework, every framework as an optional peer, and the .uf.tsx sources for apps that compile them.

Packaging lets a design system publish one package that every framework's apps can install: a React app imports my-ds/react, a Vue app my-ds/vue, and each gets components built the way its framework's own libraries are. Think of seven packages to version and release in step, a React app that has to install Vue because a peer dependency says so, or an Angular library that a consumer's compiler refuses because it wasn't built in the Angular Package Format.

unframework build --package writes such a package to <outDir>/package/, beside the targets' trees. Point pnpm pack or npm publish at that directory.

The package

Our sample design system has four components and a composable, and its build script packages them:

package.json
"scripts": {
  "pack:dist": "unframework build --package"
}

The package's manifest takes name, version and license from the project's own package.json, and adds the rest. For all seven targets, exports reads:

dist/package/package.json
"exports": {
  "./angular": {
    "types": "./angular/types/unframework-angular.d.ts",
    "default": "./angular/fesm2022/unframework-angular.mjs"
  },
  "./astro": "./astro/index.ts",
  "./qwik": {
    "types": "./qwik/index.d.ts",
    "qwik": "./qwik/index.qwik.mjs",
    "default": "./qwik/index.qwik.mjs"
  },
  "./react": {
    "types": "./react/index.d.ts",
    "default": "./react/index.js"
  },
  "./solid": {
    "types": "./solid/index.d.ts",
    "solid": "./solid/index.jsx",
    "default": "./solid/index.js"
  },
  "./svelte": {
    "types": "./svelte/index.d.ts",
    "svelte": "./svelte/index.js"
  },
  "./vue": {
    "types": "./vue/index.d.ts",
    "default": "./vue/index.ts"
  },
  "./source/*": "./source/*",
  "./package.json": "./package.json"
}
  • A subpath per target, whose entry is that target's barrel, so every component and composable is a named import: import { Button, Card, Counter, TextField, useToggle } from "@unframework/e2e-design-system/react";.
  • ./source/*, the .uf.tsx sources and the files under srcDir they read, so an app can compile them with the bundler plugin instead. One tarball serves both ways.
  • peerDependencies, each packaged target's framework, every one optional in peerDependenciesMeta, so a React app installs no Vue.

The manifest lists the targets in this order, whatever order your config gives.

Each framework's format

Each target ships the way that framework's own libraries do:

TargetShips asPackager
ReactJS and .d.ts, one module to a file, with each sheet beside its importertsdown
SolidThe same, plus preserved JSX under a solid condition, which a consumer's Solid compiler readstsdown, Babel with babel-preset-solid
VueThe SFC sources and .ts modules, with declarations beside them, which the consumer's Vue compiler buildsvue-tsc
SvelteSvelte components, JS modules and declarations, under types and svelte conditions@sveltejs/package, typescript
AngularThe Angular Package Format, partially compiled, so the consumer's linker finishes itng-packagr
QwikOne index.qwik.mjs, with each sheet inlined, under a qwik condition, which the app's optimiser splitsvite and @qwik.dev/core, typescript
AstroThe emitted .astro sources, which Astro's compiler builds in the consumer's appnone

A few targets need a word more:

  • Angular's entry point also exports the slot directives of the components it exports, which a consumer imports to fill a named slot, like CardHeader for <ng-template ufCardHeader>. @angular/common is an optional peer beside @angular/core.
  • Qwik's build is Vite's library mode, which keeps every QRL in the one file for the app's optimiser to split.
  • Svelte's peer range starts at 5.29.0, the first release with the {@attach} its components use.

Packaged outputs ship without source maps for now. Maps are proven on the bundler plugin's path, so an app that needs them can compile ./source/* instead.

Install the packagers

Unframework doesn't install any packager. Each one, with what it needs, resolves from your project's root, or from package.resolveFrom in your config: one directory, or one per target. Each target's framework types must also resolve from outDir, where the package is built, so that a declaration's inferred types aren't any: @types/react, vue, svelte, solid-js, @angular/core and @qwik.dev/core.

What's missing is UF7008, checked before anything is written, so nothing is cleaned or built:

error[UF7008] (react): The react packager needs tsdown, which doesn't resolve from the project's root. Install it there, point package.resolveFrom at a directory that has it, or leave react out of package.targets.

To package fewer frameworks, list them in package.targets. Every target in targets still gets its tree in <outDir>/<target>.

When it fails

build --package checks your project before any packager runs. A project with no package.json, or one with no name or version, exits 2 and writes nothing. So does a source that reads a file above srcDir, like a sheet, since ./source/* couldn't hold it.

A packager that fails reports UF7009, with the packager's own error in its message. Every packager writes declarations without type-checking, so it's usually a syntax error, or a type a declaration can't name. By then the targets' trees are written, and the last good package stays in place: the new one replaces it only once every packager has passed.

The decision behind this page is ADR-0077.

Copyright © 2026