uidex scan
CLI reference for uidex scan, --check, --audit, --json, scaffold, configuration, and the JSDoc migration path.
uidex scan is the entry point to the static analyser. It walks your sources,
extracts data-uidex* attributes and export const uidex = {...} declarations,
and emits a typed registry (src/uidex.gen.ts by default) that the runtime SDK
consumes.
The gen file is auto-generated and committed — consumers import types from it directly.
Commands
npx uidex scan # regenerate src/uidex.gen.ts
npx uidex scan --check # fail non-zero if the committed gen file is stale
npx uidex scan --audit # validate (--check + --lint; exits non-zero on errors)
npx uidex scan --json # emit diagnostics as JSON
npx uidex scaffold widget <id> # emit Playwright stub from declared acceptanceRun uidex scan after any data-uidex* attribute change, export const uidex
edit, or new flow.
--check
Compares the generated registry to the committed uidex.gen.ts byte-for-byte.
Exits non-zero on drift. Intended for CI.
--audit
Runs --check plus the lint rules. Exits non-zero on errors. Warnings are
printed but do not fail.
--json
Emits diagnostics as a JSON array on stdout for tooling consumption.
scaffold
npx uidex scaffold widget video-playerEmits a Playwright spec with exactly one test() per declared acceptance
criterion. The describe carries { tag: "@uidex:flow" }. Each test has a
// TODO body for you to fill in.
- Deterministic file path: the scaffold writes to
<flows-glob-root>/flow-<id>.spec.ts. - Idempotent: re-running will refuse to overwrite an existing file unless
--forceis passed. - Without
--force, exits with a clear message naming the existing path.
Audit coverage
uidex scan --audit emits one diagnostic per uncovered acceptance criterion.
The diagnostic identifies the entity, the criterion text, and the source
location, and includes a hint:
warning acceptance/uncovered video-player: "Scrubs with arrow keys"
at src/features/video/video-player.tsx:12
hint: run `uidex scaffold widget video-player` to generate a spec stub
Disable per-section with audit.acceptance: false in .uidex.json.
Bundler plugins
Optional opt-in watch integrations that regenerate uidex.gen.ts on file save:
@uidex/vite-plugin—import { uidex } from "@uidex/vite-plugin"; add toplugins: [uidex()]. Dev server integrates with Vite's HMR error overlay; production builds runscan --checkonce and fail on stale gen.@uidex/webpack-plugin—new UidexPlugin()inwebpack.config.js. HookswatchRun/done.@uidex/next-plugin—withUidex(config)wrapper fornext.config.*. Installs the webpack plugin on the server compiler. Webpack transport only today; Turbopack support is a follow-up because the Turbopack plugin API is not yet stable.
All three wrap the shared @uidex/plugin-core watcher so behaviour, debounce
window (≥100ms coalescing), and diagnostic shapes are identical across
bundlers.
Projects without a plugin fall back to uidex scan on demand.
Configuration
.uidex.json is flat. Example:
{
"sources": [{ "rootDir": "src" }],
"output": "src/uidex.gen.ts",
"flows": ["e2e/**/*.spec.ts"],
"typeMode": "strict"
}typeMode: "strict" (default) emits literal id unions — tsc rejects unknown
ids at the satisfies Uidex.X site. typeMode: "loose" emits string — a
temporary migration knob for repos mid-migration from legacy JSDoc.
Multi-source (monorepo) example:
{
"sources": [
{ "rootDir": "apps/web/src" },
{ "rootDir": "packages/ui/src", "prefix": "@org/ui" }
],
"output": "apps/web/src/uidex.gen.ts",
"flows": ["apps/web/e2e/**/*.spec.ts"]
}Migration from legacy JSDoc
Previous versions of uidex recognised JSDoc tags (@uidex page|feature|widget,
@acceptance, @uidex:not-flow) as the module-scoped annotation surface.
Those tags have been removed. The scanner no longer registers anything from
them; they only trigger a lint warning.
The legacy-jsdoc lint rule
uidex scan --lint (and --audit) emits a warning for every legacy tag it
finds, pointing at the replacement:
warning lint/legacy-jsdoc @uidex widget — migrate to `export const uidex = { widget: "id", acceptance: [...] }`
at src/features/video/video-player.tsx:3
Detected tags:
@uidex page <id>→export const uidex = { page: "<id>", ... } as const satisfies Uidex.Page@uidex feature <id>→export const uidex = { feature: "<id>", ... } as const satisfies Uidex.Feature@uidex widget <id>→export const uidex = { widget: "<id>", ... } as const satisfies Uidex.Widget(keep thedata-uidex-widgetattribute on the DOM root)@acceptance <text>→ one entry in theacceptance: string[]array on the enclosinguidexexport@uidex:not-flow→export const uidex = { notFlow: true } as const satisfies Uidex.NotFlowat the top of the spec file
Step-by-step
- Run
uidex scan --lintto surface every legacy JSDoc location. - For each page / feature / widget / primitive / flow, add an
export const uidex = { ... } as const satisfies Uidex.<Kind>at the top of the module. Move@acceptanceitems into anacceptance: string[]field. Remove the JSDoc block. - Run
uidex scanto regenerateuidex.gen.ts. - Commit the regenerated file.
The typeMode: "loose" migration knob
.uidex.json accepts a typeMode: "strict" | "loose" field (default
"strict").
"strict"—FeatureId = "billing" | "auth"is a literal union.tscrejects unknown ids at thesatisfies Uidex.Xsite. This is the end state."loose"—FeatureId = string. No compile-time id validation, minimal gen-file diff. This is a temporary migration knob.
Why it exists: during a migration, half the repo may have landed on the new
export const uidex surface while the other half still carries legacy JSDoc.
In strict mode, tsc floods with "not assignable to FeatureId" errors on the
migrated half, because the emitted FeatureId union is missing the ids still
expressed in JSDoc (which the scanner no longer registers). Setting
"typeMode": "loose" silences those errors without silencing tsc globally.
Flip back to "strict" and regenerate as soon as every JSDoc tag is gone.
typeMode: "loose" is not promoted for long-term use.