Tooling

CLI

The unframework command builds every target's source tree, checks a project and prints diagnostics for people, agents and CI, explains a diagnostic code and scaffolds a new project.

The unframework command is how a design system turns its components into seven frameworks' code outside an app: it compiles every source to every target you configured, reports what it found in a form that people, agents and CI can each read, and starts new projects. Think of a release that ships last week's Vue output because nobody rebuilt it, a CI job that can't tell a warning from an error, or an agent that guesses what UF3013 means instead of reading it.

The command comes with the unframework package and runs from the directory that holds your unframework.config.ts. It has four commands:

unframework build [--package] [--type-oracle syntactic|tsgo]
unframework check [--format pretty|json|sarif] [--type-oracle syntactic|tsgo]
unframework explain <code>
unframework init [--force]

Exit codes

Every command exits with one of three codes, so a script or a CI step can tell a broken component from a broken setup:

CodeMeaning
0Success, warnings and notes included, but for what strict makes an error
1Error diagnostics: in a source, because no source was found, or from a packager
2A usage error, init's refusal, a config that won't load or is invalid, a project --package can't ship, or a write error

build

build compiles every .uf.tsx and .uf.ts file under srcDir, skipping node_modules and outDir, and writes one tree per target to <outDir>/<target>. Each output sits in the directory its source has under srcDir. With the defaults, a project with one src/Button.uf.tsx and its stylesheet gets this for React and Vue, and a tree in each other target's own file names:

dist/react/Button.tsx
dist/react/Button.tsx.map
dist/react/Button.uf.tsx.css
dist/react/index.ts
dist/vue/Button.uf.tsx.css
dist/vue/Button.vue
dist/vue/index.ts

Each tree holds:

  • The outputs. Each file equals what the compiler gives, byte for byte, but for the comment below.
  • The shared .ts modules a source reaches, copied unchanged.
  • A source map for a JavaScript or TypeScript output: <file>.map beside it, and a trailing //# sourceMappingURL=<file>.map comment. A .vue, .svelte or .astro file and a scoped sheet get neither, since the comment is written for JavaScript and TypeScript.
  • A barrel, index.ts, which exports each exported component and composable by its name. A default export takes its component's name: export { default as Button } from "./Button";.

build cleans each <outDir>/<target> before it writes it, so change the source and build again rather than editing an output. A build with an error diagnostic from a compile, or with a config it refuses, cleans and writes nothing, so the last good build stays in place. A packager's failure is the exception: it comes after the trees are written (Packaging). A successful build prints only its warnings and notes, on standard error.

build and check also report what no single component's compile can see:

  • UF7007: srcDir holds no source.
  • UF1104: two sources would write one file, or a barrel would export one name from two files.
  • UF7002 at /outDir: cleaning a target's tree would delete the project, a source or a file a source reads.
  • UF7002 at /srcDir: a source imports a child the search didn't find, or a shared module above srcDir, which no tree could hold beside the source.

build --package also writes a package you can publish, with every target in its framework's own format. Packaging covers it.

check

check runs everything a plain build runs and writes no file, so it fails wherever such a build would. It doesn't run what --package checks, like a package.json with no name or a missing packager (UF7008), so a project check passes can still fail build --package. It prints the diagnostics on standard output, in the format you ask for:

  • pretty, the default, prints a code frame for each, and nothing when there are none.
  • json prints an array, one object per diagnostic, with its code, severity, message, file, start and end, and its documentation url. Agents and scripts read this one.
  • sarif prints a SARIF 2.1.0 log, which code-scanning tools in CI read.

A component that writes React's className gets this from unframework check, and an exit code of 1:

error[UF3004]: `className` is written `class`: elements take HTML's attribute names, in lower case.

A usage error isn't a diagnostic, so it goes to standard error as plain text, whatever the format.

explain

explain prints what a diagnostic code means and how to fix it, with the link to its section of Diagnostics:

unframework explain UF7005
UF7005 target-not-detected (error)
The bundler plugin could not detect a target

The bundler plugin has no `target` option, and none of the plugins in the Vite configuration
belongs to a framework it knows.

Add the framework's Vite plugin to the configuration, or pass `target` to the Unframework
plugin.

https://unframework.dev/diagnostics/UF7005

An unknown code exits 2.

init

init starts a project in the directory it runs in, and names each file it writes:

  • unframework.config.ts, which names all seven targets, src and dist.
  • src/Button.uf.tsx and src/Button.css, an example component and its stylesheet.
  • tsconfig.uf.json, which type-checks your .uf.tsx and .uf.ts files against Unframework's JSX types: tsc --noEmit -p tsconfig.uf.json.
  • AGENTS.md, which points agents at these docs and names the commands that check and build.

check passes on what init writes, with the info UF4002 for Angular and Qwik, which every component with a stylesheet gets.

init adds no dependency, so add typescript to the project to run the type check. It doesn't edit your own tsconfig.json either. If you have one, add **/*.uf.tsx and **/*.uf.ts to its exclude, so your framework's JSX types don't check these files (Type-checking in an app says why).

init never overwrites a file without --force. It exits 2, names every file in the way, says what --force would do with each and writes nothing:

unframework init: In the way: AGENTS.md, src/Button.css, src/Button.uf.tsx, tsconfig.uf.json, unframework.config.ts. --force overwrites unframework.config.ts, src/Button.uf.tsx, src/Button.css, tsconfig.uf.json, AGENTS.md.

With --force, it overwrites those five files and leaves every other file alone. A unframework.config.mts, .js or .mjs also counts as in the way, because the new .ts file would quietly take its place: with --force it stays on disk, and init tells you that the .ts file takes precedence.

init writes no test file yet, since the testing package stays private until a later release.

create-unframework

create-unframework starts a project in a new directory:

create-unframework my-components

It creates the directory if it has to and writes a private package.json that depends on unframework. Then it runs that project's own unframework init. It installs nothing itself, so when unframework isn't in the directory's node_modules yet, it stops after the package.json, exits 1 and tells you to install the dependencies and run unframework init there. It never overwrites an existing package.json.

Installation says how to install the packages before the first release.

The decision behind this page is ADR-0076.

Copyright © 2026