Tooling

The CLI, its config, multiple entries, source maps and the editor — every terminal below is real output.

Each panel is the real stdout, stderr and exit status of the uxdsl CLI in this repository, run against a small project by scripts/capture-capabilities.js; npm test runs it again and fails when the CLI prints something else. Paths are shown as <project>.

Commands

Bare uxdsl asks, it does not build: the general help exit 0
$ uxdsl 
Usage: uxdsl <command> [options]

Commands:
  init              First-run setup: uxdsl.config.cjs, src/styles.uxdsl (the file you
                    edit), the generated src/uxdsl-entry.uxdsl that imports it and,
                    in a Next.js project, postcss.config.js. Never overwrites a file.
  generate-entry    Rewrite the entry file's @import list from the .uxdsl files
                    found under a directory. Run it after adding or removing files.
  build             Compile the entry (or every entry in "builds") to CSS, once;
                    --watch keeps rebuilding as files change.
  theme             Print the effective theme as JSON: the same discovery and
                    resolution as build, no CSS written.

Global options:
  --help, -h        This help. "uxdsl <command> --help" describes one command.
  --version, -v     Print the installed uxdsl version.

Set UXDSL_DEBUG=1 to log which config and theme files were discovered.
An unrecognized flag, or a flag used on the wrong command, fails with a
suggestion instead of being ignored.

uxdsl init --multi scaffolds a theme entry plus one component entry instead of one entry. uxdsl generate-entry --src <dir> --out <file> --exclude <a,b> rewrites the entry's @import list from the .uxdsl files under a directory. uxdsl <command> --help prints one command's options, uxdsl --version the installed version. An unknown command or flag fails before anything runs, with a suggestion.

Two files, two jobs

The build config, uxdsl.config.cjs (or .js), says what to compile and where. The theme file, uxdsl.theme.json (or uxdsl.theme.js/.cjs exporting the object), holds the theme and nothing else; it is discovered next to the config, or in the working directory, by the CLI and by the PostCSS plugin, the Vite plugin and the Webpack loader alike. A config that carries theme or breakpoints is refused, naming where each belongs.

Config keyWhat it does
entry, outFileOne entry and its output.
buildsSeveral entries, [{ entry, outFile, includeTheme? }], compiled from one theme.
includeThemeEmit the theme's :root definitions (default true); per entry in builds.
watchExtra paths for --watch, beyond the entries, their imports and the config and theme files.
referencesTokens defined outside the theme (externalTokens, css) and how strictly a missing one fails.
strictThemeFamilies the project must declare completely, such as ['palette'].
sourcemapfalse, 'inline' or 'external'.

The project every command below runs in — a config made with defineConfig from uxdsl/config, a theme file whose $schema points at the packaged schema, three sources:

uxdsl.config.cjs
// The build config of the small project whose real CLI output the playground's /docs/tooling
// page shows (captured by scripts/capture-capabilities.js). `defineConfig` is the typed
// helper from `uxdsl/config`. The theme is not named here: `uxdsl build` discovers
// `uxdsl.theme.json` next to this file.
const path = require('path');
const { defineConfig } = require('uxdsl/config');

module.exports = defineConfig({
  entry: path.join(__dirname, 'src/main.uxdsl'),
  outFile: path.join(__dirname, 'dist/app.css'),
});
uxdsl.theme.json
{
  "$schema": "../../node_modules/uxdsl/schema/theme.schema.json",
  "breakpoints": { "xs": 0, "sm": 480, "md": 768, "lg": 1024, "xl": 1280 },
  "palette": {
    "primary": { "main": "#0f766e" }
  }
}
src/card.uxdsl
.card {
  @ds-surface(contained);
  display: flex;
  flex-direction: xs(column) md(row);
  gap: density(4);
}
src/button.uxdsl
.save {
  @ds-button(contained primary 2);
}
src/legacy.uxdsl
/* Left out of the entry with --exclude: an old stylesheet nothing imports any more. */
.old-banner {
  color: palette(primary.main);
}

Build: discovery, --config, --entry

Build with the discovered config (uxdsl.config.cjs + uxdsl.theme.json) exit 0
$ uxdsl build
[uxdsl] Theme config detected
[uxdsl] built dist/app.css (49478 bytes)
bytes
49478
rootBlocks
16
mediaQueries
9
The same build with the config named explicitly exit 0
$ uxdsl build --config uxdsl.config.cjs
[uxdsl] Theme config detected
[uxdsl] unchanged dist/app.css (compiled output identical to the file on disk; not rewritten)
Compile one file to one output, bypassing the config's entry exit 0
$ uxdsl build --entry src/card.uxdsl --out dist/card.css
[uxdsl] Theme config detected
[uxdsl] built dist/card.css (47671 bytes)
bytes
47671
rootBlocks
16
Per-command help: only that command's options exit 0
$ uxdsl build --help
Usage: uxdsl build [options]

Compiles the entry from uxdsl.config.cjs (or --config, or --entry/--out) to
one CSS file. The theme comes from the theme file next to the config
(uxdsl.theme.json, or uxdsl.theme.js/.cjs exporting a computed theme).

