ink-framework 0.1.0-alpha.12 → 0.1.0-alpha.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.
- package/DESIGN.md +168 -0
- package/package.json +8 -4
- package/src/compiler-runtime.ts +6 -0
- package/src/controller-react.ts +33 -0
- package/src/file-router.tsx +1 -1
- package/src/fixed-list-row.ts +65 -0
- package/src/image.ts +16 -2
- package/src/index.ts +29 -2
- package/src/jsx-runtime.ts +21 -0
- package/src/list.ts +71 -34
- package/src/native-list.ts +12 -18
- package/src/native.ts +8 -0
- package/src/prefresh.d.ts +3 -0
- package/src/react-compat.ts +24 -0
- package/src/react.ts +26 -6
- package/src/refresh.ts +29 -0
- package/src/renderer.ts +183 -140
- package/src/reorder.ts +12 -15
- package/src/visibility.ts +264 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { options, type Component, type VNode } from "preact";
|
|
2
|
+
import { useEffect as effectSlot, useLayoutEffect as layoutSlot, useState } from "preact/hooks";
|
|
3
|
+
import type { DependencyList, EffectCallback, ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
type Phase = "layout" | "passive";
|
|
6
|
+
type Slot = { __c?: () => void; inkEffect?: Effect };
|
|
7
|
+
type Scope = { visible: boolean; committed: boolean; changed: boolean; parent?: Scope };
|
|
8
|
+
type Owner = Component<Record<string, unknown>> & {
|
|
9
|
+
__v: Tree; __H?: { __: Slot[] }; __n: { __inkVisibility?: Scope };
|
|
10
|
+
inkScope?: Scope; getChildContext?: () => { __inkVisibility: Scope };
|
|
11
|
+
};
|
|
12
|
+
type Tree = VNode<Record<string, unknown>> & { __?: Tree; __k?: (Tree | null)[]; __c?: Owner; __v?: number | null };
|
|
13
|
+
type Effect = {
|
|
14
|
+
owner: Effects; slot: Slot; phase: Phase; callback: EffectCallback;
|
|
15
|
+
deps?: DependencyList; nextDeps?: DependencyList; pending: boolean; active: boolean;
|
|
16
|
+
};
|
|
17
|
+
type Effects = { component: Owner; effects: Effect[]; rendering: Effect[]; visible: boolean; mounted: boolean };
|
|
18
|
+
const owners = new WeakMap<Owner, Effects>();
|
|
19
|
+
const pending = new Set<Effects>();
|
|
20
|
+
const boundaries = new Set<Tree>();
|
|
21
|
+
let current: Owner;
|
|
22
|
+
let index = 0;
|
|
23
|
+
let previousHook: typeof hooks.__h;
|
|
24
|
+
let passive: (() => void)[] = [];
|
|
25
|
+
let flushing = false;
|
|
26
|
+
|
|
27
|
+
// Preact 10.29's published option-hook ABI. Keep this with the renderer ABI.
|
|
28
|
+
const hooks = options as typeof options & {
|
|
29
|
+
__r?: (vnode: Tree) => void;
|
|
30
|
+
__h?: (component: Owner, index: number, type: number) => void;
|
|
31
|
+
__c?: (vnode: Tree, queue: unknown[]) => void;
|
|
32
|
+
__e: (error: unknown, vnode: Tree) => void;
|
|
33
|
+
__s?: boolean;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** Keep state and host nodes while disconnecting effects in the hidden subtree. */
|
|
37
|
+
export function Activity(this: Owner, { mode, children }: { mode: "visible" | "hidden"; children?: ReactNode }): ReactNode {
|
|
38
|
+
if (!this.inkScope) {
|
|
39
|
+
const scope = this.inkScope = { visible: mode === "visible", committed: mode === "visible", changed: false, parent: this.__n.__inkVisibility };
|
|
40
|
+
const context = { __inkVisibility: scope };
|
|
41
|
+
this.getChildContext = () => context;
|
|
42
|
+
// Keep the last reconciled children while hidden. Preact retains the latest
|
|
43
|
+
// props on this component, so showing it applies deferred parent updates.
|
|
44
|
+
this.shouldComponentUpdate = next => {
|
|
45
|
+
scope.visible = next.mode === "visible";
|
|
46
|
+
return scope.visible;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
this.inkScope.visible = mode === "visible";
|
|
50
|
+
return children;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function isComponentVisible(component: Component): boolean {
|
|
54
|
+
const owner = component as Owner;
|
|
55
|
+
for (let scope = owner.inkScope ?? owner.__n?.__inkVisibility; scope; scope = scope.parent)
|
|
56
|
+
if (!scope.visible) return false;
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function activityChanged(component: Component): boolean {
|
|
61
|
+
return (component as Owner).inkScope!.changed;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function effectHook(component: Owner, slot: number, type: number) {
|
|
65
|
+
index = slot;
|
|
66
|
+
previousHook?.(component, slot, type);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function changed(previous: DependencyList | undefined, next: DependencyList | undefined) {
|
|
70
|
+
if (!previous || !next || previous.length !== next.length) return true;
|
|
71
|
+
for (let i = 0; i < next.length; i++) if (!Object.is(previous[i], next[i])) return true;
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function useScreenEffect(callback: EffectCallback, deps: DependencyList | undefined, phase: Phase) {
|
|
76
|
+
// Allocate a genuine Preact effect slot: Prefresh can clean it and reset it.
|
|
77
|
+
// Ink owns its scheduling, so Preact must not also enqueue the effect.
|
|
78
|
+
const skipped = hooks.__s;
|
|
79
|
+
const hook = hooks.__h;
|
|
80
|
+
previousHook = hook;
|
|
81
|
+
hooks.__h = effectHook;
|
|
82
|
+
hooks.__s = true;
|
|
83
|
+
try { (phase === "layout" ? layoutSlot : effectSlot)(callback, deps); }
|
|
84
|
+
finally { hooks.__s = skipped; hooks.__h = hook; previousHook = undefined; }
|
|
85
|
+
const slot = current.__H!.__[index];
|
|
86
|
+
let owner = owners.get(current);
|
|
87
|
+
if (!owner) {
|
|
88
|
+
owner = { component: current, effects: [], rendering: [], visible: isComponentVisible(current), mounted: true };
|
|
89
|
+
owners.set(current, owner);
|
|
90
|
+
}
|
|
91
|
+
const effect = slot.inkEffect ??= { owner, slot, phase, callback, pending: true, active: false };
|
|
92
|
+
effect.callback = callback;
|
|
93
|
+
effect.nextDeps = deps;
|
|
94
|
+
effect.pending = !effect.active || changed(effect.deps, deps);
|
|
95
|
+
owner.rendering.push(effect);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function useEffect(callback: EffectCallback, deps?: DependencyList) {
|
|
99
|
+
useScreenEffect(callback, deps, "passive");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function useLayoutEffect(callback: EffectCallback, deps?: DependencyList) {
|
|
103
|
+
useScreenEffect(callback, deps, "layout");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function clean(effect: Effect) {
|
|
107
|
+
const cleanup = effect.slot.__c;
|
|
108
|
+
effect.slot.__c = undefined;
|
|
109
|
+
effect.active = false;
|
|
110
|
+
cleanup?.();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function start(effect: Effect) {
|
|
114
|
+
if (!effect.owner.mounted || !effect.owner.visible || !effect.pending) return;
|
|
115
|
+
effect.pending = false;
|
|
116
|
+
effect.active = true;
|
|
117
|
+
effect.deps = effect.nextDeps;
|
|
118
|
+
const cleanup = effect.callback();
|
|
119
|
+
// Prefresh invokes this same cleanup before refreshing or resetting hooks.
|
|
120
|
+
effect.slot.__c = () => {
|
|
121
|
+
effect.active = false;
|
|
122
|
+
effect.deps = undefined;
|
|
123
|
+
cleanup?.();
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function run(owner: Effects, phase: Phase, setup: boolean, all = false) {
|
|
128
|
+
for (const effect of owner.effects) {
|
|
129
|
+
if (effect.phase !== phase) continue;
|
|
130
|
+
try {
|
|
131
|
+
if (setup) start(effect);
|
|
132
|
+
else if (effect.active && (all || effect.pending)) clean(effect);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
hooks.__e(error, owner.component.__v);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function flushPassive() {
|
|
140
|
+
if (flushing || !passive.length) return;
|
|
141
|
+
const work = passive;
|
|
142
|
+
passive = [];
|
|
143
|
+
flushing = true;
|
|
144
|
+
try { for (const callback of work) callback(); }
|
|
145
|
+
finally { flushing = false; }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const beforeRender = hooks.__r;
|
|
149
|
+
hooks.__r = vnode => {
|
|
150
|
+
flushPassive();
|
|
151
|
+
beforeRender?.(vnode);
|
|
152
|
+
current = vnode.__c!;
|
|
153
|
+
const owner = owners.get(current);
|
|
154
|
+
if (owner) owner.rendering = [];
|
|
155
|
+
};
|
|
156
|
+
const afterDiff = options.diffed;
|
|
157
|
+
options.diffed = vnode => {
|
|
158
|
+
afterDiff?.(vnode);
|
|
159
|
+
const tree = vnode as Tree;
|
|
160
|
+
if (tree.__v === null) return;
|
|
161
|
+
const owner = tree.__c && owners.get(tree.__c);
|
|
162
|
+
if (owner && owner.rendering.length) {
|
|
163
|
+
owner.effects = owner.rendering;
|
|
164
|
+
pending.add(owner);
|
|
165
|
+
}
|
|
166
|
+
if (tree.type === Activity && tree.__c) {
|
|
167
|
+
const scope = tree.__c.inkScope!;
|
|
168
|
+
scope.changed = scope.committed !== scope.visible;
|
|
169
|
+
scope.committed = scope.visible;
|
|
170
|
+
if (scope.changed) boundaries.add(tree);
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
function collect(tree: Tree, before: Effects[], after: Effects[]) {
|
|
175
|
+
const owner = tree.__c && owners.get(tree.__c);
|
|
176
|
+
if (owner) before.push(owner);
|
|
177
|
+
for (const child of tree.__k ?? []) if (child) collect(child, before, after);
|
|
178
|
+
if (owner) after.push(owner);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const commit = hooks.__c;
|
|
182
|
+
hooks.__c = (root, queue) => {
|
|
183
|
+
let work = [...pending];
|
|
184
|
+
const preorder: Effects[] = [];
|
|
185
|
+
// A memoised child may not render when its Activity changes visibility.
|
|
186
|
+
if (boundaries.size) {
|
|
187
|
+
work = [];
|
|
188
|
+
collect(root, preorder, work);
|
|
189
|
+
}
|
|
190
|
+
const hidden: Effects[] = [];
|
|
191
|
+
for (const owner of preorder.length ? preorder : work) {
|
|
192
|
+
const next = isComponentVisible(owner.component);
|
|
193
|
+
if (next !== owner.visible) {
|
|
194
|
+
owner.visible = next;
|
|
195
|
+
if (!next) hidden.push(owner);
|
|
196
|
+
else for (const effect of owner.effects) effect.pending = true;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
pending.clear();
|
|
200
|
+
boundaries.clear();
|
|
201
|
+
// Effect-event refs are commit bookkeeping and must update before cleanups.
|
|
202
|
+
commit?.(root, queue);
|
|
203
|
+
for (const owner of hidden) run(owner, "layout", false, true);
|
|
204
|
+
for (const owner of work) if (owner.visible) run(owner, "layout", false);
|
|
205
|
+
for (const owner of work) if (owner.visible) run(owner, "layout", true);
|
|
206
|
+
if (hidden.length || work.length) {
|
|
207
|
+
passive.push(() => {
|
|
208
|
+
for (const owner of hidden) run(owner, "passive", false, true);
|
|
209
|
+
for (const owner of work) if (owner.visible) run(owner, "passive", false);
|
|
210
|
+
for (const owner of work) if (owner.visible) run(owner, "passive", true);
|
|
211
|
+
});
|
|
212
|
+
queueMicrotask(flushPassive);
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const unmount = options.unmount;
|
|
217
|
+
options.unmount = vnode => {
|
|
218
|
+
const component = (vnode as Tree).__c;
|
|
219
|
+
const owner = component && owners.get(component);
|
|
220
|
+
if (owner) {
|
|
221
|
+
owner.mounted = false;
|
|
222
|
+
pending.delete(owner);
|
|
223
|
+
run(owner, "layout", false, true);
|
|
224
|
+
// Preact's unmount hook must not run passive cleanups in the layout phase.
|
|
225
|
+
const cleanups = owner.effects.filter(effect => effect.phase === "passive" && effect.active)
|
|
226
|
+
.map(effect => { const cleanup = effect.slot.__c; effect.slot.__c = undefined; effect.active = false; return cleanup; });
|
|
227
|
+
passive.push(() => { for (const cleanup of cleanups) cleanup?.(); });
|
|
228
|
+
queueMicrotask(flushPassive);
|
|
229
|
+
}
|
|
230
|
+
unmount?.(vnode);
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
/** Subscribe only while visible; read the current snapshot when reconnecting. */
|
|
234
|
+
export function useSyncExternalStore<T>(subscribe: (callback: () => void) => () => void, getSnapshot: () => T, _getServerSnapshot?: () => T): T {
|
|
235
|
+
const value = getSnapshot();
|
|
236
|
+
const [{ instance }, update] = useState(() => ({ instance: { value, getSnapshot } }));
|
|
237
|
+
const check = () => {
|
|
238
|
+
try { return !Object.is(instance.value, instance.getSnapshot()); }
|
|
239
|
+
catch { return true; }
|
|
240
|
+
};
|
|
241
|
+
useLayoutEffect(() => {
|
|
242
|
+
instance.value = value;
|
|
243
|
+
instance.getSnapshot = getSnapshot;
|
|
244
|
+
if (check()) update({ instance });
|
|
245
|
+
}, [subscribe, value, getSnapshot]);
|
|
246
|
+
useEffect(() => {
|
|
247
|
+
if (check()) update({ instance });
|
|
248
|
+
return subscribe(() => { if (check()) update({ instance }); });
|
|
249
|
+
}, [subscribe]);
|
|
250
|
+
return value;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function useImperativeHandle<T>(ref: import("react").Ref<T> | undefined, create: () => T, deps?: DependencyList) {
|
|
254
|
+
useLayoutEffect(() => {
|
|
255
|
+
if (typeof ref === "function") {
|
|
256
|
+
const cleanup = ref(create());
|
|
257
|
+
return typeof cleanup === "function" ? cleanup : () => { ref(null); };
|
|
258
|
+
}
|
|
259
|
+
if (ref) {
|
|
260
|
+
ref.current = create();
|
|
261
|
+
return () => { ref.current = null; };
|
|
262
|
+
}
|
|
263
|
+
}, deps ? [...deps, ref] : undefined);
|
|
264
|
+
}
|