@atolljs/react-island 0.1.7 → 0.1.9
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/README.md +41 -1
- package/package.json +3 -3
- package/src/index.tsx +27 -3
- package/src/subIsland.tsx +42 -0
- package/src/worker.ts +8 -0
package/README.md
CHANGED
|
@@ -111,7 +111,47 @@ it with `<Island worker={mapWorker}/>` or `islandComponent<P>()` with no key.
|
|
|
111
111
|
|
|
112
112
|
For multi-island-per-worker, share a client:
|
|
113
113
|
`client={connectIslandWorker({ worker })}` mounts each island's instance into
|
|
114
|
-
the SAME worker (separate reconcilers, op queues, and pids
|
|
114
|
+
the SAME worker (separate reconcilers, op queues, and pids, one OS thread).
|
|
115
|
+
|
|
116
|
+
## Islands inside worker islands
|
|
117
|
+
|
|
118
|
+
A worker-rendered React app mounts islands with the **same components**:
|
|
119
|
+
`@atolljs/react-island/worker` re-exports `Island`, `islandComponent`,
|
|
120
|
+
`lazyIsland` and `connectIslandWorker`. Inside a worker the container div is
|
|
121
|
+
a proxy element, so the mount is routed to a sub-worker whose DOM tunnels
|
|
122
|
+
up through the parent island. Props and semantics are identical:
|
|
123
|
+
|
|
124
|
+
```tsx
|
|
125
|
+
// a component in the PARENT worker's registry: its module is in the parent
|
|
126
|
+
// worker's bundle, so the `new Worker` literal stays bundler-detectable
|
|
127
|
+
import { useState } from 'react';
|
|
128
|
+
import { connectIslandWorker, emit, Island } from '@atolljs/react-island/worker';
|
|
129
|
+
|
|
130
|
+
const regionEntry = () =>
|
|
131
|
+
new Worker(new URL('./region.worker.tsx', import.meta.url), { type: 'module' });
|
|
132
|
+
|
|
133
|
+
function Regions() {
|
|
134
|
+
const [cards] = useState(() => connectIslandWorker({ worker: regionEntry }));
|
|
135
|
+
return (
|
|
136
|
+
<>
|
|
137
|
+
<Island client={cards} app="region" props={{ region: 'us-east' }} />
|
|
138
|
+
<Island client={cards} app="region" props={{ region: 'eu-central' }} />
|
|
139
|
+
<Island worker={regionEntry} app="forecast" onEvent={(n, p) => emit(n, p)} />
|
|
140
|
+
</>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
- `worker` spawns a sub-worker for that mount; a shared `client` adds an
|
|
146
|
+
instance to one sub-worker (the two cards above share a thread, the
|
|
147
|
+
forecast doesn't).
|
|
148
|
+
- `onEvent` runs in the parent island's scope, so `emit` relays to the page.
|
|
149
|
+
- `slots` portal worker-side content into the sub-island's anchors; slot
|
|
150
|
+
names you don't list bubble up to the page shell's `slots`.
|
|
151
|
+
- `SubIsland` is kept as an alias of `Island`.
|
|
152
|
+
|
|
153
|
+
Nested `new Worker` needs an engine that supports it (Chrome, Firefox,
|
|
154
|
+
Node).
|
|
115
155
|
|
|
116
156
|
## Documentation
|
|
117
157
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atolljs/react-island",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "React shell components for atoll-islands islands — <Island/>, islandComponent, lazyIsland",
|
|
5
5
|
"homepage": "https://jwhenry3.github.io/atolljs/consumer/quickstart/",
|
|
6
6
|
"type": "module",
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
"registry": "https://registry.npmjs.org"
|
|
17
17
|
},
|
|
18
18
|
"peerDependencies": {
|
|
19
|
-
"@atolljs/core": "0.1.
|
|
20
|
-
"@atolljs/islands": "0.1.
|
|
19
|
+
"@atolljs/core": "0.1.9",
|
|
20
|
+
"@atolljs/islands": "0.1.9",
|
|
21
21
|
"react": "^19.3.0",
|
|
22
22
|
"react-dom": "^19.3.0",
|
|
23
23
|
"react-reconciler": "^0.34.0"
|
package/src/index.tsx
CHANGED
|
@@ -36,6 +36,14 @@
|
|
|
36
36
|
* is SHARED (several `<Island client={c}/>` mount mounts into one worker —
|
|
37
37
|
* the instance unmounts and the worker dies with the last island to leave).
|
|
38
38
|
*
|
|
39
|
+
* Both threads, one API: `@atolljs/react-island/worker` re-exports these
|
|
40
|
+
* same `Island`/`islandComponent`/`lazyIsland` for worker-rendered trees.
|
|
41
|
+
* Inside a worker the container div is a proxy element, so `mountIsland`
|
|
42
|
+
* routes the mount to the nested path (a sub-worker spawned from this
|
|
43
|
+
* worker, ops tunneled through the parent island). `worker` vs `client`
|
|
44
|
+
* means the same thing on either thread: spawn a worker for this mount, or
|
|
45
|
+
* add an instance to an existing one.
|
|
46
|
+
*
|
|
39
47
|
* The `app` prop accepts the registry name OR the app itself — a React
|
|
40
48
|
* component, an `islandApp`-stamped value, or an `{ imperative }` def. Pass
|
|
41
49
|
* the component to get `props` inference; `islandApp('name', Comp)` is the
|
|
@@ -87,8 +95,8 @@ export interface IslandProps<A = string>
|
|
|
87
95
|
worker?: (() => Worker) | URL;
|
|
88
96
|
client?: IslandClient;
|
|
89
97
|
/**
|
|
90
|
-
* Extra
|
|
91
|
-
*
|
|
98
|
+
* Extra client options — taskTimeout, respawn, lazy… (always one
|
|
99
|
+
* dedicated worker, no pool). `doorbell: false` builds the shorthand client message-only — no
|
|
92
100
|
* SharedArrayBuffer, no COOP/COEP requirement (same as `mode: 'poll'` on
|
|
93
101
|
* a direct `mountIsland` shorthand mount).
|
|
94
102
|
*/
|
|
@@ -101,6 +109,8 @@ export interface IslandProps<A = string>
|
|
|
101
109
|
* Mount-time only; switch later via the handle's `setMode`.
|
|
102
110
|
*/
|
|
103
111
|
mode?: Mode;
|
|
112
|
+
/** Renderer tag reported to devtools on mount ('react', 'vue', …). */
|
|
113
|
+
framework?: string;
|
|
104
114
|
/** Initial + updated root props — serialized to the worker. */
|
|
105
115
|
props?: IslandAppProps<A>;
|
|
106
116
|
/** Island → shell channel: every `emit` op lands here. */
|
|
@@ -127,6 +137,7 @@ export function Island<A = string>({
|
|
|
127
137
|
client: clientProp,
|
|
128
138
|
workerOptions,
|
|
129
139
|
mode,
|
|
140
|
+
framework,
|
|
130
141
|
props,
|
|
131
142
|
onEvent,
|
|
132
143
|
onActivity,
|
|
@@ -193,7 +204,12 @@ export function Island<A = string>({
|
|
|
193
204
|
const client =
|
|
194
205
|
clientProp ??
|
|
195
206
|
(resolvedWorker !== undefined
|
|
196
|
-
? connectIslandWorker({
|
|
207
|
+
? connectIslandWorker({
|
|
208
|
+
name: appName,
|
|
209
|
+
worker: resolvedWorker,
|
|
210
|
+
doorbell: mode !== 'poll',
|
|
211
|
+
...workerOptions,
|
|
212
|
+
})
|
|
197
213
|
: undefined);
|
|
198
214
|
if (client === undefined) {
|
|
199
215
|
report(new Error('<Island> requires either `worker` or `client`'));
|
|
@@ -203,10 +219,13 @@ export function Island<A = string>({
|
|
|
203
219
|
// React portal anchors: the worker marks `data-atoll-slot="name"`, the
|
|
204
220
|
// driver hands us the real element, and we render the matching slot
|
|
205
221
|
// content into it with createPortal.
|
|
222
|
+
// `has` answers for the slots actually passed: a nested mount only
|
|
223
|
+
// claims names it lists and lets the rest bubble to the outer island.
|
|
206
224
|
const slotProxy = new Proxy({} as Record<string, (el: HTMLElement | null) => void>, {
|
|
207
225
|
get: (_t, name: string) => (e: HTMLElement | null) => {
|
|
208
226
|
setSlotTargets((prev) => (prev[name] === e ? prev : { ...prev, [name]: e }));
|
|
209
227
|
},
|
|
228
|
+
has: (_t, name: string) => slotsRef.current?.[name] != null,
|
|
210
229
|
});
|
|
211
230
|
|
|
212
231
|
void (async () => {
|
|
@@ -216,6 +235,7 @@ export function Island<A = string>({
|
|
|
216
235
|
el,
|
|
217
236
|
app: appName,
|
|
218
237
|
mode,
|
|
238
|
+
framework,
|
|
219
239
|
// Serialized across the wire either way — cast preserves inference
|
|
220
240
|
// for props types without an index signature.
|
|
221
241
|
props: (propsRef.current ?? {}) as Record<string, unknown>,
|
|
@@ -290,6 +310,8 @@ export interface IslandShellProps {
|
|
|
290
310
|
workerOptions?: IslandWorkerOptions & { doorbell?: boolean };
|
|
291
311
|
/** Initial flush mode — see IslandProps.mode. */
|
|
292
312
|
mode?: Mode;
|
|
313
|
+
/** Devtools renderer tag — see IslandProps.framework. */
|
|
314
|
+
framework?: string;
|
|
293
315
|
/** Island → shell channel: every `emit` op lands here. */
|
|
294
316
|
onEvent?: (name: string, payload: unknown) => void;
|
|
295
317
|
/** Fired after each applied op batch. */
|
|
@@ -314,6 +336,7 @@ const SHELL_PROP_KEYS: ReadonlySet<string> = new Set([
|
|
|
314
336
|
'client',
|
|
315
337
|
'workerOptions',
|
|
316
338
|
'mode',
|
|
339
|
+
'framework',
|
|
317
340
|
'onEvent',
|
|
318
341
|
'onActivity',
|
|
319
342
|
'onReady',
|
|
@@ -412,6 +435,7 @@ function ProxyIsland({
|
|
|
412
435
|
client={shell.client}
|
|
413
436
|
workerOptions={shell.workerOptions}
|
|
414
437
|
mode={shell.mode}
|
|
438
|
+
framework={shell.framework}
|
|
415
439
|
props={islandProps as Record<string, unknown>}
|
|
416
440
|
onEvent={shell.onEvent}
|
|
417
441
|
onActivity={shell.onActivity}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `SubIsland` — the original name for a nested mount, kept as an alias.
|
|
3
|
+
* New code uses `Island` from `@atolljs/react-island/worker`, which is the
|
|
4
|
+
* same component (and props) the page uses: inside a worker its container
|
|
5
|
+
* is a proxy element, so `mountIsland` routes to the nested path.
|
|
6
|
+
*
|
|
7
|
+
* const subWorker = () =>
|
|
8
|
+
* new Worker(new URL('./counter.worker.tsx', import.meta.url), { type: 'module' });
|
|
9
|
+
*
|
|
10
|
+
* function Dashboard() {
|
|
11
|
+
* return <Island worker={subWorker} app="counter" props={{ label: 'nested' }} />;
|
|
12
|
+
* }
|
|
13
|
+
*
|
|
14
|
+
* `SubIsland` differs only in defaults: it tags devtools mounts as
|
|
15
|
+
* `framework: 'react'` and stamps `data-atoll-sub-island` on its host div.
|
|
16
|
+
*/
|
|
17
|
+
import type { CSSProperties, ReactElement } from 'react';
|
|
18
|
+
import type { IslandClient, IslandHandle, Mode } from '@atolljs/islands/worker';
|
|
19
|
+
import { Island } from './index';
|
|
20
|
+
|
|
21
|
+
export interface SubIslandProps {
|
|
22
|
+
/** Bundler-detectable sub-worker entry, evaluated INSIDE this worker. */
|
|
23
|
+
worker?: (() => Worker) | URL;
|
|
24
|
+
/** A shared `connectIslandWorker` client built inside this worker. */
|
|
25
|
+
client?: IslandClient;
|
|
26
|
+
/** Registry app name — a key of the sub-worker's `apps` map. */
|
|
27
|
+
app?: string;
|
|
28
|
+
/** Wire props for the sub-app's root — serializable data only. */
|
|
29
|
+
props?: Record<string, unknown>;
|
|
30
|
+
/** Sub-island emit channel — a plain worker-side call. */
|
|
31
|
+
onEvent?: (name: string, payload: unknown) => void;
|
|
32
|
+
/** Flush transport — 'push' (default) or 'poll'. */
|
|
33
|
+
mode?: Mode;
|
|
34
|
+
className?: string;
|
|
35
|
+
style?: CSSProperties;
|
|
36
|
+
/** Receives the mounted handle (pid, instance, updateProps, destroy). */
|
|
37
|
+
onReady?: (handle: IslandHandle) => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function SubIsland(props: SubIslandProps): ReactElement {
|
|
41
|
+
return <Island framework="react" data-atoll-sub-island="" {...props} />;
|
|
42
|
+
}
|
package/src/worker.ts
CHANGED
|
@@ -52,6 +52,13 @@ import { createReactInstance } from './reactInstance';
|
|
|
52
52
|
// island→shell channel — `import { emit } from '@atolljs/react-island/worker'`.
|
|
53
53
|
export { emit, runInInstance } from '@atolljs/islands/worker';
|
|
54
54
|
export { Slot } from './slot';
|
|
55
|
+
// The page's mount components, unchanged: inside a worker the container is
|
|
56
|
+
// a proxy element and mountIsland takes the nested (sub-worker) path.
|
|
57
|
+
export { Island, islandComponent, lazyIsland } from './index';
|
|
58
|
+
export type { IslandProps, IslandShellProps, IslandAppRef } from './index';
|
|
59
|
+
export { connectIslandWorker } from '@atolljs/islands/worker';
|
|
60
|
+
export { SubIsland } from './subIsland';
|
|
61
|
+
export type { SubIslandProps } from './subIsland';
|
|
55
62
|
|
|
56
63
|
type AnyComponent = ComponentType<Record<string, unknown>>;
|
|
57
64
|
|
|
@@ -72,6 +79,7 @@ export function reactIslandApp(
|
|
|
72
79
|
contract?: IslandContract,
|
|
73
80
|
): RenderedIslandApp {
|
|
74
81
|
const app: RenderedIslandApp = {
|
|
82
|
+
renderer: 'react',
|
|
75
83
|
mount({ instance, props }: RenderContext): RenderedHandle {
|
|
76
84
|
const react = createReactInstance(instance);
|
|
77
85
|
const render = (next: Record<string, unknown>): void =>
|