ink-framework 0.1.0-alpha.10 → 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.
@@ -1,10 +1,11 @@
1
- import { createElement, Fragment, useLayoutEffect, useMemo, useRef, type ComponentProps, type ReactNode } from "./react";
2
- import { ListLoadError, ReactList, type ListProps } from "./list";
3
- import { useAction } from "./action";
1
+ import { createElement, useLayoutEffect, useMemo, useRef, useState, type ComponentProps, type ReactNode } from "./react";
2
+ import { ReactList, useListPaginationEvents, type ListPagination, type ListProps } from "./list";
4
3
  import type { Row } from "./image";
5
4
  import { pressHandler } from "./press";
6
5
  import { rememberListPatch, type NativeListItem } from "./list-patch";
7
6
 
7
+ export { fixedListRow } from "./fixed-list-row";
8
+
8
9
  type Captured = { value: unknown; prototype?: object | null; fields?: [PropertyKey, boolean, Captured][] };
9
10
  type Scope = { identity: string; values: readonly Captured[] };
10
11
  type Capture = { identity: string; values: () => readonly unknown[]; usesIndex?: boolean };
@@ -15,6 +16,8 @@ const keys = /* @__PURE__ */ new WeakMap<Function, string>();
15
16
  const plainRecords = /* @__PURE__ */ new WeakSet<object>();
16
17
  declare const __inkIsProxy: (value: unknown) => boolean;
17
18
  const isProxy = typeof __inkIsProxy === "function" ? __inkIsProxy : undefined;
19
+ declare const __inkIsDenseArray: (value: unknown) => boolean;
20
+ const isDenseArray = typeof __inkIsDenseArray === "function" ? __inkIsDenseArray : undefined;
18
21
 
