@atolljs/react-island 0.1.5 → 0.1.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atolljs/react-island",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
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.5",
20
- "@atolljs/islands": "0.1.5",
19
+ "@atolljs/core": "0.1.7",
20
+ "@atolljs/islands": "0.1.7",
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
@@ -47,6 +47,7 @@ import { createPortal } from 'react-dom';
47
47
  import type { HTMLAttributes, ReactElement, ReactNode, Ref } from 'react';
48
48
  import {
49
49
  connectIslandWorker,
50
+ contractWorkerOf,
50
51
  islandAppNameOf,
51
52
  mountIsland,
52
53
  } from '@atolljs/islands';
@@ -54,6 +55,9 @@ import type {
54
55
  IslandAppLike,
55
56
  IslandAppProps,
56
57
  IslandClient,
58
+ IslandContract,
59
+ IslandContractEventHandler,
60
+ IslandContractProps,
57
61
  IslandHandle,
58
62
  IslandWorkerOptions,
59
63
  Mode,
@@ -183,9 +187,14 @@ export function Island<A = string>({
183
187
  return;
184
188
  }
185
189
 
190
+ // A contract `app` can carry its own worker factory — the contract
191
+ // module is then the whole connection, call sites pass no worker.
192
+ const resolvedWorker = worker ?? contractWorkerOf(app);
186
193
  const client =
187
194
  clientProp ??
188
- (worker !== undefined ? connectIslandWorker({ worker, ...workerOptions }) : undefined);
195
+ (resolvedWorker !== undefined
196
+ ? connectIslandWorker({ worker: resolvedWorker, ...workerOptions })
197
+ : undefined);
189
198
  if (client === undefined) {
190
199
  report(new Error('<Island> requires either `worker` or `client`'));
191
200
  return;
@@ -423,11 +432,18 @@ function ProxyIsland({
423
432
  * contract-module shape `{ app: A, worker? }`, passes bare A through.
424
433
  */
425
434
  export type LazyResolvedApp<T extends Promise<unknown>> =
426
- Awaited<T> extends { app: infer A }
427
- ? A
428
- : Awaited<T> extends { default: infer D }
429
- ? D
430
- : Awaited<T>;
435
+ // A contract object IS a lazy module (`{ app, worker? }` + brand) — match
436
+ // it before `{ app: infer A }` so props infer `P`, not `app`'s string.
437
+ // The never guard keeps a `Promise.reject` loader on the untyped path.
438
+ [Awaited<T>] extends [never]
439
+ ? string
440
+ : Awaited<T> extends { readonly islandContract: true }
441
+ ? Awaited<T>
442
+ : Awaited<T> extends { app: infer A }
443
+ ? A
444
+ : Awaited<T> extends { default: infer D }
445
+ ? D
446
+ : Awaited<T>;
431
447
 
432
448
  /**
433
449
  * `islandComponent<P>('charts')` — a proxy component for a worker app that
@@ -441,6 +457,21 @@ export type LazyResolvedApp<T extends Promise<unknown>> =
441
457
  * <TableIsland worker={renderWorker} filter={filter} desc={desc} />
442
458
  * ```
443
459
  */
460
+ /**
461
+ * `islandComponent(checkoutContract)` — the cross-framework facade: a
462
+ * worker app described by `defineIslandContract` mounts by the contract's
463
+ * `app` key, props come from its schema, and `onEvent` narrows to the
464
+ * declared event vocabulary. The shell imports the CONTRACT module only —
465
+ * never the component or its framework.
466
+ */
467
+ export function islandComponent<C extends IslandContract>(
468
+ app: C,
469
+ ): (
470
+ props: IslandContractProps<C> &
471
+ Omit<IslandShellProps, 'onEvent'> & {
472
+ onEvent?: IslandContractEventHandler<C>;
473
+ },
474
+ ) => ReactElement;
444
475
  export function islandComponent<P extends object = Record<string, unknown>>(
445
476
  app?: string,
446
477
  ): (props: P & IslandShellProps) => ReactElement;
package/src/worker.ts CHANGED
@@ -35,10 +35,12 @@ import {
35
35
  definePolyWorker,
36
36
  islandApp,
37
37
  islandAppNameOf,
38
+ withContract,
38
39
  } from '@atolljs/islands/worker';
39
40
  import type {
40
41
  DoorbellSpec,
41
42
  ImperativeIslandApp,
43
+ IslandContract,
42
44
  IslandWorkerMethods,
43
45
  RenderContext,
44
46
  RenderedHandle,
@@ -65,7 +67,10 @@ type AnyComponent = ComponentType<Record<string, unknown>>;
65
67
  * the wrap, so registry warn-checks and `defineMonoWorker` name resolution
66
68
  * still see the shell-facing name.
67
69
  */
68
- export function reactIslandApp(App: AnyComponent): RenderedIslandApp {
70
+ export function reactIslandApp(
71
+ App: AnyComponent,
72
+ contract?: IslandContract,
73
+ ): RenderedIslandApp {
69
74
  const app: RenderedIslandApp = {
70
75
  mount({ instance, props }: RenderContext): RenderedHandle {
71
76
  const react = createReactInstance(instance);
@@ -82,7 +87,9 @@ export function reactIslandApp(App: AnyComponent): RenderedIslandApp {
82
87
  };
83
88
  const stamp = islandAppNameOf(App);
84
89
  if (stamp !== undefined) (app as { islandAppName?: string }).islandAppName = stamp;
85
- return app;
90
+ // Contract stamp — the engine validates mount/updateProps props and
91
+ // declared emit payloads against it.
92
+ return contract !== undefined ? withContract(contract, app) : app;
86
93
  }
87
94
 
88
95
  /** Stamp + wrap in one step — `reactIsland('charts', ChartsApp)` yields the
@@ -90,8 +97,9 @@ export function reactIslandApp(App: AnyComponent): RenderedIslandApp {
90
97
  export const reactIsland = (
91
98
  name: string,
92
99
  App: AnyComponent,
100
+ contract?: IslandContract,
93
101
  ): RenderedIslandApp & { readonly islandAppName: string } =>
94
- islandApp(name, reactIslandApp(App));
102
+ islandApp(name, reactIslandApp(App, contract));
95
103
 
96
104
  export interface ReactPolyWorkerRegistry {
97
105
  /**
@@ -131,9 +139,11 @@ export function defineReactPolyWorker(
131
139
  */
132
140
  export function defineReactMonoWorker(
133
141
  app: AnyComponent | ImperativeIslandApp | RenderedIslandApp,
134
- options?: { sharedMemory?: SharedMemory<DoorbellSpec> },
142
+ options?: { sharedMemory?: SharedMemory<DoorbellSpec>; contract?: IslandContract },
135
143
  ): WorkerDefinition<DoorbellSpec, IslandWorkerMethods> {
136
- return typeof app === 'function'
137
- ? defineMonoWorker(reactIslandApp(app), options)
138
- : defineMonoWorker(app, options);
144
+ const resolved = typeof app === 'function' ? reactIslandApp(app) : app;
145
+ return defineMonoWorker(
146
+ options?.contract !== undefined ? withContract(options.contract, resolved) : resolved,
147
+ options,
148
+ );
139
149
  }