pudui 0.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.
package/README.md ADDED
@@ -0,0 +1,161 @@
1
+ # pudui
2
+
3
+ A tiny JSX UI runtime built from the ground up. The model is intentionally
4
+ small: functional component factories return `Component` instances, and
5
+ component state lives in ordinary closures.
6
+
7
+ ## Constraints
8
+
9
+ - No runtime dependencies.
10
+ - No hooks.
11
+ - No class components as a user-facing component model.
12
+ - Functional components only.
13
+ - Stateful behavior is explicit: mutate closure data and call
14
+ `component.update()`.
15
+
16
+ ## MVP API
17
+
18
+ - `Component<Props>` stores a render function and exposes `update(callback?)`
19
+ and `mount(callback)`.
20
+ - `component.update(callback?)` requests an update. When a callback is passed,
21
+ it runs after the update has committed to the DOM.
22
+ - `Component<Props>` accepts an optional `error(props, reason)` function that
23
+ renders fallback output when that component or one of its children throws.
24
+ - `render(child, container)` mounts JSX or a component into a DOM container and
25
+ returns a root with `rerender(child)` and `unmount()`.
26
+ - `hydrate(root, options)` hydrates server-rendered boundaries and returns the
27
+ same root API synchronously.
28
+ - `renderToString(child, props?)` from `pudui/server` renders JSX or a component
29
+ to escaped HTML.
30
+ - `createElement`, `jsx`, `jsxs`, and `jsxDEV` provide classic and automatic JSX
31
+ runtimes.
32
+ - `ref` accepts one callback or nested arrays of callbacks. Each callback
33
+ receives the element after insertion and may return a cleanup function that
34
+ runs before removal. Known JSX tag names infer the matching DOM element type.
35
+ - `component.mount(callback)` registers callbacks that run once, after the
36
+ component first enters the DOM and after ref callbacks for that commit.
37
+ - `event(type, handler)` returns a ref callback that adds an event listener and
38
+ removes it during cleanup. Known DOM event names infer the matching event
39
+ object type.
40
+ - `Fragment` renders children without adding an extra element.
41
+
42
+ ## Child Diffing and Keys
43
+
44
+ Pudui flattens nested child arrays and fragments before rendering. Empty
45
+ children (`null`, `undefined`, and booleans) are ignored.
46
+
47
+ Child arrays are reconciled by position. During DOM updates, the child at index
48
+ 0 is patched with the next child at index 0, index 1 with index 1, and so on;
49
+ new trailing children are appended together and removed trailing children are
50
+ cleaned up together. Pudui does not compute keyed list moves across an array.
51
+
52
+ `key` still affects identity at the current position. Host elements are reused
53
+ only when their tag name and key match, and child components rendered by a
54
+ parent are cached by explicit key or, when no key is provided, by render order.
55
+ Use keys to preserve component state across conditional output, but do not rely
56
+ on keys to reorder existing DOM nodes in a list.
57
+
58
+ ## JSX Setup
59
+
60
+ Use the automatic runtime in applications:
61
+
62
+ ```json
63
+ {
64
+ "compilerOptions": {
65
+ "jsx": "react-jsx",
66
+ "jsxImportSource": "pudui"
67
+ }
68
+ }
69
+ ```
70
+
71
+ ## Stateless Component
72
+
73
+ ```tsx
74
+ import { Component } from "pudui";
75
+ import { renderToString } from "pudui/server";
76
+
77
+ type Props = {
78
+ name: string;
79
+ };
80
+
81
+ function HelloWorld(_initialProps: Props) {
82
+ return new Component<Props>({
83
+ render({ name }) {
84
+ return <div>Hello, {name}!</div>;
85
+ },
86
+ });
87
+ }
88
+
89
+ renderToString(<HelloWorld name="Ada" />);
90
+ ```
91
+
92
+ ## Error Handling
93
+
94
+ ```tsx
95
+ import { type Child, Component } from "pudui";
96
+
97
+ type Props = {
98
+ children?: Child;
99
+ };
100
+
101
+ function Boundary(_initialProps: Props) {
102
+ return new Component<Props>({
103
+ error(_props, reason) {
104
+ return <p>Something went wrong: {String(reason)}</p>;
105
+ },
106
+ render({ children }) {
107
+ return <section>{children}</section>;
108
+ },
109
+ });
110
+ }
111
+ ```
112
+
113
+ ## Stateful Component
114
+
115
+ ```tsx
116
+ import { Component, event } from "pudui";
117
+
118
+ type Props = {
119
+ initialCount?: number;
120
+ };
121
+
122
+ function Counter(initialProps: Props) {
123
+ let count = initialProps.initialCount ?? 0;
124
+
125
+ const component = new Component<Props>({
126
+ render(renderProps) {
127
+ if (initialProps.initialCount !== renderProps.initialCount) {
128
+ count = renderProps.initialCount ?? 0;
129
+ }
130
+
131
+ return (
132
+ <div>
133
+ <p>Count: {count}</p>
134
+ <button
135
+ type="button"
136
+ ref={[
137
+ event("click", () => {
138
+ count++;
139
+ component.update();
140
+ }),
141
+ ]}
142
+ >
143
+ Increment
144
+ </button>
145
+ </div>
146
+ );
147
+ },
148
+ });
149
+
150
+ return component;
151
+ }
152
+ ```
153
+
154
+ ## Development
155
+
156
+ ```bash
157
+ vp check
158
+ vp test
159
+ vp run bundle-size
160
+ vp pack
161
+ ```
@@ -0,0 +1,275 @@
1
+ //#region src/invariant.ts
2
+ /**
3
+ * Throws a `TypeError` when a condition is not truthy.
4
+ *
5
+ * @param condition Condition to assert.
6
+ * @param message Error message for failed assertions.
7
+ */
8
+ function invariant(condition, message) {
9
+ if (!condition) fail(message);
10
+ }
11
+ /**
12
+ * Throws a `TypeError` with the provided message.
13
+ *
14
+ * @param message Error message to throw.
15
+ * @returns This function never returns.
16
+ */
17
+ function fail(message) {
18
+ throw new TypeError(message);
19
+ }
20
+ //#endregion
21
+ //#region src/core/component.ts
22
+ const componentInternals = /* @__PURE__ */ new WeakMap();
23
+ let updateFrameScheduled = false;
24
+ let scheduledUpdates = /* @__PURE__ */ new Map();
25
+ /**
26
+ * Stateful unit of rendering in Pudui.
27
+ *
28
+ * Components own their render function, mount callbacks, child component
29
+ * instances, and update scheduling. Create components from JSX factories and
30
+ * call {@link update} when internal state changes.
31
+ *
32
+ * @example
33
+ * ```tsx
34
+ * import { Component, event } from "pudui";
35
+ *
36
+ * export function Counter() {
37
+ * let count = 0;
38
+ * const counter = new Component({
39
+ * render() {
40
+ * return <button ref={event("click", () => { count++; counter.update(); })}>{count}</button>;
41
+ * },
42
+ * });
43
+ *
44
+ * return counter;
45
+ * }
46
+ * ```
47
+ */
48
+ var Component = class {
49
+ /**
50
+ * Creates a component from render options.
51
+ *
52
+ * @param options Render, error, and hydration options for this component.
53
+ */
54
+ constructor(options) {
55
+ componentInternals.set(this, {
56
+ i: 0,
57
+ c: /* @__PURE__ */ new Map(),
58
+ e: options.error,
59
+ h: options.hydrate,
60
+ f: /* @__PURE__ */ new Map(),
61
+ m: false,
62
+ o: [],
63
+ p: void 0,
64
+ r: options.render,
65
+ u: []
66
+ });
67
+ }
68
+ /**
69
+ * Queues a callback to run after the component's first browser mount.
70
+ *
71
+ * If the component is already mounted, the callback is ignored.
72
+ *
73
+ * @param callback Function to run after the initial mount commits.
74
+ */
75
+ mount(callback) {
76
+ const internals = getComponentInternals(this);
77
+ if (!internals.m) internals.o.push(callback);
78
+ }
79
+ /**
80
+ * Schedules this component to rerender.
81
+ *
82
+ * Multiple updates in the same turn are batched. The optional callback runs
83
+ * after the update commits, unless the root has been unmounted.
84
+ *
85
+ * @param callback Function to run after the update commits.
86
+ */
87
+ update(callback) {
88
+ for (const listener of getComponentInternals(this).f.keys()) scheduleUpdate(listener, callback);
89
+ }
90
+ };
91
+ /**
92
+ * Checks whether a value is a Pudui component instance.
93
+ *
94
+ * @param value Value to inspect.
95
+ * @returns `true` when the value is a {@link Component}.
96
+ */
97
+ function isComponent(value) {
98
+ return value instanceof Component;
99
+ }
100
+ function getComponentInternals(component) {
101
+ const internals = componentInternals.get(component);
102
+ invariant(internals, "component");
103
+ return internals;
104
+ }
105
+ /**
106
+ * Replaces the props stored on a component instance.
107
+ *
108
+ * @param component Component to update.
109
+ * @param props Next props.
110
+ */
111
+ function setComponentProps(component, props) {
112
+ getComponentInternals(component).p = props;
113
+ }
114
+ /**
115
+ * Reads the current props stored on a component instance.
116
+ *
117
+ * @param component Component to inspect.
118
+ * @returns Current props, or an empty object before props are assigned.
119
+ */
120
+ function readComponentProps(component) {
121
+ return getComponentInternals(component).p ?? {};
122
+ }
123
+ /**
124
+ * Reads hydration metadata associated with a component.
125
+ *
126
+ * @param component Component to inspect.
127
+ * @returns Hydration metadata supplied in component options.
128
+ */
129
+ function readComponentHydrateMeta(component) {
130
+ return getComponentInternals(component).h;
131
+ }
132
+ /**
133
+ * Runs a component's render callback.
134
+ *
135
+ * @param component Component to render.
136
+ * @returns Rendered child output.
137
+ */
138
+ function renderComponentOutput(component) {
139
+ return getComponentInternals(component).r(readComponentProps(component));
140
+ }
141
+ /**
142
+ * Runs a component's error callback for a failed render.
143
+ *
144
+ * @param component Component whose render failed.
145
+ * @param reason Error or rejection reason from rendering.
146
+ * @returns Fallback child output.
147
+ */
148
+ function renderComponentErrorOutput(component, reason) {
149
+ const internals = getComponentInternals(component);
150
+ if (!internals.e) throw reason;
151
+ return internals.e(readComponentProps(component), reason);
152
+ }
153
+ /**
154
+ * Marks the beginning of a component render pass.
155
+ *
156
+ * @param component Component being rendered.
157
+ */
158
+ function beginComponentRender(component) {
159
+ const internals = getComponentInternals(component);
160
+ internals.i = 0;
161
+ internals.u = [];
162
+ }
163
+ /**
164
+ * Marks the end of a component render pass and prunes unused child instances.
165
+ *
166
+ * @param component Component that finished rendering.
167
+ */
168
+ function finishComponentRender(component) {
169
+ const internals = getComponentInternals(component);
170
+ for (const childSlot of internals.c.keys()) if (!internals.u.includes(childSlot)) internals.c.delete(childSlot);
171
+ }
172
+ /**
173
+ * Resolves a component virtual node to a component instance.
174
+ *
175
+ * @param vnode Component virtual node.
176
+ * @param owner Parent component that owns child instance slots.
177
+ * @returns Existing or newly created component instance.
178
+ */
179
+ function resolveComponent(vnode, owner) {
180
+ if (!owner) return createComponent(vnode.type, vnode.props);
181
+ const internals = getComponentInternals(owner);
182
+ const slot = vnode.key === void 0 ? "." + internals.i++ : "#" + String(vnode.key);
183
+ const existing = internals.c.get(slot);
184
+ internals.u.push(slot);
185
+ if (existing?.t === vnode.type) {
186
+ setComponentProps(existing.v, vnode.props);
187
+ return existing.v;
188
+ }
189
+ const component = createComponent(vnode.type, vnode.props);
190
+ internals.c.set(slot, {
191
+ t: vnode.type,
192
+ v: component
193
+ });
194
+ return component;
195
+ }
196
+ /**
197
+ * Creates a component instance from a component factory.
198
+ *
199
+ * @param factory Component factory to invoke.
200
+ * @param props Initial props.
201
+ * @returns Created component instance.
202
+ */
203
+ function createComponent(factory, props) {
204
+ const component = factory(props);
205
+ if (!isComponent(component)) throw new TypeError("Component");
206
+ setComponentProps(component, props);
207
+ return component;
208
+ }
209
+ /**
210
+ * Subscribes a renderer to component updates.
211
+ *
212
+ * @param component Component to observe.
213
+ * @param listener Update listener to schedule.
214
+ * @returns Cleanup function that removes the subscription.
215
+ */
216
+ function subscribeComponent(component, listener) {
217
+ const listeners = getComponentInternals(component).f;
218
+ listeners.set(listener, (listeners.get(listener) ?? 0) + 1);
219
+ return () => {
220
+ const count = listeners.get(listener);
221
+ if (count !== void 0) if (count <= 1) listeners.delete(listener);
222
+ else listeners.set(listener, count - 1);
223
+ };
224
+ }
225
+ /**
226
+ * Queues a component's mount callbacks into a commit effect list.
227
+ *
228
+ * @param component Component that may need mounting.
229
+ * @param mountEffects Effect list for the current commit.
230
+ */
231
+ function queueComponentMount(component, mountEffects) {
232
+ if (getComponentInternals(component).m) return;
233
+ mountEffects.push(() => {
234
+ mountComponent(component);
235
+ });
236
+ }
237
+ function mountComponent(component) {
238
+ const internals = getComponentInternals(component);
239
+ if (internals.m) return;
240
+ internals.m = true;
241
+ const callbacks = internals.o;
242
+ internals.o = [];
243
+ for (const callback of callbacks) callback();
244
+ }
245
+ function scheduleUpdate(listener, callback) {
246
+ let callbacks = scheduledUpdates.get(listener);
247
+ if (!callbacks) scheduledUpdates.set(listener, callbacks = []);
248
+ callback && callbacks.push(callback);
249
+ if (updateFrameScheduled) return;
250
+ updateFrameScheduled = true;
251
+ Promise.resolve().then(() => {
252
+ queueMicrotask(() => {
253
+ updateFrameScheduled = false;
254
+ const updates = scheduledUpdates;
255
+ scheduledUpdates = /* @__PURE__ */ new Map();
256
+ for (const [update, callbacks] of updates) {
257
+ const committed = update();
258
+ if (isPromiseLike(committed)) {
259
+ committed.then((asyncCommitted) => {
260
+ if (asyncCommitted === false) return;
261
+ for (const callback of callbacks) callback();
262
+ });
263
+ continue;
264
+ }
265
+ if (committed === false) continue;
266
+ for (const callback of callbacks) callback();
267
+ }
268
+ });
269
+ });
270
+ }
271
+ function isPromiseLike(value) {
272
+ return typeof value?.then === "function";
273
+ }
274
+ //#endregion
275
+ export { isComponent as a, readComponentProps as c, resolveComponent as d, setComponentProps as f, invariant as h, finishComponentRender as i, renderComponentErrorOutput as l, fail as m, beginComponentRender as n, queueComponentMount as o, subscribeComponent as p, createComponent as r, readComponentHydrateMeta as s, Component as t, renderComponentOutput as u };