@nifrajs/web-preact 4.0.2 → 5.0.0

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,3 @@
1
+ import { type NativeRangeOptions } from "@nifrajs/web/server-components/range";
2
+ export declare function preactComponentClient(options: Omit<NativeRangeOptions, "adapter" | "runtime">): import("@nifrajs/web/server-components/client").ComponentClientAdapter;
3
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/server-components/client.ts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,kBAAkB,EAExB,MAAM,sCAAsC,CAAA;AAG7C,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,IAAI,CAAC,kBAAkB,EAAE,SAAS,GAAG,SAAS,CAAC,0EAiD7F"}
@@ -0,0 +1,55 @@
1
+ import { disposeComponentPortals } from "@nifrajs/web/server-components/portal";
2
+ import { nativeComponentClient, } from "@nifrajs/web/server-components/range";
3
+ import { h, hydrate, render } from "preact";
4
+ export function preactComponentClient(options) {
5
+ return nativeComponentClient({
6
+ ...options,
7
+ adapter: "preact",
8
+ runtime: {
9
+ hydrate(host, contract, record, current) {
10
+ // biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: The shared range validates the registered island and projects public props before native hydration.
11
+ const component = contract.component;
12
+ // biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: The shared range validates the registered island and projects public props before native hydration.
13
+ let props = record.props;
14
+ const slots = Object.fromEntries(contract.slots.map((name) => [
15
+ name,
16
+ '<nifra-slot data-nifra-slot="' +
17
+ name +
18
+ '">' +
19
+ (record.slots?.[name] ?? "") +
20
+ "</nifra-slot>",
21
+ ]));
22
+ let isCurrent = current;
23
+ let previous = h(component, { ...props, slots });
24
+ const Island = () => {
25
+ if (isCurrent())
26
+ previous = h(component, { ...props, slots });
27
+ return previous;
28
+ };
29
+ hydrate(h(Island, null), host);
30
+ let disposed = false;
31
+ return {
32
+ update(next, valid) {
33
+ if (disposed || !valid())
34
+ return;
35
+ isCurrent = valid;
36
+ props = next;
37
+ render(h(Island, null), host);
38
+ },
39
+ async dispose() {
40
+ if (!disposed) {
41
+ disposed = true;
42
+ try {
43
+ await disposeComponentPortals(host);
44
+ }
45
+ finally {
46
+ render(null, host);
47
+ }
48
+ }
49
+ },
50
+ };
51
+ },
52
+ },
53
+ });
54
+ }
55
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/server-components/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,uCAAuC,CAAA;AAC/E,OAAO,EAEL,qBAAqB,GACtB,MAAM,sCAAsC,CAAA;AAC7C,OAAO,EAAsB,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAE/D,MAAM,UAAU,qBAAqB,CAAC,OAAwD;IAC5F,OAAO,qBAAqB,CAAC;QAC3B,GAAG,OAAO;QACV,OAAO,EAAE,QAAQ;QACjB,OAAO,EAAE;YACP,OAAO,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO;gBACrC,qKAAqK;gBACrK,MAAM,SAAS,GAAG,QAAQ,CAAC,SAAmD,CAAA;gBAC9E,qKAAqK;gBACrK,IAAI,KAAK,GAAG,MAAM,CAAC,KAAgC,CAAA;gBACnD,MAAM,KAAK,GAAG,MAAM,CAAC,WAAW,CAC9B,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;oBAC3B,IAAI;oBACJ,+BAA+B;wBAC7B,IAAI;wBACJ,IAAI;wBACJ,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;wBAC5B,eAAe;iBAClB,CAAC,CACH,CAAA;gBACD,IAAI,SAAS,GAAG,OAAO,CAAA;gBACvB,IAAI,QAAQ,GAAG,CAAC,CAAC,SAAS,EAAE,EAAE,GAAG,KAAK,EAAE,KAAK,EAAE,CAAC,CAAA;gBAChD,MAAM,MAAM,GAAG,GAAG,EAAE;oBAClB,IAAI,SAAS,EAAE;wBAAE,QAAQ,GAAG,CAAC,CAAC,SAAS,EAAE,EAAE,GAAG,KAAK,EAAE,KAAK,EAAE,CAAC,CAAA;oBAC7D,OAAO,QAAQ,CAAA;gBACjB,CAAC,CAAA;gBACD,OAAO,CAAC,CAAC,CAAC,MAAM,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC,CAAA;gBAC9B,IAAI,QAAQ,GAAG,KAAK,CAAA;gBACpB,OAAO;oBACL,MAAM,CAAC,IAAI,EAAE,KAAK;wBAChB,IAAI,QAAQ,IAAI,CAAC,KAAK,EAAE;4BAAE,OAAM;wBAChC,SAAS,GAAG,KAAK,CAAA;wBACjB,KAAK,GAAG,IAAI,CAAA;wBACZ,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC,CAAA;oBAC/B,CAAC;oBACD,KAAK,CAAC,OAAO;wBACX,IAAI,CAAC,QAAQ,EAAE,CAAC;4BACd,QAAQ,GAAG,IAAI,CAAA;4BACf,IAAI,CAAC;gCACH,MAAM,uBAAuB,CAAC,IAAI,CAAC,CAAA;4BACrC,CAAC;oCAAS,CAAC;gCACT,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,CAAA;4BACpB,CAAC;wBACH,CAAC;oBACH,CAAC;iBACF,CAAA;YACH,CAAC;SACF;KACF,CAAC,CAAA;AACJ,CAAC"}
@@ -0,0 +1,20 @@
1
+ import { type ComponentClientAdapter, ComponentController, type ServerComponent as ComponentHandle, type ComponentKey, type ComponentState } from "@nifrajs/web/server-components/client";
2
+ export interface ServerComponentProps<Input, Props, View> {
3
+ readonly state: ComponentState;
4
+ readonly component: ComponentHandle<Input, Props, View>;
5
+ readonly instanceKey: ComponentKey;
6
+ readonly input: NoInfer<Input>;
7
+ readonly parent?: string;
8
+ readonly controller?: ComponentController | undefined;
9
+ readonly clientAdapter?: ComponentClientAdapter | undefined;
10
+ readonly onError?: ((code: string) => void) | undefined;
11
+ }
12
+ export declare function ServerComponent<Input, Props, View>(props: ServerComponentProps<Input, Props, View>): import("preact").VNode<import("preact").Attributes & {
13
+ key: string;
14
+ state: ComponentState;
15
+ record: import("@nifrajs/web/server-components").ComponentRecord;
16
+ controller: ComponentController | undefined;
17
+ clientAdapter: ComponentClientAdapter | undefined;
18
+ onError: ((code: string) => void) | undefined;
19
+ }>;
20
+ //# sourceMappingURL=outlet.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"outlet.d.ts","sourceRoot":"","sources":["../../src/server-components/outlet.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,sBAAsB,EAC3B,mBAAmB,EACnB,KAAK,eAAe,IAAI,eAAe,EACvC,KAAK,YAAY,EACjB,KAAK,cAAc,EAEpB,MAAM,uCAAuC,CAAA;AAI9C,MAAM,WAAW,oBAAoB,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI;IACtD,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAA;IAC9B,QAAQ,CAAC,SAAS,EAAE,eAAe,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,CAAC,CAAA;IACvD,QAAQ,CAAC,WAAW,EAAE,YAAY,CAAA;IAClC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,KAAK,CAAC,CAAA;IAC9B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,UAAU,CAAC,EAAE,mBAAmB,GAAG,SAAS,CAAA;IACrD,QAAQ,CAAC,aAAa,CAAC,EAAE,sBAAsB,GAAG,SAAS,CAAA;IAC3D,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC,GAAG,SAAS,CAAA;CACxD;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAChD,KAAK,EAAE,oBAAoB,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,CAAC;;;;;;qBAJpB,MAAM,KAAK,IAAI;GAqB3C"}
@@ -0,0 +1,61 @@
1
+ import { ComponentController, resolveComponentRecord, } from "@nifrajs/web/server-components/client";
2
+ import { h } from "preact";
3
+ import { useLayoutEffect, useRef } from "preact/hooks";
4
+ export function ServerComponent(props) {
5
+ const record = resolveComponentRecord(props.state, props.component, props.instanceKey, props.input, props.parent ?? null);
6
+ return h(ComponentRange, {
7
+ key: record.instance,
8
+ state: props.state,
9
+ record,
10
+ controller: props.controller,
11
+ clientAdapter: props.clientAdapter,
12
+ onError: props.onError,
13
+ });
14
+ }
15
+ function ComponentRange(props) {
16
+ const record = props.record;
17
+ const host = useRef(null);
18
+ // Preact owns the shell while the controller owns its independently hydrated native children.
19
+ const initial = useRef({
20
+ __html: `<nifra-component data-nifra-component="${record.instance}">${record.html}</nifra-component>`,
21
+ });
22
+ const active = useRef(undefined);
23
+ const lifecycle = useRef(props);
24
+ lifecycle.current = props;
25
+ useLayoutEffect(() => {
26
+ const options = lifecycle.current;
27
+ if (options.controller === undefined && options.clientAdapter === undefined)
28
+ throw new Error("component_contract");
29
+ const element = host.current?.firstElementChild;
30
+ if (!(element instanceof HTMLElement) || element.localName !== "nifra-component")
31
+ throw new Error("component_contract");
32
+ const controller = options.controller ??
33
+ new ComponentController({
34
+ state: options.state,
35
+ // biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: The effect rejects missing adapters before creating a controller; callers with a controller bypass this expression.
36
+ adapter: options.clientAdapter,
37
+ onError: (code) => lifecycle.current.onError?.(code),
38
+ });
39
+ active.current = controller;
40
+ void controller
41
+ .mount(element, record.instance)
42
+ .catch(() => controller.reportError("component_failed"));
43
+ return () => {
44
+ void (options.controller === undefined
45
+ ? controller.dispose()
46
+ : controller.unmount(record.instance)).catch(() => controller.reportError("component_failed"));
47
+ };
48
+ }, [record.instance]);
49
+ useLayoutEffect(() => {
50
+ if (active.current !== undefined)
51
+ void active.current
52
+ .adopt(props.state)
53
+ .catch(() => active.current?.reportError("component_failed"));
54
+ }, [props.state]);
55
+ return h("nifra-component-outlet", {
56
+ ref: host,
57
+ // Record HTML passed the shared component policy before an outlet can resolve it.
58
+ dangerouslySetInnerHTML: initial.current,
59
+ });
60
+ }
61
+ //# sourceMappingURL=outlet.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"outlet.js","sourceRoot":"","sources":["../../src/server-components/outlet.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,mBAAmB,EAInB,sBAAsB,GACvB,MAAM,uCAAuC,CAAA;AAC9C,OAAO,EAAE,CAAC,EAAE,MAAM,QAAQ,CAAA;AAC1B,OAAO,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AAatD,MAAM,UAAU,eAAe,CAC7B,KAA+C;IAE/C,MAAM,MAAM,GAAG,sBAAsB,CACnC,KAAK,CAAC,KAAK,EACX,KAAK,CAAC,SAAS,EACf,KAAK,CAAC,WAAW,EACjB,KAAK,CAAC,KAAK,EACX,KAAK,CAAC,MAAM,IAAI,IAAI,CACrB,CAAA;IACD,OAAO,CAAC,CAAC,cAAc,EAAE;QACvB,GAAG,EAAE,MAAM,CAAC,QAAQ;QACpB,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,MAAM;QACN,UAAU,EAAE,KAAK,CAAC,UAAU;QAC5B,aAAa,EAAE,KAAK,CAAC,aAAa;QAClC,OAAO,EAAE,KAAK,CAAC,OAAO;KACvB,CAAC,CAAA;AACJ,CAAC;AAED,SAAS,cAAc,CAAC,KAMvB;IACC,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAA;IAC3B,MAAM,IAAI,GAAG,MAAM,CAAc,IAAI,CAAC,CAAA;IACtC,8FAA8F;IAC9F,MAAM,OAAO,GAAG,MAAM,CAAC;QACrB,MAAM,EAAE,0CAA0C,MAAM,CAAC,QAAQ,KAAK,MAAM,CAAC,IAAI,oBAAoB;KACtG,CAAC,CAAA;IACF,MAAM,MAAM,GAAG,MAAM,CAAkC,SAAS,CAAC,CAAA;IACjE,MAAM,SAAS,GAAG,MAAM,CAAC,KAAK,CAAC,CAAA;IAC/B,SAAS,CAAC,OAAO,GAAG,KAAK,CAAA;IACzB,eAAe,CAAC,GAAG,EAAE;QACnB,MAAM,OAAO,GAAG,SAAS,CAAC,OAAO,CAAA;QACjC,IAAI,OAAO,CAAC,UAAU,KAAK,SAAS,IAAI,OAAO,CAAC,aAAa,KAAK,SAAS;YACzE,MAAM,IAAI,KAAK,CAAC,oBAAoB,CAAC,CAAA;QACvC,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,EAAE,iBAAiB,CAAA;QAC/C,IAAI,CAAC,CAAC,OAAO,YAAY,WAAW,CAAC,IAAI,OAAO,CAAC,SAAS,KAAK,iBAAiB;YAC9E,MAAM,IAAI,KAAK,CAAC,oBAAoB,CAAC,CAAA;QACvC,MAAM,UAAU,GACd,OAAO,CAAC,UAAU;YAClB,IAAI,mBAAmB,CAAC;gBACtB,KAAK,EAAE,OAAO,CAAC,KAAK;gBACpB,qLAAqL;gBACrL,OAAO,EAAE,OAAO,CAAC,aAAuC;gBACxD,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,SAAS,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC;aACrD,CAAC,CAAA;QACJ,MAAM,CAAC,OAAO,GAAG,UAAU,CAAA;QAC3B,KAAK,UAAU;aACZ,KAAK,CAAC,OAAO,EAAE,MAAM,CAAC,QAAQ,CAAC;aAC/B,KAAK,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,WAAW,CAAC,kBAAkB,CAAC,CAAC,CAAA;QAC1D,OAAO,GAAG,EAAE;YACV,KAAK,CACH,OAAO,CAAC,UAAU,KAAK,SAAS;gBAC9B,CAAC,CAAC,UAAU,CAAC,OAAO,EAAE;gBACtB,CAAC,CAAC,UAAU,CAAC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,CACxC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,WAAW,CAAC,kBAAkB,CAAC,CAAC,CAAA;QAC3D,CAAC,CAAA;IACH,CAAC,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAA;IACrB,eAAe,CAAC,GAAG,EAAE;QACnB,IAAI,MAAM,CAAC,OAAO,KAAK,SAAS;YAC9B,KAAK,MAAM,CAAC,OAAO;iBAChB,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC;iBAClB,KAAK,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,OAAO,EAAE,WAAW,CAAC,kBAAkB,CAAC,CAAC,CAAA;IACnE,CAAC,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAA;IACjB,OAAO,CAAC,CAAC,wBAAwB,EAAE;QACjC,GAAG,EAAE,IAAI;QACT,kFAAkF;QAClF,uBAAuB,EAAE,OAAO,CAAC,OAAO;KACzC,CAAC,CAAA;AACJ,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { ServerComponentRenderAdapter } from "@nifrajs/web/server-components";
2
+ export declare const preactServerComponents: () => ServerComponentRenderAdapter;
3
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/server-components/server.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,gCAAgC,CAAA;AAKlF,eAAO,MAAM,sBAAsB,QAAO,4BAKtC,CAAA"}
@@ -0,0 +1,9 @@
1
+ import { nativeServerComponents } from "@nifrajs/web/server-components/native";
2
+ import { h } from "preact";
3
+ import { preactRenderToString } from "../preact-render.js";
4
+ export const preactServerComponents = () => nativeServerComponents("preact", async (component, props) => {
5
+ const renderer = await preactRenderToString();
6
+ // biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: The native renderer receives only its registered component and schema-projected props from the shared executor.
7
+ return renderer.renderToString(h(component, props));
8
+ });
9
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/server-components/server.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,sBAAsB,EAAE,MAAM,uCAAuC,CAAA;AAC9E,OAAO,EAAsB,CAAC,EAAE,MAAM,QAAQ,CAAA;AAC9C,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAA;AAE1D,MAAM,CAAC,MAAM,sBAAsB,GAAG,GAAiC,EAAE,CACvE,sBAAsB,CAAC,QAAQ,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,EAAE;IAC1D,MAAM,QAAQ,GAAG,MAAM,oBAAoB,EAAE,CAAA;IAC7C,iLAAiL;IACjL,OAAO,QAAQ,CAAC,cAAc,CAAC,CAAC,CAAC,SAAmD,EAAE,KAAK,CAAC,CAAC,CAAA;AAC/F,CAAC,CAAC,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nifrajs/web-preact",
3
- "version": "4.0.2",
3
+ "version": "5.0.0",
4
4
  "description": "Preact render adapter for @nifrajs/web - streaming SSR + hydration (no build plugin).",
