export const uidex
The module-scoped authoring surface — one named export per file, plain AST literals only.
One named export per module, on the file that defines the entity. The RHS
must be a plain AST literal — object / array / string / number / boolean
literals, optional as const, optional satisfies Uidex.<Kind>. Identifier
references in value position, calls, spreads, conditionals, and template
literals with expressions are rejected with an error diagnostic.
// app/settings/page.tsx
import type { Uidex } from "@/uidex.gen"
export const uidex = {
page: "org-settings",
acceptance: [
"User can update the organization name",
"Error message is shown if the update fails",
],
features: ["billing"],
} as const satisfies Uidex.Page
export default function SettingsPage() { ... }Kinds
Each kind has a unique discriminator field plus a small set of allowed companion fields.
| Kind | Discriminator | Other fields |
|---|---|---|
page | page: PageId | false | acceptance?, features?, widgets?, description? |
feature | feature: FeatureId | false | acceptance?, description? |
widget | widget: WidgetId | acceptance?, description? |
primitive | primitive: PrimitiveId | false | description? |
flow | flow: FlowId | description? |
| opt-out | notFlow: true | (no other fields — opts a Playwright spec out of flow auto-promotion) |
Setting page: false / feature: false / primitive: false explicitly opts a
module out of auto-promotion.
Page
// app/settings/page.tsx
import type { Uidex } from "@/uidex.gen"
export const uidex = {
page: "org-settings",
acceptance: [
"User can update the organization name",
"Error message is shown if the update fails",
],
features: ["billing"],
widgets: ["avatar-uploader"],
} as const satisfies Uidex.Page
export default function SettingsPage() { ... }Feature
// src/features/billing/index.tsx
import type { Uidex } from "@/uidex.gen"
export const uidex = {
feature: "billing",
acceptance: [
"User can view the current plan",
"User can upgrade the plan",
],
} as const satisfies Uidex.FeatureWidget
The widget id appears in two places: data-uidex-widget="..." on the DOM
root (runtime boundary) and widget: "..." in the export (scan-time metadata).
The scanner cross-validates; mismatch or one-sided presence is an error.
// src/features/video/video-player.tsx
import type { Uidex } from "@/uidex.gen"
export const uidex = {
widget: "video-player",
acceptance: [
"Plays/pauses on Space",
"Scrubber updates as video plays",
"M toggles mute",
],
} as const satisfies Uidex.Widget
export function VideoPlayer() {
return (
<div data-uidex-widget="video-player">
<button data-uidex="play">Play</button>
<input data-uidex="scrubber" type="range" />
</div>
)
}Primitive
Optional — files under src/components/ui/** auto-register without any export.
Add export const uidex = { primitive: ... } only to attach a description or to
opt out of convention registration.
// src/components/ui/button.tsx
import type { Uidex } from "@/uidex.gen"
export const uidex = {
primitive: "button",
description: "The shared pill-shaped button used throughout the product.",
} as const satisfies Uidex.Primitive
export function Button({ children, ...props }) {
return <button {...props}>{children}</button>
}primitive: false opts a file under the primitives glob out of
auto-registration.
Flow / NotFlow
Flows live in Playwright specs — see Playwright flows.
Use notFlow: true at the top of a non-flow spec file to opt out of
auto-promotion:
// e2e/helper.spec.ts — a shared fixture spec that is NOT a flow
import type { Uidex } from "@/uidex.gen"
export const uidex = { notFlow: true } as const satisfies Uidex.NotFlowAST-literal rule
The scanner reads export const uidex by AST walk. The RHS must be a plain
literal. Allowed:
- object literals, nested object literals
- string / number / boolean literals
- array literals of the above
- trailing
as const - trailing
satisfies Uidex.<Kind>
Rejected (scanner emits an error diagnostic, skips the module, keeps scanning other files):
- identifier references in value position (
features: SHARED_IDS) - function / tagged-template calls
- spreads (
...base) - conditionals / ternaries
- template literals with expressions
- non-literal computed property keys
The rule keeps extraction deterministic — one AST walk per file, no import
resolution, no tsconfig coupling. Shared ids are recovered through satisfies Uidex.X referring to the emitted id unions in uidex.gen.ts.
Typed cross-references
uidex.gen.ts exports per-kind id unions (PageId, FeatureId, WidgetId,
PrimitiveId, FlowId, RouteId, RegionId, ElementId) and a Uidex
namespace of shape types. satisfies Uidex.Page makes tsc flag unknown ids at
the literal site — no uidex scan --audit round-trip required:
export const uidex = {
page: "checkout",
features: ["billng"], // tsc error: "billng" is not assignable to FeatureId
} as const satisfies Uidex.PageSet typeMode: "loose" in .uidex.json to emit FeatureId = string etc.
instead of literal unions — see the
migration section in uidex scan.
Acceptance criteria
Acceptance criteria are first-class metadata. They attach to widgets, features, and pages; they drive audit coverage, scaffolded Playwright specs, and the widget detail panel.
Declare criteria in the export
import type { Uidex } from "@/uidex.gen"
export const uidex = {
widget: "video-player",
acceptance: [
"Plays on space",
"Mutes with m",
"Scrubs with arrow keys",
],
} as const satisfies Uidex.WidgetRules:
- One entry per criterion in the
acceptance: string[]array. Source order is preserved. - Text is copied verbatim — no JSDoc whitespace trimming involved.
- Valid on
Uidex.Page,Uidex.Feature, andUidex.Widgetshapes. Absent onUidex.Primitive,Uidex.Flow, andUidex.NotFlow.
Pages and features declare criteria the same way:
export const uidex = {
page: "checkout",
acceptance: [
"User enters payment details",
"Submitting with valid input redirects to /thanks",
],
} as const satisfies Uidex.PageHow the scanner uses them
The scanner extracts criteria into entity.meta.acceptance (preserved as a
string array) and, for every entity carrying acceptance, computes
entity.meta.flows — the list of flow ids whose touches array includes the
entity (or, for widgets, any descendant element).
meta.flows is always an array; an entity with no covering flows gets
meta.flows: [], never undefined.
The end-to-end loop
acceptance criterion (in export const uidex) ─► scan --audit (flagged as uncovered)
│
└──► uidex scaffold widget <id> ─► Playwright spec stub
│
▼
write test body, run `uidex scan`
│
▼
flow touches entity ─► meta.flows grows
│
▼
scan --audit passes
See uidex scan for the full audit and scaffold reference.