@lankajs/react 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 +47 -0
- package/dist/chunk-7D4SUZUM.js +38 -0
- package/dist/chunk-7D4SUZUM.js.map +1 -0
- package/dist/index.d.ts +173 -0
- package/dist/index.js +94 -0
- package/dist/index.js.map +1 -0
- package/dist/testing.d.ts +43 -0
- package/dist/testing.js +45702 -0
- package/dist/testing.js.map +1 -0
- package/package.json +56 -0
- package/skills/lanka-react/SKILL.md +160 -0
- package/skills/lanka-react/reference.md +262 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 lankajs contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# @lankajs/react
|
|
2
|
+
|
|
3
|
+
**▸ module** · React binding
|
|
4
|
+
|
|
5
|
+
> One hook — `useLankaVM` — and the access tracking core already does.
|
|
6
|
+
|
|
7
|
+
A library in the same box. The app imports and calls it; core does not know it exists.
|
|
8
|
+
|
|
9
|
+
**Runs in:** the browser and React Native.
|
|
10
|
+
|
|
11
|
+
**Requires:** React. Enforced by `check-runtime.mjs`, which refuses an import of any other.
|
|
12
|
+
|
|
13
|
+
**How to use it:** [GUIDE.md](./GUIDE.md) — the user guide, with examples. **How to change it:** [SKILL.md](./SKILL.md).
|
|
14
|
+
|
|
15
|
+
## Contents
|
|
16
|
+
|
|
17
|
+
- `useLankaVM` — the one name, and the same one every member of this shelf publishes
|
|
18
|
+
- `renderWithLanka` (from `@lankajs/react/testing`) — a render with a bootstrapped framework
|
|
19
|
+
|
|
20
|
+
## What this package is, and what it deliberately is not
|
|
21
|
+
|
|
22
|
+
It is a subscription and a render trigger. The ACCESS TRACKING — which state keys a
|
|
23
|
+
component read, and whether a change touched them — is `createLankaAccessTracker` in
|
|
24
|
+
core, published through `lanka/extend`, and every binding on this shelf calls it.
|
|
25
|
+
That is why the behaviour a consumer sees is the framework's rather than each
|
|
26
|
+
binding's re-reading of it, and it is what `lankaViewBindingConformance` checks.
|
|
27
|
+
|
|
28
|
+
So the whole of `useLankaVM` is a ref, a stable `subscribe` and
|
|
29
|
+
`useSyncExternalStore`. If it ever needs more than the port gives it, the port is
|
|
30
|
+
the thing with the defect.
|
|
31
|
+
|
|
32
|
+
## `"use client"` is here and not in core
|
|
33
|
+
|
|
34
|
+
React Server Components make an import of a hook a build error. Core has no hook any
|
|
35
|
+
more, so `lanka/viewmodel` is server-safe and this barrel carries the directive —
|
|
36
|
+
which is the split doing its job: a Next application's server components may read a
|
|
37
|
+
ViewModel's state, and only the components that RENDER it are client components.
|
|
38
|
+
|
|
39
|
+
## One binding for React and React Native
|
|
40
|
+
|
|
41
|
+
`useSyncExternalStore` is React's, not the DOM's. Expo installs this package and
|
|
42
|
+
nothing else changes — which is also why `runtime` says `browser, native` and not
|
|
43
|
+
`node`.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
Repository map: [../../../README.md](../../../README.md)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
8
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
9
|
+
}) : x)(function(x) {
|
|
10
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
11
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
12
|
+
});
|
|
13
|
+
var __commonJS = (cb, mod) => function __require2() {
|
|
14
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
15
|
+
};
|
|
16
|
+
var __copyProps = (to, from, except, desc) => {
|
|
17
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
18
|
+
for (let key of __getOwnPropNames(from))
|
|
19
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
20
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
21
|
+
}
|
|
22
|
+
return to;
|
|
23
|
+
};
|
|
24
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
25
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
26
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
27
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
28
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
29
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
30
|
+
mod
|
|
31
|
+
));
|
|
32
|
+
|
|
33
|
+
export {
|
|
34
|
+
__require,
|
|
35
|
+
__commonJS,
|
|
36
|
+
__toESM
|
|
37
|
+
};
|
|
38
|
+
//# sourceMappingURL=chunk-7D4SUZUM.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { ILankaReadableVM } from 'lanka/viewmodel';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A ViewModel that is also a hook — what React called a ViewModel before the
|
|
5
|
+
* port existed, and what it may go on calling one.
|
|
6
|
+
*
|
|
7
|
+
* Both call shapes, because both were there: no argument gives the tracked read,
|
|
8
|
+
* a selector gives what the selector picked and bypasses tracking.
|
|
9
|
+
*/
|
|
10
|
+
type TLankaReactVMHook<TState extends object> = {
|
|
11
|
+
(): TState;
|
|
12
|
+
<TSelected>(selector: (state: TState) => TSelected): TSelected;
|
|
13
|
+
};
|
|
14
|
+
/** The ViewModel it was given, plus the ability to be called like a hook. */
|
|
15
|
+
type TLankaReactVM<TViewModel extends ILankaReadableVM<object>> = TViewModel & TLankaReactVMHook<ReturnType<TViewModel["getState"]>>;
|
|
16
|
+
/**
|
|
17
|
+
* Gives a ViewModel React's own ergonomics back.
|
|
18
|
+
*
|
|
19
|
+
* ```ts
|
|
20
|
+
* // the ViewModel, framework-free, exactly as Vue and Svelte receive it
|
|
21
|
+
* const todoVM = createLankaVM({ … });
|
|
22
|
+
*
|
|
23
|
+
* // the same object, callable
|
|
24
|
+
* export const useTodoVM = toLankaReactVM(todoVM);
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* ```tsx
|
|
28
|
+
* const { todos, load } = useTodoVM();
|
|
29
|
+
* const count = useTodoVM((state) => state.todos.length);
|
|
30
|
+
* const todos = useTodoVM.getState().todos; // outside a component, as always
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* ## Why this exists
|
|
34
|
+
*
|
|
35
|
+
* Until 2.0 a ViewModel WAS a React hook: `createLankaVM` returned a callable,
|
|
36
|
+
* and every screen in every application on lanka called it. Making the framework
|
|
37
|
+
* framework-free took the call signature away — correctly, because four of the
|
|
38
|
+
* five bindings have no use for one and core may not know what a hook is.
|
|
39
|
+
*
|
|
40
|
+
* That is a fact about CORE, and it was allowed to become a fact about React,
|
|
41
|
+
* which it never had to be. A React consumer's familiar spelling costs one
|
|
42
|
+
* wrapper in the one package that is allowed to know what a hook is, so here it
|
|
43
|
+
* is: `useTodoVM()` reads, `useTodoVM(selector)` selects, `useTodoVM.getState()`
|
|
44
|
+
* and `useTodoVM.subscribe()` do what they always did.
|
|
45
|
+
*
|
|
46
|
+
* ## What it does NOT do
|
|
47
|
+
*
|
|
48
|
+
* It does not change the ViewModel. There is exactly one store, and the call
|
|
49
|
+
* forwards to `useLankaVM` — the same function the five bindings' conformance
|
|
50
|
+
* suite drives. A ViewModel read through this and the same ViewModel read in Vue
|
|
51
|
+
* answer identically, notify identically and skip identically, because it is the
|
|
52
|
+
* same object either way and this adds no state of its own.
|
|
53
|
+
*
|
|
54
|
+
* It is also not required. `useLankaVM(todoVM)` is the portable spelling, it
|
|
55
|
+
* stays the one the guides teach, and a codebase that has moved to it needs
|
|
56
|
+
* nothing here.
|
|
57
|
+
*
|
|
58
|
+
* ## Laziness survives
|
|
59
|
+
*
|
|
60
|
+
* The forwarding is a Proxy rather than copied properties, so a ViewModel that
|
|
61
|
+
* builds on first access still builds on first access: reading `useTodoVM.name`
|
|
62
|
+
* answers from the config and constructs nothing.
|
|
63
|
+
*/
|
|
64
|
+
declare const toLankaReactVM: <TViewModel extends ILankaReadableVM<object>>(viewModel: TViewModel) => TLankaReactVM<TViewModel>;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Keeps a selector's answer stable when nothing in it changed.
|
|
68
|
+
*
|
|
69
|
+
* ```tsx
|
|
70
|
+
* const { title, status } = useLankaVM(missionVM, useLankaShallow((s) => ({
|
|
71
|
+
* title: s.title,
|
|
72
|
+
* status: s.status,
|
|
73
|
+
* })));
|
|
74
|
+
* ```
|
|
75
|
+
*
|
|
76
|
+
* ## The waste this exists for
|
|
77
|
+
*
|
|
78
|
+
* `useLankaVM(vm, (s) => ({ a: s.a }))` is the commonest thing a React reader
|
|
79
|
+
* writes, and unwrapped it wakes the component for EVERY change in the
|
|
80
|
+
* ViewModel. The binding holds a selection against the state object it came
|
|
81
|
+
* from, which is what a snapshot has to be; a fresh object is new whenever the
|
|
82
|
+
* state is new, so a reader that took a selector to say "only `a`" is repainted
|
|
83
|
+
* by a change to `z`. This is the comparison that makes the statement mean
|
|
84
|
+
* something.
|
|
85
|
+
*
|
|
86
|
+
* A selector returning a primitive never had the problem, which is what makes
|
|
87
|
+
* the waste quiet: the shape that is free and the shape that repaints on
|
|
88
|
+
* everything look the same on the page.
|
|
89
|
+
*
|
|
90
|
+
* It used to be worse. Until `useLankaVM` ran its selector once per state
|
|
91
|
+
* object, an unwrapped one CRASHED — `useSyncExternalStore` reads the snapshot
|
|
92
|
+
* during render and again after committing, a fresh object disagreed with
|
|
93
|
+
* itself, and the component rendered until React stopped it with "Maximum update
|
|
94
|
+
* depth exceeded". That is closed in the binding, for everybody.
|
|
95
|
+
*
|
|
96
|
+
* ## What is React's here, and what is not
|
|
97
|
+
*
|
|
98
|
+
* The comparison is `createLankaShallowHold` in core, and every binding on the
|
|
99
|
+
* shelf can reach it. It was this file's, and that made it a CAPABILITY React had
|
|
100
|
+
* and four siblings did not — an idiom is a spelling, and this changes which
|
|
101
|
+
* notifications reach a reader. `skills/parity/SKILL.md` 3c.
|
|
102
|
+
*
|
|
103
|
+
* What is left is the part only React needs. A component re-runs this hook on
|
|
104
|
+
* every render, so the holding has to SURVIVE a render while the selector stays
|
|
105
|
+
* the current one: the hold lives in a ref initialised once, and the closure
|
|
106
|
+
* returned below closes over this render's `selector`. A selector computed from
|
|
107
|
+
* props therefore stays honest, and the hold does not reset under it.
|
|
108
|
+
*
|
|
109
|
+
* ## Why a wrapper and not an equality argument
|
|
110
|
+
*
|
|
111
|
+
* `useLankaVM(vm, selector, isEqual)` was the other option, and it puts the
|
|
112
|
+
* comparison in the binding for every caller — including the ones whose
|
|
113
|
+
* selection is a string and pay for a comparison they cannot fail. This is opt
|
|
114
|
+
* in at the call site, which is also where a reader can see it.
|
|
115
|
+
*
|
|
116
|
+
* The shape is React's own: a hook that returns a selector. A consumer arriving
|
|
117
|
+
* from zustand has typed `useShallow` and needs no explanation, which is the
|
|
118
|
+
* whole point of an idiom.
|
|
119
|
+
*/
|
|
120
|
+
declare const useLankaShallow: <TState, TSelected>(selector: (state: TState) => TSelected) => ((state: TState) => TSelected);
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Reads a ViewModel from a React component.
|
|
124
|
+
*
|
|
125
|
+
* ```tsx
|
|
126
|
+
* export const TodoScreen = () => {
|
|
127
|
+
* const { todos, isLoading, load } = useLankaVM(todoVM);
|
|
128
|
+
* …
|
|
129
|
+
* };
|
|
130
|
+
* ```
|
|
131
|
+
*
|
|
132
|
+
* Without a selector the component receives a Proxy that records which keys it
|
|
133
|
+
* read, and the next change re-renders it only if one of THOSE keys moved. With
|
|
134
|
+
* a selector the selector decides and tracking is bypassed.
|
|
135
|
+
*
|
|
136
|
+
* ## What this function does NOT contain
|
|
137
|
+
*
|
|
138
|
+
* The recording, the comparison and the blind-spot warning are
|
|
139
|
+
* `createLankaAccessTracker` in core. Every binding on this shelf calls it, which
|
|
140
|
+
* is what makes "a screen re-renders for the keys it read" a fact about lanka
|
|
141
|
+
* rather than a fact about React — and what
|
|
142
|
+
* `lankaViewBindingConformance` holds all of them to.
|
|
143
|
+
*
|
|
144
|
+
* What is left is React: a tracker per mounted component and per ViewModel, a
|
|
145
|
+
* stable `subscribe`, and `useSyncExternalStore`. If this file ever needs more than the port gives it,
|
|
146
|
+
* the port has the defect and the fix belongs in core, for everybody.
|
|
147
|
+
*
|
|
148
|
+
* ## The blind spot, unchanged
|
|
149
|
+
*
|
|
150
|
+
* A component re-renders only for keys it READ off the returned proxy. An action
|
|
151
|
+
* that DERIVES a value reads the store through `get`, which the proxy never sees
|
|
152
|
+
* — so a component whose only link to a key is such a getter never re-renders
|
|
153
|
+
* for it. Set `enableAccessTrackingOptimization: false` on that ViewModel; in
|
|
154
|
+
* development the mismatch announces itself by name. Canon: `skills/parity`.
|
|
155
|
+
*/
|
|
156
|
+
declare function useLankaVM<TState extends object>(viewModel: ILankaReadableVM<TState>): TState;
|
|
157
|
+
declare function useLankaVM<TState extends object, TSelected>(viewModel: ILankaReadableVM<TState>, selector: (state: TState) => TSelected): TSelected;
|
|
158
|
+
/**
|
|
159
|
+
* The overload a WRAPPER needs: a selector it was handed, which may be absent.
|
|
160
|
+
*
|
|
161
|
+
* The two above describe the two things a screen does, and neither accepts
|
|
162
|
+
* `undefined` — so a hook that forwards its own optional argument had to branch,
|
|
163
|
+
* and a branch around a hook call is the one thing React's lint rule refuses
|
|
164
|
+
* outright. `toLankaReactVM` is such a wrapper, and so is every wrapper a
|
|
165
|
+
* consumer writes over this one.
|
|
166
|
+
*
|
|
167
|
+
* The answer widens to `TState | TSelected` because it genuinely is not known
|
|
168
|
+
* which: that is the price of not knowing at the type level whether a selector
|
|
169
|
+
* arrived, and a caller who does know keeps one of the two overloads above.
|
|
170
|
+
*/
|
|
171
|
+
declare function useLankaVM<TState extends object, TSelected>(viewModel: ILankaReadableVM<TState>, selector: ((state: TState) => TSelected) | undefined): TState | TSelected;
|
|
172
|
+
|
|
173
|
+
export { type TLankaReactVM, type TLankaReactVMHook, toLankaReactVM, useLankaShallow, useLankaVM };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import "./chunk-7D4SUZUM.js";
|
|
3
|
+
|
|
4
|
+
// src/use-lanka-vm/useLankaVM.ts
|
|
5
|
+
import { useCallback, useMemo, useSyncExternalStore } from "react";
|
|
6
|
+
import { createLankaAccessTracker } from "lanka/extend";
|
|
7
|
+
function useLankaVM(viewModel, selector) {
|
|
8
|
+
const tracker = useMemo(() => createLankaAccessTracker(viewModel), [viewModel]);
|
|
9
|
+
const hasSelector = selector !== void 0;
|
|
10
|
+
const subscribe = useCallback(
|
|
11
|
+
(onStoreChange) => viewModel.subscribe((next, prev) => {
|
|
12
|
+
if (hasSelector) {
|
|
13
|
+
onStoreChange();
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
if (tracker.shouldNotify(next, prev)) {
|
|
17
|
+
onStoreChange();
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
tracker.reportSkipped(next, prev);
|
|
21
|
+
}),
|
|
22
|
+
// `tracker` moves only when `viewModel` does, so naming it costs no rebuild
|
|
23
|
+
// the first dependency was not already going to cause.
|
|
24
|
+
[viewModel, hasSelector, tracker]
|
|
25
|
+
);
|
|
26
|
+
const selectFromState = useMemo(() => {
|
|
27
|
+
if (!selector) return null;
|
|
28
|
+
let taken = false;
|
|
29
|
+
let takenFrom;
|
|
30
|
+
let picked;
|
|
31
|
+
return (state) => {
|
|
32
|
+
if (taken && Object.is(takenFrom, state)) return picked;
|
|
33
|
+
taken = true;
|
|
34
|
+
takenFrom = state;
|
|
35
|
+
picked = selector(state);
|
|
36
|
+
return picked;
|
|
37
|
+
};
|
|
38
|
+
}, [selector]);
|
|
39
|
+
const readTracked = () => selectFromState ? selectFromState(viewModel.getState()) : tracker.read();
|
|
40
|
+
const readUntracked = () => {
|
|
41
|
+
const state = viewModel.getState();
|
|
42
|
+
return selector ? selector(state) : state;
|
|
43
|
+
};
|
|
44
|
+
return useSyncExternalStore(subscribe, readTracked, readUntracked);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// src/to-lanka-react-vm/toLankaReactVM.ts
|
|
48
|
+
var FUNCTION_MEMBERS = /* @__PURE__ */ new Set([
|
|
49
|
+
"prototype",
|
|
50
|
+
"length",
|
|
51
|
+
"arguments",
|
|
52
|
+
"caller",
|
|
53
|
+
"constructor",
|
|
54
|
+
"call",
|
|
55
|
+
"apply",
|
|
56
|
+
"bind",
|
|
57
|
+
"toString"
|
|
58
|
+
]);
|
|
59
|
+
var forwardToViewModel = (viewModel) => {
|
|
60
|
+
const members = viewModel;
|
|
61
|
+
return {
|
|
62
|
+
get: (target, property, receiver) => typeof property === "symbol" || FUNCTION_MEMBERS.has(property) ? Reflect.get(target, property, receiver) : members[property],
|
|
63
|
+
/**
|
|
64
|
+
* `in` answers for the ViewModel too.
|
|
65
|
+
*
|
|
66
|
+
* Without this the hook would report that it has no `getState`, while
|
|
67
|
+
* reading `getState` hands one back — and `"getState" in useTodoVM` is how a
|
|
68
|
+
* devtool, a serialiser and a duck-typed helper ask. The ViewModel behind
|
|
69
|
+
* this may be a lazy proxy with no `has` trap of its own, so the question is
|
|
70
|
+
* answered by READING the property, which for a lazy ViewModel builds
|
|
71
|
+
* nothing.
|
|
72
|
+
*/
|
|
73
|
+
has: (target, property) => Reflect.has(target, property) || typeof property === "string" && members[property] !== void 0
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
var toLankaReactVM = (viewModel) => {
|
|
77
|
+
const useViewModel = (selector) => useLankaVM(viewModel, selector);
|
|
78
|
+
return new Proxy(useViewModel, forwardToViewModel(viewModel));
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// src/use-lanka-shallow/useLankaShallow.ts
|
|
82
|
+
import { useRef } from "react";
|
|
83
|
+
import { createLankaShallowHold } from "lanka/viewmodel";
|
|
84
|
+
var useLankaShallow = (selector) => {
|
|
85
|
+
const hold = useRef(null);
|
|
86
|
+
hold.current ??= createLankaShallowHold();
|
|
87
|
+
return (state) => hold.current(selector(state));
|
|
88
|
+
};
|
|
89
|
+
export {
|
|
90
|
+
toLankaReactVM,
|
|
91
|
+
useLankaShallow,
|
|
92
|
+
useLankaVM
|
|
93
|
+
};
|
|
94
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/use-lanka-vm/useLankaVM.ts","../src/to-lanka-react-vm/toLankaReactVM.ts","../src/use-lanka-shallow/useLankaShallow.ts"],"sourcesContent":["import { useCallback, useMemo, useSyncExternalStore } from \"react\";\nimport { createLankaAccessTracker } from \"lanka/extend\";\nimport type { ILankaReadableVM } from \"lanka/viewmodel\";\n\n/**\n * Reads a ViewModel from a React component.\n *\n * ```tsx\n * export const TodoScreen = () => {\n * \tconst { todos, isLoading, load } = useLankaVM(todoVM);\n * \t…\n * };\n * ```\n *\n * Without a selector the component receives a Proxy that records which keys it\n * read, and the next change re-renders it only if one of THOSE keys moved. With\n * a selector the selector decides and tracking is bypassed.\n *\n * ## What this function does NOT contain\n *\n * The recording, the comparison and the blind-spot warning are\n * `createLankaAccessTracker` in core. Every binding on this shelf calls it, which\n * is what makes \"a screen re-renders for the keys it read\" a fact about lanka\n * rather than a fact about React — and what\n * `lankaViewBindingConformance` holds all of them to.\n *\n * What is left is React: a tracker per mounted component and per ViewModel, a\n * stable `subscribe`, and `useSyncExternalStore`. If this file ever needs more than the port gives it,\n * the port has the defect and the fix belongs in core, for everybody.\n *\n * ## The blind spot, unchanged\n *\n * A component re-renders only for keys it READ off the returned proxy. An action\n * that DERIVES a value reads the store through `get`, which the proxy never sees\n * — so a component whose only link to a key is such a getter never re-renders\n * for it. Set `enableAccessTrackingOptimization: false` on that ViewModel; in\n * development the mismatch announces itself by name. Canon: `skills/parity`.\n */\nexport function useLankaVM<TState extends object>(viewModel: ILankaReadableVM<TState>): TState;\n\nexport function useLankaVM<TState extends object, TSelected>(\n\tviewModel: ILankaReadableVM<TState>,\n\tselector: (state: TState) => TSelected,\n): TSelected;\n\n/**\n * The overload a WRAPPER needs: a selector it was handed, which may be absent.\n *\n * The two above describe the two things a screen does, and neither accepts\n * `undefined` — so a hook that forwards its own optional argument had to branch,\n * and a branch around a hook call is the one thing React's lint rule refuses\n * outright. `toLankaReactVM` is such a wrapper, and so is every wrapper a\n * consumer writes over this one.\n *\n * The answer widens to `TState | TSelected` because it genuinely is not known\n * which: that is the price of not knowing at the type level whether a selector\n * arrived, and a caller who does know keeps one of the two overloads above.\n */\nexport function useLankaVM<TState extends object, TSelected>(\n\tviewModel: ILankaReadableVM<TState>,\n\tselector: ((state: TState) => TSelected) | undefined,\n): TState | TSelected;\n\nexport function useLankaVM<TState extends object, TSelected>(\n\tviewModel: ILankaReadableVM<TState>,\n\tselector?: (state: TState) => TSelected,\n): TState | TSelected {\n\t/**\n\t * One tracker per mounted component, and per ViewModel it is pointed at.\n\t *\n\t * Two components over one ViewModel read different keys and must re-render for\n\t * different changes, so the recording belongs to the reader — which is why\n\t * this is built here and not shared.\n\t *\n\t * Keyed on the ViewModel, because `createLankaAccessTracker` closes over the\n\t * one it was given, permanently. This was a ref initialised once, and a\n\t * component handed a DIFFERENT ViewModel at the same mount point — an ordinary\n\t * prop change — kept a tracker reading the first one: `subscribe` WAS rebuilt\n\t * for the new ViewModel and woke the component correctly, and every render\n\t * then re-read the old one's state. A live subscription and a frozen screen,\n\t * with no error anywhere.\n\t *\n\t * `useMemo` rather than a ref written during render: the ref is the shape\n\t * `react-hooks/refs` allows only for initialise-once, and rightly — a ref\n\t * written on a condition during render is the impure render the rule exists to\n\t * catch. The factory runs when the ViewModel moves and at no other time, which\n\t * is exactly the lifetime the recording should have. If React ever discards\n\t * the cache it discards `subscribe` with it, so the two cannot disagree; a\n\t * fresh tracker has recorded nothing, and a reader that has read nothing is\n\t * notified of everything — more renders, never fewer.\n\t */\n\tconst tracker = useMemo(() => createLankaAccessTracker(viewModel), [viewModel]);\n\n\t/**\n\t * Whether a selector was passed, which is all `subscribe` needs to know.\n\t *\n\t * Not the selector itself. A selector is usually an inline arrow with a new\n\t * identity every render, so keying the subscription on it would tear the\n\t * subscription down and rebuild it on EVERY render — the failure measured at\n\t * 201 subscriptions for 200 renders. Whether there IS one is a boolean that\n\t * does not change at a given call site, so the subscription stands still.\n\t */\n\tconst hasSelector = selector !== undefined;\n\n\t/**\n\t * Stable identity, and the dependencies are the two things that genuinely\n\t * change what the subscription DOES.\n\t *\n\t * `useSyncExternalStore` keeps `subscribe` in an effect keyed on its identity.\n\t * A different ViewModel is a different subscription and must be rebuilt; so is\n\t * switching between tracked and selected reads. Neither moves in practice —\n\t * a ViewModel is a module-level object — so a mounted component subscribes\n\t * once and stays subscribed.\n\t */\n\tconst subscribe = useCallback(\n\t\t(onStoreChange: () => void) =>\n\t\t\tviewModel.subscribe((next, prev) => {\n\t\t\t\tif (hasSelector) {\n\t\t\t\t\tonStoreChange();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tif (tracker.shouldNotify(next, prev)) {\n\t\t\t\t\tonStoreChange();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// Reaching here means NO re-render will follow. If the changed key is\n\t\t\t\t// linked to this component through a getter it read, the screen froze —\n\t\t\t\t// and in development core says so by name.\n\t\t\t\ttracker.reportSkipped(next, prev);\n\t\t\t}),\n\t\t// `tracker` moves only when `viewModel` does, so naming it costs no rebuild\n\t\t// the first dependency was not already going to cause.\n\t\t[viewModel, hasSelector, tracker],\n\t);\n\n\t/**\n\t * The selection, remembered against the STATE it was taken from.\n\t *\n\t * `useSyncExternalStore` reads the snapshot during render and AGAIN after\n\t * committing, and re-renders when the two differ by `Object.is`. A selector\n\t * that builds its answer — `(state) => ({ id: state.id })`, `(state) =>\n\t * rows.filter(…)`, the first shape a consumer reaches for — is never identical\n\t * to its own previous result, so the two reads never agreed and the component\n\t * rendered until React stopped it: \"Maximum update depth exceeded\", on the\n\t * commonest selector there is. The conformance suite's fresh-object scene is\n\t * what named it; the other four bindings compare the selection to the last one\n\t * and merely wake more often than they need to.\n\t *\n\t * So the selector runs once per STATE object and the answer is held. The two\n\t * reads of one commit then see the same reference, and the loop closes.\n\t *\n\t * Rebuilt when the selector's identity moves, which is what keeps a selector\n\t * computed from props honest: an inline arrow is a new function every render,\n\t * so the memo is fresh at the start of each render and warm by the time the\n\t * post-commit read arrives — which is the whole of what the comparison needs.\n\t * The subscription does not depend on it and stands still regardless.\n\t */\n\tconst selectFromState = useMemo(() => {\n\t\tif (!selector) return null;\n\n\t\tlet taken = false;\n\t\tlet takenFrom: TState;\n\t\tlet picked: TSelected;\n\n\t\treturn (state: TState): TSelected => {\n\t\t\tif (taken && Object.is(takenFrom, state)) return picked;\n\n\t\t\ttaken = true;\n\t\t\ttakenFrom = state;\n\t\t\tpicked = selector(state);\n\n\t\t\treturn picked;\n\t\t};\n\t}, [selector]);\n\n\t/**\n\t * Read during render, so it may close over this render's selector directly.\n\t *\n\t * `getSnapshot` is not kept in an effect and has no stability requirement —\n\t * which is what lets the selector stay a plain argument. The alternative was a\n\t * ref written during render to keep the latest one, and a ref written during\n\t * render is an impure render that React's own lint rule refuses.\n\t */\n\tconst readTracked = (): TState | TSelected =>\n\t\tselectFromState ? selectFromState(viewModel.getState()) : tracker.read();\n\n\t/**\n\t * The server snapshot: the state itself, never the Proxy.\n\t *\n\t * Tracking exists to skip renders a client would otherwise do; a server\n\t * renders once, and handing it a recording Proxy would only add work whose\n\t * result nothing reads.\n\t */\n\tconst readUntracked = (): TState | TSelected => {\n\t\tconst state = viewModel.getState();\n\n\t\treturn selector ? selector(state) : state;\n\t};\n\n\treturn useSyncExternalStore(subscribe, readTracked, readUntracked);\n}\n","import { useLankaVM } from \"../use-lanka-vm/useLankaVM\";\nimport type { ILankaReadableVM } from \"lanka/viewmodel\";\n\n/**\n * A ViewModel that is also a hook — what React called a ViewModel before the\n * port existed, and what it may go on calling one.\n *\n * Both call shapes, because both were there: no argument gives the tracked read,\n * a selector gives what the selector picked and bypasses tracking.\n */\nexport type TLankaReactVMHook<TState extends object> = {\n\t(): TState;\n\t<TSelected>(selector: (state: TState) => TSelected): TSelected;\n};\n\n/** The ViewModel it was given, plus the ability to be called like a hook. */\nexport type TLankaReactVM<TViewModel extends ILankaReadableVM<object>> = TViewModel &\n\tTLankaReactVMHook<ReturnType<TViewModel[\"getState\"]>>;\n\n/**\n * What must keep coming from the FUNCTION rather than from the ViewModel.\n *\n * Everything else a caller reads by name is the ViewModel's — including `name`,\n * which is the ViewModel's name and was the ViewModel's name before this\n * function existed, because `build()` defines it over the store.\n *\n * Symbols are excluded wholesale, and that is not tidiness. The ViewModel behind\n * this may be a LAZY proxy, which answers an unknown property with a wrapper\n * function; a wrapper handed back for `Symbol.iterator` makes the hook look\n * iterable, one for `Symbol.toPrimitive` breaks every string coercion of it, and\n * one for `$$typeof` makes React look at it as an element. None of those is a\n * member of any ViewModel, so none of them may be forwarded.\n */\nconst FUNCTION_MEMBERS: ReadonlySet<string> = new Set([\n\t\"prototype\",\n\t\"length\",\n\t\"arguments\",\n\t\"caller\",\n\t\"constructor\",\n\t\"call\",\n\t\"apply\",\n\t\"bind\",\n\t\"toString\",\n]);\n\n/**\n * How the callable answers for the ViewModel behind it.\n *\n * Its own function, because the two traps are the whole mechanism and the\n * factory above is then three lines — one hook, one Proxy, one cast. Read\n * together they were forty-two lines whose shape said \"a function doing two\n * things\", which is what the composition canon calls it.\n */\nconst forwardToViewModel = <TState extends object>(\n\tviewModel: ILankaReadableVM<TState>,\n): ProxyHandler<(selector?: (state: object) => unknown) => unknown> => {\n\tconst members = viewModel as unknown as Record<string, unknown>;\n\n\treturn {\n\t\tget: (target, property, receiver): unknown =>\n\t\t\ttypeof property === \"symbol\" || FUNCTION_MEMBERS.has(property)\n\t\t\t\t? Reflect.get(target, property, receiver)\n\t\t\t\t: members[property],\n\n\t\t/**\n\t\t * `in` answers for the ViewModel too.\n\t\t *\n\t\t * Without this the hook would report that it has no `getState`, while\n\t\t * reading `getState` hands one back — and `\"getState\" in useTodoVM` is how a\n\t\t * devtool, a serialiser and a duck-typed helper ask. The ViewModel behind\n\t\t * this may be a lazy proxy with no `has` trap of its own, so the question is\n\t\t * answered by READING the property, which for a lazy ViewModel builds\n\t\t * nothing.\n\t\t */\n\t\thas: (target, property) =>\n\t\t\tReflect.has(target, property) ||\n\t\t\t(typeof property === \"string\" && members[property] !== undefined),\n\t};\n};\n\n/**\n * Gives a ViewModel React's own ergonomics back.\n *\n * ```ts\n * // the ViewModel, framework-free, exactly as Vue and Svelte receive it\n * const todoVM = createLankaVM({ … });\n *\n * // the same object, callable\n * export const useTodoVM = toLankaReactVM(todoVM);\n * ```\n *\n * ```tsx\n * const { todos, load } = useTodoVM();\n * const count = useTodoVM((state) => state.todos.length);\n * const todos = useTodoVM.getState().todos; // outside a component, as always\n * ```\n *\n * ## Why this exists\n *\n * Until 2.0 a ViewModel WAS a React hook: `createLankaVM` returned a callable,\n * and every screen in every application on lanka called it. Making the framework\n * framework-free took the call signature away — correctly, because four of the\n * five bindings have no use for one and core may not know what a hook is.\n *\n * That is a fact about CORE, and it was allowed to become a fact about React,\n * which it never had to be. A React consumer's familiar spelling costs one\n * wrapper in the one package that is allowed to know what a hook is, so here it\n * is: `useTodoVM()` reads, `useTodoVM(selector)` selects, `useTodoVM.getState()`\n * and `useTodoVM.subscribe()` do what they always did.\n *\n * ## What it does NOT do\n *\n * It does not change the ViewModel. There is exactly one store, and the call\n * forwards to `useLankaVM` — the same function the five bindings' conformance\n * suite drives. A ViewModel read through this and the same ViewModel read in Vue\n * answer identically, notify identically and skip identically, because it is the\n * same object either way and this adds no state of its own.\n *\n * It is also not required. `useLankaVM(todoVM)` is the portable spelling, it\n * stays the one the guides teach, and a codebase that has moved to it needs\n * nothing here.\n *\n * ## Laziness survives\n *\n * The forwarding is a Proxy rather than copied properties, so a ViewModel that\n * builds on first access still builds on first access: reading `useTodoVM.name`\n * answers from the config and constructs nothing.\n */\nexport const toLankaReactVM = <TViewModel extends ILankaReadableVM<object>>(\n\tviewModel: TViewModel,\n): TLankaReactVM<TViewModel> => {\n\t/**\n\t * The call signature, and the whole of it.\n\t *\n\t * Named `useViewModel` rather than `hook`: this IS a custom hook — it calls\n\t * one, it may only be called during a render, and a name not starting with\n\t * `use` hid both facts from every reader and from React's lint rule.\n\t *\n\t * ONE call, with the selector forwarded as it arrived — there is a\n\t * `useLankaVM` overload for exactly this. Written as a branch first, and the\n\t * lint rule was right to refuse it: a hook inside a ternary is a hook React\n\t * cannot promise to call in the same order, and the fact that both arms\n\t * happened to call the same one is not something a reader or a rule can see.\n\t *\n\t * Whether a selector was passed is a property of the CALL SITE and never\n\t * changes between renders, which is what `useLankaVM` relies on to keep one\n\t * subscription standing across a component's life.\n\t */\n\tconst useViewModel = (selector?: (state: object) => unknown): unknown =>\n\t\tuseLankaVM(viewModel, selector);\n\n\treturn new Proxy(useViewModel, forwardToViewModel(viewModel)) as TLankaReactVM<TViewModel>;\n};\n","import { useRef } from \"react\";\nimport { createLankaShallowHold } from \"lanka/viewmodel\";\n\n/**\n * Keeps a selector's answer stable when nothing in it changed.\n *\n * ```tsx\n * const { title, status } = useLankaVM(missionVM, useLankaShallow((s) => ({\n * \ttitle: s.title,\n * \tstatus: s.status,\n * })));\n * ```\n *\n * ## The waste this exists for\n *\n * `useLankaVM(vm, (s) => ({ a: s.a }))` is the commonest thing a React reader\n * writes, and unwrapped it wakes the component for EVERY change in the\n * ViewModel. The binding holds a selection against the state object it came\n * from, which is what a snapshot has to be; a fresh object is new whenever the\n * state is new, so a reader that took a selector to say \"only `a`\" is repainted\n * by a change to `z`. This is the comparison that makes the statement mean\n * something.\n *\n * A selector returning a primitive never had the problem, which is what makes\n * the waste quiet: the shape that is free and the shape that repaints on\n * everything look the same on the page.\n *\n * It used to be worse. Until `useLankaVM` ran its selector once per state\n * object, an unwrapped one CRASHED — `useSyncExternalStore` reads the snapshot\n * during render and again after committing, a fresh object disagreed with\n * itself, and the component rendered until React stopped it with \"Maximum update\n * depth exceeded\". That is closed in the binding, for everybody.\n *\n * ## What is React's here, and what is not\n *\n * The comparison is `createLankaShallowHold` in core, and every binding on the\n * shelf can reach it. It was this file's, and that made it a CAPABILITY React had\n * and four siblings did not — an idiom is a spelling, and this changes which\n * notifications reach a reader. `skills/parity/SKILL.md` 3c.\n *\n * What is left is the part only React needs. A component re-runs this hook on\n * every render, so the holding has to SURVIVE a render while the selector stays\n * the current one: the hold lives in a ref initialised once, and the closure\n * returned below closes over this render's `selector`. A selector computed from\n * props therefore stays honest, and the hold does not reset under it.\n *\n * ## Why a wrapper and not an equality argument\n *\n * `useLankaVM(vm, selector, isEqual)` was the other option, and it puts the\n * comparison in the binding for every caller — including the ones whose\n * selection is a string and pay for a comparison they cannot fail. This is opt\n * in at the call site, which is also where a reader can see it.\n *\n * The shape is React's own: a hook that returns a selector. A consumer arriving\n * from zustand has typed `useShallow` and needs no explanation, which is the\n * whole point of an idiom.\n */\nexport const useLankaShallow = <TState, TSelected>(\n\tselector: (state: TState) => TSelected,\n): ((state: TState) => TSelected) => {\n\tconst hold = useRef<((next: TSelected) => TSelected) | null>(null);\n\thold.current ??= createLankaShallowHold<TSelected>();\n\n\treturn (state: TState): TSelected => hold.current!(selector(state));\n};\n"],"mappings":";;;;AAAA,SAAS,aAAa,SAAS,4BAA4B;AAC3D,SAAS,gCAAgC;AA8DlC,SAAS,WACf,WACA,UACqB;AAyBrB,QAAM,UAAU,QAAQ,MAAM,yBAAyB,SAAS,GAAG,CAAC,SAAS,CAAC;AAW9E,QAAM,cAAc,aAAa;AAYjC,QAAM,YAAY;AAAA,IACjB,CAAC,kBACA,UAAU,UAAU,CAAC,MAAM,SAAS;AACnC,UAAI,aAAa;AAChB,sBAAc;AACd;AAAA,MACD;AAEA,UAAI,QAAQ,aAAa,MAAM,IAAI,GAAG;AACrC,sBAAc;AACd;AAAA,MACD;AAKA,cAAQ,cAAc,MAAM,IAAI;AAAA,IACjC,CAAC;AAAA;AAAA;AAAA,IAGF,CAAC,WAAW,aAAa,OAAO;AAAA,EACjC;AAwBA,QAAM,kBAAkB,QAAQ,MAAM;AACrC,QAAI,CAAC,SAAU,QAAO;AAEtB,QAAI,QAAQ;AACZ,QAAI;AACJ,QAAI;AAEJ,WAAO,CAAC,UAA6B;AACpC,UAAI,SAAS,OAAO,GAAG,WAAW,KAAK,EAAG,QAAO;AAEjD,cAAQ;AACR,kBAAY;AACZ,eAAS,SAAS,KAAK;AAEvB,aAAO;AAAA,IACR;AAAA,EACD,GAAG,CAAC,QAAQ,CAAC;AAUb,QAAM,cAAc,MACnB,kBAAkB,gBAAgB,UAAU,SAAS,CAAC,IAAI,QAAQ,KAAK;AASxE,QAAM,gBAAgB,MAA0B;AAC/C,UAAM,QAAQ,UAAU,SAAS;AAEjC,WAAO,WAAW,SAAS,KAAK,IAAI;AAAA,EACrC;AAEA,SAAO,qBAAqB,WAAW,aAAa,aAAa;AAClE;;;ACzKA,IAAM,mBAAwC,oBAAI,IAAI;AAAA,EACrD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACD,CAAC;AAUD,IAAM,qBAAqB,CAC1B,cACsE;AACtE,QAAM,UAAU;AAEhB,SAAO;AAAA,IACN,KAAK,CAAC,QAAQ,UAAU,aACvB,OAAO,aAAa,YAAY,iBAAiB,IAAI,QAAQ,IAC1D,QAAQ,IAAI,QAAQ,UAAU,QAAQ,IACtC,QAAQ,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAYpB,KAAK,CAAC,QAAQ,aACb,QAAQ,IAAI,QAAQ,QAAQ,KAC3B,OAAO,aAAa,YAAY,QAAQ,QAAQ,MAAM;AAAA,EACzD;AACD;AAkDO,IAAM,iBAAiB,CAC7B,cAC+B;AAkB/B,QAAM,eAAe,CAAC,aACrB,WAAW,WAAW,QAAQ;AAE/B,SAAO,IAAI,MAAM,cAAc,mBAAmB,SAAS,CAAC;AAC7D;;;ACxJA,SAAS,cAAc;AACvB,SAAS,8BAA8B;AAwDhC,IAAM,kBAAkB,CAC9B,aACoC;AACpC,QAAM,OAAO,OAAgD,IAAI;AACjE,OAAK,YAAY,uBAAkC;AAEnD,SAAO,CAAC,UAA6B,KAAK,QAAS,SAAS,KAAK,CAAC;AACnE;","names":[]}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { RenderOptions, RenderResult } from '@testing-library/react';
|
|
2
|
+
import { ReactElement } from 'react';
|
|
3
|
+
import { IPrepareLankaRenderOptions } from '@lankajs/tool-testing';
|
|
4
|
+
import { ILankaInstance } from 'lanka';
|
|
5
|
+
|
|
6
|
+
interface IRenderWithLankaOptions extends Omit<RenderOptions, "wrapper">, IPrepareLankaRenderOptions {
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* What a render with a bootstrapped framework ADDS to the library's own result.
|
|
10
|
+
*
|
|
11
|
+
* An interface over the addition rather than over the whole result, and the same
|
|
12
|
+
* in all five bindings: Svelte Testing Library's result carries a string index
|
|
13
|
+
* signature for its bound queries, so a named member added by extension has to
|
|
14
|
+
* satisfy it — and `lanka` is an instance, not a query. Describing only the
|
|
15
|
+
* addition is true of every library and needs no cast anywhere.
|
|
16
|
+
*/
|
|
17
|
+
interface IRenderWithLankaResult {
|
|
18
|
+
/** The instance the render used. */
|
|
19
|
+
lanka: ILankaInstance;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Rendering a React tree with a bootstrapped framework.
|
|
23
|
+
*
|
|
24
|
+
* ## Why
|
|
25
|
+
*
|
|
26
|
+
* A component reading a ViewModel needs a live instance: without one the first
|
|
27
|
+
* scenario or locator access fails. Assembling bootstrap in every component test
|
|
28
|
+
* is twenty lines of preamble that diverge between files silently — one test
|
|
29
|
+
* creates an instance, another relies on the previous one, and file order starts
|
|
30
|
+
* deciding the outcome.
|
|
31
|
+
*
|
|
32
|
+
* ## What is here, and what is in the kit
|
|
33
|
+
*
|
|
34
|
+
* The four bindings publish this name and differ only in which `render` they
|
|
35
|
+
* call. Everything else — a fresh instance, the doubles, the caller's setup, and
|
|
36
|
+
* the scenario layer brought up in that order — is `prepareLankaRender` in
|
|
37
|
+
* `@lankajs/tool-testing`, which is the one place all four already look. It also
|
|
38
|
+
* carries the reasons: why the instance is fresh, and why the order matters to a
|
|
39
|
+
* ViewModel built at module level.
|
|
40
|
+
*/
|
|
41
|
+
declare const renderWithLanka: (ui: ReactElement, options?: IRenderWithLankaOptions) => RenderResult & IRenderWithLankaResult;
|
|
42
|
+
|
|
43
|
+
export { type IRenderWithLankaOptions, type IRenderWithLankaResult, renderWithLanka };
|