5
5
  "keywords": [
6
6
  "bun",
@@ -70,6 +70,21 @@
70
70
  "bun": "./src/fn.ts",
71
71
  "types": "./dist/fn.d.ts",
72
72
  "default": "./dist/fn.js"
73
+ },
74
+ "./server-components": {
75
+ "bun": "./src/server-components/server.ts",
76
+ "types": "./dist/server-components/server.d.ts",
77
+ "default": "./dist/server-components/server.js"
78
+ },
79
+ "./server-components/client": {
80
+ "bun": "./src/server-components/client.ts",
81
+ "types": "./dist/server-components/client.d.ts",
82
+ "default": "./dist/server-components/client.js"
83
+ },
84
+ "./server-components/outlet": {
85
+ "bun": "./src/server-components/outlet.ts",
86
+ "types": "./dist/server-components/outlet.d.ts",
87
+ "default": "./dist/server-components/outlet.js"
73
88
  }
74
89
  },
75
90
  "files": [
@@ -85,10 +100,10 @@
85
100
  "access": "public"
86
101
  },
87
102
  "dependencies": {
88
- "@nifrajs/core": "4.0.2",
89
- "@nifrajs/i18n": "4.0.2",
90
- "@nifrajs/image": "4.0.2",
91
- "@nifrajs/web": "4.0.2",
103
+ "@nifrajs/core": "5.0.0",
104
+ "@nifrajs/i18n": "5.0.0",
105
+ "@nifrajs/image": "5.0.0",
106
+ "@nifrajs/web": "5.0.0",
92
107
  "preact-render-to-string": "^6.5.0"
93
108
  },
