@atolljs/react-island 0.1.12 → 0.1.13

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.
@@ -0,0 +1,226 @@
1
+ // src/index.tsx
2
+ import { useEffect, useRef, useState } from "react";
3
+ import { createPortal } from "react-dom";
4
+ import {
5
+ connectIslandWorker,
6
+ contractWorkerOf,
7
+ islandAppNameOf,
8
+ mountIsland
9
+ } from "@atolljs/islands";
10
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
11
+ function Island({
12
+ app,
13
+ worker,
14
+ client: clientProp,
15
+ workerOptions,
16
+ mode,
17
+ framework,
18
+ props,
19
+ onEvent,
20
+ onActivity,
21
+ slots,
22
+ onReady,
23
+ onError,
24
+ ref,
25
+ ...rest
26
+ }) {
27
+ const containerRef = useRef(null);
28
+ const islandRef = useRef(null);
29
+ const [slotTargets, setSlotTargets] = useState({});
30
+ const propsRef = useRef(props);
31
+ const onEventRef = useRef(onEvent);
32
+ const onActivityRef = useRef(onActivity);
33
+ const slotsRef = useRef(slots);
34
+ const onReadyRef = useRef(onReady);
35
+ const onErrorRef = useRef(onError);
36
+ propsRef.current = props;
37
+ onEventRef.current = onEvent;
38
+ onActivityRef.current = onActivity;
39
+ slotsRef.current = slots;
40
+ onReadyRef.current = onReady;
41
+ onErrorRef.current = onError;
42
+ const setRefs = (el) => {
43
+ containerRef.current = el;
44
+ if (typeof ref === "function") ref(el);
45
+ else if (ref != null) ref.current = el;
46
+ };
47
+ const appName = app === void 0 ? "main" : islandAppNameOf(app);
48
+ useEffect(() => {
49
+ const el = containerRef.current;
50
+ if (el === null) return;
51
+ let cancelled = false;
52
+ let island = null;
53
+ const report = (err) => {
54
+ if (onErrorRef.current !== void 0) onErrorRef.current(err);
55
+ else console.error("[Island] mount failed:", err);
56
+ };
57
+ if (appName === void 0) {
58
+ report(
59
+ new Error(
60
+ "<Island> could not resolve an app name \u2014 pass the registry key or stamp the app with islandApp(name, app)"
61
+ )
62
+ );
63
+ return;
64
+ }
65
+ const resolvedWorker = worker ?? contractWorkerOf(app);
66
+ const client = clientProp ?? (resolvedWorker !== void 0 ? connectIslandWorker({
67
+ name: appName,
68
+ worker: resolvedWorker,
69
+ doorbell: mode !== "poll",
70
+ ...workerOptions
71
+ }) : void 0);
72
+ if (client === void 0) {
73
+ report(new Error("<Island> requires either `worker` or `client`"));
74
+ return;
75
+ }
76
+ const slotProxy = new Proxy({}, {
77
+ get: (_t, name) => (e) => {
78
+ setSlotTargets((prev) => prev[name] === e ? prev : { ...prev, [name]: e });
79
+ },
80
+ has: (_t, name) => slotsRef.current?.[name] != null
81
+ });
82
+ void (async () => {
83
+ try {
84
+ const handle = await mountIsland({
85
+ client,
86
+ el,
87
+ app: appName,
88
+ mode,
89
+ framework,
90
+ // Serialized across the wire either way — cast preserves inference
91
+ // for props types without an index signature.
92
+ props: propsRef.current ?? {},
93
+ onEvent: (name, payload) => onEventRef.current?.(name, payload),
94
+ onActivity: () => onActivityRef.current?.(),
95
+ slots: slotProxy
96
+ });
97
+ if (cancelled) {
98
+ handle.destroy();
99
+ return;
100
+ }
101
+ island = handle;
102
+ islandRef.current = handle;
103
+ onReadyRef.current?.(handle);
104
+ } catch (err) {
105
+ if (!cancelled) report(err);
106
+ }
107
+ })();
108
+ return () => {
109
+ cancelled = true;
110
+ islandRef.current = null;
111
+ island?.destroy();
112
+ };
113
+ }, [app, appName, clientProp]);
114
+ const propsJson = JSON.stringify(props ?? {});
115
+ useEffect(() => {
116
+ const island = islandRef.current;
117
+ if (island === null) return;
118
+ island.updateProps(props ?? {}).catch((err) => {
119
+ if (onErrorRef.current !== void 0) onErrorRef.current(err);
120
+ else console.error("[Island] updateProps failed:", err);
121
+ });
122
+ }, [propsJson]);
123
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
124
+ /* @__PURE__ */ jsx("div", { ref: setRefs, ...rest }),
125
+ Object.entries(slotTargets).map(
126
+ ([name, el]) => el && slotsRef.current?.[name] != null ? createPortal(slotsRef.current[name], el, `atoll-slot-${name}`) : null
127
+ )
128
+ ] });
129
+ }
130
+ var SHELL_PROP_KEYS = /* @__PURE__ */ new Set([
131
+ "worker",
132
+ "client",
133
+ "workerOptions",
134
+ "mode",
135
+ "framework",
136
+ "onEvent",
137
+ "onActivity",
138
+ "onReady",
139
+ "onError",
140
+ "slots",
141
+ "fallback",
142
+ "containerProps"
143
+ ]);
144
+ var splitProxyProps = (raw) => {
145
+ const shell = {};
146
+ const islandProps = {};
147
+ for (const [key, value] of Object.entries(raw)) {
148
+ if (SHELL_PROP_KEYS.has(key)) shell[key] = value;
149
+ else islandProps[key] = value;
150
+ }
151
+ return { shell, islandProps };
152
+ };
153
+ var lazyCache = /* @__PURE__ */ new WeakMap();
154
+ function useLazyApp(loader) {
155
+ let state = lazyCache.get(loader);
156
+ if (state === void 0) {
157
+ state = {
158
+ promise: Promise.resolve().then(loader).then((mod) => {
159
+ const unwrapped = mod !== null && typeof mod === "object" && "default" in mod ? mod.default : mod;
160
+ const isContract = unwrapped !== null && typeof unwrapped === "object" && "app" in unwrapped;
161
+ const resolvedApp = isContract ? unwrapped.app : unwrapped;
162
+ const resolvedWorker = isContract ? unwrapped.worker : void 0;
163
+ state.value = resolvedApp;
164
+ state.worker = resolvedWorker;
165
+ return resolvedApp;
166
+ }).catch((err) => {
167
+ state.error = err;
168
+ throw err;
169
+ })
170
+ };
171
+ lazyCache.set(loader, state);
172
+ }
173
+ if (state.error !== void 0) throw state.error;
174
+ if (state.value === void 0) throw state.promise;
175
+ return { app: state.value, worker: state.worker };
176
+ }
177
+ function ProxyIsland({
178
+ source,
179
+ ...raw
180
+ }) {
181
+ const resolved = source.kind === "static" ? { app: source.app } : useLazyApp(source.loader);
182
+ const { shell, islandProps } = splitProxyProps(raw);
183
+ const [ready, setReady] = useState(false);
184
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
185
+ !ready ? shell.fallback : null,
186
+ /* @__PURE__ */ jsx(
187
+ Island,
188
+ {
189
+ app: resolved.app,
190
+ worker: shell.worker ?? resolved.worker,
191
+ client: shell.client,
192
+ workerOptions: shell.workerOptions,
193
+ mode: shell.mode,
194
+ framework: shell.framework,
195
+ props: islandProps,
196
+ onEvent: shell.onEvent,
197
+ onActivity: shell.onActivity,
198
+ slots: shell.slots,
199
+ onReady: (h) => {
200
+ shell.onReady?.(h);
201
+ setReady(true);
202
+ },
203
+ onError: shell.onError,
204
+ ...shell.containerProps
205
+ }
206
+ )
207
+ ] });
208
+ }
209
+ function islandComponent(app) {
210
+ const Proxy2 = (props) => /* @__PURE__ */ jsx(ProxyIsland, { source: { kind: "static", app }, ...props });
211
+ Proxy2.displayName = `IslandComponent(${islandAppNameOf(app) ?? "main"})`;
212
+ return Proxy2;
213
+ }
214
+ function lazyIsland(loader) {
215
+ const Proxy2 = (props) => /* @__PURE__ */ jsx(ProxyIsland, { source: { kind: "lazy", loader }, ...props });
216
+ Proxy2.displayName = "LazyIsland";
217
+ return Proxy2;
218
+ }
219
+ var index_default = Island;
220
+
221
+ export {
222
+ Island,
223
+ islandComponent,
224
+ lazyIsland,
225
+ index_default
226
+ };
@@ -0,0 +1,203 @@
1
+ /**
2
+ * React-reconciler host adapter for the worker DOM backend.
3
+ *
4
+ * The generic instance state (op queues, instance ids, handler registry,
5
+ * doorbell, geometry) lives in `@atolljs/islands/worker`'s instance module.
6
+ * This file only wires the reconciler's create/append/update/remove/commit
7
+ * hooks to that backend.
8
+ *
9
+ * react-reconciler@0.34 reads ~150 fields off the config object. Most are
10
+ * stubs for features this renderer doesn't implement (hydration,
11
+ * persistence, resources, singletons, view transitions, test selectors,
12
+ * scope/hydratable APIs) — they exist so nothing throws when the reconciler
13
+ * destructures them; they are never invoked behind the `supports*` flags.
14
+ */
15
+ import { bumpOpsVersion, type ElementInstance, type HostInstance, type TextInstance } from '@atolljs/islands/worker';
16
+ declare const noop: () => void;
17
+ declare const NULL: () => null;
18
+ declare const FALSE: () => boolean;
19
+ declare const TRUE: () => boolean;
20
+ interface HostContext {
21
+ ns?: string;
22
+ }
23
+ export declare const hostConfig: {
24
+ rendererVersion: string;
25
+ rendererPackageName: string;
26
+ extraDevToolsConfig: null;
27
+ isPrimaryRenderer: boolean;
28
+ warnsIfNotActing: boolean;
29
+ supportsMutation: boolean;
30
+ supportsPersistence: boolean;
31
+ supportsHydration: boolean;
32
+ supportsResources: boolean;
33
+ supportsSingletons: boolean;
34
+ supportsMicrotasks: boolean;
35
+ supportsTestSelectors: boolean;
36
+ getPublicInstance: (instance: HostInstance) => unknown;
37
+ getRootHostContext: (container: unknown) => HostContext;
38
+ getChildHostContext: (parent: HostContext, type: string) => HostContext;
39
+ prepareForCommit: typeof NULL;
40
+ resetAfterCommit: typeof bumpOpsVersion;
41
+ createInstance: (type: string, props: Record<string, unknown>, rootContainer: unknown, hostContext: unknown, _internalHandle: unknown) => ElementInstance;
42
+ createTextInstance: (text: string, rootContainer: unknown, _hostContext: unknown, _internalHandle: unknown) => TextInstance;
43
+ finalizeInitialChildren: typeof FALSE;
44
+ shouldSetTextContent: typeof FALSE;
45
+ appendInitialChild: (parent: HostInstance, child: HostInstance) => void;
46
+ appendChild: (parent: HostInstance, child: HostInstance) => void;
47
+ appendChildToContainer: (container: unknown, child: HostInstance) => void;
48
+ insertBefore: (parent: HostInstance, child: HostInstance, before: HostInstance) => void;
49
+ insertInContainerBefore: (container: unknown, child: HostInstance, before: HostInstance) => void;
50
+ removeChild: (_parent: HostInstance, child: HostInstance) => void;
51
+ removeChildFromContainer: (_container: unknown, child: HostInstance) => void;
52
+ clearContainer: () => void;
53
+ commitUpdate: (instance: ElementInstance, _type: string, _oldProps: Record<string, unknown>, newProps: Record<string, unknown>, _internalHandle: unknown) => void;
54
+ commitTextUpdate: (instance: TextInstance, _oldText: string, newText: string) => void;
55
+ commitMount: typeof noop;
56
+ resetTextContent: (instance: ElementInstance) => void;
57
+ hideInstance: (instance: ElementInstance) => void;
58
+ unhideInstance: (instance: ElementInstance) => void;
59
+ hideTextInstance: (instance: TextInstance) => void;
60
+ unhideTextInstance: (instance: TextInstance, text: string) => void;
61
+ detachDeletedInstance: (instance: HostInstance) => void;
62
+ scheduleTimeout: typeof setTimeout;
63
+ cancelTimeout: typeof clearTimeout;
64
+ noTimeout: number;
65
+ scheduleMicrotask: (fn: () => void) => void;
66
+ setCurrentUpdatePriority: (p: number) => void;
67
+ getCurrentUpdatePriority: () => number;
68
+ resolveUpdatePriority: () => number;
69
+ resolveEventType: typeof NULL;
70
+ resolveEventTimeStamp: () => number;
71
+ trackSchedulerEvent: typeof noop;
72
+ shouldAttemptEagerTransition: typeof FALSE;
73
+ getInstanceFromNode: typeof NULL;
74
+ beforeActiveInstanceBlur: typeof noop;
75
+ afterActiveInstanceBlur: typeof noop;
76
+ preparePortalMount: typeof noop;
77
+ prepareScopeUpdate: typeof noop;
78
+ getInstanceFromScope: typeof NULL;
79
+ requestPostPaintCallback: typeof noop;
80
+ maySuspendCommit: typeof FALSE;
81
+ maySuspendCommitOnUpdate: typeof FALSE;
82
+ maySuspendCommitInSyncRender: typeof FALSE;
83
+ preloadInstance: typeof TRUE;
84
+ startSuspendingCommit: typeof noop;
85
+ suspendInstance: typeof noop;
86
+ suspendOnActiveViewTransition: typeof noop;
87
+ waitForCommitToBeReady: typeof NULL;
88
+ getSuspendedCommitReason: () => number;
89
+ NotPendingTransition: Readonly<{
90
+ pending: false;
91
+ data: null;
92
+ method: null;
93
+ action: null;
94
+ }>;
95
+ HostTransitionContext: import("react").Context<Readonly<{
96
+ pending: false;
97
+ data: null;
98
+ method: null;
99
+ action: null;
100
+ }>>;
101
+ resetFormInstance: typeof noop;
102
+ bindToConsole: (methodName: string, args: unknown[]) => (...args: unknown[]) => void;
103
+ createContainerChildSet: () => never[];
104
+ appendChildToContainerChildSet: typeof noop;
105
+ finalizeContainerChildren: typeof noop;
106
+ replaceContainerChildren: typeof noop;
107
+ cloneInstance: typeof NULL;
108
+ cloneMutableInstance: (instance: HostInstance) => HostInstance;
109
+ cloneMutableTextInstance: (instance: HostInstance) => HostInstance;
110
+ cloneHiddenInstance: (instance: HostInstance) => HostInstance;
111
+ cloneHiddenTextInstance: (instance: HostInstance) => HostInstance;
112
+ createFragmentInstance: () => {};
113
+ updateFragmentInstanceFiber: typeof noop;
114
+ commitNewChildToFragmentInstance: typeof noop;
115
+ deleteChildFromFragmentInstance: typeof noop;
116
+ createViewTransitionInstance: (name: string) => {
117
+ name: string;
118
+ autoName: null;
119
+ };
120
+ applyViewTransitionName: typeof noop;
121
+ restoreViewTransitionName: typeof noop;
122
+ cancelViewTransitionName: typeof noop;
123
+ cancelRootViewTransitionName: typeof noop;
124
+ restoreRootViewTransitionName: typeof noop;
125
+ cloneRootViewTransitionContainer: typeof noop;
126
+ removeRootViewTransitionClone: typeof noop;
127
+ measureInstance: typeof NULL;
128
+ measureClonedInstance: typeof NULL;
129
+ wasInstanceInViewport: typeof FALSE;
130
+ hasInstanceChanged: typeof FALSE;
131
+ hasInstanceAffectedParent: typeof FALSE;
132
+ startViewTransition: typeof NULL;
133
+ startGestureTransition: typeof NULL;
134
+ stopViewTransition: typeof noop;
135
+ addViewTransitionFinishedListener: typeof noop;
136
+ getCurrentGestureOffset: () => number;
137
+ isSuspenseInstancePending: typeof FALSE;
138
+ isSuspenseInstanceFallback: typeof FALSE;
139
+ getSuspenseInstanceFallbackErrorDetails: () => {};
140
+ registerSuspenseInstanceRetry: typeof noop;
141
+ getNextHydratableSibling: typeof NULL;
142
+ getNextHydratableSiblingAfterSingleton: typeof NULL;
143
+ getFirstHydratableChild: typeof NULL;
144
+ getFirstHydratableChildWithinContainer: typeof NULL;
145
+ getFirstHydratableChildWithinActivityInstance: typeof NULL;
146
+ getFirstHydratableChildWithinSingleton: typeof NULL;
147
+ getFirstHydratableChildWithinSuspenseInstance: typeof NULL;
148
+ canHydrateInstance: typeof NULL;
149
+ canHydrateTextInstance: typeof NULL;
150
+ canHydrateActivityInstance: typeof NULL;
151
+ canHydrateSuspenseInstance: typeof NULL;
152
+ canHydrateFormStateMarker: typeof NULL;
153
+ isFormStateMarkerMatching: typeof FALSE;
154
+ hydrateInstance: typeof noop;
155
+ hydrateTextInstance: typeof noop;
156
+ hydrateActivityInstance: typeof noop;
157
+ hydrateSuspenseInstance: typeof noop;
158
+ getNextHydratableInstanceAfterActivityInstance: typeof NULL;
159
+ getNextHydratableInstanceAfterSuspenseInstance: typeof NULL;
160
+ commitHydratedInstance: typeof noop;
161
+ commitHydratedContainer: typeof noop;
162
+ commitHydratedSuspenseInstance: typeof noop;
163
+ finalizeHydratedChildren: typeof FALSE;
164
+ flushHydrationEvents: typeof noop;
165
+ clearActivityBoundary: typeof FALSE;
166
+ clearSuspenseBoundary: typeof FALSE;
167
+ clearActivityBoundaryFromContainer: typeof FALSE;
168
+ clearSuspenseBoundaryFromContainer: typeof FALSE;
169
+ hideDehydratedBoundary: typeof noop;
170
+ unhideDehydratedBoundary: typeof noop;
171
+ shouldDeleteUnhydratedTailInstances: typeof FALSE;
172
+ diffHydratedPropsForDevWarnings: typeof NULL;
173
+ diffHydratedTextForDevWarnings: typeof NULL;
174
+ describeHydratableInstanceForDevWarnings: typeof NULL;
175
+ validateHydratableInstance: typeof TRUE;
176
+ validateHydratableTextInstance: typeof TRUE;
177
+ isHostHoistableType: typeof FALSE;
178
+ getHoistableRoot: typeof NULL;
179
+ getResource: typeof NULL;
180
+ acquireResource: typeof noop;
181
+ releaseResource: typeof noop;
182
+ hydrateHoistable: typeof noop;
183
+ mountHoistable: typeof noop;
184
+ unmountHoistable: typeof noop;
185
+ createHoistableInstance: typeof NULL;
186
+ prepareToCommitHoistables: typeof noop;
187
+ mayResourceSuspendCommit: typeof FALSE;
188
+ preloadResource: typeof TRUE;
189
+ suspendResource: typeof noop;
190
+ resolveSingletonInstance: typeof NULL;
191
+ acquireSingletonInstance: typeof noop;
192
+ releaseSingletonInstance: typeof noop;
193
+ isHostSingletonType: typeof FALSE;
194
+ isSingletonScope: typeof FALSE;
195
+ findFiberRoot: typeof NULL;
196
+ getBoundingRect: typeof NULL;
197
+ getTextContent: typeof NULL;
198
+ isHiddenSubtree: typeof FALSE;
199
+ matchAccessibilityRole: typeof FALSE;
200
+ setFocusIfFocusable: typeof FALSE;
201
+ setupIntersectionObserver: typeof noop;
202
+ };
203
+ export {};
@@ -0,0 +1,165 @@
1
+ import type { HTMLAttributes, ReactElement, ReactNode, Ref } from 'react';
2
+ import type { IslandAppLike, IslandAppProps, IslandClient, IslandContract, IslandContractEventHandler, IslandContractProps, IslandHandle, IslandWorkerOptions, Mode } from '@atolljs/islands';
3
+ export type { IslandAppLike, IslandAppProps, IslandHandle };
4
+ /** What `app` accepts: the registry name, or a component/def to resolve. */
5
+ export type IslandAppRef<A> = A | string;
6
+ export interface IslandProps<A = string> extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
7
+ /**
8
+ * Which app to mount — the `apps` registry key ('charts') or the app
9
+ * itself (`ChartsApp`, an `islandApp(...)`-stamped def, `{ imperative }`).
10
+ * A reference infers `props` from its own signature. Optional against a
11
+ * `defineMonoWorker` (1:1) worker — its single app mounts regardless.
12
+ */
13
+ app?: IslandAppRef<A>;
14
+ /**
15
+ * How to reach the worker — either a bundler-detectable factory
16
+ * `() => new Worker(new URL('./x.worker.ts', import.meta.url), { type: 'module' })`
17
+ * (the common case) or a pre-connected `IslandClient`. Exactly one is required.
18
+ */
19
+ worker?: (() => Worker) | URL;
20
+ client?: IslandClient;
21
+ /**
22
+ * Extra client options — taskTimeout, respawn, lazy… (always one
23
+ * dedicated worker, no pool). `doorbell: false` builds the shorthand client message-only — no
24
+ * SharedArrayBuffer, no COOP/COEP requirement (same as `mode: 'poll'` on
25
+ * a direct `mountIsland` shorthand mount).
26
+ */
27
+ workerOptions?: IslandWorkerOptions & {
28
+ doorbell?: boolean;
29
+ };
30
+ /**
31
+ * Initial flush mode — 'push' (default) drives op replay off the
32
+ * shared-memory doorbell (needs cross-origin isolation); 'poll' drains on
33
+ * a 50ms interval and, for `worker`-shorthand mounts, builds the client
34
+ * doorbell-free — no SharedArrayBuffer, no COOP/COEP requirement.
35
+ * Mount-time only; switch later via the handle's `setMode`.
36
+ */
37
+ mode?: Mode;
38
+ /** Renderer tag reported to devtools on mount ('react', 'vue', …). */
39
+ framework?: string;
40
+ /** Initial + updated root props — serialized to the worker. */
41
+ props?: IslandAppProps<A>;
42
+ /** Island → shell channel: every `emit` op lands here. */
43
+ onEvent?: (name: string, payload: unknown) => void;
44
+ /**
45
+ * Transclusion slots — a worker `<div data-atoll-slot="name">` becomes a
46
+ * React portal mount point. The element's contents are rendered by React
47
+ * on the main thread; the worker only owns the empty anchor.
48
+ */
49
+ slots?: Record<string, ReactNode>;
50
+ /** Fired after each applied op batch — stats hooks. */
51
+ onActivity?: () => void;
52
+ /** The mounted handle (pid, updateProps, flush, setMode…) once ready. */
53
+ onReady?: (island: IslandHandle) => void;
54
+ /** Mount/update errors surface here instead of an unhandled rejection. */
55
+ onError?: (err: unknown) => void;
56
+ /** The container div (React 19 ref-as-prop). */
57
+ ref?: Ref<HTMLDivElement>;
58
+ }
59
+ export declare function Island<A = string>({ app, worker, client: clientProp, workerOptions, mode, framework, props, onEvent, onActivity, slots, onReady, onError, ref, ...rest }: IslandProps<A>): ReactElement;
60
+ /**
61
+ * Props consumed by a proxy component itself — everything else forwards to
62
+ * the island as its props.
63
+ */
64
+ export interface IslandShellProps {
65
+ /** How to reach the worker — see IslandProps.worker/client. */
66
+ worker?: (() => Worker) | URL;
67
+ client?: IslandClient;
68
+ /** See IslandProps.workerOptions — `doorbell` included. */
69
+ workerOptions?: IslandWorkerOptions & {
70
+ doorbell?: boolean;
71
+ };
72
+ /** Initial flush mode — see IslandProps.mode. */
73
+ mode?: Mode;
74
+ /** Devtools renderer tag — see IslandProps.framework. */
75
+ framework?: string;
76
+ /** Island → shell channel: every `emit` op lands here. */
77
+ onEvent?: (name: string, payload: unknown) => void;
78
+ /** Fired after each applied op batch. */
79
+ onActivity?: () => void;
80
+ /** The mounted handle (pid, updateProps, flush, setMode…) once ready. */
81
+ onReady?: (island: IslandHandle) => void;
82
+ /** Mount/update errors surface here instead of an unhandled rejection. */
83
+ onError?: (err: unknown) => void;
84
+ /** Transclusion slots — see IslandProps.slots. */
85
+ slots?: Record<string, ReactNode>;
86
+ /** Rendered while the worker mounts — the lazy-side fallback. */
87
+ fallback?: ReactNode;
88
+ /**
89
+ * Attributes for the island's container div (className/id/style/data-*).
90
+ * `onError` is omitted — it collides with the mount-error callback.
91
+ */
92
+ containerProps?: Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'onError'>;
93
+ }
94
+ type LazyModule<A> = A | {
95
+ default: A;
96
+ } | {
97
+ app: A | string;
98
+ worker?: (() => Worker) | URL;
99
+ };
100
+ /**
101
+ * The app a lazy module resolves to — unwraps `{ default: A }` and the
102
+ * contract-module shape `{ app: A, worker? }`, passes bare A through.
103
+ */
104
+ export type LazyResolvedApp<T extends Promise<unknown>> = [
105
+ Awaited<T>
106
+ ] extends [never] ? string : Awaited<T> extends {
107
+ readonly islandContract: true;
108
+ } ? Awaited<T> : Awaited<T> extends {
109
+ app: infer A;
110
+ } ? A : Awaited<T> extends {
111
+ default: infer D;
112
+ } ? D : Awaited<T>;
113
+ /**
114
+ * `islandComponent<P>('charts')` — a proxy component for a worker app that
115
+ * the shell NEVER imports. `P` is the contract (usually an `import type` of
116
+ * the worker component's props); the string is the registry key. Against a
117
+ * `defineMonoWorker` (1:1) worker the name can be omitted entirely.
118
+ *
119
+ * ```tsx
120
+ * import type { TableProps } from './worker/apps.js';
121
+ * const TableIsland = islandComponent<TableProps>('data-table');
122
+ * <TableIsland worker={renderWorker} filter={filter} desc={desc} />
123
+ * ```
124
+ */
125
+ /**
126
+ * `islandComponent(checkoutContract)` — the cross-framework facade: a
127
+ * worker app described by `defineIslandContract` mounts by the contract's
128
+ * `app` key, props come from its schema, and `onEvent` narrows to the
129
+ * declared event vocabulary. The shell imports the CONTRACT module only —
130
+ * never the component or its framework.
131
+ */
132
+ export declare function islandComponent<C extends IslandContract>(app: C): (props: IslandContractProps<C> & Omit<IslandShellProps, 'onEvent'> & {
133
+ onEvent?: IslandContractEventHandler<C>;
134
+ }) => ReactElement;
135
+ export declare function islandComponent<P extends object = Record<string, unknown>>(app?: string): (props: P & IslandShellProps) => ReactElement;
136
+ export declare function islandComponent<A extends IslandAppLike>(app: A): (props: IslandAppProps<A> & IslandShellProps) => ReactElement;
137
+ /**
138
+ * `lazyIsland(() => import('./worker/apps.js').then(m => ({ default: m.ChartsApp })))`
139
+ * — the `React.lazy` mirror for worker apps. The returned component suspends
140
+ * while the module loads (wrap it in `<Suspense>`), then proxies all props to
141
+ * the island. The dynamic import gives bundlers a split point, so the worker
142
+ * component's dependencies only load when the island mounts.
143
+ *
144
+ * ```tsx
145
+ * const ChartsIsland = lazyIsland(() => import('./worker/apps.js').then(m => ({ default: m.ChartsApp })));
146
+ * <Suspense fallback="loading…"><ChartsIsland width={520} worker={renderWorker} /></Suspense>
147
+ * ```
148
+ *
149
+ * CONTRACT MODULES: for `defineMonoWorker` (1:1) topologies the loader can
150
+ * resolve `{ app, worker }` — the island then carries its own worker factory
151
+ * and call sites need no `worker` prop at all:
152
+ *
153
+ * ```ts
154
+ * // worker/map.island.ts — shell-safe contract (never the .worker.ts entry
155
+ * // itself: defining a worker installs instance globals, don't import it)
156
+ * export { mapApp as app } from './map.js';
157
+ * export const worker = () => new Worker(new URL('./map.worker.ts', import.meta.url), { type: 'module' });
158
+ *
159
+ * // shell.tsx
160
+ * const MapIsland = lazyIsland(() => import('./worker/map.island.js'));
161
+ * <MapIsland /> // app + worker both from the contract
162
+ * ```
163
+ */
164
+ export declare function lazyIsland<T extends Promise<LazyModule<IslandAppLike>>>(loader: () => T): (props: IslandAppProps<LazyResolvedApp<T>> & IslandShellProps) => ReactElement;
165
+ export default Island;
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ import {
2
+ Island,
3
+ index_default,
4
+ islandComponent,
5
+ lazyIsland
6
+ } from "./chunk-Q7NIXFLZ.js";
7
+ export {
8
+ Island,
9
+ index_default as default,
10
+ islandComponent,
11
+ lazyIsland
12
+ };
@@ -0,0 +1,42 @@
1
+ /**
2
+ * The React-runtime half of a React island — `react`, `react-reconciler`
3
+ * and the host config live HERE so a worker bundle whose registry holds only
4
+ * non-React apps (Vue/Svelte/Solid/Angular/imperative) never pulls the
5
+ * reconciler in. `@atolljs/react-island/worker` reaches this module through
6
+ * `reactIslandApp`'s mount, inside the RenderedIslandApp contract.
7
+ */
8
+ import type { ReactElement } from 'react';
9
+ import Reconciler from 'react-reconciler';
10
+ /**
11
+ * A mounted React tree — carries its own reconciler + container and the
12
+ * `render`/`unmountTree`/`flush` verbs the adapter maps onto the
13
+ * RenderedHandle contract. Identity (key/app/pid) lives on the core
14
+ * Instance — this object is only the renderer half.
15
+ */
16
+ export interface ReactInstance {
17
+ reconciler: ReturnType<typeof Reconciler>;
18
+ container: unknown;
19
+ /**
20
+ * Commit synchronously around `fn`.
21
+ *
22
+ * In 0.34, `updateContainer`/setState only *schedule* work — the actual
23
+ * render+commit happens when the root scheduler task runs (a macrotask via
24
+ * the `scheduler` package). `flushSyncFromReconciler` pins the update
25
+ * priority to the discrete/sync lane for the duration of `fn` and
26
+ * `flushSyncWork` flushes pending sync work, so by the time it returns the
27
+ * mutation hooks have run and the instance's op queue is full.
28
+ */
29
+ sync(fn: () => void): void;
30
+ /** Schedule a mount/re-render of `el` — call inside `sync`. */
31
+ render(el: ReactElement): void;
32
+ /** Schedule the detach — emits the remove ops; call inside `sync`. */
33
+ unmountTree(): void;
34
+ /** Passive effects + pending sync work — the `flush` task's React branch. */
35
+ flush(): void;
36
+ }
37
+ /**
38
+ * Legacy root (tag 0) — no concurrent features. Container creation is
39
+ * per-instance, not module-level, so a second mount() can't collide with the
40
+ * first instance's tree.
41
+ */
42
+ export declare function createReactInstance(instance: string): ReactInstance;
package/dist/slot.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ /**
2
+ * A transclusion slot — renders a leaf element marked `data-atoll-slot`.
3
+ * The element's BOX belongs to this worker tree (layout/style ops apply as
4
+ * usual); its CONTENTS belong to the shell: when the create op lands on the
5
+ * main thread, the island's `slots[name]` callback gets the real element
6
+ * and can mount anything inside — a canvas, Monaco, a main-thread React
7
+ * root — all real DOM with real events and zero wire traffic.
8
+ *
9
+ * Must stay a LEAF in this tree: if worker-side children were appended here
10
+ * their ops would interleave with shell-owned content. There is also no
11
+ * measurement channel — the island knows the box it laid out, not the
12
+ * pixel size the shell put inside it.
13
+ */
14
+ import type { ReactElement } from 'react';
15
+ export declare function Slot({ name, style }: {
16
+ name: string;
17
+ style?: Record<string, unknown>;
18
+ }): ReactElement;
@@ -0,0 +1,37 @@
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
+ export interface SubIslandProps {
20
+ /** Bundler-detectable sub-worker entry, evaluated INSIDE this worker. */
21
+ worker?: (() => Worker) | URL;
22
+ /** A shared `connectIslandWorker` client built inside this worker. */
23
+ client?: IslandClient;
24
+ /** Registry app name — a key of the sub-worker's `apps` map. */
25
+ app?: string;
26
+ /** Wire props for the sub-app's root — serializable data only. */
27
+ props?: Record<string, unknown>;
28
+ /** Sub-island emit channel — a plain worker-side call. */
29
+ onEvent?: (name: string, payload: unknown) => void;
30
+ /** Flush transport — 'push' (default) or 'poll'. */
31
+ mode?: Mode;
32
+ className?: string;
33
+ style?: CSSProperties;
34
+ /** Receives the mounted handle (pid, instance, updateProps, destroy). */
35
+ onReady?: (handle: IslandHandle) => void;
36
+ }
37
+ export declare function SubIsland(props: SubIslandProps): ReactElement;
@@ -0,0 +1,87 @@
1
+ /**
2
+ * `@atolljs/react-island/worker` — the worker-side half of React
3
+ * islands: `reactIslandApp(Component)` wraps a React component as a islands
4
+ * `RenderedIslandApp`, so it can sit in a `definePolyWorker` `apps`
5
+ * registry beside Vue/Svelte/Solid/Angular and imperative apps:
6
+ *
7
+ * // render.worker.ts — React-only registry
8
+ * import { defineReactPolyWorker } from '@atolljs/react-island/worker';
9
+ * export const renderWorker = defineReactPolyWorker({
10
+ * apps: { controls: ControlsApp, charts: ChartsApp },
11
+ * });
12
+ *
13
+ * // one-app worker
14
+ * import { defineReactMonoWorker } from '@atolljs/react-island/worker';
15
+ * export const worker = defineReactMonoWorker(ChartsApp);
16
+ *
17
+ * // mixed-framework registry — compose the adapters directly
18
+ * import { definePolyWorker } from '@atolljs/islands/worker';
19
+ * import { reactIslandApp } from '@atolljs/react-island/worker';
20
+ * export const renderWorker = definePolyWorker({ apps: {
21
+ * charts: reactIslandApp(ChartsApp),
22
+ * badge: vueIslandApp(VueBadge),
23
+ * }});
24
+ *
25
+ * The reconciler + host config live in `reactInstance.ts`/`hostConfig.ts`:
26
+ * importing THIS module is what pulls `react` + `react-reconciler` into a
27
+ * worker bundle (once, shared across every React app in the registry).
28
+ * `@atolljs/islands/worker` itself stays framework-neutral — non-React
29
+ * registries never see this code.
30
+ */
31
+ import { type ComponentType } from 'react';
32
+ import type { SharedMemory, WorkerDefinition } from '@atolljs/core';
33
+ import type { DoorbellSpec, ImperativeIslandApp, IslandContract, IslandWorkerMethods, RenderedIslandApp } from '@atolljs/islands/worker';
34
+ export { emit, runInInstance } from '@atolljs/islands/worker';
35
+ export { Slot } from './slot.js';
36
+ export { Island, islandComponent, lazyIsland } from './index.js';
37
+ export type { IslandProps, IslandShellProps, IslandAppRef } from './index.js';
38
+ export { connectIslandWorker } from '@atolljs/islands/worker';
39
+ export { SubIsland } from './subIsland.js';
40
+ export type { SubIslandProps } from './subIsland.js';
41
+ type AnyComponent = ComponentType<Record<string, unknown>>;
42
+ /**
43
+ * Wrap a React component as a `RenderedIslandApp` — `mount` creates the
44
+ * instance's reconciler + container and renders the initial tree inside the
45
+ * sync lane; `update` re-renders with new props (the reconciler diffs, only
46
+ * changed ops emit); `sync` is the dispatch-time commit pin; `dispose`
47
+ * schedules the unmount; `flush` drains passive effects + pending sync work
48
+ * for the `flush` task.
49
+ *
50
+ * A stamped component (`islandApp('name', Comp)`) keeps its stamp through
51
+ * the wrap, so registry warn-checks and `defineMonoWorker` name resolution
52
+ * still see the shell-facing name.
53
+ */
54
+ export declare function reactIslandApp(App: AnyComponent, contract?: IslandContract): RenderedIslandApp;
55
+ /** Stamp + wrap in one step — `reactIsland('charts', ChartsApp)` yields the
56
+ * registry value AND the name the shell mounts by. */
57
+ export declare const reactIsland: (name: string, App: AnyComponent, contract?: IslandContract) => RenderedIslandApp & {
58
+ readonly islandAppName: string;
59
+ };
60
+ export interface ReactPolyWorkerRegistry {
61
+ /**
62
+ * Name → React component (or `{ imperative }` app) registry, mirroring
63
+ * `definePolyWorker({ apps })`. Components pass through `reactIslandApp`;
64
+ * imperative defs and already-wrapped apps (e.g. `reactIsland()` results)
65
+ * register as-is.
66
+ */
67
+ apps: Record<string, AnyComponent | ImperativeIslandApp | RenderedIslandApp>;
68
+ /** Doorbell contract override — forwarded to `definePolyWorker`. */
69
+ sharedMemory?: SharedMemory<DoorbellSpec>;
70
+ }
71
+ /**
72
+ * `definePolyWorker` for React apps — maps each component in the registry
73
+ * through `reactIslandApp` and delegates. One worker, many React islands;
74
+ * for a mixed-framework registry call `definePolyWorker` with the per-app
75
+ * adapters instead.
76
+ */
77
+ export declare function defineReactPolyWorker(registry: ReactPolyWorkerRegistry, options?: {
78
+ sharedMemory?: SharedMemory<DoorbellSpec>;
79
+ }): WorkerDefinition<DoorbellSpec, IslandWorkerMethods>;
80
+ /**
81
+ * `defineMonoWorker` for React apps — one worker pinned to a single
82
+ * component (or `{ imperative }` app), the isolated-bundle host shape.
83
+ */
84
+ export declare function defineReactMonoWorker(app: AnyComponent | ImperativeIslandApp | RenderedIslandApp, options?: {
85
+ sharedMemory?: SharedMemory<DoorbellSpec>;
86
+ contract?: IslandContract;
87
+ }): WorkerDefinition<DoorbellSpec, IslandWorkerMethods>;
package/dist/worker.js ADDED
@@ -0,0 +1,450 @@
1
+ import {
2
+ Island,
3
+ islandComponent,
4
+ lazyIsland
5
+ } from "./chunk-Q7NIXFLZ.js";
6
+
7
+ // src/worker.ts
8
+ import { createElement } from "react";
9
+ import {
10
+ defineMonoWorker,
11
+ definePolyWorker,
12
+ islandApp,
13
+ islandAppNameOf,
14
+ withContract
15
+ } from "@atolljs/islands/worker";
16
+
17
+ // src/reactInstance.ts
18
+ import Reconciler from "react-reconciler";
19
+
20
+ // src/hostConfig.ts
21
+ import { createContext } from "react";
22
+ import {
23
+ bumpOpsVersion,
24
+ docForInstance,
25
+ getActiveInstance,
26
+ getLastActiveInstance,
27
+ instances,
28
+ newElement,
29
+ newText,
30
+ pushOp,
31
+ serializeProps,
32
+ unregisterHandler
33
+ } from "@atolljs/islands/worker";
34
+ var currentUpdatePriority = 0;
35
+ var DEFAULT_EVENT_PRIORITY = 32;
36
+ var NotPendingTransition = Object.freeze({ pending: false, data: null, method: null, action: null });
37
+ var HostTransitionContext = createContext(NotPendingTransition);
38
+ var noop = () => {
39
+ };
40
+ var NULL = () => null;
41
+ var FALSE = () => false;
42
+ var TRUE = () => true;
43
+ var publicInstanceFor = (instance) => docForInstance(instance.instance).adopt(instance);
44
+ var containerParentId = (container) => {
45
+ const inst = container?.instance;
46
+ if (inst && typeof inst.id === "number") return inst.id;
47
+ const id = container?.id;
48
+ return typeof id === "number" ? id : 0;
49
+ };
50
+ var containerInstance = (container) => {
51
+ const inner = container?.instance;
52
+ if (typeof inner === "string") return inner;
53
+ return inner?.instance ?? (getActiveInstance() !== "" ? getActiveInstance() : getLastActiveInstance());
54
+ };
55
+ var SVG_NS = "http://www.w3.org/2000/svg";
56
+ var MATH_NS = "http://www.w3.org/1998/Math/MathML";
57
+ var HTML_CTX = Object.freeze({});
58
+ var SVG_CTX = Object.freeze({ ns: SVG_NS });
59
+ var MATH_CTX = Object.freeze({ ns: MATH_NS });
60
+ var childHostContextFor = (parent, type) => {
61
+ if (type === "svg") return SVG_CTX;
62
+ if (type === "math") return MATH_CTX;
63
+ if (type === "foreignObject" || type === "desc" || type === "title") return HTML_CTX;
64
+ return parent;
65
+ };
66
+ var hostConfig = {
67
+ // Identity / capabilities
68
+ rendererVersion: "0.34.0",
69
+ rendererPackageName: "@atolljs/react-island",
70
+ extraDevToolsConfig: null,
71
+ isPrimaryRenderer: true,
72
+ warnsIfNotActing: false,
73
+ supportsMutation: true,
74
+ supportsPersistence: false,
75
+ supportsHydration: false,
76
+ supportsResources: false,
77
+ supportsSingletons: false,
78
+ supportsMicrotasks: false,
79
+ supportsTestSelectors: false,
80
+ getPublicInstance: (instance) => publicInstanceFor(instance),
81
+ // Host contexts must be non-null objects (the reconciler pushes them on a
82
+ // context stack and warns "Expected host context to exist" on null). This
83
+ // renderer carries no per-subtree context — echo a single frozen sentinel.
84
+ // Host contexts must be non-null objects (the reconciler warns on null).
85
+ // Ours carries only the element namespace: <svg>/<math> enter their
86
+ // namespaces; foreignObject/desc/title are HTML-integration points that
87
+ // flip children back to HTML; everything else inherits the parent's.
88
+ // For portals, this becomes the context the portal subtree renders under —
89
+ // derived from the container's own namespace so children of an SVG portal
90
+ // target (recharts zIndex <g> layers) stay in the SVG namespace.
91
+ getRootHostContext: (container) => {
92
+ const ns = container?.instance?.ns;
93
+ if (ns === SVG_NS) return SVG_CTX;
94
+ if (ns === MATH_NS) return MATH_CTX;
95
+ return HTML_CTX;
96
+ },
97
+ getChildHostContext: (parent, type) => childHostContextFor(parent, type),
98
+ prepareForCommit: NULL,
99
+ // The doorbell: every commit bumps opsVersion once — the main thread's
100
+ // observe() wakes (Atomics.waitAsync) and flushes the op queue, so commits
101
+ // made outside task calls (effects, timers, async setState) arrive as a
102
+ // push instead of waiting on a poll. Task-returned ops bump too; the
103
+ // follow-up flush just finds an empty queue.
104
+ resetAfterCommit: bumpOpsVersion,
105
+ // Creation — emit ops
106
+ createInstance: (type, props, rootContainer, hostContext, _internalHandle) => {
107
+ const ctx = childHostContextFor(hostContext, type);
108
+ const instance = newElement(type, props, ctx.ns, containerInstance(rootContainer));
109
+ pushOp(instance.instance, {
110
+ t: "create",
111
+ id: instance.id,
112
+ type,
113
+ props: instance.props,
114
+ ns: instance.ns
115
+ });
116
+ return instance;
117
+ },
118
+ createTextInstance: (text, rootContainer, _hostContext, _internalHandle) => {
119
+ const instance = newText(text, containerInstance(rootContainer));
120
+ pushOp(instance.instance, { t: "text", id: instance.id, text });
121
+ return instance;
122
+ },
123
+ finalizeInitialChildren: FALSE,
124
+ // Always false: string/number children become real text instances, so the
125
+ // tree arrives as linear ops and `utext` handles every text update. (The
126
+ // `true` path would expect commitUpdate to set textContent from
127
+ // props.children — which we deliberately never serialize.)
128
+ shouldSetTextContent: FALSE,
129
+ // Tree wiring — emit ops
130
+ appendInitialChild: (parent, child) => {
131
+ pushOp(parent.instance, { t: "append", parent: parent.id, child: child.id });
132
+ },
133
+ appendChild: (parent, child) => {
134
+ pushOp(parent.instance, { t: "append", parent: parent.id, child: child.id });
135
+ },
136
+ appendChildToContainer: (container, child) => {
137
+ pushOp(child.instance, { t: "append", parent: containerParentId(container), child: child.id });
138
+ },
139
+ insertBefore: (parent, child, before) => {
140
+ pushOp(parent.instance, { t: "append", parent: parent.id, child: child.id, before: before.id });
141
+ },
142
+ insertInContainerBefore: (container, child, before) => {
143
+ pushOp(child.instance, {
144
+ t: "append",
145
+ parent: containerParentId(container),
146
+ child: child.id,
147
+ before: before.id
148
+ });
149
+ },
150
+ removeChild: (_parent, child) => {
151
+ pushOp(child.instance, { t: "remove", child: child.id });
152
+ },
153
+ removeChildFromContainer: (_container, child) => {
154
+ pushOp(child.instance, { t: "remove", child: child.id });
155
+ },
156
+ // No instance argument — routed by the task's active instance (only reached
157
+ // inside mount/remount's syncCommit).
158
+ clearContainer: () => {
159
+ pushOp(getActiveInstance(), { t: "clear" });
160
+ },
161
+ // Updates — emit ops
162
+ commitUpdate: (instance, _type, _oldProps, newProps, _internalHandle) => {
163
+ instance.props = serializeProps(instance, newProps);
164
+ pushOp(instance.instance, { t: "update", id: instance.id, props: instance.props });
165
+ },
166
+ commitTextUpdate: (instance, _oldText, newText2) => {
167
+ instance.text = newText2;
168
+ pushOp(instance.instance, { t: "utext", id: instance.id, text: newText2 });
169
+ },
170
+ commitMount: noop,
171
+ // Unreachable while shouldSetTextContent is always false; still emit the
172
+ // spec'd op for completeness.
173
+ resetTextContent: (instance) => {
174
+ instance.props = {};
175
+ pushOp(instance.instance, { t: "update", id: instance.id, props: {} });
176
+ },
177
+ // Suspense visibility — `hidden` is a global HTML attribute the main
178
+ // thread applies like any other prop.
179
+ hideInstance: (instance) => {
180
+ pushOp(instance.instance, { t: "update", id: instance.id, props: { ...instance.props, hidden: true } });
181
+ },
182
+ unhideInstance: (instance) => {
183
+ pushOp(instance.instance, { t: "update", id: instance.id, props: { ...instance.props } });
184
+ },
185
+ hideTextInstance: (instance) => {
186
+ pushOp(instance.instance, { t: "utext", id: instance.id, text: "" });
187
+ },
188
+ unhideTextInstance: (instance, text) => {
189
+ pushOp(instance.instance, { t: "utext", id: instance.id, text });
190
+ },
191
+ // GC hook — drop the record and its handler slots.
192
+ detachDeletedInstance: (instance) => {
193
+ instances.delete(instance.id);
194
+ if (instance.kind === "element") {
195
+ for (const hid of Object.values(instance.listenerSlots)) unregisterHandler(hid);
196
+ }
197
+ },
198
+ // Scheduler
199
+ scheduleTimeout: setTimeout,
200
+ cancelTimeout: clearTimeout,
201
+ noTimeout: -1,
202
+ // supportsMicrotasks is false, so the reconciler schedules root work via
203
+ // the `scheduler` package (MessageChannel in a worker). This export is
204
+ // still destructured — provide a real one for completeness.
205
+ scheduleMicrotask: (fn) => queueMicrotask(fn),
206
+ // Event priorities (see note above — values are lane numbers).
207
+ setCurrentUpdatePriority: (p) => {
208
+ currentUpdatePriority = p;
209
+ },
210
+ getCurrentUpdatePriority: () => currentUpdatePriority,
211
+ resolveUpdatePriority: () => currentUpdatePriority !== 0 ? currentUpdatePriority : DEFAULT_EVENT_PRIORITY,
212
+ resolveEventType: NULL,
213
+ resolveEventTimeStamp: () => -1.1,
214
+ trackSchedulerEvent: noop,
215
+ shouldAttemptEagerTransition: FALSE,
216
+ // Events/portals/scopes — no DOM event system exists worker-side.
217
+ getInstanceFromNode: NULL,
218
+ beforeActiveInstanceBlur: noop,
219
+ afterActiveInstanceBlur: noop,
220
+ preparePortalMount: noop,
221
+ prepareScopeUpdate: noop,
222
+ getInstanceFromScope: NULL,
223
+ // React 19 "suspend on commit" — never suspend.
224
+ requestPostPaintCallback: noop,
225
+ maySuspendCommit: FALSE,
226
+ maySuspendCommitOnUpdate: FALSE,
227
+ maySuspendCommitInSyncRender: FALSE,
228
+ preloadInstance: TRUE,
229
+ startSuspendingCommit: noop,
230
+ suspendInstance: noop,
231
+ suspendOnActiveViewTransition: noop,
232
+ waitForCommitToBeReady: NULL,
233
+ getSuspendedCommitReason: () => 0,
234
+ // Transitions / forms / console
235
+ NotPendingTransition,
236
+ HostTransitionContext,
237
+ resetFormInstance: noop,
238
+ bindToConsole: (methodName, args) => console[methodName]?.bind(console, ...args) ?? noop,
239
+ // Persistence (supportsPersistence: false — destructured, never called)
240
+ createContainerChildSet: () => [],
241
+ appendChildToContainerChildSet: noop,
242
+ finalizeContainerChildren: noop,
243
+ replaceContainerChildren: noop,
244
+ cloneInstance: NULL,
245
+ cloneMutableInstance: (instance) => instance,
246
+ cloneMutableTextInstance: (instance) => instance,
247
+ cloneHiddenInstance: (instance) => instance,
248
+ cloneHiddenTextInstance: (instance) => instance,
249
+ // Fragment instances (only used for `<Fragment ref={…}>` — unimplemented)
250
+ createFragmentInstance: () => ({}),
251
+ updateFragmentInstanceFiber: noop,
252
+ commitNewChildToFragmentInstance: noop,
253
+ deleteChildFromFragmentInstance: noop,
254
+ // View transitions (React 19) — all stubs
255
+ createViewTransitionInstance: (name) => ({ name, autoName: null }),
256
+ applyViewTransitionName: noop,
257
+ restoreViewTransitionName: noop,
258
+ cancelViewTransitionName: noop,
259
+ cancelRootViewTransitionName: noop,
260
+ restoreRootViewTransitionName: noop,
261
+ cloneRootViewTransitionContainer: noop,
262
+ removeRootViewTransitionClone: noop,
263
+ measureInstance: NULL,
264
+ measureClonedInstance: NULL,
265
+ wasInstanceInViewport: FALSE,
266
+ hasInstanceChanged: FALSE,
267
+ hasInstanceAffectedParent: FALSE,
268
+ startViewTransition: NULL,
269
+ startGestureTransition: NULL,
270
+ stopViewTransition: noop,
271
+ addViewTransitionFinishedListener: noop,
272
+ getCurrentGestureOffset: () => 0,
273
+ // Hydration (supportsHydration: false — destructured, never called)
274
+ isSuspenseInstancePending: FALSE,
275
+ isSuspenseInstanceFallback: FALSE,
276
+ getSuspenseInstanceFallbackErrorDetails: () => ({}),
277
+ registerSuspenseInstanceRetry: noop,
278
+ getNextHydratableSibling: NULL,
279
+ getNextHydratableSiblingAfterSingleton: NULL,
280
+ getFirstHydratableChild: NULL,
281
+ getFirstHydratableChildWithinContainer: NULL,
282
+ getFirstHydratableChildWithinActivityInstance: NULL,
283
+ getFirstHydratableChildWithinSingleton: NULL,
284
+ getFirstHydratableChildWithinSuspenseInstance: NULL,
285
+ canHydrateInstance: NULL,
286
+ canHydrateTextInstance: NULL,
287
+ canHydrateActivityInstance: NULL,
288
+ canHydrateSuspenseInstance: NULL,
289
+ canHydrateFormStateMarker: NULL,
290
+ isFormStateMarkerMatching: FALSE,
291
+ hydrateInstance: noop,
292
+ hydrateTextInstance: noop,
293
+ hydrateActivityInstance: noop,
294
+ hydrateSuspenseInstance: noop,
295
+ getNextHydratableInstanceAfterActivityInstance: NULL,
296
+ getNextHydratableInstanceAfterSuspenseInstance: NULL,
297
+ commitHydratedInstance: noop,
298
+ commitHydratedContainer: noop,
299
+ commitHydratedSuspenseInstance: noop,
300
+ finalizeHydratedChildren: FALSE,
301
+ flushHydrationEvents: noop,
302
+ clearActivityBoundary: FALSE,
303
+ clearSuspenseBoundary: FALSE,
304
+ clearActivityBoundaryFromContainer: FALSE,
305
+ clearSuspenseBoundaryFromContainer: FALSE,
306
+ hideDehydratedBoundary: noop,
307
+ unhideDehydratedBoundary: noop,
308
+ shouldDeleteUnhydratedTailInstances: FALSE,
309
+ diffHydratedPropsForDevWarnings: NULL,
310
+ diffHydratedTextForDevWarnings: NULL,
311
+ describeHydratableInstanceForDevWarnings: NULL,
312
+ validateHydratableInstance: TRUE,
313
+ validateHydratableTextInstance: TRUE,
314
+ // Host resources (stylesheets/scripts — supportsResources: false)
315
+ isHostHoistableType: FALSE,
316
+ getHoistableRoot: NULL,
317
+ getResource: NULL,
318
+ acquireResource: noop,
319
+ releaseResource: noop,
320
+ hydrateHoistable: noop,
321
+ mountHoistable: noop,
322
+ unmountHoistable: noop,
323
+ createHoistableInstance: NULL,
324
+ prepareToCommitHoistables: noop,
325
+ mayResourceSuspendCommit: FALSE,
326
+ preloadResource: TRUE,
327
+ suspendResource: noop,
328
+ // Singletons (supportsSingletons: false)
329
+ resolveSingletonInstance: NULL,
330
+ acquireSingletonInstance: noop,
331
+ releaseSingletonInstance: noop,
332
+ isHostSingletonType: FALSE,
333
+ isSingletonScope: FALSE,
334
+ // Test selectors (supportsTestSelectors: false)
335
+ findFiberRoot: NULL,
336
+ getBoundingRect: NULL,
337
+ getTextContent: NULL,
338
+ isHiddenSubtree: FALSE,
339
+ matchAccessibilityRole: FALSE,
340
+ setFocusIfFocusable: FALSE,
341
+ setupIntersectionObserver: noop
342
+ };
343
+
344
+ // src/reactInstance.ts
345
+ function createReactInstance(instance) {
346
+ const reconciler = Reconciler(hostConfig);
347
+ const container = reconciler.createContainer(
348
+ // Instance-stamped root container — createInstance reads `.instance` off it,
349
+ // binding every element to this instance even in out-of-task commits.
350
+ { id: 0, instance },
351
+ 0,
352
+ null,
353
+ false,
354
+ null,
355
+ "",
356
+ console.error,
357
+ // onUncaughtError
358
+ console.error,
359
+ // onCaughtError
360
+ console.error,
361
+ // onRecoverableError
362
+ null
363
+ // onDefaultTransitionIndicator
364
+ );
365
+ return {
366
+ reconciler,
367
+ container,
368
+ sync: (fn) => {
369
+ reconciler.flushSyncFromReconciler(fn);
370
+ reconciler.flushSyncWork();
371
+ },
372
+ render: (el) => reconciler.updateContainer(el, container, null, null),
373
+ unmountTree: () => reconciler.updateContainer(null, container, null, null),
374
+ flush: () => {
375
+ reconciler.flushPassiveEffects();
376
+ reconciler.flushSyncWork();
377
+ }
378
+ };
379
+ }
380
+
381
+ // src/worker.ts
382
+ import { emit, runInInstance } from "@atolljs/islands/worker";
383
+
384
+ // src/slot.tsx
385
+ import { jsx } from "react/jsx-runtime";
386
+ function Slot({ name, style }) {
387
+ return /* @__PURE__ */ jsx("div", { "data-atoll-slot": name, style });
388
+ }
389
+
390
+ // src/worker.ts
391
+ import { connectIslandWorker } from "@atolljs/islands/worker";
392
+
393
+ // src/subIsland.tsx
394
+ import { jsx as jsx2 } from "react/jsx-runtime";
395
+ function SubIsland(props) {
396
+ return /* @__PURE__ */ jsx2(Island, { framework: "react", "data-atoll-sub-island": "", ...props });
397
+ }
398
+
399
+ // src/worker.ts
400
+ function reactIslandApp(App, contract) {
401
+ const app = {
402
+ renderer: "react",
403
+ mount({ instance, props }) {
404
+ const react = createReactInstance(instance);
405
+ const render = (next) => react.sync(() => react.render(createElement(App, next)));
406
+ render(props);
407
+ return {
408
+ update: render,
409
+ sync: (fn) => react.sync(fn),
410
+ dispose: () => react.sync(() => react.unmountTree()),
411
+ flush: () => react.flush()
412
+ };
413
+ }
414
+ };
415
+ const stamp = islandAppNameOf(App);
416
+ if (stamp !== void 0) app.islandAppName = stamp;
417
+ return contract !== void 0 ? withContract(contract, app) : app;
418
+ }
419
+ var reactIsland = (name, App, contract) => islandApp(name, reactIslandApp(App, contract));
420
+ function defineReactPolyWorker(registry, options) {
421
+ const apps = {};
422
+ for (const [key, app] of Object.entries(registry.apps)) {
423
+ apps[key] = typeof app === "function" ? reactIslandApp(app) : app;
424
+ }
425
+ return definePolyWorker({
426
+ apps,
427
+ sharedMemory: registry.sharedMemory ?? options?.sharedMemory
428
+ });
429
+ }
430
+ function defineReactMonoWorker(app, options) {
431
+ const resolved = typeof app === "function" ? reactIslandApp(app) : app;
432
+ return defineMonoWorker(
433
+ options?.contract !== void 0 ? withContract(options.contract, resolved) : resolved,
434
+ options
435
+ );
436
+ }
437
+ export {
438
+ Island,
439
+ Slot,
440
+ SubIsland,
441
+ connectIslandWorker,
442
+ defineReactMonoWorker,
443
+ defineReactPolyWorker,
444
+ emit,
445
+ islandComponent,
446
+ lazyIsland,
447
+ reactIsland,
448
+ reactIslandApp,
449
+ runInInstance
450
+ };
package/package.json CHANGED
@@ -1,23 +1,33 @@
1
1
  {
2
2
  "name": "@atolljs/react-island",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
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",
7
7
  "exports": {
8
- ".": "./src/index.tsx",
9
- "./worker": "./src/worker.ts"
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ },
12
+ "./worker": {
13
+ "types": "./dist/worker.d.ts",
14
+ "default": "./dist/worker.js"
15
+ }
10
16
  },
11
17
  "files": [
12
- "src"
18
+ "src",
19
+ "dist"
13
20
  ],
21
+ "scripts": {
22
+ "build": "node ../../scripts/build-lib.mjs"
23
+ },
14
24
  "publishConfig": {
15
25
  "access": "public",
16
26
  "registry": "https://registry.npmjs.org"
17
27
  },
18
28
  "peerDependencies": {
19
- "@atolljs/core": "0.1.12",
20
- "@atolljs/islands": "0.1.12",
29
+ "@atolljs/core": "0.1.13",
30
+ "@atolljs/islands": "0.1.13",
21
31
  "react": "^19.3.0",
22
32
  "react-dom": "^19.3.0",
23
33
  "react-reconciler": "^0.34.0"