Options:
  --config, -c      Path to the build config (default: uxdsl.config.cjs or
                    uxdsl.config.js in the current directory).
  --entry, -e       Entry .uxdsl file; with --out, compiles it without a config.
  --out, -o         Output CSS file path (with --entry).
  --watch, -w       Rebuild on file changes. Survives a broken edit: the last
                    good CSS stays on disk until the next valid save.
  --include-theme, --no-include-theme
                    Emit (or skip) the global :root token definitions.
                    Default true. Pass --no-include-theme for a component or
                    CSS-Module entry that consumes tokens another entry
                    defines. Overrides "includeTheme" in the config.
  --sourcemap, --sourcemap=inline, --no-sourcemap
                    Emit a source map. Bare --sourcemap means "external":
                    writes <outFile>.map and a sourceMappingURL comment;
                    =inline embeds it as a data URI. Default off. Overrides
                    "sourcemap" in the config.
  --strict-theme=<family,...>, --no-strict-theme
                    Fail the build, before writing anything, when one of the
                    named theme families is only partly declared by the
                    project and the rest was filled from the base theme
                    (e.g. --strict-theme=palette,breakpoints). A scope is
                    required: the theme model is partial overrides, so an
                    unscoped check flagged the recommended usage as
                    incomplete. --no-strict-theme turns off "strictTheme"
                    from the config for one run.

Multiple entries and CSS Modules

