first commit
This commit is contained in:
78
docs/ui/react/react-ui/src/pack/pack-context.md
Normal file
78
docs/ui/react/react-ui/src/pack/pack-context.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# `pack-context` — React context plumbing for the pack registry
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
- [pack-registry-provider](./pack-registry-provider/pack-registry-provider.md) — <!-- begin:auto-docs --> _(no submodules)_- [runtime-override](./runtime-override/runtime-override.md) — <!-- begin:auto-docs --> _(no submodules)_ - [runtime-override](./runtime-override/runtime-override.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-pack-component](./use-pack-component/use-pack-component.md) — <!-- begin:auto-docs --> _(no submodules)_ - [use-pack-component](./use-pack-component/use-pack-component.md) — <!-- begin:auto-docs --> _(no submodules)_- [version-check](./version-check/version-check.md) — <!-- begin:auto-docs --> _(no submodules)_ - [version-check](./version-check/version-check.md) — <!-- begin:auto-docs --> _(no submodules)_- [pack-context](./pack-context.md) — <!-- begin:auto-docs --> _(no submodules)_- pack-registry-provider — <!-- begin:auto-docs --> _(…
|
||||
- [pack-registry-provider](./pack-registry-provider/pack-registry-provider.md) — <!-- begin:auto-docs --> _(no submodules)_- [runtime-override](./runtime-override/runtime-override.md) — <!-- begin:auto-docs --> _(no submodules)_- [runtime-override](./runtime-override/runtime-override.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-pack-component](./use-pack-component/use-pack-component.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-pack-component](./use-pack-component/use-pack-component.md) — <!-- begin:auto-docs --> _(no submodules)_- [version-check](./version-check/version-check.md) — <!-- begin:auto-docs --> _(no submodules)_- [version-check](./version-check/version-check.md) — <!-- begin:auto-docs --> _(no submodules)_- [pack-context](./pack-context.md) — <!-- begin:auto-docs --> _(no submodules)_- [pack-registry-provider](./pack-registry-provider/pack-registry-provider.md) — <!-- begin:auto-docs --> _(no submodules)_- [runtime-override](./runtime-override/runtime-override.md) — <!-- begin:auto-docs --> _(no submodules)_- [runtime-override](./runtime-override/runtime-override.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-pack-component](./use-pack-component/use-pack-component.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-pack-component](./use-pack-component/use-pack-component.md) — <!-- begin:auto-docs --> _(no submodules)_- [version-check](./version-check/version-check.md) — <!-- begin:auto-docs --> _(no submodules)_- [version-check](./version-check/version-check.md) — <!-- begin:auto-docs --> _(no submodules)_- [pack-context](./pack-context.md) — <!-- begin:auto-docs --> _(no submodules)_- pack-registry-provider — <!-…
|
||||
- [pack-registry-provider](./pack-registry-provider/pack-registry-provider.md) — <!-- begin:auto-docs --> _(no submodules)_- [runtime-override](./runtime-override/runtime-override.md) — <!-- begin:auto-docs --> _(no submodules)_- [runtime-override](./runtime-override/runtime-override.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-pack-component](./use-pack-component/use-pack-component.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-pack-component](./use-pack-component/use-pack-component.md) — <!-- begin:auto-docs --> _(no submodules)_- [version-check](./version-check/version-check.md) — <!-- begin:auto-docs --> _(no submodules)_- [version-check](./version-check/version-check.md) — <!-- begin:auto-docs --> _(no submodules)_- [pack-context](./pack-context.md) — <!-- begin:auto-docs --> _(no submodules)_- pack-registry-provider — <!-- begin:auto-docs --> _…
|
||||
- [pack-registry-provider](./pack-registry-provider/pack-registry-provider.md) — <!-- begin:auto-docs --> _(no submodules)_- [runtime-override](./runtime-override/runtime-override.md) — <!-- begin:auto-docs --> _(no submodules)_ - [runtime-override](./runtime-override/runtime-override.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-pack-component](./use-pack-component/use-pack-component.md) — <!-- begin:auto-docs --> _(no submodules)_ - [use-pack-component](./use-pack-component/use-pack-component.md) — <!-- begin:auto-docs --> _(no submodules)_- [version-check](./version-check/version-check.md) — <!-- begin:auto-docs --> _(no submodules)_ - [version-check](./version-check/version-check.md) — <!-- begin:auto-docs --> _(no submodules)_- [pack-context](./pack-context.md) — <!-- begin:auto-docs --> _(no submodules)_
|
||||
## Purpose
|
||||
|
||||
Owns the React `Context` `<PackRegistryProvider>` writes into and the
|
||||
`usePackComponent` hook reads from. The context carries two slices: a
|
||||
`packs` map (raw `ComponentPack` references keyed by pack name) and a
|
||||
`resolved` map (the merged `(packName -> componentName -> Component)`
|
||||
view that already accounts for runtime overrides). Splitting raw and
|
||||
resolved lets debugging surfaces inspect either layer.
|
||||
|
||||
## Features
|
||||
|
||||
- Single source-of-truth context for the chassis pack system
|
||||
- `null` discriminator drives provider-missing detection in every
|
||||
consumer hook
|
||||
- `displayName` set so React DevTools surfaces the context as
|
||||
`ReactUiPackRegistryContext` rather than the generic
|
||||
`Context.Provider`
|
||||
- `resolved` map is computed inside the Provider's `useMemo` so a
|
||||
re-render does not rebuild the merged maps unless `packs` or the
|
||||
override prop change
|
||||
|
||||
## Usage: Basic example
|
||||
|
||||
```ts
|
||||
import { useContext } from "react";
|
||||
import { PackRegistryContext } from "@suaveplan/react-ui/pack";
|
||||
|
||||
function PackInspector() {
|
||||
const ctx = useContext(PackRegistryContext);
|
||||
if (!ctx) return null;
|
||||
return <pre>{[...ctx.packs.keys()].join(", ")}</pre>;
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced example
|
||||
|
||||
```ts
|
||||
import { useContext } from "react";
|
||||
import { PackRegistryContext } from "@suaveplan/react-ui/pack";
|
||||
|
||||
function ResolvedView() {
|
||||
const ctx = useContext(PackRegistryContext);
|
||||
if (!ctx) return null;
|
||||
const rows = [];
|
||||
for (const [pack, components] of ctx.resolved) {
|
||||
for (const componentName of components.keys()) {
|
||||
rows.push(`${pack}/${componentName}`);
|
||||
}
|
||||
}
|
||||
return <ul>{rows.map((r) => <li key={r}>{r}</li>)}</ul>;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `PackRegistryContext: React.Context<PackRegistryContextValue | null>`
|
||||
- `interface PackRegistryContextValue`
|
||||
- `packs: ReadonlyMap<string, ComponentPack>`
|
||||
- `resolved: ReadonlyMap<string, ReadonlyMap<string, ComponentType<unknown>>>`
|
||||
|
||||
## Implementation notes
|
||||
|
||||
The two-map split allows the consumer-facing `usePackComponent` hook
|
||||
to read directly from `resolved` (a flat lookup) without walking the
|
||||
raw pack array. Test fixtures and DevTools surfaces that need to see
|
||||
the original pack lifecycle hooks consult `packs` instead. The
|
||||
context default `null` makes provider-missing failures surface at the
|
||||
hook call site rather than producing a confusing read-from-empty-map
|
||||
scenario.
|
||||
@@ -0,0 +1,86 @@
|
||||
# `<PackRegistryProvider>` — chassis pack registry
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Mounts the active pack registry, validates every pack against
|
||||
`ComponentPackSchema` plus the no-collision invariant, runs each pack's
|
||||
`onRegister(ctx)` hook in declared order on mount, runs
|
||||
`onUnregister(ctx)` in reverse order on unmount, forwards each pack's
|
||||
translation bundles into the active i18n catalog under the pack's name
|
||||
as the namespace, and emits the OTel signals defined in the proposal.
|
||||
|
||||
## Features
|
||||
|
||||
- Validates packs synchronously at mount; throws
|
||||
`ReactUiError(REACT_UI.PACK_NAME_COLLISION)` when two packs share a
|
||||
`name`
|
||||
- Registration in declared order; teardown in reverse order
|
||||
- Each pack receives a `PackContext` containing the active i18n
|
||||
instance, active theme, a `PackLoggerLike` (defaulting to a
|
||||
console-backed shim), and an `AbortSignal` scoped to the pack's
|
||||
mount lifetime
|
||||
- Pack translations auto-registered via `i18n.catalog.set(locale,
|
||||
packName, bundle)` so consumers reading `t('packName:key')` see the
|
||||
registered values
|
||||
- `peerRange` mismatch logs a structured warning AND increments
|
||||
`react_ui.pack.version_mismatch_total` — the pack still registers
|
||||
(degrades gracefully, never throws)
|
||||
- Runtime override map (`<ReactUiProvider components={...}>`) merges
|
||||
on top of pack defaults at registration time
|
||||
- OTel: counters `react_ui.pack.register_total`,
|
||||
`react_ui.pack.unregister_total`,
|
||||
`react_ui.pack.version_mismatch_total`; span
|
||||
`react_ui.pack.register{name}` around each `onRegister`
|
||||
|
||||
## Usage: Basic example
|
||||
|
||||
```tsx
|
||||
import { PackRegistryProvider } from "@suaveplan/react-ui/pack/pack-registry-provider";
|
||||
|
||||
<PackRegistryProvider i18n={i18n} packs={[authPack]}>
|
||||
<App />
|
||||
</PackRegistryProvider>;
|
||||
```
|
||||
|
||||
## Usage: Advanced example
|
||||
|
||||
```tsx
|
||||
import { PackRegistryProvider } from "@suaveplan/react-ui/pack/pack-registry-provider";
|
||||
|
||||
<PackRegistryProvider
|
||||
i18n={i18n}
|
||||
packs={[authPack, socialPack, billingPack]}
|
||||
components={{
|
||||
auth: { LoginForm: AcmeLoginForm },
|
||||
social: { ShareButton: AcmeShareButton },
|
||||
}}
|
||||
logger={{ warn: log.warn, info: log.info }}
|
||||
>
|
||||
<App />
|
||||
</PackRegistryProvider>;
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `<PackRegistryProvider>` — props per `PackRegistryProviderProps`:
|
||||
- `i18n: I18nInstance` — required; pack translations register here
|
||||
- `packs: ReadonlyArray<ComponentPack>` — required
|
||||
- `components?: RuntimeComponentOverride` — optional override map
|
||||
- `logger?: PackLoggerLike` — optional; defaults to a console-backed
|
||||
shim
|
||||
- `children: ReactNode`
|
||||
|
||||
## Implementation notes
|
||||
|
||||
The provider enforces TWO validation passes at mount time: the
|
||||
collision check (synchronous, throws on duplicate name) and the schema
|
||||
check (synchronous, throws on bad shape). Both run BEFORE any
|
||||
`onRegister` hook fires so a partially-registered registry never
|
||||
exists. `onRegister` runs inside an OTel span so failures inside a
|
||||
pack's hook are attributable to the pack name in trace storage. The
|
||||
teardown loop iterates in reverse so packs that depend on earlier
|
||||
packs (e.g. a `billing` pack assuming `auth` is mounted) tear down in
|
||||
the safe order.
|
||||
@@ -0,0 +1,76 @@
|
||||
# `runtime-override` — pack-component override merger
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Pure helper that merges a `ComponentPack`'s default components with the
|
||||
`<ReactUiProvider components={...}>` runtime override map. The result
|
||||
is a flat `Map<componentName, ComponentType<unknown>>` the chassis
|
||||
stores in the `PackRegistryContext`'s `resolved` slot. White-label and
|
||||
runtime-swap scenarios drive the override mechanism — consumers swap a
|
||||
pack's `LoginForm` for their bespoke implementation without forking the
|
||||
pack package.
|
||||
|
||||
## Features
|
||||
|
||||
- Pure / synchronous merge of pack defaults with overrides
|
||||
- Skips `undefined` override entries so consumers can spread a partial
|
||||
override map without wiping a pack default
|
||||
- Skips overrides for unrelated packs so multiple packs share a single
|
||||
override map without cross-talk
|
||||
- Returns a frozen-style `ReadonlyMap` so consumers cannot mutate
|
||||
the registry post-construction
|
||||
|
||||
## Usage: Basic example
|
||||
|
||||
```ts
|
||||
import { mergePackComponents } from "@suaveplan/react-ui/pack";
|
||||
|
||||
const merged = mergePackComponents({
|
||||
pack: authPack,
|
||||
overrides: {
|
||||
auth: { LoginForm: AcmeLoginForm },
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced example
|
||||
|
||||
```ts
|
||||
import {
|
||||
mergePackComponents,
|
||||
type RuntimeComponentOverride,
|
||||
} from "@suaveplan/react-ui/pack";
|
||||
|
||||
const overrides: RuntimeComponentOverride = {
|
||||
auth: {
|
||||
LoginForm: AcmeLoginForm,
|
||||
RegisterForm: AcmeRegisterForm,
|
||||
},
|
||||
social: {
|
||||
ShareButton: AcmeShareButton,
|
||||
},
|
||||
};
|
||||
|
||||
const merged = mergePackComponents({ pack: authPack, overrides });
|
||||
const Login = merged.get("LoginForm"); // → AcmeLoginForm
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `mergePackComponents({ pack, overrides? })` →
|
||||
`ReadonlyMap<string, ComponentType<unknown>>`
|
||||
- `type RuntimeComponentOverride = Partial<Record<string,
|
||||
Partial<Record<string, ComponentType<unknown>>>>>`
|
||||
|
||||
## Implementation notes
|
||||
|
||||
The override semantics are intentionally simple: the override entry,
|
||||
if present and not `undefined`, replaces the pack's default for that
|
||||
component name. There is no deep-merge or composition step because
|
||||
React components are atomic units — swapping `LoginForm` is the
|
||||
intent, not "augment the existing component with override props". A
|
||||
consumer that wants compositional swapping wraps their override
|
||||
component around the imported pack default explicitly.
|
||||
@@ -0,0 +1,76 @@
|
||||
# `usePackComponent()` — typed pack-component lookup
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
The primary read-side hook for the chassis pack registry. Returns the
|
||||
`ComponentType` registered under `(packName, componentName)`, applying
|
||||
runtime overrides automatically. Throws structured `ReactUiError`
|
||||
instances so misuses fail fast instead of rendering `undefined`.
|
||||
|
||||
## Features
|
||||
|
||||
- Generic-typed signature so consumers narrowing on the pack name AND
|
||||
component name (`usePackComponent('auth', 'LoginForm')`) get the
|
||||
exact `ComponentType` declared in the pack's `PackComponents`
|
||||
augmentation
|
||||
- Throws `ReactUiError(REACT_UI.NO_PROVIDER)` outside the registry
|
||||
- Throws `ReactUiError(REACT_UI.PACK_NOT_FOUND)` for an unknown pack
|
||||
with `error.context.packName` populated
|
||||
- Throws `ReactUiError(REACT_UI.PACK_COMPONENT_NOT_FOUND)` for a known
|
||||
pack but unknown component, with both `packName` and `componentName`
|
||||
in context
|
||||
- Reads from the resolved `(packName -> componentName ->
|
||||
ComponentType)` map populated by `<PackRegistryProvider>` so runtime
|
||||
overrides flow through automatically
|
||||
|
||||
## Usage: Basic example
|
||||
|
||||
```tsx
|
||||
import { usePackComponent } from "@suaveplan/react-ui/pack/use-pack-component";
|
||||
|
||||
function LoginScreen() {
|
||||
const LoginForm = usePackComponent("auth", "LoginForm");
|
||||
return <LoginForm />;
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced example
|
||||
|
||||
```tsx
|
||||
import { usePackComponent } from "@suaveplan/react-ui/pack/use-pack-component";
|
||||
|
||||
declare module "@suaveplan/react-ui/pack" {
|
||||
interface PackComponents {
|
||||
readonly auth: {
|
||||
readonly LoginForm: ComponentType<{ redirectTo?: string }>;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function LoginRoute({ redirectTo }: { redirectTo?: string }) {
|
||||
const LoginForm = usePackComponent("auth", "LoginForm");
|
||||
return <LoginForm redirectTo={redirectTo} />;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `usePackComponent<TPack, TKey>(packName: TPack, componentName: TKey)`
|
||||
- Returns `PackComponents[TPack][TKey]` when the pack has been
|
||||
augmented, otherwise `ComponentType<unknown>`
|
||||
- Throws on missing Provider, missing pack, or missing component
|
||||
|
||||
## Implementation notes
|
||||
|
||||
The lookup walks the `PackRegistryContext.resolved` map directly so
|
||||
overrides supplied via `<ReactUiProvider components={...}>` win over
|
||||
pack defaults without an extra merge step at the call site. The
|
||||
generic narrowing is intentionally permissive — string-typed
|
||||
`packName` / `componentName` arguments still typecheck so consumers
|
||||
that compose pack lookups dynamically (e.g. inside an
|
||||
admin-configurable route table) can use the hook without casting. The
|
||||
runtime contract is the source of truth; the type narrowing is a
|
||||
convenience layer for the common static-key call site.
|
||||
@@ -0,0 +1,66 @@
|
||||
# `version-check` — pack peerRange satisfier
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Pure helper that asks `semver.satisfies(reactUiVersion, peerRange)` so
|
||||
the chassis can decide whether a pack's declared `peerRange` is
|
||||
satisfied by the running `@suaveplan/react-ui` version. The function
|
||||
catches semver parse failures and returns `false` so a malformed range
|
||||
is treated as a mismatch (the pack is still registered but a warning
|
||||
fires).
|
||||
|
||||
## Features
|
||||
|
||||
- Uses `semver.satisfies` with `includePrerelease: true` so an
|
||||
in-flight `0.0.0-canary.1` build still matches `^0.1.0`-shaped
|
||||
ranges
|
||||
- Returns `false` on parser failure — never throws
|
||||
- Pure / synchronous; safe to call in any context
|
||||
|
||||
## Usage: Basic example
|
||||
|
||||
```ts
|
||||
import { satisfiesPeerRange } from "@suaveplan/react-ui/pack";
|
||||
|
||||
const ok = satisfiesPeerRange({
|
||||
reactUiVersion: "0.1.0",
|
||||
peerRange: "^0.1.0",
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced example
|
||||
|
||||
```ts
|
||||
import { satisfiesPeerRange } from "@suaveplan/react-ui/pack";
|
||||
import { REACT_UI_VERSION } from "@suaveplan/react-ui/version";
|
||||
|
||||
function gate(packPeerRange: string): "ok" | "warn" {
|
||||
return satisfiesPeerRange({
|
||||
reactUiVersion: REACT_UI_VERSION,
|
||||
peerRange: packPeerRange,
|
||||
})
|
||||
? "ok"
|
||||
: "warn";
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `satisfiesPeerRange(input: { reactUiVersion: string; peerRange: string }): boolean`
|
||||
- Returns `true` when the version satisfies the range
|
||||
- Returns `false` when the version does not satisfy or the range
|
||||
fails to parse
|
||||
|
||||
## Implementation notes
|
||||
|
||||
The helper deliberately swallows parse errors rather than re-throwing
|
||||
because the caller (the chassis pack-registry provider) needs a binary
|
||||
"is this satisfied?" answer at register time. The
|
||||
`includePrerelease: true` flag keeps in-flight chassis dev builds
|
||||
working with realistic peer ranges; production releases pinned to
|
||||
exact versions still satisfy a `^X.Y.Z` range. The helper imports the
|
||||
`semver` library statically so tree-shaking does not strip it from the
|
||||
bundle.
|
||||
Reference in New Issue
Block a user