@lankajs/svelte 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 +42 -0
- package/dist/chunk-5WRI5ZAA.js +31 -0
- package/dist/chunk-5WRI5ZAA.js.map +1 -0
- package/dist/index.d.ts +130 -0
- package/dist/index.js +78 -0
- package/dist/index.js.map +1 -0
- package/dist/testing.d.ts +53 -0
- package/dist/testing.js +12855 -0
- package/dist/testing.js.map +1 -0
- package/dist/wrapper-scaffold-3RO7M4FF.js +39 -0
- package/dist/wrapper-scaffold-3RO7M4FF.js.map +1 -0
- package/dist/wrapper-scaffold-legacy-UQDPD4HL.js +48 -0
- package/dist/wrapper-scaffold-legacy-UQDPD4HL.js.map +1 -0
- package/package.json +56 -0
- package/skills/lanka-svelte/SKILL.md +175 -0
- package/skills/lanka-svelte/reference.md +247 -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,42 @@
|
|
|
1
|
+
# @lankajs/svelte
|
|
2
|
+
|
|
3
|
+
**▸ module** · Svelte binding
|
|
4
|
+
|
|
5
|
+
> One function — `useLankaVM` — over `createSubscriber`, 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.
|
|
10
|
+
|
|
11
|
+
**Requires:** Svelte. 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/svelte/testing`) — a render with a bootstrapped framework
|
|
19
|
+
|
|
20
|
+
## `createSubscriber`, not the store contract
|
|
21
|
+
|
|
22
|
+
Svelte 5 reads a `{ subscribe }` object as a store, and a ViewModel nearly is one —
|
|
23
|
+
the shapes differ only in that Svelte calls the listener immediately and lanka does
|
|
24
|
+
not. Bridging that is two lines, and it was rejected anyway: the store contract is
|
|
25
|
+
Svelte 4's way, it does not compose with `$state`, and a consumer would have written
|
|
26
|
+
`$todoVM` where every other framework writes a plain read.
|
|
27
|
+
|
|
28
|
+
`createSubscriber` from `svelte/reactivity` is the current answer, and it is plain
|
|
29
|
+
TypeScript — which is why this package needs no compiler and builds with `tsup` like
|
|
30
|
+
every other one here. What a consumer gets back is an object whose properties are
|
|
31
|
+
getters, so reading one inside an effect or a template subscribes to it.
|
|
32
|
+
|
|
33
|
+
## The getters are the tracking, not a convenience
|
|
34
|
+
|
|
35
|
+
Svelte's reactivity is read-driven: it knows what an effect depends on because the
|
|
36
|
+
effect READ it. That is the same question `createLankaAccessTracker` answers, so the
|
|
37
|
+
two line up exactly — a component reading `state.todos` records `todos` in the
|
|
38
|
+
tracker AND registers with Svelte's graph in one access.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
Repository map: [../../../README.md](../../../README.md)
|
|
@@ -0,0 +1,31 @@
|
|
|
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 __commonJS = (cb, mod) => function __require() {
|
|
8
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
19
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
20
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
21
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
22
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
23
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
24
|
+
mod
|
|
25
|
+
));
|
|
26
|
+
|
|
27
|
+
export {
|
|
28
|
+
__commonJS,
|
|
29
|
+
__toESM
|
|
30
|
+
};
|
|
31
|
+
//# sourceMappingURL=chunk-5WRI5ZAA.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { ILankaReadableVM } from 'lanka/viewmodel';
|
|
2
|
+
|
|
3
|
+
/** How a subscriber stops listening. */
|
|
4
|
+
type TLankaVMUnsubscriber = () => void;
|
|
5
|
+
/**
|
|
6
|
+
* Svelte's store contract, which is an interface and not a class — satisfied by
|
|
7
|
+
* a ViewModel rather than by a store of ours.
|
|
8
|
+
*
|
|
9
|
+
* One method. Anything with it works with `$store`, `derived`, `get` and every
|
|
10
|
+
* helper in `svelte/store` — which is the whole reason the contract is that
|
|
11
|
+
* small.
|
|
12
|
+
*/
|
|
13
|
+
interface ILankaSvelteVM<TValue> {
|
|
14
|
+
subscribe: (run: (value: TValue) => void) => TLankaVMUnsubscriber;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A ViewModel that satisfies Svelte's store contract, so `$` works on it.
|
|
18
|
+
*
|
|
19
|
+
* ```svelte
|
|
20
|
+
* <script lang="ts">
|
|
21
|
+
* import { toLankaSvelteVM } from "@lankajs/svelte";
|
|
22
|
+
* const todos = toLankaSvelteVM(todosVM);
|
|
23
|
+
* </script>
|
|
24
|
+
*
|
|
25
|
+
* {#each $todos.rows as row}<li>{row}</li>{/each}
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* ## Why this exists beside `useLankaVM`
|
|
29
|
+
*
|
|
30
|
+
* `useLankaVM` answers an object of getters, which is Svelte 5's own shape and
|
|
31
|
+
* the right default: a read registers with the reactivity graph and with the
|
|
32
|
+
* access tracker in ONE access, and nothing needs a `$`.
|
|
33
|
+
*
|
|
34
|
+
* The store contract is the other half of Svelte, and it has not gone anywhere.
|
|
35
|
+
* `$page`, `$navigating` and every store SvelteKit hands a route are read with a
|
|
36
|
+
* `$`; `derived`, `get` and `writable` all speak it; and a codebase that has not
|
|
37
|
+
* moved to runes speaks nothing else. A consumer with that habit reaches for
|
|
38
|
+
* `$todosVM`, and without this they are told a ViewModel is not a store.
|
|
39
|
+
*
|
|
40
|
+
* ## It is the same subscription
|
|
41
|
+
*
|
|
42
|
+
* One `subscribe` on the ViewModel per Svelte subscriber, the access tracker's
|
|
43
|
+
* decision about whether a change is worth an update, and no state of its own —
|
|
44
|
+
* the rules the parity canon sets for an idiom. What it does NOT do is share one
|
|
45
|
+
* ViewModel subscription between Svelte subscribers: each gets its own tracker,
|
|
46
|
+
* because two readers of one ViewModel read different keys and must be woken for
|
|
47
|
+
* different changes. That is the same rule every binding on the shelf follows.
|
|
48
|
+
*
|
|
49
|
+
* ## The contract's own rule: call `run` immediately
|
|
50
|
+
*
|
|
51
|
+
* Svelte requires the current value on subscription, synchronously, before
|
|
52
|
+
* `subscribe` returns — `$store` reads it during the component's first render
|
|
53
|
+
* and would otherwise be `undefined`. `subscribe` on the ViewModel does NOT fire
|
|
54
|
+
* on registration, which is correct for a port and is why the first call is made
|
|
55
|
+
* here by hand.
|
|
56
|
+
*/
|
|
57
|
+
declare const toLankaSvelteVM: <TState extends object>(viewModel: ILankaReadableVM<TState>) => ILankaSvelteVM<TState>;
|
|
58
|
+
|
|
59
|
+
/** A ViewModel read from Svelte: the state by getters, and a way to stop reading. */
|
|
60
|
+
/**
|
|
61
|
+
* What a TRACKED read answers: the state's own keys, as getters.
|
|
62
|
+
*
|
|
63
|
+
* Reading one registers with Svelte's graph and with the access tracker in a
|
|
64
|
+
* single access, which is why this shape and not a ref.
|
|
65
|
+
*/
|
|
66
|
+
type TLankaVMView<TValue> = TValue & {
|
|
67
|
+
stop: () => void;
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* What a SELECTED read answers: one value, under `current`.
|
|
71
|
+
*
|
|
72
|
+
* `.current` is Svelte's own convention for a reactive value a class exposes —
|
|
73
|
+
* `MediaQuery` and the rest of `svelte/reactivity` read that way — so a consumer
|
|
74
|
+
* needs no explanation.
|
|
75
|
+
*
|
|
76
|
+
* A getter object rather than the state's keys, and that is not a preference. A
|
|
77
|
+
* selector may answer anything, including a number, and there are no keys to
|
|
78
|
+
* define on a number: the shape that carried the selection's own keys accepted
|
|
79
|
+
* `TSelected extends object` and refused `(state) => state.count`, which is a
|
|
80
|
+
* member of this shelf NARROWING the shared name. The conformance suite's
|
|
81
|
+
* selector scenes found it.
|
|
82
|
+
*/
|
|
83
|
+
type TLankaVMSelectedView<TSelected> = {
|
|
84
|
+
readonly current: TSelected;
|
|
85
|
+
stop: () => void;
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Reads a ViewModel from Svelte.
|
|
89
|
+
*
|
|
90
|
+
* ```svelte
|
|
91
|
+
* <script lang="ts">
|
|
92
|
+
* const state = useLankaVM(todoVM);
|
|
93
|
+
* </script>
|
|
94
|
+
*
|
|
95
|
+
* {#each state.todos as todo (todo.id)}
|
|
96
|
+
* <li>{todo.title}</li>
|
|
97
|
+
* {/each}
|
|
98
|
+
* ```
|
|
99
|
+
*
|
|
100
|
+
* Without a selector the view records which keys were read and updates only when
|
|
101
|
+
* one of THOSE moves. With a selector the selector decides and tracking is
|
|
102
|
+
* bypassed.
|
|
103
|
+
*
|
|
104
|
+
* ## `createSubscriber`, and not the store contract
|
|
105
|
+
*
|
|
106
|
+
* Svelte reads a `{ subscribe }` object as a store, and a ViewModel nearly is
|
|
107
|
+
* one — the shapes differ only in that Svelte calls the listener immediately.
|
|
108
|
+
* Bridging that is two lines and was rejected anyway: the store contract is
|
|
109
|
+
* Svelte 4's way, it does not compose with `$state`, and a consumer would write
|
|
110
|
+
* `$todoVM` where every other framework writes a plain read.
|
|
111
|
+
*
|
|
112
|
+
* `createSubscriber` is plain TypeScript, which is why this package needs no
|
|
113
|
+
* compiler and builds like every other one here.
|
|
114
|
+
*
|
|
115
|
+
* ## Why the properties are getters
|
|
116
|
+
*
|
|
117
|
+
* Svelte's reactivity is read-driven: an effect depends on what it READ. That is
|
|
118
|
+
* the same question `createLankaAccessTracker` answers, so the two line up
|
|
119
|
+
* exactly — reading `state.todos` records `todos` in the tracker AND registers
|
|
120
|
+
* the effect with Svelte's graph, in one access.
|
|
121
|
+
*
|
|
122
|
+
* ## What this function does NOT contain
|
|
123
|
+
*
|
|
124
|
+
* The recording, the comparison and the blind-spot warning are in core. If this
|
|
125
|
+
* file ever needs more than the port gives it, the port has the defect.
|
|
126
|
+
*/
|
|
127
|
+
declare function useLankaVM<TState extends object>(viewModel: ILankaReadableVM<TState>): TLankaVMView<TState>;
|
|
128
|
+
declare function useLankaVM<TState extends object, TSelected>(viewModel: ILankaReadableVM<TState>, selector: (state: TState) => TSelected): TLankaVMSelectedView<TSelected>;
|
|
129
|
+
|
|
130
|
+
export { type ILankaSvelteVM, type TLankaVMUnsubscriber, type TLankaVMView, toLankaSvelteVM, useLankaVM };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import "./chunk-5WRI5ZAA.js";
|
|
2
|
+
|
|
3
|
+
// src/to-lanka-svelte-vm/toLankaSvelteVM.ts
|
|
4
|
+
import { createLankaViewSubscription } from "lanka/extend";
|
|
5
|
+
var toLankaSvelteVM = (viewModel) => ({
|
|
6
|
+
subscribe: (run) => {
|
|
7
|
+
const view = createLankaViewSubscription(viewModel, () => run(view.read()));
|
|
8
|
+
run(view.read());
|
|
9
|
+
return view.stop;
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
// src/use-lanka-vm/useLankaVM.ts
|
|
14
|
+
import { createSubscriber } from "svelte/reactivity";
|
|
15
|
+
import { createLankaAccessTracker } from "lanka/extend";
|
|
16
|
+
function useLankaVM(viewModel, selector) {
|
|
17
|
+
const tracker = createLankaAccessTracker(viewModel);
|
|
18
|
+
let stop = () => void 0;
|
|
19
|
+
let selected = selector ? selector(viewModel.getState()) : void 0;
|
|
20
|
+
const subscribe = createSubscriber((update) => {
|
|
21
|
+
stop = viewModel.subscribe((next, prev) => {
|
|
22
|
+
if (selector) {
|
|
23
|
+
const picked = selector(next);
|
|
24
|
+
if (Object.is(picked, selected)) return;
|
|
25
|
+
selected = picked;
|
|
26
|
+
update();
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (!tracker.shouldNotify(next, prev)) {
|
|
30
|
+
tracker.reportSkipped(next, prev);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
update();
|
|
34
|
+
});
|
|
35
|
+
return () => {
|
|
36
|
+
stop();
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
if (selector) {
|
|
40
|
+
const selectedView = {};
|
|
41
|
+
Object.defineProperty(selectedView, "current", {
|
|
42
|
+
enumerable: true,
|
|
43
|
+
get: () => {
|
|
44
|
+
subscribe();
|
|
45
|
+
return selector(viewModel.getState());
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
Object.defineProperty(selectedView, "stop", {
|
|
49
|
+
enumerable: false,
|
|
50
|
+
value: () => {
|
|
51
|
+
stop();
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
return selectedView;
|
|
55
|
+
}
|
|
56
|
+
const view = {};
|
|
57
|
+
for (const key of Object.keys(viewModel.getState())) {
|
|
58
|
+
Object.defineProperty(view, key, {
|
|
59
|
+
enumerable: true,
|
|
60
|
+
get: () => {
|
|
61
|
+
subscribe();
|
|
62
|
+
return tracker.read()[key];
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
Object.defineProperty(view, "stop", {
|
|
67
|
+
enumerable: false,
|
|
68
|
+
value: () => {
|
|
69
|
+
stop();
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
return view;
|
|
73
|
+
}
|
|
74
|
+
export {
|
|
75
|
+
toLankaSvelteVM,
|
|
76
|
+
useLankaVM
|
|
77
|
+
};
|
|
78
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/to-lanka-svelte-vm/toLankaSvelteVM.ts","../src/use-lanka-vm/useLankaVM.ts"],"sourcesContent":["import { createLankaViewSubscription } from \"lanka/extend\";\nimport type { ILankaReadableVM } from \"lanka/viewmodel\";\n\n/** How a subscriber stops listening. */\nexport type TLankaVMUnsubscriber = () => void;\n\n/**\n * Svelte's store contract, which is an interface and not a class — satisfied by\n * a ViewModel rather than by a store of ours.\n *\n * One method. Anything with it works with `$store`, `derived`, `get` and every\n * helper in `svelte/store` — which is the whole reason the contract is that\n * small.\n */\nexport interface ILankaSvelteVM<TValue> {\n\tsubscribe: (run: (value: TValue) => void) => TLankaVMUnsubscriber;\n}\n\n/**\n * A ViewModel that satisfies Svelte's store contract, so `$` works on it.\n *\n * ```svelte\n * <script lang=\"ts\">\n * \timport { toLankaSvelteVM } from \"@lankajs/svelte\";\n * \tconst todos = toLankaSvelteVM(todosVM);\n * </script>\n *\n * {#each $todos.rows as row}<li>{row}</li>{/each}\n * ```\n *\n * ## Why this exists beside `useLankaVM`\n *\n * `useLankaVM` answers an object of getters, which is Svelte 5's own shape and\n * the right default: a read registers with the reactivity graph and with the\n * access tracker in ONE access, and nothing needs a `$`.\n *\n * The store contract is the other half of Svelte, and it has not gone anywhere.\n * `$page`, `$navigating` and every store SvelteKit hands a route are read with a\n * `$`; `derived`, `get` and `writable` all speak it; and a codebase that has not\n * moved to runes speaks nothing else. A consumer with that habit reaches for\n * `$todosVM`, and without this they are told a ViewModel is not a store.\n *\n * ## It is the same subscription\n *\n * One `subscribe` on the ViewModel per Svelte subscriber, the access tracker's\n * decision about whether a change is worth an update, and no state of its own —\n * the rules the parity canon sets for an idiom. What it does NOT do is share one\n * ViewModel subscription between Svelte subscribers: each gets its own tracker,\n * because two readers of one ViewModel read different keys and must be woken for\n * different changes. That is the same rule every binding on the shelf follows.\n *\n * ## The contract's own rule: call `run` immediately\n *\n * Svelte requires the current value on subscription, synchronously, before\n * `subscribe` returns — `$store` reads it during the component's first render\n * and would otherwise be `undefined`. `subscribe` on the ViewModel does NOT fire\n * on registration, which is correct for a port and is why the first call is made\n * here by hand.\n */\nexport const toLankaSvelteVM = <TState extends object>(\n\tviewModel: ILankaReadableVM<TState>,\n): ILankaSvelteVM<TState> => ({\n\tsubscribe: (run) => {\n\t\tconst view = createLankaViewSubscription(viewModel, () => run(view.read()));\n\n\t\t// The contract's own rule: the current value, synchronously, before\n\t\t// `subscribe` returns. `$store` reads it during the first render and would\n\t\t// otherwise be `undefined` — and the ViewModel's `subscribe` deliberately\n\t\t// does not fire on registration, which is correct for a port.\n\t\trun(view.read());\n\n\t\treturn view.stop;\n\t},\n});\n","import { createSubscriber } from \"svelte/reactivity\";\nimport { createLankaAccessTracker } from \"lanka/extend\";\nimport type { ILankaReadableVM } from \"lanka/viewmodel\";\n\n/** A ViewModel read from Svelte: the state by getters, and a way to stop reading. */\n/**\n * What a TRACKED read answers: the state's own keys, as getters.\n *\n * Reading one registers with Svelte's graph and with the access tracker in a\n * single access, which is why this shape and not a ref.\n */\nexport type TLankaVMView<TValue> = TValue & {\n\tstop: () => void;\n};\n\n/**\n * What a SELECTED read answers: one value, under `current`.\n *\n * `.current` is Svelte's own convention for a reactive value a class exposes —\n * `MediaQuery` and the rest of `svelte/reactivity` read that way — so a consumer\n * needs no explanation.\n *\n * A getter object rather than the state's keys, and that is not a preference. A\n * selector may answer anything, including a number, and there are no keys to\n * define on a number: the shape that carried the selection's own keys accepted\n * `TSelected extends object` and refused `(state) => state.count`, which is a\n * member of this shelf NARROWING the shared name. The conformance suite's\n * selector scenes found it.\n */\nexport type TLankaVMSelectedView<TSelected> = {\n\treadonly current: TSelected;\n\tstop: () => void;\n};\n\n/**\n * Reads a ViewModel from Svelte.\n *\n * ```svelte\n * <script lang=\"ts\">\n * const state = useLankaVM(todoVM);\n * </script>\n *\n * {#each state.todos as todo (todo.id)}\n * <li>{todo.title}</li>\n * {/each}\n * ```\n *\n * Without a selector the view records which keys were read and updates only when\n * one of THOSE moves. With a selector the selector decides and tracking is\n * bypassed.\n *\n * ## `createSubscriber`, and not the store contract\n *\n * Svelte reads a `{ subscribe }` object as a store, and a ViewModel nearly is\n * one — the shapes differ only in that Svelte calls the listener immediately.\n * Bridging that is two lines and was rejected anyway: the store contract is\n * Svelte 4's way, it does not compose with `$state`, and a consumer would write\n * `$todoVM` where every other framework writes a plain read.\n *\n * `createSubscriber` is plain TypeScript, which is why this package needs no\n * compiler and builds like every other one here.\n *\n * ## Why the properties are getters\n *\n * Svelte's reactivity is read-driven: an effect depends on what it READ. That is\n * the same question `createLankaAccessTracker` answers, so the two line up\n * exactly — reading `state.todos` records `todos` in the tracker AND registers\n * the effect with Svelte's graph, in one access.\n *\n * ## What this function does NOT contain\n *\n * The recording, the comparison and the blind-spot warning are in core. If this\n * file ever needs more than the port gives it, the port has the defect.\n */\nexport function useLankaVM<TState extends object>(\n\tviewModel: ILankaReadableVM<TState>,\n): TLankaVMView<TState>;\n\nexport function useLankaVM<TState extends object, TSelected>(\n\tviewModel: ILankaReadableVM<TState>,\n\tselector: (state: TState) => TSelected,\n): TLankaVMSelectedView<TSelected>;\n\nexport function useLankaVM<TState extends object, TSelected>(\n\tviewModel: ILankaReadableVM<TState>,\n\tselector?: (state: TState) => TSelected,\n): TLankaVMView<TState> | TLankaVMSelectedView<TSelected> {\n\tconst tracker = createLankaAccessTracker(viewModel);\n\n\tlet stop = (): void => undefined;\n\tlet selected: TSelected | undefined = selector ? selector(viewModel.getState()) : undefined;\n\n\tconst subscribe = createSubscriber((update) => {\n\t\tstop = viewModel.subscribe((next, prev) => {\n\t\t\tif (selector) {\n\t\t\t\tconst picked = selector(next);\n\n\t\t\t\t// Only when the SELECTION moved. `update()` invalidates whoever read\n\t\t\t\t// this view, and a selected reader that invalidated on every change\n\t\t\t\t// would be narrowing what it READS and nothing else — which is what\n\t\t\t\t// the suite's selector scenes refuse.\n\t\t\t\tif (Object.is(picked, selected)) return;\n\n\t\t\t\tselected = picked;\n\t\t\t\tupdate();\n\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (!tracker.shouldNotify(next, prev)) {\n\t\t\t\t// No update will follow. If the changed key is linked to this view\n\t\t\t\t// through a getter it read, the screen froze — and in development core\n\t\t\t\t// says so by name.\n\t\t\t\ttracker.reportSkipped(next, prev);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tupdate();\n\t\t});\n\n\t\treturn () => {\n\t\t\tstop();\n\t\t};\n\t});\n\n\t/**\n\t * A selected read answers ONE value, under `current`.\n\t *\n\t * Svelte's own convention for a reactive value — `MediaQuery` and the rest of\n\t * `svelte/reactivity` read that way — and the only shape that can carry a\n\t * selection which is not an object.\n\t */\n\tif (selector) {\n\t\tconst selectedView = {} as Record<string, unknown>;\n\n\t\tObject.defineProperty(selectedView, \"current\", {\n\t\t\tenumerable: true,\n\t\t\tget: () => {\n\t\t\t\tsubscribe();\n\n\t\t\t\treturn selector(viewModel.getState());\n\t\t\t},\n\t\t});\n\t\tObject.defineProperty(selectedView, \"stop\", {\n\t\t\tenumerable: false,\n\t\t\tvalue: () => {\n\t\t\t\tstop();\n\t\t\t},\n\t\t});\n\n\t\treturn selectedView as TLankaVMSelectedView<TSelected>;\n\t}\n\n\t/**\n\t * One getter per key, over the keys the ViewModel has right now.\n\t *\n\t * Built from the CURRENT state rather than left to a Proxy, because Svelte's\n\t * compiler and its `$inspect` walk an object's own descriptors: a Proxy would\n\t * track correctly and show a consumer nothing in devtools.\n\t */\n\tconst view = {} as Record<string, unknown>;\n\n\tfor (const key of Object.keys(viewModel.getState())) {\n\t\tObject.defineProperty(view, key, {\n\t\t\tenumerable: true,\n\t\t\tget: () => {\n\t\t\t\tsubscribe();\n\n\t\t\t\t// `tracker.read()` and not a shared `read` helper: this getter is only\n\t\t\t\t// built on the TRACKED path — the selected view returns above it — so a\n\t\t\t\t// helper that branched on the selector carried an arm nothing could\n\t\t\t\t// reach, and an unreachable branch is a line no test can ever cover.\n\t\t\t\treturn (tracker.read() as Record<string, unknown>)[key];\n\t\t\t},\n\t\t});\n\t}\n\n\tObject.defineProperty(view, \"stop\", {\n\t\tenumerable: false,\n\t\tvalue: () => {\n\t\t\tstop();\n\t\t},\n\t});\n\n\treturn view as TLankaVMView<TState>;\n}\n"],"mappings":";;;AAAA,SAAS,mCAAmC;AA2DrC,IAAM,kBAAkB,CAC9B,eAC6B;AAAA,EAC7B,WAAW,CAAC,QAAQ;AACnB,UAAM,OAAO,4BAA4B,WAAW,MAAM,IAAI,KAAK,KAAK,CAAC,CAAC;AAM1E,QAAI,KAAK,KAAK,CAAC;AAEf,WAAO,KAAK;AAAA,EACb;AACD;;;ACzEA,SAAS,wBAAwB;AACjC,SAAS,gCAAgC;AAkFlC,SAAS,WACf,WACA,UACyD;AACzD,QAAM,UAAU,yBAAyB,SAAS;AAElD,MAAI,OAAO,MAAY;AACvB,MAAI,WAAkC,WAAW,SAAS,UAAU,SAAS,CAAC,IAAI;AAElF,QAAM,YAAY,iBAAiB,CAAC,WAAW;AAC9C,WAAO,UAAU,UAAU,CAAC,MAAM,SAAS;AAC1C,UAAI,UAAU;AACb,cAAM,SAAS,SAAS,IAAI;AAM5B,YAAI,OAAO,GAAG,QAAQ,QAAQ,EAAG;AAEjC,mBAAW;AACX,eAAO;AAEP;AAAA,MACD;AAEA,UAAI,CAAC,QAAQ,aAAa,MAAM,IAAI,GAAG;AAItC,gBAAQ,cAAc,MAAM,IAAI;AAChC;AAAA,MACD;AAEA,aAAO;AAAA,IACR,CAAC;AAED,WAAO,MAAM;AACZ,WAAK;AAAA,IACN;AAAA,EACD,CAAC;AASD,MAAI,UAAU;AACb,UAAM,eAAe,CAAC;AAEtB,WAAO,eAAe,cAAc,WAAW;AAAA,MAC9C,YAAY;AAAA,MACZ,KAAK,MAAM;AACV,kBAAU;AAEV,eAAO,SAAS,UAAU,SAAS,CAAC;AAAA,MACrC;AAAA,IACD,CAAC;AACD,WAAO,eAAe,cAAc,QAAQ;AAAA,MAC3C,YAAY;AAAA,MACZ,OAAO,MAAM;AACZ,aAAK;AAAA,MACN;AAAA,IACD,CAAC;AAED,WAAO;AAAA,EACR;AASA,QAAM,OAAO,CAAC;AAEd,aAAW,OAAO,OAAO,KAAK,UAAU,SAAS,CAAC,GAAG;AACpD,WAAO,eAAe,MAAM,KAAK;AAAA,MAChC,YAAY;AAAA,MACZ,KAAK,MAAM;AACV,kBAAU;AAMV,eAAQ,QAAQ,KAAK,EAA8B,GAAG;AAAA,MACvD;AAAA,IACD,CAAC;AAAA,EACF;AAEA,SAAO,eAAe,MAAM,QAAQ;AAAA,IACnC,YAAY;AAAA,IACZ,OAAO,MAAM;AACZ,WAAK;AAAA,IACN;AAAA,EACD,CAAC;AAED,SAAO;AACR;","names":[]}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { render } from '@testing-library/svelte';
|
|
2
|
+
import { IPrepareLankaRenderOptions } from '@lankajs/tool-testing';
|
|
3
|
+
import { ILankaInstance } from 'lanka';
|
|
4
|
+
|
|
5
|
+
type TSvelteRender = typeof render;
|
|
6
|
+
interface IRenderWithLankaOptions extends Omit<NonNullable<Parameters<TSvelteRender>[1]>, "wrapper">, IPrepareLankaRenderOptions {
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* What a render answers, plus the instance it used.
|
|
10
|
+
*
|
|
11
|
+
* An intersection rather than an `interface … extends`: Svelte Testing Library's
|
|
12
|
+
* result carries a string index signature for its queries, and a named member
|
|
13
|
+
* added by extension has to satisfy it. An intersection does not ask that.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* What a render with a bootstrapped framework ADDS to the library's own result.
|
|
17
|
+
*
|
|
18
|
+
* An interface over the addition rather than over the whole result, and the same
|
|
19
|
+
* in all five bindings: Svelte Testing Library's result carries a string index
|
|
20
|
+
* signature for its bound queries, so a named member added by extension has to
|
|
21
|
+
* satisfy it — and `lanka` is an instance, not a query. Describing only the
|
|
22
|
+
* addition is true of every library and needs no cast anywhere.
|
|
23
|
+
*/
|
|
24
|
+
interface IRenderWithLankaResult {
|
|
25
|
+
/** The instance the render used. */
|
|
26
|
+
lanka: ILankaInstance;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Rendering a Svelte tree with a bootstrapped framework.
|
|
30
|
+
*
|
|
31
|
+
* ## Why
|
|
32
|
+
*
|
|
33
|
+
* A component reading a ViewModel needs a live instance: without one the first
|
|
34
|
+
* scenario or locator access fails. Assembling bootstrap in every component test
|
|
35
|
+
* is twenty lines of preamble that diverge between files silently.
|
|
36
|
+
*
|
|
37
|
+
* ## What is here, and what is in the kit
|
|
38
|
+
*
|
|
39
|
+
* The five bindings publish this name and differ only in which `render` they
|
|
40
|
+
* call. Everything else — a fresh instance, the doubles, the caller's setup and
|
|
41
|
+
* the scenario layer brought up in that order — is `prepareLankaRender` in
|
|
42
|
+
* `@lankajs/tool-testing`, which is the one place all five already look.
|
|
43
|
+
*
|
|
44
|
+
* ## The option and result types are DERIVED, not named
|
|
45
|
+
*
|
|
46
|
+
* Svelte Testing Library's `RenderResult` is generic over the component and its
|
|
47
|
+
* queries, so naming it here would mean guessing three type arguments and
|
|
48
|
+
* re-guessing them whenever the library changes. Reading them off `render`
|
|
49
|
+
* itself cannot drift.
|
|
50
|
+
*/
|
|
51
|
+
declare const renderWithLanka: (ui: Parameters<TSvelteRender>[0], options?: IRenderWithLankaOptions) => ReturnType<TSvelteRender> & IRenderWithLankaResult;
|
|
52
|
+
|
|
53
|
+
export { type IRenderWithLankaOptions, type IRenderWithLankaResult, renderWithLanka };
|