The first entry defines every token in :root, once (--include-theme, the default). Every other entry — a component sheet, a CSS Module — compiles with --no-include-theme (or includeTheme: false in builds, or in the PostCSS plugin's options) and only consumes those tokens: no :root, so a CSS Modules loader accepts it, and nothing is repeated. This site does exactly that: every component's .uxdsl is a CSS Module compiled with includeTheme: false.

With --include-theme (the default): the entry also defines every token in :root exit 0
$ uxdsl build --entry src/card.uxdsl --out dist/card-with-theme.css --include-theme
[uxdsl] Theme config detected
[uxdsl] built dist/card-with-theme.css (47671 bytes)
bytes
47671
rootBlocks
16
With --no-include-theme: only the component rules, consuming tokens another entry defines exit 0
$ uxdsl build --entry src/card.uxdsl --out dist/card-only.css --no-include-theme
[uxdsl] built dist/card-only.css (464 bytes)

It wrote dist/card-only.css:

.card {
  padding: var(--uxdsl__surface__contained-padding);
  border-radius: var(--uxdsl__surface__contained-radius);
  background: var(--uxdsl__surface__contained-bg);
  color: var(--uxdsl__surface__contained-color);
  border: var(--uxdsl__surface__contained-border);
  box-shadow: var(--uxdsl__surface__contained-shadow);
  display: flex;
  flex-direction: column;
  gap: var(--uxdsl__density__4);
}@media (min-width: 768px) {.card {
  flex-direction: row;
}
}
bytes
464
rootBlocks
0

Source maps

--sourcemap writes <outFile>.map and a sourceMappingURL comment, so devtools point at the .uxdsl line that produced a rule; --sourcemap=inline embeds the map. The Vite plugin and the Webpack loader pass their maps on to the next loader.

With --sourcemap: a .map next to the CSS, pointing devtools at the .uxdsl source exit 0
$ uxdsl build --entry src/card.uxdsl --out dist/card-mapped.css --no-include-theme --sourcemap
[uxdsl] built dist/card-mapped.css (508 bytes) + card-mapped.css.map (408 bytes)

It wrote dist/card-mapped.css:

.card {
  padding: var(--uxdsl__surface__contained-padding);
  border-radius: var(--uxdsl__surface__contained-radius);
  background: var(--uxdsl__surface__contained-bg);
  color: var(--uxdsl__surface__contained-color);
  border: var(--uxdsl__surface__contained-border);
  box-shadow: var(--uxdsl__surface__contained-shadow);
  display: flex;
  flex-direction: column;
  gap: var(--uxdsl__density__4);
}@media (min-width: 768px) {.card {
  flex-direction: row;
}
}

/*# sourceMappingURL=card-mapped.css.map */
mapSources
["../src/card.uxdsl","<no source>"]
mapFile
card-mapped.css
sourceMappingURL
card-mapped.css.map

Watch

uxdsl build --watch rebuilds when a source, an import, the config or the theme file changes. A broken edit is reported and the last good CSS stays on disk; the next valid save recovers. A session recorded by the capture script as it edited a file:

  1. start: uxdsl build --watch

    [uxdsl] Theme config detected
    [uxdsl] built dist/app.css (49478 bytes)
    [uxdsl] watching for changes...
    
  2. edit src/card.uxdsl: add border-radius: radius(3);

    [uxdsl] change src/card.uxdsl
    [uxdsl] Theme config detected
    [uxdsl] built dist/app.css (49520 bytes)
    
  3. edit src/card.uxdsl: add xxl(2rem), a breakpoint that is not configured

    [uxdsl] change src/card.uxdsl
    [uxdsl] Theme config detected
    [uxdsl] build failed: uxdsl: src/card.uxdsl:5:3: UXD_BREAKPOINT_UNKNOWN: xxl(...) is not a configured breakpoint or a known CSS function; configured breakpoints: xs, sm, md, lg, xl.
    
  4. undo the edit

    [uxdsl] change src/card.uxdsl
    [uxdsl] Theme config detected
    [uxdsl] built dist/app.css (49478 bytes)
    

The theme command: --diff, --contrast, --strict-theme

uxdsl theme prints the effective theme (no CSS written; it works from a theme file alone). --diff prints only what the theme file mentions, each leaf labeled project or default. --contrast runs the WCAG gate of Accessibility. --strict-theme=<families> fails when a named family was partly inherited — on build too; a scope is required.

Only what this project mentions, each leaf labeled project or default exit 0
$ uxdsl theme --diff
[uxdsl] palette.primary mixes your values (main) with base values (light, dark, contrast)

stdout is a JSON document; an excerpt:

[
  {
    "path": "palette.primary.main",
    "value": "#0f766e",
    "source": "project"
  },
  {
    "path": "palette.primary.light",
    "value": "color(purple.500)",
    "source": "default"
  },
  {
    "path": "palette.primary.dark",
    "value": "color(purple.900)",
    "source": "default"
  },
  {
    "path": "palette.primary.contrast",
    "value": "color(white)",
    "source": "default"
  }
]
Check every text and border pair of the effective theme against WCAG exit 0
$ uxdsl theme --contrast
[uxdsl] --contrast: no blocking failure. 111 more pairs fail and are covered by an exception (listed under "excepted", not passing).

stdout is a JSON document of 377744 bytes; an excerpt:

[]
With --strict-theme=palette: fail when a family you named was partly filled from the base exit 1
$ uxdsl build --strict-theme=palette
[uxdsl] Error: --strict-theme (scoped to: palette): the following theme families you declared are partially filled from defaults: palette. Provide every key of these families explicitly, or drop --strict-theme/strictTheme if inheriting some of them is intentional.
--strict-theme=breakpoints: a family this project declares completely passes exit 0
$ uxdsl build --strict-theme=breakpoints
[uxdsl] Theme config detected
[uxdsl] unchanged dist/app.css (compiled output identical to the file on disk; not rewritten)
The bare flag is refused: a scope is required exit 1
$ uxdsl build --strict-theme
[uxdsl] Error: --strict-theme needs a scope: name the families that must be completely declared, e.g. --strict-theme=palette,breakpoints. An unscoped check treats every partial override — the theme model itself — as incomplete, so it is not accepted.
Write the entry from what is in src/, leaving one file out exit 0
$ uxdsl generate-entry --src src --out src/main.uxdsl --exclude legacy.uxdsl
[uxdsl] Scanning src for .uxdsl files...
[uxdsl] Generated entry file at src/main.uxdsl with 2 imports.

It wrote src/main.uxdsl:

/* AUTO-GENERATED FILE - DO NOT EDIT MANUALLY */
/* Generated by uxdsl generate-entry */

/* --- Application & Component Imports --- */
@import './button.uxdsl';
@import './card.uxdsl';
An unknown command is one line with a hint exit 1
$ uxdsl watch
[uxdsl] Unknown command "watch": use "uxdsl build --watch"

Editor support

  • VS Code: the uxdsl-vscode extension highlights .uxdsl files and completes the token functions, breakpoints, directives, @uxdsl theme;, and the roles, tones and sizes of the base theme (not yet a project's own roles). It does not compile or validate; the build does.
  • The theme file: "$schema": "./node_modules/uxdsl/schema/theme.schema.json" gives any JSON editor validation and completion of every family.
  • The config: init writes // @ts-check and a JSDoc type on the config object, so the editor flags a mistyped key; nothing is loaded at build time for it.

uxdsl/engine, for tools

The per-family engines the compiler is built from, for editors, inspectors and generators. It is exempt from semantic versioning — do not build an application on it. Two of them, run on the active theme:

buttonDeclarations · inputDeclarations · resolveTypographyRole

What @ds-button(contained) emits for the active theme:

base
paddingvar(--uxdsl__surface__contained-padding)
border-radiusvar(--uxdsl__surface__contained-radius)
backgroundvar(--uxdsl__surface__contained-bg)
colorvar(--uxdsl__surface__contained-color)
bordervar(--uxdsl__surface__contained-border)
box-shadowvar(--uxdsl__surface__contained-shadow)
state: hover
backgroundvar(--uxdsl__button__contained-hover-bg)
colorvar(--uxdsl__button__contained-hover-color)
state: focusvisible
outlinevar(--uxdsl__button__contained-focusvisible-outline)
outline-offsetvar(--uxdsl__button__contained-focusvisible-outline-offset)
state: selected
backgroundvar(--uxdsl__button__contained-selected-bg)
colorvar(--uxdsl__button__contained-selected-color)
state: disabled
opacityvar(--uxdsl__button__contained-disabled-opacity)
cursorvar(--uxdsl__button__contained-disabled-cursor)

inspectReferences

Loading PostCSS…