94
109
  "peerDependencies": {
@@ -0,0 +1,57 @@
1
+ import { disposeComponentPortals } from "@nifrajs/web/server-components/portal"
2
+ import {
3
+ type NativeRangeOptions,
4
+ nativeComponentClient,
5
+ } from "@nifrajs/web/server-components/range"
6
+ import { type ComponentType, h, hydrate, render } from "preact"
7
+
8
+ export function preactComponentClient(options: Omit<NativeRangeOptions, "adapter" | "runtime">) {
9
+ return nativeComponentClient({
10
+ ...options,
11
+ adapter: "preact",
12
+ runtime: {
13
+ hydrate(host, contract, record, current) {
14
+ // biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: The shared range validates the registered island and projects public props before native hydration.
15
+ const component = contract.component as ComponentType<Record<string, unknown>>
16
+ // biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: The shared range validates the registered island and projects public props before native hydration.
17
+ let props = record.props as Record<string, unknown>
18
+ const slots = Object.fromEntries(
19
+ contract.slots.map((name) => [
20
+ name,
21
+ '<nifra-slot data-nifra-slot="' +
22
+ name +
23
+ '">' +
24
+ (record.slots?.[name] ?? "") +
25
+ "</nifra-slot>",
26
+ ]),
27
+ )
28
+ let isCurrent = current
29
+ let previous = h(component, { ...props, slots })
30
+ const Island = () => {
31
+ if (isCurrent()) previous = h(component, { ...props, slots })
32
+ return previous
33
+ }
34
+ hydrate(h(Island, null), host)
35
+ let disposed = false
36
+ return {
37
+ update(next, valid) {
38
+ if (disposed || !valid()) return
39
+ isCurrent = valid
40
+ props = next
41
+ render(h(Island, null), host)
42
+ },
43
+ async dispose() {
44
+ if (!disposed) {
45
+ disposed = true
46
+ try {
47
+ await disposeComponentPortals(host)
48
+ } finally {
49
+ render(null, host)
50
+ }
51
+ }
52
+ },
53
+ }
54
+ },
55
+ },
56
+ })
57
+ }
@@ -0,0 +1,97 @@
1
+ import {
2
+ type ComponentClientAdapter,
3
+ ComponentController,
4
+ type ServerComponent as ComponentHandle,
5
+ type ComponentKey,
6
+ type ComponentState,
7
+ resolveComponentRecord,
8
+ } from "@nifrajs/web/server-components/client"
9
+ import { h } from "preact"
10
+ import { useLayoutEffect, useRef } from "preact/hooks"
11
+
12
+ export interface ServerComponentProps<Input, Props, View> {
13
+ readonly state: ComponentState
14
+ readonly component: ComponentHandle<Input, Props, View>
15
+ readonly instanceKey: ComponentKey
16
+ readonly input: NoInfer<Input>
17
+ readonly parent?: string
18
+ readonly controller?: ComponentController | undefined
19
+ readonly clientAdapter?: ComponentClientAdapter | undefined
20
+ readonly onError?: ((code: string) => void) | undefined
21
+ }
22
+
23
+ export function ServerComponent<Input, Props, View>(
24
+ props: ServerComponentProps<Input, Props, View>,
25
+ ) {
26
+ const record = resolveComponentRecord(
27
+ props.state,
28
+ props.component,
29
+ props.instanceKey,
30
+ props.input,
31
+ props.parent ?? null,
32
+ )
33
+ return h(ComponentRange, {
34
+ key: record.instance,
35
+ state: props.state,
36
+ record,
37
+ controller: props.controller,
38
+ clientAdapter: props.clientAdapter,
39
+ onError: props.onError,
40
+ })
41
+ }
42
+
43
+ function ComponentRange(props: {
44
+ readonly state: ComponentState
45
+ readonly record: import("@nifrajs/web/server-components/client").ComponentRecord
46
+ readonly controller?: ComponentController | undefined
47
+ readonly clientAdapter?: ComponentClientAdapter | undefined
48
+ readonly onError?: ((code: string) => void) | undefined
49
+ }) {
50
+ const record = props.record
51
+ const host = useRef<HTMLElement>(null)
52
+ // Preact owns the shell while the controller owns its independently hydrated native children.
53
+ const initial = useRef({
54
+ __html: `<nifra-component data-nifra-component="${record.instance}">${record.html}</nifra-component>`,
55
+ })
56
+ const active = useRef<ComponentController | undefined>(undefined)
57
+ const lifecycle = useRef(props)
58
+ lifecycle.current = props
59
+ useLayoutEffect(() => {
60
+ const options = lifecycle.current
61
+ if (options.controller === undefined && options.clientAdapter === undefined)
62
+ throw new Error("component_contract")
63
+ const element = host.current?.firstElementChild
64
+ if (!(element instanceof HTMLElement) || element.localName !== "nifra-component")
65
+ throw new Error("component_contract")
66
+ const controller =
67
+ options.controller ??
68
+ new ComponentController({
69
+ state: options.state,
70
+ // biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: The effect rejects missing adapters before creating a controller; callers with a controller bypass this expression.
71
+ adapter: options.clientAdapter as ComponentClientAdapter,
72
+ onError: (code) => lifecycle.current.onError?.(code),
73
+ })
74
+ active.current = controller
75
+ void controller
76
+ .mount(element, record.instance)
77
+ .catch(() => controller.reportError("component_failed"))
78
+ return () => {
79
+ void (
80
+ options.controller === undefined
81
+ ? controller.dispose()
82
+ : controller.unmount(record.instance)
83
+ ).catch(() => controller.reportError("component_failed"))
84
+ }
85
+ }, [record.instance])
86
+ useLayoutEffect(() => {
87
+ if (active.current !== undefined)
88
+ void active.current
89
+ .adopt(props.state)
90
+ .catch(() => active.current?.reportError("component_failed"))
91
+ }, [props.state])
92
+ return h("nifra-component-outlet", {
93
+ ref: host,
94
+ // Record HTML passed the shared component policy before an outlet can resolve it.
95
+ dangerouslySetInnerHTML: initial.current,
96
+ })
97
+ }
@@ -0,0 +1,11 @@
1
+ import type { ServerComponentRenderAdapter } from "@nifrajs/web/server-components"
2
+ import { nativeServerComponents } from "@nifrajs/web/server-components/native"
3
+ import { type ComponentType, h } from "preact"
4
+ import { preactRenderToString } from "../preact-render.ts"
5
+
6
+ export const preactServerComponents = (): ServerComponentRenderAdapter =>
7
+ nativeServerComponents("preact", async (component, props) => {
8
+ const renderer = await preactRenderToString()
9
+ // biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: The native renderer receives only its registered component and schema-projected props from the shared executor.
10
+ return renderer.renderToString(h(component as ComponentType<Record<string, unknown>>, props))
11
+ })