@gears-frontx/react 0.2.0-alpha.0 → 0.2.0-alpha.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +118 -61
- package/dist/index.cjs +607 -426
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +365 -100
- package/dist/index.d.ts +365 -100
- package/dist/index.js +437 -253
- package/dist/index.js.map +1 -1
- package/dist/testing.cjs +7 -4
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.js +7 -2
- package/dist/testing.js.map +1 -1
- package/dist/{types-Cjd20AT-.d.cts → types-JtpziA_B.d.cts} +5 -12
- package/dist/{types-Cjd20AT-.d.ts → types-JtpziA_B.d.ts} +5 -12
- package/dist/types.cjs.map +1 -1
- package/dist/types.d.cts +1 -1
- package/dist/types.d.ts +1 -1
- package/llms.txt +38 -12
- package/package.json +20 -15
- package/commands/frontx-duplicate-screenset.md +0 -58
- package/commands/frontx-new-component.md +0 -57
- package/commands/frontx-new-mfe.md +0 -153
- package/commands/frontx-new-screen.md +0 -45
- package/commands/frontx-new-screenset.md +0 -57
package/CLAUDE.md
CHANGED
|
@@ -8,47 +8,44 @@ This package is part of the **React Layer (L3)** - it depends only on @gears-fro
|
|
|
8
8
|
|
|
9
9
|
## Core Concepts
|
|
10
10
|
|
|
11
|
-
###
|
|
11
|
+
### FrontXProvider
|
|
12
12
|
|
|
13
|
-
Wrap your app with
|
|
13
|
+
Wrap your app with FrontXProvider to enable all hooks. A runtime builds exactly one app, once at module level; the provider never creates one. The three compositions below are alternatives: choose one per runtime.
|
|
14
14
|
|
|
15
15
|
```tsx
|
|
16
|
-
import {
|
|
16
|
+
import { createFrontX, FrontXProvider, presets } from '@gears-frontx/react';
|
|
17
|
+
|
|
18
|
+
// Alternative 1: the full preset
|
|
19
|
+
const app = createFrontX({ devMode: true }).useAll(presets.full()).build();
|
|
17
20
|
|
|
18
21
|
function App() {
|
|
19
22
|
return (
|
|
20
|
-
<
|
|
23
|
+
<FrontXProvider app={app}>
|
|
21
24
|
<YourApp />
|
|
22
|
-
</
|
|
25
|
+
</FrontXProvider>
|
|
23
26
|
);
|
|
24
27
|
}
|
|
25
28
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
// apiRegistry.register / initialize before .build; createGears FrontX().use(effects()).use(queryCacheShared()).use(mock()).build();
|
|
39
|
-
// registerSlice after .build when slices exist.
|
|
40
|
-
const mfeApp = createGears FrontX().use(effects()).use(queryCacheShared()).use(mock()).build();
|
|
41
|
-
<Gears FrontXProvider app={mfeApp}>
|
|
42
|
-
<YourApp />
|
|
43
|
-
</Gears FrontXProvider>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Alternative 2: a host shell with a custom plugin set (a host typically uses `queryCache()`):
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
const app = createFrontX().use(effects()).use(queryCache()).build();
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Alternative 3: a child MFE app. The canonical bootstrap matches `src/mfe_packages/*/init.ts`: `apiRegistry.register` / `initialize` before `.build()`, `registerSlice` after `.build()` when slices exist:
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
const app = createFrontX().use(effects()).use(queryCacheShared()).use(mock()).build();
|
|
44
41
|
```
|
|
45
42
|
|
|
46
43
|
The shared `QueryClient` is created and owned by the `queryCache()` framework plugin at L2.
|
|
47
|
-
`
|
|
44
|
+
`FrontXProvider` resolves that client from the app instance — it does not create its own `QueryClient`.
|
|
48
45
|
|
|
49
|
-
When the host uses `queryCache()` and the child MFE app uses `queryCacheShared()` (with `effects()` and `mock()` on the same chain as in repo MFE inits), both roots join the same shared `QueryClient` while keeping separate React trees. `ThemeAwareReactLifecycle` relies on that shared plugin-owned client through the app instance. If the shared client is missing for a mounted MFE, the lifecycle
|
|
46
|
+
When the host uses `queryCache()` and the child MFE app uses `queryCacheShared()` (with `effects()` and `mock()` on the same chain as in repo MFE inits), both roots join the same shared `QueryClient` while keeping separate React trees. `ThemeAwareReactLifecycle` relies on that shared plugin-owned client through the app instance. If the shared client is missing for a mounted MFE, the lifecycle fails explicitly instead of silently falling back.
|
|
50
47
|
|
|
51
|
-
For separate roots, build each app with the appropriate query-cache plugin (`queryCache()` for the host, `queryCacheShared()` for child MFE shells) so every tree joins the same cache through plugin composition.
|
|
48
|
+
For separate roots, build each app with the appropriate query-cache plugin (`queryCache()` for the host, `queryCacheShared()` for child MFE shells) so every tree joins the same cache through plugin composition. Host apps should register domains/extensions during bootstrap; **`ExtensionDomainSlot`** is the preferred host-side renderer for screen slots while the framework wires `mount_ext`.
|
|
52
49
|
|
|
53
50
|
### Data Fetching with Endpoint Descriptors
|
|
54
51
|
|
|
@@ -58,7 +55,7 @@ Services define endpoints as descriptors. Components consume them via `useApiQue
|
|
|
58
55
|
import { useApiQuery, useApiMutation, apiRegistry } from '@gears-frontx/react';
|
|
59
56
|
import { AccountsApiService } from '../api/AccountsApiService';
|
|
60
57
|
|
|
61
|
-
function ProfileScreen() {
|
|
58
|
+
function ProfileScreen({ teamId }: { teamId?: string }) {
|
|
62
59
|
const service = apiRegistry.getService(AccountsApiService);
|
|
63
60
|
|
|
64
61
|
// Read — pass descriptor directly
|
|
@@ -90,15 +87,23 @@ function ProfileScreen() {
|
|
|
90
87
|
|
|
91
88
|
// Per-endpoint cache override (rare)
|
|
92
89
|
const { data: config } = useApiQuery(service.getConfig, { staleTime: 0 });
|
|
90
|
+
|
|
91
|
+
// Conditional query: stays idle until enabled (not on the suspense hooks).
|
|
92
|
+
// The hook still needs a descriptor on every render, so one is built with a
|
|
93
|
+
// stand-in id; while enabled is false it is not fetched automatically, and
|
|
94
|
+
// refetch would still fetch it, so do not call refetch before teamId is set.
|
|
95
|
+
const { data: team } = useApiQuery(service.getTeam({ id: teamId ?? '' }), { enabled: Boolean(teamId) });
|
|
93
96
|
}
|
|
94
97
|
```
|
|
95
98
|
|
|
96
99
|
Cache keys are derived automatically by the service — `QueryCache` methods accept
|
|
97
100
|
endpoint descriptors directly (e.g., `queryCache.get(service.getCurrentUser)`).
|
|
98
101
|
|
|
102
|
+
The `queryCache` a mutation callback receives is bound to the cache scope (mock mode, tenant, session) the mutation started under: once that scope changes, its writes (`set`, `remove`, `invalidate`, `invalidateMany`, `cancel`) do nothing and reads still work, so a response or snapshot from the old scope never lands in the new cache. Nothing else is scope-bound: a `useQueryCache()` handle, the raw `QueryClient`, `cache/*` eventBus emits and per-call `mutate(vars, { onSuccess })` options write to whatever scope is current, so a write after an `await` or inside a mutation callback must go through the callback's `queryCache`.
|
|
103
|
+
|
|
99
104
|
### SSE Streaming with Stream Descriptors
|
|
100
105
|
|
|
101
|
-
Services declare SSE endpoints as stream descriptors. Components consume them via `useApiStream`, which manages the
|
|
106
|
+
Services declare SSE endpoints as stream descriptors. Components consume them via `useApiStream`, which manages the connection lifecycle automatically (connect on mount, disconnect on unmount).
|
|
102
107
|
|
|
103
108
|
```tsx
|
|
104
109
|
import { useApiStream, apiRegistry } from '@gears-frontx/react';
|
|
@@ -124,18 +129,38 @@ function ChatStream() {
|
|
|
124
129
|
);
|
|
125
130
|
|
|
126
131
|
if (status === 'connecting') return <Loading />;
|
|
127
|
-
if (error) return <Error error={error} />;
|
|
132
|
+
if (status === 'error') return <Error error={error} />;
|
|
128
133
|
|
|
129
134
|
return <div>{data?.text}</div>;
|
|
130
135
|
}
|
|
131
136
|
```
|
|
132
137
|
|
|
133
138
|
`useApiStream` returns `{ data, events, status, error, disconnect }`:
|
|
134
|
-
- `data` — latest event payload (
|
|
139
|
+
- `data` — latest event payload (set in both modes)
|
|
135
140
|
- `events` — all received events when `mode: 'accumulate'`; empty array in `'latest'` mode
|
|
136
|
-
- `status` —
|
|
137
|
-
- `error` —
|
|
138
|
-
- `disconnect()` —
|
|
141
|
+
- `status` — connection lifecycle state, `StreamStatus` (see below)
|
|
142
|
+
- `error` — the last connect attempt's error; retained until the next connect attempt resets it, so check `status` rather than `error` truthiness
|
|
143
|
+
- `disconnect()` — close the connection manually
|
|
144
|
+
|
|
145
|
+
#### Status lifecycle
|
|
146
|
+
|
|
147
|
+
`status` is a `StreamStatus`: `'idle' | 'connecting' | 'connected' | 'disconnected' | 'error'`.
|
|
148
|
+
|
|
149
|
+
The TSDoc block on `useApiStream` in `src/hooks/useApiStream.ts` is the detailed and authoritative state-machine reference, including teardown edge cases. This guide summarizes consumer-facing usage and gating.
|
|
150
|
+
|
|
151
|
+
`disconnect()` closes the connection and does not reconnect. There is no reconnect function: to open a new connection, change the descriptor key, change `mode`, or toggle `enabled` back to true.
|
|
152
|
+
|
|
153
|
+
#### The `enabled` option
|
|
154
|
+
|
|
155
|
+
`enabled` (default `true`) gates whether the hook connects at all. While it is false the hook never calls the descriptor's `connect()` and ordinarily holds `status: 'idle'` (calling `disconnect()` while disabled is the one exception — see the TSDoc). Flipping it to true re-runs the connect effect and starts a fresh connection; flipping it back to false tears the connection down and returns `status` to `'idle'`.
|
|
156
|
+
|
|
157
|
+
Tearing down via `enabled` does not clear `data` or `events` — a descriptor-key or `mode` change, or re-enabling, does. A component reading `data` while the stream is disabled still sees the last event from the previous connection.
|
|
158
|
+
|
|
159
|
+
Gating advice, since `'idle'` covers both "not started yet" and "deferred by `enabled`":
|
|
160
|
+
|
|
161
|
+
- Gate the loading state on `'connecting'` alone. Treating `'idle'` as loading renders a permanent spinner for a stream deliberately held at `enabled: false`.
|
|
162
|
+
- Handle `'idle'` as "not started / deferred". A component that both defers and shows loading should key its spinner off its own active flag, not off `'idle'`.
|
|
163
|
+
- Check `status === 'error'` rather than the truthiness of `error` to render failures.
|
|
139
164
|
|
|
140
165
|
### Available Hooks
|
|
141
166
|
|
|
@@ -150,11 +175,13 @@ function MyComponent() {
|
|
|
150
175
|
const app = useFrontX();
|
|
151
176
|
|
|
152
177
|
// Access MFE-enabled registry
|
|
153
|
-
const extensions = app.
|
|
178
|
+
const extensions = app.mfeRegistry.getExtensionsForDomain('screen');
|
|
154
179
|
|
|
155
|
-
// Access MFE actions
|
|
156
|
-
|
|
157
|
-
await
|
|
180
|
+
// Access MFE actions — fire-and-forget: each dispatches an actions chain
|
|
181
|
+
// through the acceptance-only registry surface and returns nothing to
|
|
182
|
+
// await.
|
|
183
|
+
app.actions.loadExtension('home');
|
|
184
|
+
app.actions.mountExtension('home');
|
|
158
185
|
}
|
|
159
186
|
```
|
|
160
187
|
|
|
@@ -240,47 +267,78 @@ Access the MFE bridge for child MFEs:
|
|
|
240
267
|
|
|
241
268
|
```tsx
|
|
242
269
|
import { useMfeBridge } from '@gears-frontx/react';
|
|
243
|
-
import {
|
|
270
|
+
import { FRONTX_ACTION_LOAD_EXT, FRONTX_SHARED_PROPERTY_THEME } from '@gears-frontx/react';
|
|
244
271
|
|
|
245
272
|
function MyExtension() {
|
|
246
273
|
const bridge = useMfeBridge();
|
|
247
274
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
275
|
+
const handleLoad = () => {
|
|
276
|
+
// executeActionsChain is acceptance-only: it returns void, never
|
|
277
|
+
// throws, and never yields anything to await for the chain's own
|
|
278
|
+
// execution.
|
|
279
|
+
bridge.executeActionsChain({
|
|
280
|
+
action: { type: FRONTX_ACTION_LOAD_EXT, target: 'screen', payload: { subject: 'other' } }
|
|
281
|
+
});
|
|
282
|
+
};
|
|
252
283
|
|
|
253
284
|
// Get shared property
|
|
254
|
-
const theme = bridge.getProperty(
|
|
285
|
+
const theme = bridge.getProperty(FRONTX_SHARED_PROPERTY_THEME);
|
|
255
286
|
}
|
|
256
287
|
```
|
|
257
288
|
|
|
258
289
|
#### useSharedProperty
|
|
259
290
|
|
|
260
|
-
Subscribe to shared property changes:
|
|
291
|
+
Subscribe to shared property changes. Two forms: the context form reads the bridge from `MfeContext`, and the options form accepts an explicit bridge and a fallback.
|
|
292
|
+
|
|
293
|
+
**Context form** — `useSharedProperty(propertyTypeId)`. Reads the bridge from `MfeContext` and throws if no `MfeProvider` is mounted. Returns the value (or `undefined` if the host has not published it), updating on every host publish.
|
|
261
294
|
|
|
262
295
|
```tsx
|
|
263
|
-
import { useSharedProperty,
|
|
296
|
+
import { useSharedProperty, FRONTX_SHARED_PROPERTY_THEME } from '@gears-frontx/react';
|
|
264
297
|
|
|
265
298
|
function ThemedComponent() {
|
|
266
|
-
const theme = useSharedProperty(
|
|
299
|
+
const theme = useSharedProperty(FRONTX_SHARED_PROPERTY_THEME);
|
|
267
300
|
|
|
268
301
|
return <div style={{ backgroundColor: theme?.primaryColor }}>...</div>;
|
|
269
302
|
}
|
|
270
303
|
```
|
|
271
304
|
|
|
305
|
+
**Options form** — `useSharedProperty(propertyTypeId, { bridge, fallback })`. Use it for components that never mount `MfeProvider`, or to show a value until the host publishes one.
|
|
306
|
+
|
|
307
|
+
```tsx
|
|
308
|
+
import { useSharedProperty } from '@gears-frontx/react';
|
|
309
|
+
import type { ChildMfeBridge } from '@gears-frontx/framework';
|
|
310
|
+
|
|
311
|
+
function StandaloneComponent({ bridge }: { bridge: ChildMfeBridge }) {
|
|
312
|
+
const label = useSharedProperty<string>(LABEL_PROPERTY_ID, {
|
|
313
|
+
bridge,
|
|
314
|
+
fallback: 'Untitled',
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
return <span>{label}</span>;
|
|
318
|
+
}
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
`UseSharedPropertyOptions<T>` fields:
|
|
322
|
+
|
|
323
|
+
- `bridge` — bridge to read the property from, bypassing `MfeContext`. Pass it when the calling component never mounts `MfeProvider` (for example, a component that receives its bridge as a prop or from a non-React entry point). When omitted, the hook falls back to the bridge from `MfeContext` and throws if no `MfeProvider` is mounted. Changing `bridge` or `propertyTypeId` moves the subscription to the new source and the returned value follows on the same render.
|
|
324
|
+
- `fallback` — value returned while the host has not published the property yet. Only `undefined` is substituted; a published `null` is a real value and comes through as-is. With `fallback` set the hook returns `T` rather than `T | undefined`. It may be passed inline on every render without affecting the subscription.
|
|
325
|
+
|
|
326
|
+
There is no consumer-side validation option on purpose: the host validates every published value against the property's schema before it reaches any MFE (`TypeSystemPlugin.register()` throws on a mismatch and the update is never stored or delivered). `T` is the caller's declaration of that schema, as with `useState<T>`.
|
|
327
|
+
|
|
328
|
+
Both forms read through `useSyncExternalStore`, so the first render shows the host's current value and the result is tearing-protected under concurrent rendering. The TSDoc block on `useSharedProperty` in `src/mfe/hooks/useSharedProperty.ts` is the authoritative reference; this guide summarizes consumer-facing usage.
|
|
329
|
+
|
|
272
330
|
#### useHostAction
|
|
273
331
|
|
|
274
332
|
Invoke actions on the host application:
|
|
275
333
|
|
|
276
334
|
```tsx
|
|
277
|
-
import { useHostAction,
|
|
335
|
+
import { useHostAction, FRONTX_ACTION_LOAD_EXT } from '@gears-frontx/react';
|
|
278
336
|
|
|
279
337
|
function MyExtension() {
|
|
280
|
-
const loadExtension = useHostAction(
|
|
338
|
+
const loadExtension = useHostAction(FRONTX_ACTION_LOAD_EXT);
|
|
281
339
|
|
|
282
340
|
const handleClick = () => {
|
|
283
|
-
loadExtension({
|
|
341
|
+
loadExtension({ subject: 'other' });
|
|
284
342
|
};
|
|
285
343
|
|
|
286
344
|
return <button onClick={handleClick}>Load Extension</button>;
|
|
@@ -414,7 +472,7 @@ function Layout() {
|
|
|
414
472
|
|
|
415
473
|
## Key Rules
|
|
416
474
|
|
|
417
|
-
1. **Wrap with
|
|
475
|
+
1. **Wrap with FrontXProvider** - Required for all hooks to work
|
|
418
476
|
2. **Use hooks for state access** - Don't import selectors directly from @gears-frontx/framework
|
|
419
477
|
3. **Use endpoint descriptors for data** - `useApiQuery(service.endpoint)` for REST, `useApiStream(service.stream)` for SSE — not `queryOptions()` or manual key factories
|
|
420
478
|
4. **Service is the cache contract** - The service IS the data layer; cache keys are derived automatically
|
|
@@ -438,7 +496,7 @@ This allows users to import everything from `@gears-frontx/react` without needin
|
|
|
438
496
|
## Exports
|
|
439
497
|
|
|
440
498
|
### Components
|
|
441
|
-
- `
|
|
499
|
+
- `FrontXProvider` - Main context provider
|
|
442
500
|
- `MfeProvider` - MFE context provider
|
|
443
501
|
- `ExtensionDomainSlot` - Domain slot renderer
|
|
444
502
|
- `RefContainerProvider` - Container reference provider
|
|
@@ -455,18 +513,18 @@ This allows users to import everything from `@gears-frontx/react` without needin
|
|
|
455
513
|
- `useScreenTranslations` - Screen translation loading
|
|
456
514
|
- `useTheme` - Theme utilities
|
|
457
515
|
- `useMfeBridge` - Access MFE bridge
|
|
458
|
-
- `useSharedProperty` - Subscribe to shared property
|
|
516
|
+
- `useSharedProperty` - Subscribe to shared property, via `MfeContext` or an explicit bridge
|
|
459
517
|
- `useHostAction` - Invoke host action
|
|
460
518
|
- `useDomainExtensions` - Subscribe to domain extensions
|
|
461
519
|
- `useRegisteredPackages` - Subscribe to registered GTS packages
|
|
462
520
|
- `useActivePackage` - Subscribe to active GTS package
|
|
463
521
|
|
|
464
522
|
### Context
|
|
465
|
-
- `
|
|
523
|
+
- `FrontXContext` - React context (for advanced use)
|
|
466
524
|
- `MfeContext` - MFE context (for advanced use)
|
|
467
525
|
|
|
468
526
|
### Types
|
|
469
|
-
- `
|
|
527
|
+
- `FrontXProviderProps`
|
|
470
528
|
- `ApiQueryResult<TData>` - Gears FrontX-owned query result type (data, error, isLoading, refetch, etc.)
|
|
471
529
|
- `ApiMutationResult<TData>` - Gears FrontX-owned mutation result type (mutateAsync, isPending, error, reset, etc.)
|
|
472
530
|
- `ApiStreamResult<TEvent>` - Gears FrontX-owned stream result type (data, events, status, error, disconnect)
|
|
@@ -474,6 +532,7 @@ This allows users to import everything from `@gears-frontx/react` without needin
|
|
|
474
532
|
- `QueryCache` - Restricted cache interface (accepts descriptors or raw keys)
|
|
475
533
|
- `MutationCallbackContext` - Context with queryCache injected into mutation callbacks
|
|
476
534
|
- `MfeProviderProps`, `ExtensionDomainSlotProps`
|
|
535
|
+
- `UseSharedPropertyOptions<T>` - Options for `useSharedProperty` (bridge, fallback)
|
|
477
536
|
- `UseTranslationReturn`, `UseThemeReturn`
|
|
478
537
|
- All types from @gears-frontx/framework (including `EndpointDescriptor`, `MutationDescriptor`, `StreamDescriptor`, `StreamStatus`)
|
|
479
538
|
|
|
@@ -509,13 +568,11 @@ function MyComponent() {
|
|
|
509
568
|
const app = useFrontX();
|
|
510
569
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
511
570
|
|
|
512
|
-
const handleNavigate =
|
|
571
|
+
const handleNavigate = () => {
|
|
513
572
|
if (containerRef.current) {
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
container: containerRef.current,
|
|
518
|
-
});
|
|
573
|
+
// Fire-and-forget: dispatches an actions chain through the
|
|
574
|
+
// acceptance-only registry surface and returns nothing to await.
|
|
575
|
+
app.actions.mountExtension('home');
|
|
519
576
|
}
|
|
520
577
|
};
|
|
521
578
|
|
|
@@ -535,7 +592,7 @@ import { ExtensionDomainSlot } from '@gears-frontx/react';
|
|
|
535
592
|
function MyComponent() {
|
|
536
593
|
return (
|
|
537
594
|
<ExtensionDomainSlot
|
|
538
|
-
registry={app.
|
|
595
|
+
registry={app.mfeRegistry}
|
|
539
596
|
domainId="screen"
|
|
540
597
|
extensionId="home"
|
|
541
598
|
loadingComponent={<Loading />}
|