@pyreon/preact-compat 0.1.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.
- package/LICENSE +21 -0
- package/README.md +76 -0
- package/lib/analysis/index.js.html +5406 -0
- package/lib/index.js +110 -0
- package/lib/index.js.map +1 -0
- package/lib/types/index.d.ts +61 -0
- package/lib/types/index.d.ts.map +1 -0
- package/package.json +59 -0
- package/src/hooks.ts +131 -0
- package/src/index.ts +167 -0
- package/src/signals.ts +92 -0
- package/src/tests/preact-compat.test.ts +522 -0
- package/src/tests/setup.ts +3 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { Fragment, createContext, createRef, h as pyreonH, useContext } from "@pyreon/core";
|
|
2
|
+
import { batch, signal } from "@pyreon/reactivity";
|
|
3
|
+
import { hydrateRoot, mount } from "@pyreon/runtime-dom";
|
|
4
|
+
|
|
5
|
+
//#region src/index.ts
|
|
6
|
+
/** Alias: Preact also exports createElement */
|
|
7
|
+
const createElement = pyreonH;
|
|
8
|
+
/**
|
|
9
|
+
* Preact's `render(vnode, container)`.
|
|
10
|
+
* Maps to Pyreon's `mount(vnode, container)`.
|
|
11
|
+
*/
|
|
12
|
+
function render(vnode, container) {
|
|
13
|
+
mount(vnode, container);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Preact's `hydrate(vnode, container)`.
|
|
17
|
+
* Maps to Pyreon's `hydrateRoot(container, vnode)`.
|
|
18
|
+
*/
|
|
19
|
+
function hydrate(vnode, container) {
|
|
20
|
+
hydrateRoot(container, vnode);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Preact-compatible class-based Component.
|
|
24
|
+
*
|
|
25
|
+
* Wraps Pyreon's signal-based reactivity so `setState` triggers re-renders.
|
|
26
|
+
* Usage: `class MyComp extends Component { render() { ... } }`
|
|
27
|
+
*/
|
|
28
|
+
var Component = class {
|
|
29
|
+
props;
|
|
30
|
+
state;
|
|
31
|
+
_stateSignal;
|
|
32
|
+
constructor(props) {
|
|
33
|
+
this.props = props;
|
|
34
|
+
this.state = {};
|
|
35
|
+
this._stateSignal = signal(this.state);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Update state — accepts a partial state object or an updater function.
|
|
39
|
+
* Merges into existing state (shallow merge, like Preact/React).
|
|
40
|
+
*/
|
|
41
|
+
setState(partial) {
|
|
42
|
+
batch(() => {
|
|
43
|
+
const current = this._stateSignal();
|
|
44
|
+
const update = typeof partial === "function" ? partial(current) : partial;
|
|
45
|
+
const next = {
|
|
46
|
+
...current,
|
|
47
|
+
...update
|
|
48
|
+
};
|
|
49
|
+
this.state = next;
|
|
50
|
+
this._stateSignal.set(next);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Force a re-render. In Pyreon this triggers the state signal to re-fire.
|
|
55
|
+
*/
|
|
56
|
+
forceUpdate() {
|
|
57
|
+
this._stateSignal.set({ ...this.state });
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Override in subclass to return VNode tree.
|
|
61
|
+
*/
|
|
62
|
+
render() {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Clone a VNode with merged props (like Preact's cloneElement).
|
|
68
|
+
*/
|
|
69
|
+
function cloneElement(vnode, props, ...children) {
|
|
70
|
+
const mergedProps = {
|
|
71
|
+
...vnode.props,
|
|
72
|
+
...props ?? {}
|
|
73
|
+
};
|
|
74
|
+
const mergedChildren = children.length > 0 ? children : vnode.children;
|
|
75
|
+
return {
|
|
76
|
+
type: vnode.type,
|
|
77
|
+
props: mergedProps,
|
|
78
|
+
children: mergedChildren,
|
|
79
|
+
key: props?.key ?? vnode.key
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Flatten children into a flat array, filtering out null/undefined/boolean.
|
|
84
|
+
* Matches Preact's `toChildArray` utility.
|
|
85
|
+
*/
|
|
86
|
+
function toChildArray(children) {
|
|
87
|
+
const result = [];
|
|
88
|
+
flatten(children, result);
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
function flatten(value, out) {
|
|
92
|
+
if (value == null || typeof value === "boolean") return;
|
|
93
|
+
if (Array.isArray(value)) for (const child of value) flatten(child, out);
|
|
94
|
+
else out.push(value);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Check if a value is a VNode (like Preact's isValidElement).
|
|
98
|
+
*/
|
|
99
|
+
function isValidElement(x) {
|
|
100
|
+
return x !== null && typeof x === "object" && "type" in x && "props" in x && "children" in x;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Preact's plugin/hook system. Exposed as an empty object for compatibility
|
|
104
|
+
* with libraries that check for `options._hook`, `options.vnode`, etc.
|
|
105
|
+
*/
|
|
106
|
+
const options = {};
|
|
107
|
+
|
|
108
|
+
//#endregion
|
|
109
|
+
export { Component, Fragment, cloneElement, createContext, createElement, createRef, pyreonH as h, hydrate, isValidElement, options, render, toChildArray, useContext };
|
|
110
|
+
//# sourceMappingURL=index.js.map
|
package/lib/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * @pyreon/preact-compat\n *\n * Preact-compatible API shim that runs on Pyreon's reactive engine.\n *\n * Provides the core Preact API surface: h, Fragment, render, hydrate,\n * Component class, createContext, createRef, cloneElement, toChildArray,\n * isValidElement, and the options hook object.\n *\n * For hooks, import from \"@pyreon/preact-compat/hooks\".\n * For signals, import from \"@pyreon/preact-compat/signals\".\n */\n\nimport type { Props, VNode, VNodeChild } from \"@pyreon/core\"\nimport { createContext, createRef, Fragment, h as pyreonH, useContext } from \"@pyreon/core\"\nimport { batch, signal } from \"@pyreon/reactivity\"\nimport { hydrateRoot, mount } from \"@pyreon/runtime-dom\"\n\n// ─── Core JSX ────────────────────────────────────────────────────────────────\n\n/** Preact's hyperscript function — maps directly to Pyreon's h() */\nexport { pyreonH as h }\n\n/** Alias: Preact also exports createElement */\nexport const createElement = pyreonH\n\nexport { Fragment }\n\n// ─── Render / Hydrate ────────────────────────────────────────────────────────\n\n/**\n * Preact's `render(vnode, container)`.\n * Maps to Pyreon's `mount(vnode, container)`.\n */\nexport function render(vnode: VNodeChild, container: Element): void {\n mount(vnode, container)\n}\n\n/**\n * Preact's `hydrate(vnode, container)`.\n * Maps to Pyreon's `hydrateRoot(container, vnode)`.\n */\nexport function hydrate(vnode: VNodeChild, container: Element): void {\n hydrateRoot(container, vnode as VNode)\n}\n\n// ─── Context ─────────────────────────────────────────────────────────────────\n\nexport { createContext, useContext }\n\n// ─── Refs ────────────────────────────────────────────────────────────────────\n\nexport { createRef }\n\n// ─── Component class ─────────────────────────────────────────────────────────\n\n/**\n * Preact-compatible class-based Component.\n *\n * Wraps Pyreon's signal-based reactivity so `setState` triggers re-renders.\n * Usage: `class MyComp extends Component { render() { ... } }`\n */\nexport class Component<\n P extends Props = Props,\n S extends Record<string, unknown> = Record<string, unknown>,\n> {\n props: P\n state: S\n private _stateSignal: ReturnType<typeof signal<S>>\n\n constructor(props: P) {\n this.props = props\n this.state = {} as S\n this._stateSignal = signal<S>(this.state)\n }\n\n /**\n * Update state — accepts a partial state object or an updater function.\n * Merges into existing state (shallow merge, like Preact/React).\n */\n setState(partial: Partial<S> | ((prev: S) => Partial<S>)): void {\n batch(() => {\n const current = this._stateSignal()\n const update =\n typeof partial === \"function\" ? (partial as (prev: S) => Partial<S>)(current) : partial\n const next = { ...current, ...update } as S\n this.state = next\n this._stateSignal.set(next)\n })\n }\n\n /**\n * Force a re-render. In Pyreon this triggers the state signal to re-fire.\n */\n forceUpdate(): void {\n this._stateSignal.set({ ...this.state })\n }\n\n /**\n * Override in subclass to return VNode tree.\n */\n render(): VNodeChild {\n return null\n }\n}\n\n// ─── cloneElement ────────────────────────────────────────────────────────────\n\n/**\n * Clone a VNode with merged props (like Preact's cloneElement).\n */\nexport function cloneElement(vnode: VNode, props?: Props, ...children: VNodeChild[]): VNode {\n const mergedProps = { ...vnode.props, ...(props ?? {}) }\n const mergedChildren = children.length > 0 ? children : vnode.children\n return {\n type: vnode.type,\n props: mergedProps,\n children: mergedChildren,\n key: (props?.key as string | number | null) ?? vnode.key,\n }\n}\n\n// ─── toChildArray ────────────────────────────────────────────────────────────\n\n/**\n * Flatten children into a flat array, filtering out null/undefined/boolean.\n * Matches Preact's `toChildArray` utility.\n */\nexport function toChildArray(children: VNodeChild | VNodeChild[]): VNodeChild[] {\n const result: VNodeChild[] = []\n flatten(children, result)\n return result\n}\n\nfunction flatten(value: VNodeChild | VNodeChild[], out: VNodeChild[]): void {\n if (value == null || typeof value === \"boolean\") return\n if (Array.isArray(value)) {\n for (const child of value) {\n flatten(child as VNodeChild, out)\n }\n } else {\n out.push(value)\n }\n}\n\n// ─── isValidElement ──────────────────────────────────────────────────────────\n\n/**\n * Check if a value is a VNode (like Preact's isValidElement).\n */\nexport function isValidElement(x: unknown): x is VNode {\n return (\n x !== null &&\n typeof x === \"object\" &&\n \"type\" in (x as Record<string, unknown>) &&\n \"props\" in (x as Record<string, unknown>) &&\n \"children\" in (x as Record<string, unknown>)\n )\n}\n\n// ─── options ─────────────────────────────────────────────────────────────────\n\n/**\n * Preact's plugin/hook system. Exposed as an empty object for compatibility\n * with libraries that check for `options._hook`, `options.vnode`, etc.\n */\nexport const options: Record<string, unknown> = {}\n"],"mappings":";;;;;;AAwBA,MAAa,gBAAgB;;;;;AAU7B,SAAgB,OAAO,OAAmB,WAA0B;AAClE,OAAM,OAAO,UAAU;;;;;;AAOzB,SAAgB,QAAQ,OAAmB,WAA0B;AACnE,aAAY,WAAW,MAAe;;;;;;;;AAmBxC,IAAa,YAAb,MAGE;CACA;CACA;CACA,AAAQ;CAER,YAAY,OAAU;AACpB,OAAK,QAAQ;AACb,OAAK,QAAQ,EAAE;AACf,OAAK,eAAe,OAAU,KAAK,MAAM;;;;;;CAO3C,SAAS,SAAuD;AAC9D,cAAY;GACV,MAAM,UAAU,KAAK,cAAc;GACnC,MAAM,SACJ,OAAO,YAAY,aAAc,QAAoC,QAAQ,GAAG;GAClF,MAAM,OAAO;IAAE,GAAG;IAAS,GAAG;IAAQ;AACtC,QAAK,QAAQ;AACb,QAAK,aAAa,IAAI,KAAK;IAC3B;;;;;CAMJ,cAAoB;AAClB,OAAK,aAAa,IAAI,EAAE,GAAG,KAAK,OAAO,CAAC;;;;;CAM1C,SAAqB;AACnB,SAAO;;;;;;AASX,SAAgB,aAAa,OAAc,OAAe,GAAG,UAA+B;CAC1F,MAAM,cAAc;EAAE,GAAG,MAAM;EAAO,GAAI,SAAS,EAAE;EAAG;CACxD,MAAM,iBAAiB,SAAS,SAAS,IAAI,WAAW,MAAM;AAC9D,QAAO;EACL,MAAM,MAAM;EACZ,OAAO;EACP,UAAU;EACV,KAAM,OAAO,OAAkC,MAAM;EACtD;;;;;;AASH,SAAgB,aAAa,UAAmD;CAC9E,MAAM,SAAuB,EAAE;AAC/B,SAAQ,UAAU,OAAO;AACzB,QAAO;;AAGT,SAAS,QAAQ,OAAkC,KAAyB;AAC1E,KAAI,SAAS,QAAQ,OAAO,UAAU,UAAW;AACjD,KAAI,MAAM,QAAQ,MAAM,CACtB,MAAK,MAAM,SAAS,MAClB,SAAQ,OAAqB,IAAI;KAGnC,KAAI,KAAK,MAAM;;;;;AASnB,SAAgB,eAAe,GAAwB;AACrD,QACE,MAAM,QACN,OAAO,MAAM,YACb,UAAW,KACX,WAAY,KACZ,cAAe;;;;;;AAUnB,MAAa,UAAmC,EAAE"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Fragment, Props, VNode, VNodeChild, createContext, createRef, h as pyreonH, useContext } from "@pyreon/core";
|
|
2
|
+
|
|
3
|
+
//#region src/index.d.ts
|
|
4
|
+
/** Alias: Preact also exports createElement */
|
|
5
|
+
declare const createElement: typeof pyreonH;
|
|
6
|
+
/**
|
|
7
|
+
* Preact's `render(vnode, container)`.
|
|
8
|
+
* Maps to Pyreon's `mount(vnode, container)`.
|
|
9
|
+
*/
|
|
10
|
+
declare function render(vnode: VNodeChild, container: Element): void;
|
|
11
|
+
/**
|
|
12
|
+
* Preact's `hydrate(vnode, container)`.
|
|
13
|
+
* Maps to Pyreon's `hydrateRoot(container, vnode)`.
|
|
14
|
+
*/
|
|
15
|
+
declare function hydrate(vnode: VNodeChild, container: Element): void;
|
|
16
|
+
/**
|
|
17
|
+
* Preact-compatible class-based Component.
|
|
18
|
+
*
|
|
19
|
+
* Wraps Pyreon's signal-based reactivity so `setState` triggers re-renders.
|
|
20
|
+
* Usage: `class MyComp extends Component { render() { ... } }`
|
|
21
|
+
*/
|
|
22
|
+
declare class Component<P extends Props = Props, S extends Record<string, unknown> = Record<string, unknown>> {
|
|
23
|
+
props: P;
|
|
24
|
+
state: S;
|
|
25
|
+
private _stateSignal;
|
|
26
|
+
constructor(props: P);
|
|
27
|
+
/**
|
|
28
|
+
* Update state — accepts a partial state object or an updater function.
|
|
29
|
+
* Merges into existing state (shallow merge, like Preact/React).
|
|
30
|
+
*/
|
|
31
|
+
setState(partial: Partial<S> | ((prev: S) => Partial<S>)): void;
|
|
32
|
+
/**
|
|
33
|
+
* Force a re-render. In Pyreon this triggers the state signal to re-fire.
|
|
34
|
+
*/
|
|
35
|
+
forceUpdate(): void;
|
|
36
|
+
/**
|
|
37
|
+
* Override in subclass to return VNode tree.
|
|
38
|
+
*/
|
|
39
|
+
render(): VNodeChild;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Clone a VNode with merged props (like Preact's cloneElement).
|
|
43
|
+
*/
|
|
44
|
+
declare function cloneElement(vnode: VNode, props?: Props, ...children: VNodeChild[]): VNode;
|
|
45
|
+
/**
|
|
46
|
+
* Flatten children into a flat array, filtering out null/undefined/boolean.
|
|
47
|
+
* Matches Preact's `toChildArray` utility.
|
|
48
|
+
*/
|
|
49
|
+
declare function toChildArray(children: VNodeChild | VNodeChild[]): VNodeChild[];
|
|
50
|
+
/**
|
|
51
|
+
* Check if a value is a VNode (like Preact's isValidElement).
|
|
52
|
+
*/
|
|
53
|
+
declare function isValidElement(x: unknown): x is VNode;
|
|
54
|
+
/**
|
|
55
|
+
* Preact's plugin/hook system. Exposed as an empty object for compatibility
|
|
56
|
+
* with libraries that check for `options._hook`, `options.vnode`, etc.
|
|
57
|
+
*/
|
|
58
|
+
declare const options: Record<string, unknown>;
|
|
59
|
+
//#endregion
|
|
60
|
+
export { Component, Fragment, cloneElement, createContext, createElement, createRef, pyreonH as h, hydrate, isValidElement, options, render, toChildArray, useContext };
|
|
61
|
+
//# sourceMappingURL=index2.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index2.d.ts","names":[],"sources":["../../src/index.ts"],"mappings":";;;;cAwBa,aAAA,SAAa,OAAA;;;;;iBAUV,MAAA,CAAO,KAAA,EAAO,UAAA,EAAY,SAAA,EAAW,OAAA;;;;;iBAQrC,OAAA,CAAQ,KAAA,EAAO,UAAA,EAAY,SAAA,EAAW,OAAA;;;;;;;cAoBzC,SAAA,WACD,KAAA,GAAQ,KAAA,YACR,MAAA,oBAA0B,MAAA;EAEpC,KAAA,EAAO,CAAA;EACP,KAAA,EAAO,CAAA;EAAA,QACC,YAAA;cAEI,KAAA,EAAO,CAAA;EAU0B;;;;EAA7C,QAAA,CAAS,OAAA,EAAS,OAAA,CAAQ,CAAA,MAAO,IAAA,EAAM,CAAA,KAAM,OAAA,CAAQ,CAAA;EAjB3C;;;EA+BV,WAAA,CAAA;EA9BoC;;;EAqCpC,MAAA,CAAA,GAAU,UAAA;AAAA;;;;iBAUI,YAAA,CAAa,KAAA,EAAO,KAAA,EAAO,KAAA,GAAQ,KAAA,KAAU,QAAA,EAAU,UAAA,KAAe,KAAA;;;;;iBAiBtE,YAAA,CAAa,QAAA,EAAU,UAAA,GAAa,UAAA,KAAe,UAAA;;;;iBAsBnD,cAAA,CAAe,CAAA,YAAa,CAAA,IAAK,KAAA;;;;;cAgBpC,OAAA,EAAS,MAAA"}
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pyreon/preact-compat",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Preact-compatible API shim for Pyreon — write Preact-style code that runs on Pyreon's reactive engine",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/pyreon/pyreon.git",
|
|
9
|
+
"directory": "packages/preact-compat"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/pyreon/pyreon/tree/main/packages/preact-compat#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/pyreon/pyreon/issues"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"lib",
|
|
17
|
+
"src",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"sideEffects": false,
|
|
22
|
+
"type": "module",
|
|
23
|
+
"main": "./lib/index.js",
|
|
24
|
+
"module": "./lib/index.js",
|
|
25
|
+
"types": "./lib/types/index.d.ts",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"bun": "./src/index.ts",
|
|
29
|
+
"import": "./lib/index.js",
|
|
30
|
+
"types": "./lib/types/index.d.ts"
|
|
31
|
+
},
|
|
32
|
+
"./hooks": {
|
|
33
|
+
"bun": "./src/hooks.ts",
|
|
34
|
+
"import": "./lib/hooks.js",
|
|
35
|
+
"types": "./lib/types/hooks.d.ts"
|
|
36
|
+
},
|
|
37
|
+
"./signals": {
|
|
38
|
+
"bun": "./src/signals.ts",
|
|
39
|
+
"import": "./lib/signals.js",
|
|
40
|
+
"types": "./lib/types/signals.d.ts"
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "vl_rolldown_build",
|
|
45
|
+
"dev": "vl_rolldown_build-watch",
|
|
46
|
+
"test": "vitest run",
|
|
47
|
+
"typecheck": "tsc --noEmit",
|
|
48
|
+
"prepublishOnly": "bun run build"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"@pyreon/core": "workspace:*",
|
|
52
|
+
"@pyreon/reactivity": "workspace:*",
|
|
53
|
+
"@pyreon/runtime-dom": "workspace:*"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@happy-dom/global-registrator": "^20.8.3",
|
|
57
|
+
"happy-dom": "^20.8.3"
|
|
58
|
+
}
|
|
59
|
+
}
|
package/src/hooks.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pyreon/preact-compat/hooks
|
|
3
|
+
*
|
|
4
|
+
* Preact hooks — separate import like `preact/hooks`.
|
|
5
|
+
* All hooks run on Pyreon's reactive engine under the hood.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { CleanupFn } from "@pyreon/core"
|
|
9
|
+
import { createRef, onErrorCaptured, onMount, onUnmount, useContext } from "@pyreon/core"
|
|
10
|
+
import { computed, effect, getCurrentScope, runUntracked, signal } from "@pyreon/reactivity"
|
|
11
|
+
|
|
12
|
+
export { useContext }
|
|
13
|
+
|
|
14
|
+
// ─── useState ────────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Drop-in for Preact's `useState`.
|
|
18
|
+
* Returns `[getter, setter]` — call `getter()` to read, `setter(v)` to write.
|
|
19
|
+
*/
|
|
20
|
+
export function useState<T>(initial: T | (() => T)): [() => T, (v: T | ((prev: T) => T)) => void] {
|
|
21
|
+
const s = signal<T>(typeof initial === "function" ? (initial as () => T)() : initial)
|
|
22
|
+
const setter = (v: T | ((prev: T) => T)) => {
|
|
23
|
+
if (typeof v === "function") s.update(v as (prev: T) => T)
|
|
24
|
+
else s.set(v)
|
|
25
|
+
}
|
|
26
|
+
return [s, setter]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ─── useEffect ───────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Drop-in for Preact's `useEffect`.
|
|
33
|
+
* The `deps` array is IGNORED — Pyreon tracks dependencies automatically.
|
|
34
|
+
*/
|
|
35
|
+
// biome-ignore lint/suspicious/noConfusingVoidType: void is intentional — callers may return void
|
|
36
|
+
export function useEffect(fn: () => CleanupFn | void, deps?: unknown[]): void {
|
|
37
|
+
if (deps !== undefined && deps.length === 0) {
|
|
38
|
+
onMount((): undefined => {
|
|
39
|
+
const cleanup = runUntracked(fn)
|
|
40
|
+
if (typeof cleanup === "function") onUnmount(cleanup)
|
|
41
|
+
})
|
|
42
|
+
} else {
|
|
43
|
+
// effect() natively supports cleanup: if fn() returns a function,
|
|
44
|
+
// it's called before re-runs and on dispose.
|
|
45
|
+
const e = effect(fn)
|
|
46
|
+
onUnmount(() => {
|
|
47
|
+
e.dispose()
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ─── useLayoutEffect ─────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Drop-in for Preact's `useLayoutEffect`.
|
|
56
|
+
* No distinction from useEffect in Pyreon — same implementation.
|
|
57
|
+
*/
|
|
58
|
+
export const useLayoutEffect = useEffect
|
|
59
|
+
|
|
60
|
+
// ─── useMemo ─────────────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Drop-in for Preact's `useMemo`.
|
|
64
|
+
* Returns a getter — call `value()` to read.
|
|
65
|
+
*/
|
|
66
|
+
export function useMemo<T>(fn: () => T, _deps?: unknown[]): () => T {
|
|
67
|
+
return computed(fn)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ─── useCallback ─────────────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Drop-in for Preact's `useCallback`.
|
|
74
|
+
* Components run once in Pyreon — returns `fn` as-is.
|
|
75
|
+
*/
|
|
76
|
+
export function useCallback<T extends (...args: unknown[]) => unknown>(
|
|
77
|
+
fn: T,
|
|
78
|
+
_deps?: unknown[],
|
|
79
|
+
): T {
|
|
80
|
+
return fn
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ─── useRef ──────────────────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Drop-in for Preact's `useRef`.
|
|
87
|
+
* Returns `{ current: T }`.
|
|
88
|
+
*/
|
|
89
|
+
export function useRef<T>(initial?: T): { current: T | null } {
|
|
90
|
+
const ref = createRef<T>()
|
|
91
|
+
if (initial !== undefined) ref.current = initial as T
|
|
92
|
+
return ref
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ─── useReducer ──────────────────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Drop-in for Preact's `useReducer`.
|
|
99
|
+
*/
|
|
100
|
+
export function useReducer<S, A>(
|
|
101
|
+
reducer: (state: S, action: A) => S,
|
|
102
|
+
initial: S | (() => S),
|
|
103
|
+
): [() => S, (action: A) => void] {
|
|
104
|
+
const s = signal<S>(typeof initial === "function" ? (initial as () => S)() : initial)
|
|
105
|
+
const dispatch = (action: A) => s.update((prev) => reducer(prev, action))
|
|
106
|
+
return [s, dispatch]
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ─── useId ───────────────────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
const _idCounters = new WeakMap<object, number>()
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Drop-in for Preact's `useId`.
|
|
115
|
+
* Returns a stable unique string per component instance.
|
|
116
|
+
*/
|
|
117
|
+
export function useId(): string {
|
|
118
|
+
const scope = getCurrentScope()
|
|
119
|
+
if (!scope) return `:r${Math.random().toString(36).slice(2, 9)}:`
|
|
120
|
+
const count = _idCounters.get(scope) ?? 0
|
|
121
|
+
_idCounters.set(scope, count + 1)
|
|
122
|
+
return `:r${count.toString(36)}:`
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ─── useErrorBoundary ────────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Drop-in for Preact's `useErrorBoundary`.
|
|
129
|
+
* Wraps Pyreon's `onErrorCaptured`.
|
|
130
|
+
*/
|
|
131
|
+
export { onErrorCaptured as useErrorBoundary }
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pyreon/preact-compat
|
|
3
|
+
*
|
|
4
|
+
* Preact-compatible API shim that runs on Pyreon's reactive engine.
|
|
5
|
+
*
|
|
6
|
+
* Provides the core Preact API surface: h, Fragment, render, hydrate,
|
|
7
|
+
* Component class, createContext, createRef, cloneElement, toChildArray,
|
|
8
|
+
* isValidElement, and the options hook object.
|
|
9
|
+
*
|
|
10
|
+
* For hooks, import from "@pyreon/preact-compat/hooks".
|
|
11
|
+
* For signals, import from "@pyreon/preact-compat/signals".
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { Props, VNode, VNodeChild } from "@pyreon/core"
|
|
15
|
+
import { createContext, createRef, Fragment, h as pyreonH, useContext } from "@pyreon/core"
|
|
16
|
+
import { batch, signal } from "@pyreon/reactivity"
|
|
17
|
+
import { hydrateRoot, mount } from "@pyreon/runtime-dom"
|
|
18
|
+
|
|
19
|
+
// ─── Core JSX ────────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
/** Preact's hyperscript function — maps directly to Pyreon's h() */
|
|
22
|
+
export { pyreonH as h }
|
|
23
|
+
|
|
24
|
+
/** Alias: Preact also exports createElement */
|
|
25
|
+
export const createElement = pyreonH
|
|
26
|
+
|
|
27
|
+
export { Fragment }
|
|
28
|
+
|
|
29
|
+
// ─── Render / Hydrate ────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Preact's `render(vnode, container)`.
|
|
33
|
+
* Maps to Pyreon's `mount(vnode, container)`.
|
|
34
|
+
*/
|
|
35
|
+
export function render(vnode: VNodeChild, container: Element): void {
|
|
36
|
+
mount(vnode, container)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Preact's `hydrate(vnode, container)`.
|
|
41
|
+
* Maps to Pyreon's `hydrateRoot(container, vnode)`.
|
|
42
|
+
*/
|
|
43
|
+
export function hydrate(vnode: VNodeChild, container: Element): void {
|
|
44
|
+
hydrateRoot(container, vnode as VNode)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ─── Context ─────────────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
export { createContext, useContext }
|
|
50
|
+
|
|
51
|
+
// ─── Refs ────────────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
export { createRef }
|
|
54
|
+
|
|
55
|
+
// ─── Component class ─────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Preact-compatible class-based Component.
|
|
59
|
+
*
|
|
60
|
+
* Wraps Pyreon's signal-based reactivity so `setState` triggers re-renders.
|
|
61
|
+
* Usage: `class MyComp extends Component { render() { ... } }`
|
|
62
|
+
*/
|
|
63
|
+
export class Component<
|
|
64
|
+
P extends Props = Props,
|
|
65
|
+
S extends Record<string, unknown> = Record<string, unknown>,
|
|
66
|
+
> {
|
|
67
|
+
props: P
|
|
68
|
+
state: S
|
|
69
|
+
private _stateSignal: ReturnType<typeof signal<S>>
|
|
70
|
+
|
|
71
|
+
constructor(props: P) {
|
|
72
|
+
this.props = props
|
|
73
|
+
this.state = {} as S
|
|
74
|
+
this._stateSignal = signal<S>(this.state)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Update state — accepts a partial state object or an updater function.
|
|
79
|
+
* Merges into existing state (shallow merge, like Preact/React).
|
|
80
|
+
*/
|
|
81
|
+
setState(partial: Partial<S> | ((prev: S) => Partial<S>)): void {
|
|
82
|
+
batch(() => {
|
|
83
|
+
const current = this._stateSignal()
|
|
84
|
+
const update =
|
|
85
|
+
typeof partial === "function" ? (partial as (prev: S) => Partial<S>)(current) : partial
|
|
86
|
+
const next = { ...current, ...update } as S
|
|
87
|
+
this.state = next
|
|
88
|
+
this._stateSignal.set(next)
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Force a re-render. In Pyreon this triggers the state signal to re-fire.
|
|
94
|
+
*/
|
|
95
|
+
forceUpdate(): void {
|
|
96
|
+
this._stateSignal.set({ ...this.state })
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Override in subclass to return VNode tree.
|
|
101
|
+
*/
|
|
102
|
+
render(): VNodeChild {
|
|
103
|
+
return null
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ─── cloneElement ────────────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Clone a VNode with merged props (like Preact's cloneElement).
|
|
111
|
+
*/
|
|
112
|
+
export function cloneElement(vnode: VNode, props?: Props, ...children: VNodeChild[]): VNode {
|
|
113
|
+
const mergedProps = { ...vnode.props, ...(props ?? {}) }
|
|
114
|
+
const mergedChildren = children.length > 0 ? children : vnode.children
|
|
115
|
+
return {
|
|
116
|
+
type: vnode.type,
|
|
117
|
+
props: mergedProps,
|
|
118
|
+
children: mergedChildren,
|
|
119
|
+
key: (props?.key as string | number | null) ?? vnode.key,
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ─── toChildArray ────────────────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Flatten children into a flat array, filtering out null/undefined/boolean.
|
|
127
|
+
* Matches Preact's `toChildArray` utility.
|
|
128
|
+
*/
|
|
129
|
+
export function toChildArray(children: VNodeChild | VNodeChild[]): VNodeChild[] {
|
|
130
|
+
const result: VNodeChild[] = []
|
|
131
|
+
flatten(children, result)
|
|
132
|
+
return result
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function flatten(value: VNodeChild | VNodeChild[], out: VNodeChild[]): void {
|
|
136
|
+
if (value == null || typeof value === "boolean") return
|
|
137
|
+
if (Array.isArray(value)) {
|
|
138
|
+
for (const child of value) {
|
|
139
|
+
flatten(child as VNodeChild, out)
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
out.push(value)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ─── isValidElement ──────────────────────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Check if a value is a VNode (like Preact's isValidElement).
|
|
150
|
+
*/
|
|
151
|
+
export function isValidElement(x: unknown): x is VNode {
|
|
152
|
+
return (
|
|
153
|
+
x !== null &&
|
|
154
|
+
typeof x === "object" &&
|
|
155
|
+
"type" in (x as Record<string, unknown>) &&
|
|
156
|
+
"props" in (x as Record<string, unknown>) &&
|
|
157
|
+
"children" in (x as Record<string, unknown>)
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ─── options ─────────────────────────────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Preact's plugin/hook system. Exposed as an empty object for compatibility
|
|
165
|
+
* with libraries that check for `options._hook`, `options.vnode`, etc.
|
|
166
|
+
*/
|
|
167
|
+
export const options: Record<string, unknown> = {}
|
package/src/signals.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pyreon/preact-compat/signals
|
|
3
|
+
*
|
|
4
|
+
* Preact Signals compatibility layer (`@preact/signals` style).
|
|
5
|
+
* Wraps Pyreon's signal/computed in `{ value }` accessor objects.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Effect } from "@pyreon/reactivity"
|
|
9
|
+
import {
|
|
10
|
+
batch as pyreonBatch,
|
|
11
|
+
computed as pyreonComputed,
|
|
12
|
+
effect as pyreonEffect,
|
|
13
|
+
signal as pyreonSignal,
|
|
14
|
+
} from "@pyreon/reactivity"
|
|
15
|
+
|
|
16
|
+
// ─── Signal ──────────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
export interface ReadonlySignal<T> {
|
|
19
|
+
readonly value: T
|
|
20
|
+
peek(): T
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface WritableSignal<T> extends ReadonlySignal<T> {
|
|
24
|
+
value: T
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Create a Preact-style signal with `.value` accessor.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* const count = signal(0)
|
|
32
|
+
* count.value++ // write
|
|
33
|
+
* console.log(count.value) // read (tracked)
|
|
34
|
+
*/
|
|
35
|
+
export function signal<T>(initial: T): WritableSignal<T> {
|
|
36
|
+
const s = pyreonSignal<T>(initial)
|
|
37
|
+
return {
|
|
38
|
+
get value(): T {
|
|
39
|
+
return s()
|
|
40
|
+
},
|
|
41
|
+
set value(v: T) {
|
|
42
|
+
s.set(v)
|
|
43
|
+
},
|
|
44
|
+
peek(): T {
|
|
45
|
+
return s.peek()
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ─── Computed ────────────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Create a Preact-style computed with `.value` accessor.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* const doubled = computed(() => count.value * 2)
|
|
57
|
+
* console.log(doubled.value)
|
|
58
|
+
*/
|
|
59
|
+
export function computed<T>(fn: () => T): ReadonlySignal<T> {
|
|
60
|
+
const c = pyreonComputed(fn)
|
|
61
|
+
return {
|
|
62
|
+
get value(): T {
|
|
63
|
+
return c()
|
|
64
|
+
},
|
|
65
|
+
peek(): T {
|
|
66
|
+
// computed doesn't have peek — just read the value untracked
|
|
67
|
+
return c()
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ─── Effect ──────────────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Run a side-effect that auto-tracks signal reads.
|
|
76
|
+
* Returns a dispose function.
|
|
77
|
+
*/
|
|
78
|
+
// biome-ignore lint/suspicious/noConfusingVoidType: void is intentional — callers may return void
|
|
79
|
+
export function effect(fn: () => void | (() => void)): () => void {
|
|
80
|
+
// Pyreon's effect() natively supports cleanup return values
|
|
81
|
+
const e: Effect = pyreonEffect(fn)
|
|
82
|
+
return () => {
|
|
83
|
+
e.dispose()
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ─── Batch ───────────────────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Batch multiple signal writes into a single update.
|
|
91
|
+
*/
|
|
92
|
+
export { pyreonBatch as batch }
|