Initial Commit
This commit is contained in:
346
openspec/specs/react-ui-i18n/spec.md
Normal file
346
openspec/specs/react-ui-i18n/spec.md
Normal file
@@ -0,0 +1,346 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec: react-ui-i18n
|
||||
|
||||
**Package**: `@suaveplan/react-ui` (the i18n surface)
|
||||
**Status**: SHIPPED 2026-05-10 (`react-i18n-plugin-registry-and-ui-primitives` archived)
|
||||
|
||||
This capability describes the i18n surface that `@suaveplan/react-ui` exposes to consumers — formatted-value primitives, locale-aware behavior wiring on existing components, third-party library bridges, and the validation message bridge. It consumes `@suaveplan/react-i18n` (registry, hooks, contexts) and is the load-bearing surface for the React stack's i18n integration.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: ReactUiI18nBridge mounts third-party locale + direction context
|
||||
|
||||
The package SHALL expose `<ReactUiI18nBridge children syncDocumentAttributes? toasterDir?/>`. On mount, the bridge SHALL:
|
||||
|
||||
1. Read `useLocale()` and `useDirection()` from `@suaveplan/react-i18n`.
|
||||
2. Mount `@radix-ui/react-direction`'s `<DirectionProvider dir={direction}>` so every Radix-derived primitive descendant in the tree (popover, dropdown-menu, dialog, tooltip, select, menubar, context-menu, hover-card, scroll-area, tabs, toggle-group, …) inherits the active direction without per-component wiring.
|
||||
3. Expose the active locale through a stable `LocaleContext` consumed by `<DataTable>` defaults and `<Calendar>`.
|
||||
4. When `syncDocumentAttributes` is `true` (default), update `document.documentElement.lang` and `document.documentElement.dir` whenever the active locale or direction changes. When `false`, document attributes SHALL NOT be touched (SSR / nested-app scenarios).
|
||||
5. When `toasterDir` is `true` (default), forward direction to any `<Toaster>` from `sonner` mounted as a descendant.
|
||||
|
||||
The bridge SHALL be safe to mount inside `<ReactUiProvider>` and SHALL be a no-op (zero DOM, zero re-renders) when no `<TranslationProvider>` is mounted in the same tree.
|
||||
|
||||
**Verified by:** `src/i18n/react-ui-i18n-bridge/react-ui-i18n-bridge.test.tsx`.
|
||||
|
||||
#### Scenario: Radix popover inherits direction from bridge
|
||||
|
||||
- GIVEN locale `ar-AE` (RTL) and a tree `<TranslationProvider><ReactUiI18nBridge><Popover/></ReactUiI18nBridge></TranslationProvider>`
|
||||
- WHEN the popover opens
|
||||
- THEN the rendered Radix root carries `dir="rtl"` (verified by `data-radix-direction` attribute or equivalent)
|
||||
|
||||
#### Scenario: document attributes update on locale change
|
||||
|
||||
- GIVEN `<ReactUiI18nBridge syncDocumentAttributes/>` and an active locale switch from `en-GB` to `ar-AE`
|
||||
- WHEN the locale change settles
|
||||
- THEN `document.documentElement.lang` is `"ar-AE"` and `document.documentElement.dir` is `"rtl"`
|
||||
|
||||
#### Scenario: syncDocumentAttributes={false} leaves DOM untouched
|
||||
|
||||
- GIVEN `<ReactUiI18nBridge syncDocumentAttributes={false}/>` and an initial `<html lang="en">`
|
||||
- WHEN locale changes to `ar-AE`
|
||||
- THEN `document.documentElement.lang` and `dir` are unchanged
|
||||
|
||||
### Requirement: TanStack Table locale-aware default sort and filter
|
||||
|
||||
The package SHALL expose `localeAwareSortingFn(locale: string): SortingFn<TData>` and `localeAwareFilterFn(locale: string): FilterFn<TData>` from `react-ui/i18n/tanstack-bridge`. Both SHALL be backed by `Intl.Collator(locale)` so sort and case-insensitive filter respect the active locale's collation rules. The `<DataTable>` `formatter:` shortcut SHALL auto-install these as the column's defaults; consumers SHALL be able to opt in globally via the bridge.
|
||||
|
||||
**Verified by:** `src/i18n/tanstack-bridge/tanstack-bridge.test.tsx`.
|
||||
|
||||
#### Scenario: Swedish collation orders ä after z
|
||||
|
||||
- GIVEN locale `sv-SE` and column data `["a", "z", "ä"]`
|
||||
- WHEN `localeAwareSortingFn("sv-SE")` is applied
|
||||
- THEN the resulting order is `["a", "z", "ä"]` (Swedish collation), not `["a", "ä", "z"]` (default)
|
||||
|
||||
### Requirement: shadcn registry includes new primitives and survives ejection
|
||||
|
||||
Every new primitive (`FormattedNumber`, `FormattedCurrency`, `FormattedDate`, `FormattedList`, `FormattedRelativeTime`, `Plural`, `FormattedMessage`, `InputNumber`, `ReactUiI18nBridge`) SHALL ship with a `registry/r/<name>.json` entry that:
|
||||
|
||||
- Declares `@suaveplan/react-i18n` in `registryDependencies` so the shadcn CLI installs the binding when ejected.
|
||||
- Lists every source file the component depends on under `files`.
|
||||
|
||||
Existing modified components (Drawer, Sheet, Calendar, DataTable, Pagination) SHALL have their registry entries regenerated to capture the new imports.
|
||||
|
||||
The package SHALL expose `scripts/check-registry-eject.ts` invoked via `bun run check:registry-eject` that, for every registry entry:
|
||||
|
||||
1. Resolves the entry's `files` to source.
|
||||
2. Applies the registry transform (rewriting `@suaveplan/react-ui` imports to local relative paths, preserving `@suaveplan/react-i18n` imports).
|
||||
3. Type-checks the transformed source against a synthesized consumer `tsconfig.json` and the expected `@suaveplan/react-i18n` import shape.
|
||||
|
||||
The script SHALL exit non-zero on any registry entry whose transformed source fails to type-check. The script SHALL be wired into the package's pre-publish gate.
|
||||
|
||||
**Verified by:** `scripts/check-registry-eject.test.ts`.
|
||||
|
||||
#### Scenario: ejected FormattedNumber compiles against react-i18n
|
||||
|
||||
- GIVEN the registry transform applied to `formatted-number.json`'s files
|
||||
- WHEN the transformed source is type-checked
|
||||
- THEN `tsc --noEmit` exits 0 and the import `import { useNumber } from "@suaveplan/react-i18n"` resolves
|
||||
|
||||
#### Scenario: missing registry entry for a public primitive fails the gate
|
||||
|
||||
- GIVEN a public primitive added to `src/i18n/` without a corresponding `registry/r/<name>.json`
|
||||
- WHEN `bun run check:registry-eject` runs
|
||||
- THEN the script exits non-zero with a `MISSING REGISTRY ENTRY` diagnostic
|
||||
|
||||
### Requirement: FormattedNumber primitive
|
||||
|
||||
The package SHALL expose `<FormattedNumber value={n} options? as?/>` rendering the number through the active locale's number formatter (via `useNumber` from `@suaveplan/react-i18n`). The `as` prop SHALL accept any HTML element name or React component, defaulting to `"span"`. The component SHALL forward `className`, `style`, and any other accepted props on `as` through to the underlying element. When the active locale's `numberingSystem` is non-`latn` (e.g. `arab`, `deva`), the rendered digits SHALL match the locale's numeric script.
|
||||
|
||||
**Verified by:** `src/i18n/formatted-number/formatted-number.test.tsx`.
|
||||
|
||||
#### Scenario: en-GB locale renders Latin numerals with grouping
|
||||
|
||||
- GIVEN locale `en-GB`
|
||||
- WHEN `<FormattedNumber value={1234567}/>` renders
|
||||
- THEN the rendered text is `"1,234,567"`
|
||||
|
||||
#### Scenario: ar-AE locale renders Arabic-Indic numerals
|
||||
|
||||
- GIVEN locale `ar-AE`
|
||||
- WHEN `<FormattedNumber value={1234}/>` renders
|
||||
- THEN the rendered text uses Arabic-Indic digits matching the locale's numbering system
|
||||
|
||||
#### Scenario: as prop swaps the rendered element
|
||||
|
||||
- GIVEN `<FormattedNumber value={42} as="strong"/>`
|
||||
- WHEN the component renders
|
||||
- THEN the resulting DOM contains a `<strong>` element with text `"42"`
|
||||
|
||||
### Requirement: FormattedCurrency primitive
|
||||
|
||||
The package SHALL expose `<FormattedCurrency amount currency options? as?/>` rendering the amount through the active locale's currency formatter (via `useCurrency`). `currency` is an ISO 4217 code; `options` mirrors the underlying `useCurrency` options shape exactly.
|
||||
|
||||
**Verified by:** `src/i18n/formatted-currency/formatted-currency.test.tsx`.
|
||||
|
||||
#### Scenario: USD in en-GB
|
||||
|
||||
- GIVEN locale `en-GB`
|
||||
- WHEN `<FormattedCurrency amount={9.5} currency="USD"/>` renders
|
||||
- THEN the rendered text is `"US$9.50"` (matching `useCurrency` output)
|
||||
|
||||
### Requirement: FormattedDate primitive
|
||||
|
||||
The package SHALL expose `<FormattedDate value options? as?/>` accepting `Date | string | number` and rendering through `useDate`. The component SHALL not throw for invalid input; it SHALL render an empty string and emit a single `console.warn` (gated on dev) with the offending value.
|
||||
|
||||
**Verified by:** `src/i18n/formatted-date/formatted-date.test.tsx`.
|
||||
|
||||
#### Scenario: invalid value renders empty string
|
||||
|
||||
- GIVEN `<FormattedDate value={"not-a-date"}/>`
|
||||
- WHEN the component renders
|
||||
- THEN the rendered text is `""` and no exception is thrown
|
||||
|
||||
### Requirement: FormattedList primitive
|
||||
|
||||
The package SHALL expose `<FormattedList items options? as?/>` rendering an array of strings through `useList` with the active locale's list formatter (Intl.ListFormat semantics). Empty arrays SHALL render an empty string.
|
||||
|
||||
**Verified by:** `src/i18n/formatted-list/formatted-list.test.tsx`.
|
||||
|
||||
#### Scenario: three-item conjunction in en-GB
|
||||
|
||||
- GIVEN locale `en-GB` and `<FormattedList items={["a","b","c"]}/>`
|
||||
- WHEN the component renders
|
||||
- THEN the rendered text is `"a, b, and c"`
|
||||
|
||||
### Requirement: FormattedRelativeTime primitive with auto-refresh
|
||||
|
||||
The package SHALL expose `<FormattedRelativeTime value unit? options? auto? refreshIntervalMs?/>` rendering a relative time through `useRelativeTime`. When `auto` is `true`, the component SHALL re-render on `refreshIntervalMs` (default `60_000`) so the displayed string ages with the wall clock. The interval timer SHALL be cleared on unmount. Server-rendered output SHALL be deterministic — `auto` mode SHALL render its initial value on the first paint (no hydration mismatch from a clock tick during render).
|
||||
|
||||
**Verified by:** `src/i18n/formatted-relative-time/formatted-relative-time.test.tsx` using `fakeClock` from `@suaveplan/testing/runner`.
|
||||
|
||||
#### Scenario: auto-refresh re-renders after interval
|
||||
|
||||
- GIVEN locale `en-GB` and `<FormattedRelativeTime value={Date.now() - 30_000} auto/>` mounted
|
||||
- WHEN 60 seconds advance on `fakeClock`
|
||||
- THEN the rendered text updates from `"30 seconds ago"` to `"1 minute ago"` without a manual re-render
|
||||
|
||||
### Requirement: Plural primitive selects child by CLDR rule
|
||||
|
||||
The package SHALL expose `<Plural value zero? one few? many? two? other/>` selecting one child based on the active locale's CLDR plural rule for `value`. The `other` slot SHALL be required (TypeScript-enforced). Selection SHALL match `usePlural` output for the same `(locale, value)`.
|
||||
|
||||
**Verified by:** `src/i18n/plural/plural.test.tsx`.
|
||||
|
||||
#### Scenario: ar-AE selects different slot than en-GB for value 0
|
||||
|
||||
- GIVEN locale `ar-AE` and `<Plural value={0} zero="ZERO" one="ONE" few="FEW" many="MANY" other="OTHER"/>`
|
||||
- WHEN the component renders
|
||||
- THEN the rendered text is `"ZERO"`
|
||||
- AND the same component under locale `en-GB` renders `"OTHER"` (English groups zero into other)
|
||||
|
||||
### Requirement: FormattedMessage re-export of Trans
|
||||
|
||||
The package SHALL re-export `<FormattedMessage>` as a named alias of `<Trans>` from `@suaveplan/react-i18n` so consumers do not need a second import to render translated strings alongside `<FormattedNumber/>` etc. The branded `TranslationKey<KeyPath>` type contract SHALL carry through.
|
||||
|
||||
**Verified by:** `src/i18n/formatted-message/formatted-message.test.tsx`.
|
||||
|
||||
### Requirement: Drawer and Sheet inline-start / inline-end directions
|
||||
|
||||
`<Drawer>` and `<Sheet>` SHALL accept `inline-start` and `inline-end` for their `direction` / `side` props respectively. These values SHALL resolve to physical directions (`left` / `right`) at render time based on `useDirection()` from `@suaveplan/react-i18n`. The existing physical direction values (`left`, `right`, `top`, `bottom`) SHALL continue to work unchanged.
|
||||
|
||||
**Verified by:** `src/components/drawer/drawer.test.tsx`, `src/components/sheet/sheet.test.tsx`.
|
||||
|
||||
#### Scenario: inline-start in LTR locale resolves to left
|
||||
|
||||
- GIVEN locale `en-GB` (LTR) and `<Drawer direction="inline-start"/>`
|
||||
- WHEN the drawer opens
|
||||
- THEN the underlying `vaul` `direction` prop is `"left"`
|
||||
|
||||
#### Scenario: inline-start in RTL locale resolves to right
|
||||
|
||||
- GIVEN locale `ar-AE` (RTL) and `<Drawer direction="inline-start"/>`
|
||||
- WHEN the drawer opens
|
||||
- THEN the underlying `vaul` `direction` prop is `"right"`
|
||||
|
||||
### Requirement: DataTable column formatter shortcut
|
||||
|
||||
`ColumnDef<TData>` SHALL accept an optional `formatter?: { kind: "number" | "currency" | "date" | "relative-time"; options?: ... }`. When set, the column's cell renderer SHALL wrap the raw value in the matching `<FormattedX/>` primitive. An explicit `cell:` override SHALL take precedence over `formatter:` on the same column.
|
||||
|
||||
**Verified by:** `src/components/data-table/data-table.test.tsx`.
|
||||
|
||||
#### Scenario: number formatter shortcut renders FormattedNumber
|
||||
|
||||
- GIVEN a `ColumnDef` `{ accessorKey: "qty", formatter: { kind: "number" } }`
|
||||
- WHEN the column's cell renders for a row with `qty: 12345`
|
||||
- THEN the cell DOM contains the same text `<FormattedNumber value={12345}/>` would produce
|
||||
|
||||
#### Scenario: explicit cell beats formatter shortcut
|
||||
|
||||
- GIVEN a `ColumnDef` with both `cell: () => "X"` and `formatter: { kind: "number" }`
|
||||
- WHEN the column's cell renders
|
||||
- THEN the rendered text is `"X"`
|
||||
|
||||
### Requirement: Calendar honors active locale for weekday/month names
|
||||
|
||||
`<Calendar>` SHALL pass the active locale (resolved through `useLocale()` and a `localeStringToDateFnsLocale(locale)` helper) into `react-day-picker`'s `locale` prop. Weekday headers and month names SHALL render in the active locale. The `localeStringToDateFnsLocale` helper SHALL include explicit mappings for the five bundled locales (`en-GB`, `zh-CN`, `ar-AE`, `cy-GB`, `ru-RU`) and SHALL fall back to the default `enUS` locale with a single `console.warn` for unmapped locales.
|
||||
|
||||
**Verified by:** `src/components/calendar/calendar.test.tsx`.
|
||||
|
||||
#### Scenario: zh-CN renders Chinese month name
|
||||
|
||||
- GIVEN locale `zh-CN`
|
||||
- WHEN `<Calendar/>` is mounted on May 9 2026
|
||||
- THEN the month header contains `"5月"` (or the locale-appropriate Chinese rendering)
|
||||
|
||||
### Requirement: InputNumber locale-aware decimal/grouping
|
||||
|
||||
The package SHALL expose `<InputNumber value? defaultValue? onValueChange? locale? />` (locale defaults to `useLocale()`). When unfocused, the control SHALL render the value through `<FormattedNumber/>`. On focus, it SHALL switch to a raw editable buffer keyed off the locale's decimal separator. Parsing SHALL accept the locale's grouping and decimal separators (introspected via `Intl.NumberFormat(locale).formatToParts(12345.6)`). Empty input SHALL emit `null` through `onValueChange`. The plain `<Input>` component SHALL be untouched; `<InputNumber>` is a sibling.
|
||||
|
||||
**Verified by:** `src/components/input/input-number/input-number.test.tsx`.
|
||||
|
||||
#### Scenario: ar-AE accepts Arabic decimal separator
|
||||
|
||||
- GIVEN locale `ar-AE` and an `<InputNumber/>` mounted
|
||||
- WHEN the user types `"3٫14"` (using Arabic decimal separator U+066B)
|
||||
- THEN `onValueChange` fires with `3.14`
|
||||
|
||||
### Requirement: Pagination renders page numbers via FormattedNumber
|
||||
|
||||
`<Pagination>` and `<PaginationStatus>` SHALL render page indices through `<FormattedNumber/>` so non-Latin numbering systems display native digits. Existing translation keys (`react-ui.pagination.*`) SHALL be unchanged.
|
||||
|
||||
**Verified by:** updated `src/components/pagination/pagination.test.tsx`.
|
||||
|
||||
### Requirement: Validation message bridge
|
||||
|
||||
The package SHALL expose `useValidationMessageBridge()` returning a `(error: SuaveplanError) => string` function that resolves the error to a localized message via `useErrorMessage` from react-i18n. Toast, Alert, and ConfirmDeleteDialog SHALL accept an optional `error?: SuaveplanError` prop alongside their existing string props; when present, the resolved message renders. Existing string-only props remain valid.
|
||||
|
||||
**Verified by:** `src/i18n/validation-message-bridge/validation-message-bridge.test.tsx`.
|
||||
|
||||
#### Scenario: bridge resolves a validation error to a localized message
|
||||
|
||||
- GIVEN a `SuaveplanError` with code `VALIDATION.MIN_LENGTH` and locale `en-GB`
|
||||
- WHEN a component renders `bridge(error)`
|
||||
- THEN the returned string matches the en-GB message bundle's value for that code
|
||||
|
||||
### Requirement: Translation key extraction script
|
||||
|
||||
The package SHALL ship `scripts/extract-translation-keys.ts` and a `bun run check:i18n-keys` script. The script SHALL AST-walk every `*.tsx` under `src/components/**` and `src/i18n/**` for `t("react-ui.…")` calls, `<Trans i18nKey="react-ui.…" />` usages, and `useTranslation` resolutions, and SHALL diff the discovered keys against `src/locales/en-GB.json`. The script SHALL exit non-zero when (a) any key referenced in code is missing from the canonical en-GB bundle, or (b) any key in the bundle is not referenced anywhere in code. Output SHALL list both `MISSING` and `ORPHAN` keys with file paths.
|
||||
|
||||
**Verified by:** `scripts/extract-translation-keys.test.ts` (script-level test using a fixtures directory).
|
||||
|
||||
#### Scenario: missing key fails the gate
|
||||
|
||||
- GIVEN a component file with `t("react-ui.foo.missing")` and `en-GB.json` lacking that key
|
||||
- WHEN `bun run check:i18n-keys` runs
|
||||
- THEN the script exits non-zero and prints `MISSING: react-ui.foo.missing` with the source file path
|
||||
|
||||
#### Scenario: orphan key fails the gate
|
||||
|
||||
- GIVEN `en-GB.json` containing `react-ui.foo.unused` and no source file references it
|
||||
- WHEN `bun run check:i18n-keys` runs
|
||||
- THEN the script exits non-zero and prints `ORPHAN: react-ui.foo.unused`
|
||||
|
||||
### Requirement: Bundled translations cover every primitive
|
||||
|
||||
The five bundled locales (`en-GB`, `zh-CN`, `ar-AE`, `cy-GB`, `ru-RU`) SHALL contain real translations for every key referenced by the new primitives, `<InputNumber>`, the locale-aware behavior wiring, and the validation bridge surfaces. English-language placeholders in non-English bundles are NOT acceptable; bundles SHALL be reviewed before the change ships.
|
||||
|
||||
**Verified by:** the `check:i18n-keys` gate plus a manual review note in the change archive.
|
||||
|
||||
### Requirement: Stories ship for every i18n primitive
|
||||
|
||||
Every primitive added by the `react-ui-i18n` capability SHALL ship a sibling `.stories.tsx` alongside its implementation. Each story file SHALL expose a `Default`, a `Playground`, and at least one further story (the canonical third — `Variants`, `WithI18n`, or a more specific name like `LTR vs RTL`). Stories SHALL render inside the shared `<StoriesShell>` so the provider stack matches production composition. A consolidated story-snapshot test SHALL exercise every story's `render()` to assert it produces a tree without throwing.
|
||||
|
||||
**Verified by:** `src/i18n/i18n-stories.test.tsx` (48 scenarios across the 11 covered surfaces).
|
||||
|
||||
#### Scenario: every i18n primitive has a `.stories.tsx` sibling
|
||||
|
||||
- GIVEN the directory `src/i18n/<primitive>/` (or `src/components/input/input-number/` for `<InputNumber>`)
|
||||
- WHEN the `i18n-stories.test.tsx` aggregate snapshot test runs
|
||||
- THEN every covered surface reports the required `Default` and `Playground` stories plus at least one further variant story
|
||||
|
||||
### Requirement: bun run check:i18n-keys catches translation-key drift
|
||||
|
||||
The package SHALL ship `scripts/check-i18n-keys.ts` invoked via `bun run check:i18n-keys`. The script SHALL extract every `react-ui.*` translation-key reference from the documented chassis call shapes (`t("react-ui.…")`, `tDynamic(...)`, `i18nKey="…"`, `i18nKey: "…"`, `<name>Key={"…"}`, `?? "react-ui.…"`, `const DEFAULT_*_KEY = "…"`, `labelKey: "…"`) under `src/components/**`, `src/i18n/**`, `src/theme/**`, `src/pack/**`, `src/provider/**`, `src/hooks/**`, and `src/errors/**`, then diff the discovered set against `src/locales/en-GB.json`. The script SHALL exit non-zero when any key is referenced in code but absent from the bundle (`MISSING`); ORPHAN diagnostics (bundle keys nothing in code references) SHALL be printed as warnings by default and SHALL fail the gate when invoked with `--strict`. The script SHALL be wired into `bunx turbo lint` via the package `lint` script.
|
||||
|
||||
**Verified by:** `scripts/check-i18n-keys.test.ts` (12 scenarios — pattern coverage, line-number reporting, namespace filtering, MISSING/ORPHAN/clean diagnoses).
|
||||
|
||||
#### Scenario: missing key in code fails the gate
|
||||
|
||||
- GIVEN a source file referencing `t("react-ui.foo.absent")` and `en-GB.json` lacking that key
|
||||
- WHEN `bun run check:i18n-keys` runs
|
||||
- THEN the script exits non-zero and prints `MISSING react-ui.foo.absent at <file>:<line>`
|
||||
|
||||
#### Scenario: orphan key warns by default and fails under --strict
|
||||
|
||||
- GIVEN `en-GB.json` containing `react-ui.foo.unused` and no source reference
|
||||
- WHEN `bun run check:i18n-keys` runs (default mode)
|
||||
- THEN the script exits zero and prints `ORPHAN react-ui.foo.unused (warn — use --strict to fail)`
|
||||
- AND when invoked with `--strict` the same condition exits non-zero
|
||||
|
||||
#### Scenario: clean source + bundle pair passes
|
||||
|
||||
- GIVEN every key in `en-GB.json` has at least one source reference AND every code-referenced key is in the bundle
|
||||
- WHEN `bun run check:i18n-keys` runs
|
||||
- THEN the script exits zero with `clean — N keys in code, M keys in bundle.`
|
||||
|
||||
### Requirement: bun run check:registry-eject verifies shadcn drift
|
||||
|
||||
The package SHALL ship `scripts/check-registry-eject.ts` invoked via `bun run check:registry-eject`. The script SHALL walk every `registry/r/*.json` entry (excluding `index.json`) and verify:
|
||||
|
||||
1. The entry parses as JSON, has a non-empty `name`, has a `type` field, and has a non-empty `files` array.
|
||||
2. Every path in `files[]` resolves to an existing file under `src/components/<entry-name>/`.
|
||||
3. Every component directory under `src/components/` whose contents include a `<name>.tsx`, `<name>.ts`, `index.ts`, or `index.tsx` SHALL have a corresponding `registry/r/<name>.json` entry.
|
||||
4. No `registry/r/<name>.json` entry SHALL exist without a matching `src/components/<name>/` directory.
|
||||
|
||||
The script SHALL emit `MISSING_REGISTRY_ENTRY`, `MISSING_SOURCE_FILE`, `EMPTY_FILES_LIST`, `MALFORMED_ENTRY`, or `ORPHAN_REGISTRY_ENTRY` diagnostics and SHALL exit non-zero on any. The script SHALL be wired into `bunx turbo lint`. The full transform-roundtrip compile gate (running `tsc --noEmit` over the registry-transformed source against a synthesized consumer `tsconfig.json`) is deferred until the registry transform itself ships as a callable helper — today the registry ships source verbatim.
|
||||
|
||||
**Verified by:** `scripts/check-registry-eject.test.ts` (10 scenarios).
|
||||
|
||||
#### Scenario: missing source file fails the gate
|
||||
|
||||
- GIVEN a `registry/r/foo.json` listing `foo.tsx` and `src/components/foo/foo.tsx` not existing
|
||||
- WHEN `bun run check:registry-eject` runs
|
||||
- THEN the script exits non-zero with a `MISSING_SOURCE_FILE` diagnostic that names the entry and the expected source path
|
||||
|
||||
#### Scenario: missing registry entry for a public component fails the gate
|
||||
|
||||
- GIVEN a public component at `src/components/<new>/<new>.tsx` with no corresponding `registry/r/<new>.json`
|
||||
- WHEN `bun run check:registry-eject` runs
|
||||
- THEN the script exits non-zero with a `MISSING_REGISTRY_ENTRY` diagnostic naming the directory
|
||||
|
||||
#### Scenario: clean tree passes
|
||||
|
||||
- GIVEN every registry entry has a matching source file AND every component directory has a registry entry
|
||||
- WHEN `bun run check:registry-eject` runs
|
||||
- THEN the script exits zero with `clean — N registry entries match M component directories.`
|
||||
Reference in New Issue
Block a user