CLI
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:
| Code | Meaning |
|---|---|
0 | Success, warnings and notes included, but for what strict makes an error |
1 | Error diagnostics: in a source, because no source was found, or from a packager |
2 | A 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
.tsmodules a source reaches, copied unchanged. - A source map for a JavaScript or TypeScript output:
<file>.mapbeside it, and a trailing//# sourceMappingURL=<file>.mapcomment. A.vue,.svelteor.astrofile 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:srcDirholds no source.UF1104: two sources would write one file, or a barrel would export one name from two files.UF7002at/outDir: cleaning a target's tree would delete the project, a source or a file a source reads.UF7002at/srcDir: a source imports a child the search didn't find, or a shared module abovesrcDir, 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.jsonprints an array, one object per diagnostic, with its code, severity, message, file, start and end, and its documentationurl. Agents and scripts read this one.sarifprints 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,srcanddist.src/Button.uf.tsxandsrc/Button.css, an example component and its stylesheet.tsconfig.uf.json, which type-checks your.uf.tsxand.uf.tsfiles 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.
Configuration
unframework.config.ts tells the CLI which frameworks to build, where your components live and where the outputs go. It loads under Node's type stripping, and every mistake in it is a diagnostic that names the field.
Bundler plugin
The bundler plugin lets an app import a .uf.tsx file and get a component of its own framework, compiled in the build, with hot updates and source maps. It's tested on Vite, and Astro has an integration of its own.