19
22
  function bareFunctionPrototype(value: unknown, owner: Function): value is object {
20
23
  if (value === null || typeof value !== "object" || isProxy?.(value)) return false;
@@ -71,7 +74,7 @@ export function listText(parts: unknown[]): string {
71
74
  export function listRowFields(props: ComponentProps<typeof Row>) {
72
75
  return { image: props.image, title: props.title, titleMaxLines: props.titleMaxLines,
73
76
  titleIcon: props.titleIcon, subtitle: props.subtitle, subtitleIcon: props.subtitleIcon,
74
- onLongPress: props.onLongPress,
77
+ selected: props.selected, onLongPress: props.onLongPress,
75
78
  onPress: pressHandler(props) ?? (props.onLongPress ? () => {} : undefined) };
76
79
  }
77
80
 
@@ -99,21 +102,73 @@ function nativeValue(value: unknown, previous?: unknown): unknown {
99
102
  }
100
103
 
101
104
  type NativeItem = NativeListItem;
102
- type RowRecord<T> = { item: T; index: number; native: NativeItem };
103
- type Records<T> = { data: NativeItem[]; rows: Map<string, RowRecord<T>>; template: string;
105
+ type Records<T> = { data: NativeItem[]; positions: Map<string, number>; template: string;
104
106
  items: readonly T[]; key: string | Function; measurementKey?: string | number; scope?: Scope };
107
+ const cooperativeListSize = 512;
108
+ const preparationBudget = 4;
109
+ const minimumPreparationRows = 32;
110
+ const maximumPreparationRows = 128;
111
+ type Preparation<T> = { steps: Generator<void, Records<T> | null>; result?: { value: Records<T> | null }; error?: { value: unknown } };
112
+ type Request<T> = { items: readonly T[]; key: string | Function; plan: Plan<T>;
113
+ measurementKey?: string | number; scope?: Scope; preparation: Preparation<T>; synchronous?: { value: Records<T> | null };
114
+ eventPositions: Map<string, number | undefined> };
115
+ type Prepared<T> = { request: Request<T>; generation: number } &
116
+ ({ status: "ready"; value: Records<T> | null } | { status: "error"; error: unknown });
117
+
118
+ function listItem<T>(items: readonly T[], index: number): T {
119
+ const property = Object.getOwnPropertyDescriptor(items, index);
120
+ if (!property || !("value" in property)) throw needsReact;
121
+ return property.value;
122
+ }
123
+
124
+ /**
125
+ * Reproject only the items replaced in place, when every key stays at its position.
126
+ * Returns undefined when keys move, so the caller rebuilds the whole list.
127
+ */
128
+ function* replaceInPlace<T>(previous: Records<T>, items: readonly T[], plan: Plan<T>,
129
+ keyExtractor: ListProps<T>["keyExtractor"], dense: boolean): Generator<void, Records<T> | undefined> {
130
+ let data: NativeItem[] | undefined;
131
+ const changes: NativeItem[] = [];
132
+ const length = items.length;
133
+ for (let index = 0; index < length; index++) {
134
+ const item = dense ? items[index] : listItem(items, index);
135
+ if (item === previous.items[index]) continue;
136
+ const before = previous.data[index];
137
+ if (!plainData(item)) throw needsReact;
138
+ if (keyExtractor(item, index) !== before.key) return undefined;
139
+ const projected = nativeValue(plan.project(item, index), before.values);
140
+ if (projected !== before.values) {
141
+ const record = { key: before.key, values: projected, measurementKey: before.measurementKey };
142
+ (data ??= previous.data.slice())[index] = record;
143
+ changes.push(record);
144
+ }
145
+ yield;
146
+ }
147
+ if (data) rememberListPatch(data, { previous: previous.data, changes });
148
+ return { ...previous, data: data ?? previous.data, items };
149
+ }
105
150
 
106
- function captureValue(value: unknown, cache: WeakMap<object, Captured>): Captured {
151
+ function captureValue(value: unknown, cache: WeakMap<object, Captured>, visiting = new Set<object>()): Captured {
107
152
  if (value === null || typeof value !== "object" && typeof value !== "function") return { value };
108
153
  const previous = cache.get(value);
109
- if (previous) return previous;
154
+ if (previous && isProxy) return previous;
155
+ if (isProxy?.(value) || visiting.has(value) || visiting.size >= 64) throw needsReact;
156
+ const prototype = Object.getPrototypeOf(value);
157
+ if (prototype !== null && prototype !== Object.prototype && prototype !== Array.prototype
158
+ && !(typeof value === "function" && prototype === Function.prototype)) throw needsReact;
159
+ visiting.add(value);
110
160
  const properties: Record<PropertyKey, PropertyDescriptor> = Object.getOwnPropertyDescriptors(value);
111
- const captured: Captured = { value, prototype: Object.getPrototypeOf(value), fields: Reflect.ownKeys(properties).map(key => {
161
+ const fields: NonNullable<Captured["fields"]> = [];
162
+ for (const key of Reflect.ownKeys(properties)) {
112
163
  const property = properties[key];
164
+ if (!("value" in property)) throw needsReact;
165
+ if (["toString", "valueOf", Symbol.toPrimitive].includes(key) && typeof property.value === "function") throw needsReact;
113
166
  const field = key === "prototype" && typeof value === "function" && bareFunctionPrototype(property.value, value)
114
- ? { value: property.value } : captureValue(property.value, cache);
115
- return [key, !!property.enumerable, field];
116
- }) };
167
+ ? { value: property.value } : captureValue(property.value, cache, visiting);
168
+ fields.push([key, !!property.enumerable, field]);
169
+ }
170
+ visiting.delete(value);
171
+ const captured: Captured = { value, prototype, fields };
117
172
  cache.set(value, captured);
118
173
  return captured;
119
174
  }
@@ -135,64 +190,146 @@ function sameScopeValues(before: readonly Captured[], after: readonly Captured[]
135
190
  return before.length === after.length && after.every((value, index) => sameCapture(before[index], value, seen));
136
191
  }
137
192
 
193
+ function captureScope<T>(plan: Plan<T>, items: readonly T[]): Scope | undefined {
194
+ if (!plan.scope) return;
195
+ const captured = items.length ? plan.scope.values() : [];
196
+ // Captures can mutate between slices; validate each fresh snapshot without the row cache.
197
+ return { identity: plan.scope.identity, values: captured.map(value => captureValue(value, new WeakMap())) };
198
+ }
199
+
200
+ function sameScope(before?: Scope, after?: Scope): boolean {
201
+ return !!before && !!after && before.identity === after.identity && sameScopeValues(before.values, after.values);
202
+ }
203
+
204
+ function capturesMatch<T>(request: Request<T>, latestPlan: Plan<T> | null): boolean {
205
+ return !!latestPlan && sameScope(request.scope, captureScope(request.plan, request.items))
206
+ && (latestPlan === request.plan || sameScope(request.scope, captureScope(latestPlan, request.items)));
207
+ }
208
+
209
+ function* prepareRecords<T>(previous: Records<T> | null, items: readonly T[], plan: Plan<T>,
210
+ keyExtractor: ListProps<T>["keyExtractor"], keyIdentity: string | Function,
211
+ measurementKey: string | number | undefined, scope: Scope | undefined, dense: boolean): Generator<void, Records<T> | null> {
212
+ try {
213
+ if (isProxy?.(items) || Object.getPrototypeOf(items) !== Array.prototype) throw needsReact;
214
+ const length = items.length;
215
+ const unchangedScope = sameScope(previous?.scope, scope);
216
+ if (previous && previous.items === items && previous.key === keyIdentity
217
+ && previous.template === plan.template && previous.measurementKey === measurementKey
218
+ && unchangedScope) return previous;
219
+ if (previous && unchangedScope && typeof keyIdentity === "string" && previous.key === keyIdentity
220
+ && previous.template === plan.template && previous.measurementKey === measurementKey
221
+ && previous.items.length === length) {
222
+ const patched = yield* replaceInPlace(previous, items, plan, keyExtractor, dense);
223
+ if (patched !== undefined) return { ...patched, scope };
224
+ }
225
+ const positions = new Map<string, number>();
226
+ let unchanged = length === previous?.data.length;
227
+ let reordered = !unchanged;
228
+ const changes: NativeItem[] = [];
229
+ let data: NativeItem[] = [];
230
+ const itemKeys: string[] = [];
231
+ for (let index = 0; index < length; index++) {
232
+ const item = dense ? items[index] : listItem(items, index);
233
+ if (!plainData(item)) throw needsReact;
234
+ const key = typeof keyIdentity === "string" && previous?.key === keyIdentity && previous.items[index] === item
235
+ ? previous.data[index].key : keyExtractor(item, index);
236
+ if (typeof key !== "string" || positions.has(key)) throw new Error("List keys must be unique strings");
237
+ const beforeIndex = previous?.template === plan.template ? previous.positions.get(key) : undefined;
238
+ const before = previous && beforeIndex !== undefined ? previous.data[beforeIndex] : undefined;
239
+ const projected = previous && beforeIndex !== undefined && unchangedScope && previous.items[beforeIndex] === item
240
+ && (beforeIndex === index || plan.scope?.usesIndex === false)
241
+ ? previous.data[beforeIndex].values : nativeValue(plan.project(item, index), before?.values);
242
+ const measurement = measurementKey ?? null;
243
+ const record = before && before.values === projected && before.measurementKey === measurement
244
+ ? before : { key, values: projected, measurementKey: measurement };
245
+ positions.set(key, index);
246
+ itemKeys.push(key);
247
+ if (record !== previous?.data[index]) {
248
+ unchanged = false;
249
+ if (record !== before) changes.push(record);
250
+ if (key !== previous?.data[index]?.key) reordered = true;
251
+ }
252
+ data.push(record);
253
+ yield;
254
+ }
255
+ if (unchanged && previous) data = previous.data;
256
+ else if (previous) rememberListPatch(data, { previous: previous.data, changes,
257
+ ...(reordered ? { keys: itemKeys } : {}) });
258
+ return { data, positions, template: plan.template, items, key: keyIdentity, measurementKey, scope };
259
+ } catch (error) {
260
+ if (error !== needsReact) throw error;
261
+ return null;
262
+ }
263
+ }
264
+
265
+ function advancePreparation<T>(preparation: Preparation<T>, cooperative: boolean) {
266
+ if (preparation.result) return preparation.result;
267
+ if (preparation.error) throw preparation.error.value;
268
+ const started = cooperative ? performance.now() : 0;
269
+ let rows = 0;
270
+ while (true) {
271
+ let step;
272
+ try {
273
+ step = preparation.steps.next();
274
+ } catch (error) {
275
+ preparation.error = { value: error };
276
+ throw error;
277
+ }
278
+ if (step.done) return preparation.result = { value: step.value };
279
+ rows++;
280
+ // Identity-only sparse scans do not yield: a single replaced row stays synchronous.
281
+ if (cooperative && (rows >= maximumPreparationRows
282
+ || rows >= minimumPreparationRows && performance.now() - started >= preparationBudget)) return;
283
+ }
284
+ }
285
+
138
286
  export function nativeListPlan<T>(render: ListProps<T>["renderItem"]): Plan<T> | undefined {
139
287
  return plans.get(render) as Plan<T> | undefined;
140
288
  }
141
289
 
142
- export function CompiledList<T>({ plan, ...props }: ListProps<T> & { plan: Plan<T> }) {
143
- const { items, keyExtractor, gap = 47, followEnd, initialPosition, measurementKey, onLoadMore, onLoadOlder, hasMore = false, hasOlder = false } = props;
290
+ export function CompiledList<T>({ plan, pagination, ...props }: ListProps<T> & { plan: Plan<T>; pagination: ListPagination }) {
291
+ const { items, keyExtractor, gap = 47, followEnd, initialPosition, measurementKey } = props;
144
292
  if (!Number.isFinite(gap) || gap < 0) throw new Error("List gap must be finite and non-negative");
145
- const load = useAction(() => onLoadMore?.());
146
- const older = useAction(() => onLoadOlder?.());
147
293
  const committed = useRef<Records<T> | null>(null);
148
- const records = useMemo(() => {
149
- const previous = committed.current;
294
+ const committedRequest = useRef<Request<T> | null>(null);
295
+ const committedPlan = useRef<Plan<T> | null>(null);
296
+ const generation = useRef(0);
297
+ const [prepared, setPrepared] = useState<Prepared<T> | null>(null);
298
+ const request = useMemo<Request<T>>(() => {
150
299
  const keyIdentity = keys.get(keyExtractor) ?? keyExtractor;
151
- const captured = plan.scope && (items.length ? plan.scope.values() : []);
152
- if (captured && !captured.every(value => plainData(value, new WeakSet()))) return null;
153
- const snapshots = new WeakMap<object, Captured>();
154
- const scope = plan.scope && { identity: plan.scope.identity, values: captured?.map(value => captureValue(value, snapshots)) ?? [] };
155
- const sameScope = scope && previous?.scope?.identity === scope.identity && sameScopeValues(previous.scope.values, scope.values);
156
- if (previous && previous.items === items && previous.key === keyIdentity
157
- && previous.template === plan.template && previous.measurementKey === measurementKey
158
- && sameScope) return previous;
159
- const rows = new Map<string, RowRecord<T>>();
160
- let unchanged = items.length === previous?.data.length;
161
- let reordered = !unchanged;
162
- const changes: NativeItem[] = [];
163
- let data;
300
+ let scope: Scope | undefined;
164
301
  try {
165
- if (!items.every(item => plainData(item))) throw needsReact;
166
- data = items.map((item, index) => {
167
- const key = typeof keyIdentity === "string" && previous?.key === keyIdentity && previous.items[index] === item
168
- ? previous.data[index].key : keyExtractor(item, index);
169
- if (typeof key !== "string" || rows.has(key)) throw new Error("List keys must be unique strings");
170
- const beforeRow = previous?.template === plan.template ? previous.rows.get(key) : undefined;
171
- const before = beforeRow?.native;
172
- const projected = beforeRow && sameScope && beforeRow.item === item
173
- && (beforeRow.index === index || plan.scope?.usesIndex === false)
174
- ? beforeRow.native.values : nativeValue(plan.project(item, index), before?.values);
175
- const measurement = measurementKey ?? null;
176
- const record = before && before.values === projected && before.measurementKey === measurement
177
- ? before : { key, values: projected, measurementKey: measurement };
178
- rows.set(key, { item, index, native: record });
179
- if (record !== previous?.data[index]) {
180
- unchanged = false;
181
- if (record !== before) changes.push(record);
182
- if (key !== previous?.data[index]?.key) reordered = true;
183
- }
184
- return record;
185
- });
302
+ scope = captureScope(plan, items);
186
303
  } catch (error) {
187
304
  if (error !== needsReact) throw error;
188
- return null;
305
+ const preparation: Preparation<T> = { steps: (function* () { return null; })(), result: { value: null } };
306
+ return { items, key: keyIdentity, plan, measurementKey, preparation, synchronous: preparation.result, eventPositions: new Map() };
189
307
  }
190
- if (unchanged && previous) data = previous.data;
191
- else if (previous) rememberListPatch(data, { previous: previous.data, changes,
192
- ...(reordered ? { keys: data.map(item => item.key) } : {}) });
193
- return { data, rows, template: plan.template, items, key: keyIdentity, measurementKey, scope };
308
+ const pending = committedRequest.current;
309
+ if (pending && pending.items === items && pending.key === keyIdentity
310
+ && pending.plan.template === plan.template && pending.measurementKey === measurementKey
311
+ && sameScope(pending.scope, scope)) return pending;
312
+ const previous = committed.current;
313
+ const dense = isDenseArray?.(items) ?? false;
314
+ const preparation: Preparation<T> = { steps: prepareRecords(previous, items, plan, keyExtractor, keyIdentity, measurementKey, scope, dense) };
315
+ // First population must exist when List's one-shot initialPosition effect scrolls its Screen.
316
+ const cooperative = !!isProxy && !!scope && typeof keyIdentity === "string"
317
+ && !!previous?.items.length && previous.template === plan.template && items.length >= cooperativeListSize;
318
+ const synchronous = advancePreparation(preparation, cooperative);
319
+ return { items, key: keyIdentity, plan, measurementKey, scope, preparation, synchronous, eventPositions: new Map() };
194
320
  }, [items, keyExtractor, plan, measurementKey]);
195
- useLayoutEffect(() => { committed.current = records; }, [records]);
321
+ const outcome = prepared?.request === request && prepared.generation === generation.current ? prepared : undefined;
322
+ let records = outcome?.status === "ready" ? outcome.value
323
+ : request.synchronous ? request.synchronous.value : committed.current;
324
+ if (outcome?.status === "ready" && !request.synchronous) {
325
+ try {
326
+ if (!capturesMatch(request, committedPlan.current)) records = null;
327
+ } catch (error) {
328
+ if (error !== needsReact) throw error;
329
+ records = null;
330
+ }
331
+ }
332
+ const preparing = !request.synchronous && outcome?.status !== "ready";
196
333
  const targets = useMemo(() => {
197
334
  type Template = { id: number; props: Record<string, unknown>; children: Template[] };
198
335
  const targets = new Map<number, Record<string, (string | number)[]>>();
@@ -210,28 +347,90 @@ export function CompiledList<T>({ plan, ...props }: ListProps<T> & { plan: Plan<
210
347
  visit(JSON.parse(plan.template) as Template);
211
348
  return targets;
212
349
  }, [plan.template]);
213
- if (!records) return createElement(ReactList<T>, props);
350
+ const events = useRef<{
351
+ items: readonly T[]; keyExtractor: ListProps<T>["keyExtractor"]; plan: Plan<T>;
352
+ targets: typeof targets; positions: Map<string, number | undefined>;
353
+ } | null>(null);
354
+ useLayoutEffect(() => {
355
+ committed.current = records;
356
+ committedRequest.current = request;
357
+ committedPlan.current = plan;
358
+ events.current = { items, keyExtractor, plan, targets, positions: request.eventPositions };
359
+ return () => {
360
+ events.current = null;
361
+ committedRequest.current = null;
362
+ committedPlan.current = null;
363
+ };
364
+ });
365
+ useLayoutEffect(() => {
366
+ const currentGeneration = ++generation.current;
367
+ let timer: ReturnType<typeof setTimeout> | undefined;
368
+ const current = () => generation.current === currentGeneration && committedRequest.current === request;
369
+ const run = () => {
370
+ if (!current()) return;
371
+ try {
372
+ const result = capturesMatch(request, committedPlan.current)
373
+ ? advancePreparation(request.preparation, true) : { value: null };
374
+ if (!current()) return;
375
+ if (result) setPrepared({ request, generation: currentGeneration, status: "ready", value: result.value });
376
+ else timer = setTimeout(run, 0);
377
+ } catch (error) {
378
+ if (!current()) return;
379
+ setPrepared(error === needsReact
380
+ ? { request, generation: currentGeneration, status: "ready", value: null }
381
+ : { request, generation: currentGeneration, status: "error", error });
382
+ }
383
+ };
384
+ if (!request.synchronous) timer = setTimeout(run, 0);
385
+ return () => {
386
+ generation.current++;
387
+ if (timer !== undefined) clearTimeout(timer);
388
+ };
389
+ }, [request]);
390
+ const paginationProps = useListPaginationEvents(pagination, preparing || !records,
391
+ records?.data.length ?? 0, records?.data[0]?.key, records?.data.at(-1)?.key);
392
+ if (outcome?.status === "error") throw outcome.error;
393
+ if (!records) return createElement(ReactList<T>, { ...props, pagination });
214
394
  const nativeProps = {
215
- items: records.data, keyField: "key", template: plan.template, gap, followEnd, initialEnd: initialPosition === "end",
216
- hasMore: hasMore && load.status !== "pending" && load.status !== "error",
217
- hasOlder: hasOlder && older.status !== "pending" && older.status !== "error",
218
- onEndReached: onLoadMore ? load.run : undefined,
219
- onStartReached: onLoadOlder ? older.run : undefined,
395
+ items: records.data, keyField: "key", template: records.template, gap, followEnd, initialEnd: initialPosition === "end",
396
+ ...paginationProps,
220
397
  onRowEvent(key: string, target: number, name: string, ...args: unknown[]) {
221
- const path = targets.get(target)?.[name];
222
- const row = records.rows.get(key);
223
- let callback: unknown = row && path ? plan.project(row.item, row.index) : undefined;
224
- for (const part of path ?? []) {
398
+ const latest = events.current;
399
+ const path = latest?.targets.get(target)?.[name];
400
+ if (!latest || !path || isProxy?.(latest.items)) return;
401
+ const matches = (index: number) => {
402
+ const property = Object.getOwnPropertyDescriptor(latest.items, index);
403
+ return !!property && "value" in property && plainData(property.value)
404
+ && latest.keyExtractor(property.value, index) === key;
405
+ };
406
+ let index = latest.items === records.items ? records.positions.get(key) : latest.positions.get(key);
407
+ if (latest.items !== records.items && !latest.positions.has(key)) {
408
+ const beforeIndex = records.positions.get(key);
409
+ if (beforeIndex !== undefined && matches(beforeIndex)) index = beforeIndex;
410
+ else {
411
+ for (let candidate = 0; candidate < latest.items.length; candidate++) {
412
+ if (matches(candidate)) { index = candidate; break; }
413
+ }
414
+ }
415
+ latest.positions.set(key, index);
416
+ }
417
+ if (index === undefined) return;
418
+ let callback: unknown;
419
+ try {
420
+ const item = listItem(latest.items, index);
421
+ if (!plainData(item)) return;
422
+ callback = latest.plan.project(item, index);
423
+ } catch (error) {
424
+ if (error === needsReact) return;
425
+ throw error;
426
+ }
427
+ for (const part of path) {
225
428
  callback = callback != null && typeof callback === "object" ? Reflect.get(callback, part) : undefined;
226
429
  }
227
- if (path && typeof callback === "function") callback(...args);
430
+ if (typeof callback === "function") callback(...args);
228
431
  },
229
432
  };
230
- return createElement(Fragment, null,
231
- older.status === "error" && createElement(ListLoadError, { message: older.error.message, onRetry: older.run }),
232
- createElement("NativeList", nativeProps),
233
- load.status === "error" && createElement(ListLoadError, { message: load.error.message, onRetry: load.run }),
234
- );
433
+ return createElement("NativeList", nativeProps);
235
434
  }
236
435
 
237
436
  /** @internal Fixed native descriptions for Ink's own list components. */
package/src/native.ts CHANGED
@@ -34,6 +34,14 @@ Object.defineProperty(globalThis, "__inkReceive", {
34
34
  },
35
35
  });
36
36
 
37
+ Object.defineProperty(globalThis, "__inkResolveNativeResult", {
38
+ value: (id: number, value?: string, kind?: string, message?: string, retryable?: boolean) => {
39
+ listeners.get("result")?.(typeof value === "string"
40
+ ? { id, type: "result", value }
41
+ : { id, kind, message, retryable, type: "result" });
42
+ },
43
+ });
44
+
37
45
  onNativeMessage("result", message => {
38
46
  if (typeof message.id !== "number") throw new Error("Invalid native result ID");
39
47
  const request = pending.get(message.id);
@@ -0,0 +1,3 @@
1
+ declare module "@prefresh/utils" {
2
+ export function flush(): void;
3
+ }
@@ -0,0 +1,24 @@
1
+ import React from "preact/compat";
2
+ import { cloneElement, isValidElement } from "preact";
3
+ import { Activity, useEffect, useLayoutEffect, useSyncExternalStore, useImperativeHandle } from "./visibility";
4
+ import { useRef, useCallback, useLayoutEffect as commitLayoutEffect } from "preact/hooks";
5
+ const compatibleReact = /* @__PURE__ */ Object.assign({}, React, { Activity, cloneElement, isValidElement, useEffect, useLayoutEffect, useSyncExternalStore, useImperativeHandle, useEffectEvent });
6
+ export default compatibleReact;
7
+ export {
8
+ Children, Component, Fragment, PureComponent, StrictMode, Suspense, SuspenseList,
9
+ createContext, createElement, createFactory, createPortal, createRef,
10
+ findDOMNode, flushSync, forwardRef, hydrate, lazy, memo, render,
11
+ startTransition, unmountComponentAtNode, unstable_batchedUpdates,
12
+ useCallback, useContext, useDebugValue, useDeferredValue,
13
+ useId, useInsertionEffect, useMemo, useReducer,
14
+ useRef, useState, useTransition, version,
15
+ } from "preact/compat";
16
+ export { cloneElement, isValidElement } from "preact";
17
+ export { Activity, useEffect, useLayoutEffect, useSyncExternalStore, useImperativeHandle } from "./visibility";
18
+
19
+ /** Effect events retain their identity and read the latest committed callback. */
20
+ export function useEffectEvent<T extends (...args: never[]) => unknown>(callback: T): T {
21
+ const current = useRef(callback);
22
+ commitLayoutEffect(() => { current.current = callback; });
23
+ return useCallback(((...args: Parameters<T>) => current.current(...args)) as T, []);
24
+ }
package/src/react.ts CHANGED
@@ -1,6 +1,26 @@
1
- // Share the CommonJS React import so unused component modules contribute no wrapper code.
2
- export {
3
- Activity, Children, Fragment, createContext, createElement, isValidElement, memo,
4
- useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState,
5
- useSyncExternalStore, type ComponentProps, type ReactElement, type ReactNode,
6
- } from "react";
1
+ import { createElement as element, createContext as context, isValidElement as valid, Fragment as fragment, Component, type ComponentChildren } from "preact";
2
+ import { useCallback, useContext as contextValue, useMemo, useRef, useState } from "preact/hooks";
3
+ import type * as React from "react";
4
+
5
+ // Keep the public React declarations while executing Preact core in Ink.
6
+ export const createElement = element as unknown as typeof React.createElement;
7
+ export const createContext = context as unknown as typeof React.createContext;
8
+ export const useContext = contextValue as unknown as typeof React.useContext;
9
+ export const isValidElement = valid as unknown as typeof React.isValidElement;
10
+ export const Fragment = fragment as unknown as typeof React.Fragment;
11
+ export { Children } from "preact/compat";
12
+ export { useCallback, useMemo, useRef, useState };
13
+ export { Activity, useEffect, useLayoutEffect, useSyncExternalStore } from "./visibility";
14
+ export type { ComponentProps, ComponentType, ReactElement, ReactNode } from "react";
15
+
16
+ // A single core component avoids compat memo's extra function-component layer.
17
+ export function memo<P extends object>(render: (props: P) => React.ReactNode) {
18
+ return class extends Component<P> {
19
+ shouldComponentUpdate(next: P) {
20
+ const previous = this.props;
21
+ const keys = Object.keys(next) as (keyof P)[];
22
+ return keys.length !== Object.keys(previous).length || keys.some(key => !Object.is(previous[key], next[key]));
23
+ }
24
+ render() { return render(this.props) as ComponentChildren; }
25
+ } as unknown as (props: P) => React.ReactNode;
26
+ }
package/src/refresh.ts ADDED
@@ -0,0 +1,29 @@
1
+ import "@prefresh/core";
2
+ import { flush } from "@prefresh/utils";
3
+
4
+ declare const self: { __PREFRESH__: {
5
+ register(type: unknown, id: string): void;
6
+ sign(type: unknown, key: string, forceReset: boolean, getCustomHooks: (() => unknown[]) | undefined, status: string): string;
7
+ } };
8
+
9
+ export const register = (type: unknown, id: string) => self.__PREFRESH__.register(type, id);
10
+ export const performReactRefresh = flush;
11
+
12
+ // Oxc emits React Refresh's signature protocol; translate it to Prefresh.
13
+ export function createSignatureFunctionForTransform() {
14
+ let savedType: unknown;
15
+ let savedKey = "";
16
+ let savedReset = false;
17
+ let savedHooks: (() => unknown[]) | undefined;
18
+ let status = "begin";
19
+ return (type?: unknown, key?: string, forceReset = false, getCustomHooks?: () => unknown[]) => {
20
+ if (type) {
21
+ savedType = type;
22
+ savedKey = key ?? "";
23
+ savedReset = forceReset;
24
+ savedHooks = getCustomHooks;
25
+ }
26
+ status = self.__PREFRESH__.sign(savedType, savedKey, savedReset, savedHooks, status);
27
+ return type;
28
+ };
29
+ }