@atolljs/react-island 0.1.6 → 0.1.8

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 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 — one OS thread).
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.6",
3
+ "version": "0.1.8",
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.6",
20
- "@atolljs/islands": "0.1.6",
19
+ "@atolljs/core": "0.1.8",
20
+ "@atolljs/islands": "0.1.8",
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 pool options — concurrency, taskTimeout, respawn… (poolSize stays
91
- * 1). `doorbell: false` builds the shorthand client message-only — no
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({ worker: resolvedWorker, ...workerOptions })
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 =>