@lankajs/vue 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 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/vue
2
+
3
+ **▸ module** · Vue binding
4
+
5
+ > One composable — `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.
10
+
11
+ **Requires:** Vue. 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/vue/testing`) — a render with a bootstrapped framework
19
+
20
+ ## What a Vue call answers, and why it differs from React's
21
+
22
+ `useLankaVM(vm)` hands back a `ShallowRef`, so a template reads `state.todos` and a
23
+ script reads `state.value.todos`. React hands back the state itself. That is the one
24
+ difference the shelf does NOT hide: it is the framework's own idea of reactivity, and a
25
+ binding that flattened it would be a second reactivity system fighting the first —
26
+ every `watch` a consumer wrote would stop seeing changes.
27
+
28
+ Everything else is identical, and `lankaViewBindingConformance` is what says so rather
29
+ than this paragraph: every scene this package runs is one `@lankajs/react` runs too,
30
+ and writing this package reworded none of them.
31
+
32
+ ## No client directive, and nothing to replace it
33
+
34
+ React Server Components make importing a hook a build error; Vue has no equivalent and
35
+ needs no directive. Nuxt renders this package on the server as ordinary code.
36
+
37
+ ## The effect scope, and the one case it is not there
38
+
39
+ Inside a component or an `effectScope`, the subscription is released by
40
+ `onScopeDispose` and a consumer does nothing. Called OUTSIDE one — a module-level read,
41
+ a test — there is no scope to attach to, so the returned ref carries `stop()` and the
42
+ caller owns it. Vue warns about the first case and says nothing about the second, which
43
+ is why the second is a published member rather than a note.
44
+
45
+ ---
46
+
47
+ Repository map: [../../../README.md](../../../README.md)
@@ -0,0 +1,49 @@
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 __esm = (fn, res) => function __init() {
14
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
15
+ };
16
+ var __commonJS = (cb, mod) => function __require2() {
17
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
18
+ };
19
+ var __export = (target, all) => {
20
+ for (var name in all)
21
+ __defProp(target, name, { get: all[name], enumerable: true });
22
+ };
23
+ var __copyProps = (to, from, except, desc) => {
24
+ if (from && typeof from === "object" || typeof from === "function") {
25
+ for (let key of __getOwnPropNames(from))
26
+ if (!__hasOwnProp.call(to, key) && key !== except)
27
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
28
+ }
29
+ return to;
30
+ };
31
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
32
+ // If the importer is in node compatibility mode or this is not an ESM
33
+ // file that has been converted to a CommonJS file using a Babel-
34
+ // compatible transform (i.e. "__esModule" has not been set), then set
35
+ // "default" to the CommonJS "module.exports" for node compatibility.
36
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
37
+ mod
38
+ ));
39
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
40
+
41
+ export {
42
+ __require,
43
+ __esm,
44
+ __commonJS,
45
+ __export,
46
+ __toESM,
47
+ __toCommonJS
48
+ };
49
+ //# sourceMappingURL=chunk-PLDDJCW6.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,167 @@
1
+ import { ILankaReadableVM } from 'lanka/viewmodel';
2
+ import { ComputedRef, ShallowRef } from 'vue';
3
+
4
+ /**
5
+ * A ViewModel as Vue reads one: its own members, directly.
6
+ *
7
+ * `vm.rows`, `vm.load()` — no `.value`, in the script and in the template alike
8
+ * — plus the one meta member a caller needs. `$`-prefixed, which is Pinia's
9
+ * convention and its reason: the keys belong to the application, and a meta
10
+ * member sharing that namespace collides the day somebody adds a `stop` of their
11
+ * own.
12
+ *
13
+ * Called a ViewModel and not a store, deliberately. Pinia's word for the thing a
14
+ * component reads is "store", and this reads the way one does — but what holds
15
+ * the state, the actions and the scenario bindings is the ViewModel, and naming
16
+ * it after the shape it wears would hide where the work lives.
17
+ */
18
+ type TLankaVueVM<TState extends object> = TState & {
19
+ /** Releases the subscription. Rarely needed: a component scope does it. */
20
+ $stop: () => void;
21
+ };
22
+ /**
23
+ * Declares the composable a Vue component reads a ViewModel through.
24
+ *
25
+ * ```ts
26
+ * // todosVM.ts — at module level, the way `defineStore` is declared
27
+ * export const useTodosVM = defineLankaComposable(todosVM);
28
+ * ```
29
+ *
30
+ * ```vue
31
+ * <script setup lang="ts">
32
+ * const todos = useTodosVM();
33
+ * </script>
34
+ *
35
+ * <template><li v-for="row in todos.rows" :key="row">{{ row }}</li></template>
36
+ * ```
37
+ *
38
+ * ## Why this exists beside `useLankaVM`
39
+ *
40
+ * `useLankaVM` answers a `ShallowRef`, which is the honest shape for Vue's
41
+ * reactivity and the one every other binding on the shelf parallels. It is also
42
+ * not what a Pinia codebase reads: there a component reads members straight off
43
+ * what it was handed, in the template and the script alike, and `.value` appears
44
+ * in neither. A consumer with that habit types `todos.rows`, gets `undefined`,
45
+ * and learns that lanka is a foreign object.
46
+ *
47
+ * ## Why it answers a FUNCTION and not the reader itself
48
+ *
49
+ * Pinia's shape, and not only for the look of it. A reader built at module level
50
+ * would open its subscription at IMPORT time, outside any component scope — so
51
+ * nothing would ever release it, and every component would share ONE recording.
52
+ * Two components reading different keys would then wake each other, which is the
53
+ * whole of what access tracking exists to prevent. Measured before this shape
54
+ * existed: the component reading only `rows` re-rendered when `unread` moved.
55
+ *
56
+ * So each CALL builds a reader, inside the calling component's scope, with its
57
+ * own subscription and its own recording — and Vue releases it when that
58
+ * component goes.
59
+ *
60
+ * ## Where it differs from Pinia, and why
61
+ *
62
+ * `useTodosVM()` in two components answers two objects, where Pinia answers one.
63
+ * The ViewModel behind them is the same one and there is no second copy of the
64
+ * state — what differs is the RECORDING, which belongs to whoever did the
65
+ * reading. Sharing the object would make tracking coarse, and an idiom is not
66
+ * allowed to change behaviour: that is the rule the parity canon sets for all of
67
+ * them.
68
+ *
69
+ * Calling it outside a component is legal and gives an unscoped reader; the
70
+ * caller then owns `$stop`.
71
+ *
72
+ * ## Reading is tracking
73
+ *
74
+ * Reads go through the access tracker, so a template reading only `rows` is not
75
+ * woken by `isLoading`, and a ViewModel that turned tracking off is heard for
76
+ * everything. Both are core's answers; nothing here decides either. The ref
77
+ * holds a version counter rather than the state, because a ref holding the state
78
+ * is a SNAPSHOT — right for a template, which re-reads when the ref changes, and
79
+ * wrong for something also read from ordinary code at arbitrary moments.
80
+ *
81
+ * ## Destructuring loses reactivity, exactly as it does in Pinia
82
+ *
83
+ * `const { rows } = todos` copies a value out and stops tracking, which is the
84
+ * single most common mistake in a Pinia codebase. `lankaVMToRefs(todos)` is the
85
+ * same answer Pinia gives, under a name that says what it is reading.
86
+ */
87
+ declare const defineLankaComposable: <TState extends object>(viewModel: ILankaReadableVM<TState>) => () => TLankaVueVM<TState>;
88
+
89
+ /** Every state member of the ViewModel, as a ref that keeps tracking. */
90
+ type TLankaVMRefs<TState extends object> = {
91
+ [TKey in keyof TState]: ComputedRef<TState[TKey]>;
92
+ };
93
+ /**
94
+ * Names you can destructure, without losing the reactivity.
95
+ *
96
+ * ```ts
97
+ * const todos = useTodosVM();
98
+ * const { rows, isLoading } = lankaVMToRefs(todos);
99
+ *
100
+ * // in a template: {{ rows }} — in script: rows.value
101
+ * ```
102
+ *
103
+ * `const { rows } = todos` reads the value ONCE and stops tracking, and it is
104
+ * the commonest mistake in a Pinia codebase for the good reason that it looks
105
+ * exactly like code that works: the first paint is right and nothing updates
106
+ * after it. Pinia's answer is `storeToRefs`, so this is that answer under a name
107
+ * a reader recognises.
108
+ *
109
+ * Each ref is a `computed` over the same ViewModel, so nothing is copied and nothing
110
+ * is subscribed a second time — the composable's own subscription is still the only
111
+ * one.
112
+ *
113
+ * Actions are left OUT, and that is not an oversight: an action is a stable
114
+ * function for the life of the store, so `const { load } = todos` is correct and
115
+ * wrapping it in a ref would make every call site write `load.value()`.
116
+ */
117
+ declare const lankaVMToRefs: <TState extends object>(viewModel: TLankaVueVM<TState>) => TLankaVMRefs<TState>;
118
+
119
+ /** A ViewModel read from Vue: a ref, and a way to stop reading it. */
120
+ interface ILankaVMRef<TValue> extends ShallowRef<TValue> {
121
+ /**
122
+ * Releases the subscription.
123
+ *
124
+ * Called for you by `onScopeDispose` inside a component or an `effectScope`.
125
+ * It is published because a read made OUTSIDE a scope — at module level, in a
126
+ * test — has nobody to call it, and Vue says nothing about that case.
127
+ */
128
+ stop: () => void;
129
+ }
130
+ /**
131
+ * Reads a ViewModel from a Vue component.
132
+ *
133
+ * ```vue
134
+ * <script setup lang="ts">
135
+ * const state = useLankaVM(todoVM);
136
+ * </script>
137
+ *
138
+ * <template>
139
+ * <li v-for="todo in state.todos" :key="todo.id">{{ todo.title }}</li>
140
+ * </template>
141
+ * ```
142
+ *
143
+ * Without a selector the component receives a Proxy that records which keys it
144
+ * read, and the next change updates the ref only if one of THOSE keys moved.
145
+ * With a selector the selector decides and tracking is bypassed.
146
+ *
147
+ * ## What a Vue call answers, and why React's answers differently
148
+ *
149
+ * A `ShallowRef`. A template unwraps it (`state.todos`) and a script does not
150
+ * (`state.value.todos`), which is Vue's own idea of reactivity — and the one
151
+ * thing this shelf deliberately does NOT hide. Flattening it would mean a second
152
+ * reactivity system fighting the first, and every `watch` a consumer writes
153
+ * would stop seeing changes.
154
+ *
155
+ * Everything else is the same as every other binding, and
156
+ * `lankaViewBindingConformance` is what says so rather than this paragraph.
157
+ *
158
+ * ## What this function does NOT contain
159
+ *
160
+ * The recording, the comparison and the blind-spot warning are
161
+ * `createLankaAccessTracker` in core. If this file ever needs more than the port
162
+ * gives it, the port has the defect and the fix belongs in core, for everybody.
163
+ */
164
+ declare function useLankaVM<TState extends object>(viewModel: ILankaReadableVM<TState>): ILankaVMRef<TState>;
165
+ declare function useLankaVM<TState extends object, TSelected>(viewModel: ILankaReadableVM<TState>, selector: (state: TState) => TSelected): ILankaVMRef<TSelected>;
166
+
167
+ export { type ILankaVMRef, type TLankaVMRefs, type TLankaVueVM, defineLankaComposable, lankaVMToRefs, useLankaVM };
package/dist/index.js ADDED
@@ -0,0 +1,102 @@
1
+ import "./chunk-PLDDJCW6.js";
2
+
3
+ // src/define-lanka-composable/defineLankaComposable.ts
4
+ import { getCurrentScope, onScopeDispose, shallowRef, triggerRef } from "vue";
5
+ import { createLankaViewSubscription } from "lanka/extend";
6
+ var readsTheViewModel = (current, stop) => ({
7
+ get: (_target, key) => key === "$stop" ? stop : Reflect.get(current(), key),
8
+ has: (_target, key) => key === "$stop" || key in current(),
9
+ ownKeys: () => Reflect.ownKeys(current()),
10
+ /*
11
+ * Reported as configurable, always.
12
+ *
13
+ * A Proxy must not claim a non-configurable descriptor its target lacks — the
14
+ * runtime throws. The target here is a bare object while the keys live on the
15
+ * state, so every descriptor this hands back is invented and must say it can
16
+ * be redefined. Without it `{ ...vm }` and `Object.keys(vm)` throw rather than
17
+ * read, and a Vue devtool does one of them on sight.
18
+ */
19
+ getOwnPropertyDescriptor: (_target, key) => key === "$stop" ? { value: stop, configurable: true, enumerable: false, writable: false } : { ...Reflect.getOwnPropertyDescriptor(current(), key), configurable: true }
20
+ });
21
+ var defineLankaComposable = (viewModel) => () => {
22
+ const version = shallowRef(0);
23
+ const view = createLankaViewSubscription(viewModel, () => {
24
+ version.value += 1;
25
+ triggerRef(version);
26
+ });
27
+ if (getCurrentScope()) onScopeDispose(view.stop);
28
+ const current = () => {
29
+ void version.value;
30
+ return view.read();
31
+ };
32
+ return new Proxy({}, readsTheViewModel(current, view.stop));
33
+ };
34
+
35
+ // src/lanka-vm-to-refs/lankaVMToRefs.ts
36
+ import { computed } from "vue";
37
+ var lankaVMToRefs = (viewModel) => {
38
+ const refs = {};
39
+ for (const key of Object.keys(viewModel)) {
40
+ if (typeof viewModel[key] === "function") continue;
41
+ refs[key] = computed(() => viewModel[key]);
42
+ }
43
+ return refs;
44
+ };
45
+
46
+ // src/use-lanka-vm/useLankaVM.ts
47
+ import {
48
+ getCurrentInstance,
49
+ getCurrentScope as getCurrentScope2,
50
+ onMounted,
51
+ onScopeDispose as onScopeDispose2,
52
+ shallowRef as shallowRef2,
53
+ triggerRef as triggerRef2
54
+ } from "vue";
55
+ import { createLankaAccessTracker } from "lanka/extend";
56
+ function useLankaVM(viewModel, selector) {
57
+ const tracker = createLankaAccessTracker(viewModel);
58
+ const read = () => selector ? selector(viewModel.getState()) : tracker.read();
59
+ const state = shallowRef2(read());
60
+ const hear = (next, prev) => {
61
+ if (selector) {
62
+ const picked = read();
63
+ if (Object.is(picked, state.value)) return;
64
+ state.value = picked;
65
+ triggerRef2(state);
66
+ return;
67
+ }
68
+ if (!tracker.shouldNotify(next, prev)) {
69
+ tracker.reportSkipped(next, prev);
70
+ return;
71
+ }
72
+ state.value = read();
73
+ triggerRef2(state);
74
+ };
75
+ let stop = () => void 0;
76
+ const start = () => {
77
+ stop = viewModel.subscribe(hear);
78
+ };
79
+ const release = () => {
80
+ stop();
81
+ };
82
+ if (getCurrentInstance()) {
83
+ const stateAtSetup = viewModel.getState();
84
+ onMounted(() => {
85
+ start();
86
+ if (Object.is(viewModel.getState(), stateAtSetup)) return;
87
+ state.value = read();
88
+ triggerRef2(state);
89
+ });
90
+ } else {
91
+ start();
92
+ }
93
+ state.stop = release;
94
+ if (getCurrentScope2()) onScopeDispose2(release);
95
+ return state;
96
+ }
97
+ export {
98
+ defineLankaComposable,
99
+ lankaVMToRefs,
100
+ useLankaVM
101
+ };
102
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/define-lanka-composable/defineLankaComposable.ts","../src/lanka-vm-to-refs/lankaVMToRefs.ts","../src/use-lanka-vm/useLankaVM.ts"],"sourcesContent":["import { getCurrentScope, onScopeDispose, shallowRef, triggerRef } from \"vue\";\nimport { createLankaViewSubscription } from \"lanka/extend\";\nimport type { ILankaReadableVM } from \"lanka/viewmodel\";\n\n/**\n * A ViewModel as Vue reads one: its own members, directly.\n *\n * `vm.rows`, `vm.load()` — no `.value`, in the script and in the template alike\n * — plus the one meta member a caller needs. `$`-prefixed, which is Pinia's\n * convention and its reason: the keys belong to the application, and a meta\n * member sharing that namespace collides the day somebody adds a `stop` of their\n * own.\n *\n * Called a ViewModel and not a store, deliberately. Pinia's word for the thing a\n * component reads is \"store\", and this reads the way one does — but what holds\n * the state, the actions and the scenario bindings is the ViewModel, and naming\n * it after the shape it wears would hide where the work lives.\n */\nexport type TLankaVueVM<TState extends object> = TState & {\n\t/** Releases the subscription. Rarely needed: a component scope does it. */\n\t$stop: () => void;\n};\n\n/**\n * How the composable answers for the ViewModel behind it.\n *\n * Its own function, because the traps are the whole mechanism and the factory\n * below is then the subscription and the Proxy.\n */\nconst readsTheViewModel = <TState extends object, TFacade extends object>(\n\tcurrent: () => TState,\n\tstop: () => void,\n): ProxyHandler<TFacade> => ({\n\tget: (_target, key) => (key === \"$stop\" ? stop : Reflect.get(current(), key)),\n\n\thas: (_target, key) => key === \"$stop\" || key in current(),\n\n\townKeys: () => Reflect.ownKeys(current()),\n\n\t/*\n\t * Reported as configurable, always.\n\t *\n\t * A Proxy must not claim a non-configurable descriptor its target lacks — the\n\t * runtime throws. The target here is a bare object while the keys live on the\n\t * state, so every descriptor this hands back is invented and must say it can\n\t * be redefined. Without it `{ ...vm }` and `Object.keys(vm)` throw rather than\n\t * read, and a Vue devtool does one of them on sight.\n\t */\n\tgetOwnPropertyDescriptor: (_target, key) =>\n\t\tkey === \"$stop\"\n\t\t\t? { value: stop, configurable: true, enumerable: false, writable: false }\n\t\t\t: { ...Reflect.getOwnPropertyDescriptor(current(), key), configurable: true },\n});\n\n/**\n * Declares the composable a Vue component reads a ViewModel through.\n *\n * ```ts\n * // todosVM.ts — at module level, the way `defineStore` is declared\n * export const useTodosVM = defineLankaComposable(todosVM);\n * ```\n *\n * ```vue\n * <script setup lang=\"ts\">\n * const todos = useTodosVM();\n * </script>\n *\n * <template><li v-for=\"row in todos.rows\" :key=\"row\">{{ row }}</li></template>\n * ```\n *\n * ## Why this exists beside `useLankaVM`\n *\n * `useLankaVM` answers a `ShallowRef`, which is the honest shape for Vue's\n * reactivity and the one every other binding on the shelf parallels. It is also\n * not what a Pinia codebase reads: there a component reads members straight off\n * what it was handed, in the template and the script alike, and `.value` appears\n * in neither. A consumer with that habit types `todos.rows`, gets `undefined`,\n * and learns that lanka is a foreign object.\n *\n * ## Why it answers a FUNCTION and not the reader itself\n *\n * Pinia's shape, and not only for the look of it. A reader built at module level\n * would open its subscription at IMPORT time, outside any component scope — so\n * nothing would ever release it, and every component would share ONE recording.\n * Two components reading different keys would then wake each other, which is the\n * whole of what access tracking exists to prevent. Measured before this shape\n * existed: the component reading only `rows` re-rendered when `unread` moved.\n *\n * So each CALL builds a reader, inside the calling component's scope, with its\n * own subscription and its own recording — and Vue releases it when that\n * component goes.\n *\n * ## Where it differs from Pinia, and why\n *\n * `useTodosVM()` in two components answers two objects, where Pinia answers one.\n * The ViewModel behind them is the same one and there is no second copy of the\n * state — what differs is the RECORDING, which belongs to whoever did the\n * reading. Sharing the object would make tracking coarse, and an idiom is not\n * allowed to change behaviour: that is the rule the parity canon sets for all of\n * them.\n *\n * Calling it outside a component is legal and gives an unscoped reader; the\n * caller then owns `$stop`.\n *\n * ## Reading is tracking\n *\n * Reads go through the access tracker, so a template reading only `rows` is not\n * woken by `isLoading`, and a ViewModel that turned tracking off is heard for\n * everything. Both are core's answers; nothing here decides either. The ref\n * holds a version counter rather than the state, because a ref holding the state\n * is a SNAPSHOT — right for a template, which re-reads when the ref changes, and\n * wrong for something also read from ordinary code at arbitrary moments.\n *\n * ## Destructuring loses reactivity, exactly as it does in Pinia\n *\n * `const { rows } = todos` copies a value out and stops tracking, which is the\n * single most common mistake in a Pinia codebase. `lankaVMToRefs(todos)` is the\n * same answer Pinia gives, under a name that says what it is reading.\n */\nexport const defineLankaComposable =\n\t<TState extends object>(viewModel: ILankaReadableVM<TState>) =>\n\t(): TLankaVueVM<TState> => {\n\t\t/*\n\t\t * `triggerRef` as well as the increment for the reason every binding on this\n\t\t * shelf carries: a tracked read hands back the SAME proxy while the state\n\t\t * object is unchanged, and a shallow ref compares by identity.\n\t\t */\n\t\tconst version = shallowRef(0);\n\n\t\tconst view = createLankaViewSubscription(viewModel, () => {\n\t\t\tversion.value += 1;\n\t\t\ttriggerRef(version);\n\t\t});\n\n\t\t// Inside a component or an `effectScope`, Vue owns the lifetime and the\n\t\t// subscription goes with it. Outside one there is nothing to attach to, and\n\t\t// `onScopeDispose` would warn — so the caller keeps `$stop`.\n\t\tif (getCurrentScope()) onScopeDispose(view.stop);\n\n\t\tconst current = (): TState => {\n\t\t\t// Read for the DEPENDENCY, discard the number. A template reading\n\t\t\t// `todos.rows` must re-render when the counter moves, and the counter is\n\t\t\t// the only reactive thing in here.\n\t\t\tvoid version.value;\n\n\t\t\treturn view.read();\n\t\t};\n\n\t\treturn new Proxy({} as TLankaVueVM<TState>, readsTheViewModel(current, view.stop));\n\t};\n","import { computed } from \"vue\";\nimport type { ComputedRef } from \"vue\";\nimport type { TLankaVueVM } from \"../define-lanka-composable/defineLankaComposable\";\n\n/** Every state member of the ViewModel, as a ref that keeps tracking. */\nexport type TLankaVMRefs<TState extends object> = {\n\t[TKey in keyof TState]: ComputedRef<TState[TKey]>;\n};\n\n/**\n * Names you can destructure, without losing the reactivity.\n *\n * ```ts\n * const todos = useTodosVM();\n * const { rows, isLoading } = lankaVMToRefs(todos);\n *\n * // in a template: {{ rows }} — in script: rows.value\n * ```\n *\n * `const { rows } = todos` reads the value ONCE and stops tracking, and it is\n * the commonest mistake in a Pinia codebase for the good reason that it looks\n * exactly like code that works: the first paint is right and nothing updates\n * after it. Pinia's answer is `storeToRefs`, so this is that answer under a name\n * a reader recognises.\n *\n * Each ref is a `computed` over the same ViewModel, so nothing is copied and nothing\n * is subscribed a second time — the composable's own subscription is still the only\n * one.\n *\n * Actions are left OUT, and that is not an oversight: an action is a stable\n * function for the life of the store, so `const { load } = todos` is correct and\n * wrapping it in a ref would make every call site write `load.value()`.\n */\nexport const lankaVMToRefs = <TState extends object>(\n\tviewModel: TLankaVueVM<TState>,\n): TLankaVMRefs<TState> => {\n\tconst refs = {} as TLankaVMRefs<TState>;\n\n\tfor (const key of Object.keys(viewModel) as (keyof TState)[]) {\n\t\tif (typeof viewModel[key] === \"function\") continue;\n\n\t\trefs[key] = computed(() => viewModel[key]);\n\t}\n\n\treturn refs;\n};\n","import {\n\tgetCurrentInstance,\n\tgetCurrentScope,\n\tonMounted,\n\tonScopeDispose,\n\tshallowRef,\n\ttriggerRef,\n} from \"vue\";\nimport { createLankaAccessTracker } from \"lanka/extend\";\nimport type { ShallowRef } from \"vue\";\nimport type { ILankaReadableVM } from \"lanka/viewmodel\";\n\n/** A ViewModel read from Vue: a ref, and a way to stop reading it. */\nexport interface ILankaVMRef<TValue> extends ShallowRef<TValue> {\n\t/**\n\t * Releases the subscription.\n\t *\n\t * Called for you by `onScopeDispose` inside a component or an `effectScope`.\n\t * It is published because a read made OUTSIDE a scope — at module level, in a\n\t * test — has nobody to call it, and Vue says nothing about that case.\n\t */\n\tstop: () => void;\n}\n\n/**\n * Reads a ViewModel from a Vue component.\n *\n * ```vue\n * <script setup lang=\"ts\">\n * const state = useLankaVM(todoVM);\n * </script>\n *\n * <template>\n * <li v-for=\"todo in state.todos\" :key=\"todo.id\">{{ todo.title }}</li>\n * </template>\n * ```\n *\n * Without a selector the component receives a Proxy that records which keys it\n * read, and the next change updates the ref only if one of THOSE keys moved.\n * With a selector the selector decides and tracking is bypassed.\n *\n * ## What a Vue call answers, and why React's answers differently\n *\n * A `ShallowRef`. A template unwraps it (`state.todos`) and a script does not\n * (`state.value.todos`), which is Vue's own idea of reactivity — and the one\n * thing this shelf deliberately does NOT hide. Flattening it would mean a second\n * reactivity system fighting the first, and every `watch` a consumer writes\n * would stop seeing changes.\n *\n * Everything else is the same as every other binding, and\n * `lankaViewBindingConformance` is what says so rather than this paragraph.\n *\n * ## What this function does NOT contain\n *\n * The recording, the comparison and the blind-spot warning are\n * `createLankaAccessTracker` in core. If this file ever needs more than the port\n * gives it, the port has the defect and the fix belongs in core, for everybody.\n */\nexport function useLankaVM<TState extends object>(\n\tviewModel: ILankaReadableVM<TState>,\n): ILankaVMRef<TState>;\n\nexport function useLankaVM<TState extends object, TSelected>(\n\tviewModel: ILankaReadableVM<TState>,\n\tselector: (state: TState) => TSelected,\n): ILankaVMRef<TSelected>;\n\nexport function useLankaVM<TState extends object, TSelected>(\n\tviewModel: ILankaReadableVM<TState>,\n\tselector?: (state: TState) => TSelected,\n): ILankaVMRef<TState | TSelected> {\n\tconst tracker = createLankaAccessTracker(viewModel);\n\tconst read = (): TState | TSelected =>\n\t\tselector ? selector(viewModel.getState()) : tracker.read();\n\n\t// `as unknown` first: Vue's `shallowRef` return type is a conditional over the\n\t// value, and TypeScript cannot see that adding `stop` to it lands on this\n\t// interface. The object IS the ref — `stop` is assigned two lines below.\n\tconst state = shallowRef(read()) as unknown as ILankaVMRef<TState | TSelected>;\n\n\tconst hear = (next: TState, prev: TState): void => {\n\t\tif (selector) {\n\t\t\tconst picked = read();\n\n\t\t\t// Only when the SELECTION moved. Without this the ref is set on every\n\t\t\t// notification and the reader wakes for everything, so the same call\n\t\t\t// means one thing here and another in React — which is what the\n\t\t\t// conformance suite's selector scenes now refuse.\n\t\t\tif (Object.is(picked, state.value)) return;\n\n\t\t\tstate.value = picked;\n\t\t\ttriggerRef(state);\n\n\t\t\treturn;\n\t\t}\n\n\t\tif (!tracker.shouldNotify(next, prev)) {\n\t\t\t// No update will follow. If the changed key is linked to this component\n\t\t\t// through a getter it read, the screen froze — and in development core\n\t\t\t// says so by name.\n\t\t\ttracker.reportSkipped(next, prev);\n\t\t\treturn;\n\t\t}\n\n\t\t// `triggerRef` as well as the assignment: a tracked read hands back the SAME\n\t\t// proxy while the state object is unchanged, and a shallow ref compares by\n\t\t// identity — so an assignment alone would be a no-op exactly when the\n\t\t// tracker did its job. Vue re-renders, the proxy records afresh.\n\t\tstate.value = read();\n\t\ttriggerRef(state);\n\t};\n\n\tlet stop = (): void => undefined;\n\tconst start = (): void => {\n\t\tstop = viewModel.subscribe(hear);\n\t};\n\tconst release = (): void => {\n\t\tstop();\n\t};\n\n\t/**\n\t * Inside a component the subscription starts at MOUNT; everywhere else, now.\n\t *\n\t * A server renders once and throws the tree away. Nothing is mounted and\n\t * nothing is unmounted, so the instance's scope is never stopped and\n\t * `onScopeDispose` never runs — a subscription opened in `setup` there is a\n\t * listener on a module-level ViewModel that outlives the request, and the\n\t * process collects one per request until it dies. The conformance suite's\n\t * server scene is what found it, on the day this package started answering\n\t * that scene instead of skipping it.\n\t *\n\t * `onMounted` is the seam because it is the one lifecycle a server never\n\t * reaches. Outside a component there is no mount to wait for — a module-level\n\t * read, a test, an `effectScope` — and the subscription opens immediately, as\n\t * it always did.\n\t *\n\t * The catch-up is not optional. Between `setup` and the mount the ViewModel may\n\t * have moved, and the ref still holds what `setup` saw.\n\t *\n\t * What it compares is the STATE OBJECT, not the value the reader sees. The\n\t * value was the obvious thing to compare and it is wrong on the selector arm:\n\t * a selector building a fresh object — `(s) => ({ … })`, the shape a consumer\n\t * reaches for first — is never `Object.is`-equal to anything, so every such\n\t * component rendered a second time at mount whether or not a thing had moved.\n\t * The state object is the question both arms actually mean: core answers the\n\t * same one while nothing has changed.\n\t *\n\t * ## The window this leaves, and the trade in it\n\t *\n\t * A change made synchronously in `setup` AFTER this call — a bootstrap line, a\n\t * hydration — is no longer in the first render; it lands on the next tick.\n\t * `onBeforeMount` would close that window and open a worse one: it runs inside\n\t * the hydration render, so correcting the value there makes the client paint\n\t * something the server did not send. That is a markup mismatch in a process\n\t * this framework owns no part of, and `skills/hosts/SKILL.md` §5 is the rule\n\t * it breaks — take the frame, which is a cost inside our own layer, over a\n\t * mismatch the host reports and the application cannot act on.\n\t *\n\t * It is a cost the other four bindings do not pay, which is the part worth\n\t * knowing before anyone calls it a Vue bug.\n\t */\n\tif (getCurrentInstance()) {\n\t\tconst stateAtSetup = viewModel.getState();\n\n\t\tonMounted(() => {\n\t\t\tstart();\n\n\t\t\tif (Object.is(viewModel.getState(), stateAtSetup)) return;\n\n\t\t\tstate.value = read();\n\t\t\ttriggerRef(state);\n\t\t});\n\t} else {\n\t\tstart();\n\t}\n\n\tstate.stop = release;\n\n\t// Inside a component or an `effectScope`, Vue owns the lifetime and the\n\t// subscription goes with it. Outside one there is nothing to attach to, and\n\t// `onScopeDispose` would warn — so the caller keeps `stop`.\n\tif (getCurrentScope()) onScopeDispose(release);\n\n\treturn state;\n}\n"],"mappings":";;;AAAA,SAAS,iBAAiB,gBAAgB,YAAY,kBAAkB;AACxE,SAAS,mCAAmC;AA4B5C,IAAM,oBAAoB,CACzB,SACA,UAC4B;AAAA,EAC5B,KAAK,CAAC,SAAS,QAAS,QAAQ,UAAU,OAAO,QAAQ,IAAI,QAAQ,GAAG,GAAG;AAAA,EAE3E,KAAK,CAAC,SAAS,QAAQ,QAAQ,WAAW,OAAO,QAAQ;AAAA,EAEzD,SAAS,MAAM,QAAQ,QAAQ,QAAQ,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWxC,0BAA0B,CAAC,SAAS,QACnC,QAAQ,UACL,EAAE,OAAO,MAAM,cAAc,MAAM,YAAY,OAAO,UAAU,MAAM,IACtE,EAAE,GAAG,QAAQ,yBAAyB,QAAQ,GAAG,GAAG,GAAG,cAAc,KAAK;AAC/E;AAmEO,IAAM,wBACZ,CAAwB,cACxB,MAA2B;AAM1B,QAAM,UAAU,WAAW,CAAC;AAE5B,QAAM,OAAO,4BAA4B,WAAW,MAAM;AACzD,YAAQ,SAAS;AACjB,eAAW,OAAO;AAAA,EACnB,CAAC;AAKD,MAAI,gBAAgB,EAAG,gBAAe,KAAK,IAAI;AAE/C,QAAM,UAAU,MAAc;AAI7B,SAAK,QAAQ;AAEb,WAAO,KAAK,KAAK;AAAA,EAClB;AAEA,SAAO,IAAI,MAAM,CAAC,GAA0B,kBAAkB,SAAS,KAAK,IAAI,CAAC;AAClF;;;ACrJD,SAAS,gBAAgB;AAiClB,IAAM,gBAAgB,CAC5B,cAC0B;AAC1B,QAAM,OAAO,CAAC;AAEd,aAAW,OAAO,OAAO,KAAK,SAAS,GAAuB;AAC7D,QAAI,OAAO,UAAU,GAAG,MAAM,WAAY;AAE1C,SAAK,GAAG,IAAI,SAAS,MAAM,UAAU,GAAG,CAAC;AAAA,EAC1C;AAEA,SAAO;AACR;;;AC7CA;AAAA,EACC;AAAA,EACA,mBAAAA;AAAA,EACA;AAAA,EACA,kBAAAC;AAAA,EACA,cAAAC;AAAA,EACA,cAAAC;AAAA,OACM;AACP,SAAS,gCAAgC;AA2DlC,SAAS,WACf,WACA,UACkC;AAClC,QAAM,UAAU,yBAAyB,SAAS;AAClD,QAAM,OAAO,MACZ,WAAW,SAAS,UAAU,SAAS,CAAC,IAAI,QAAQ,KAAK;AAK1D,QAAM,QAAQD,YAAW,KAAK,CAAC;AAE/B,QAAM,OAAO,CAAC,MAAc,SAAuB;AAClD,QAAI,UAAU;AACb,YAAM,SAAS,KAAK;AAMpB,UAAI,OAAO,GAAG,QAAQ,MAAM,KAAK,EAAG;AAEpC,YAAM,QAAQ;AACd,MAAAC,YAAW,KAAK;AAEhB;AAAA,IACD;AAEA,QAAI,CAAC,QAAQ,aAAa,MAAM,IAAI,GAAG;AAItC,cAAQ,cAAc,MAAM,IAAI;AAChC;AAAA,IACD;AAMA,UAAM,QAAQ,KAAK;AACnB,IAAAA,YAAW,KAAK;AAAA,EACjB;AAEA,MAAI,OAAO,MAAY;AACvB,QAAM,QAAQ,MAAY;AACzB,WAAO,UAAU,UAAU,IAAI;AAAA,EAChC;AACA,QAAM,UAAU,MAAY;AAC3B,SAAK;AAAA,EACN;AA2CA,MAAI,mBAAmB,GAAG;AACzB,UAAM,eAAe,UAAU,SAAS;AAExC,cAAU,MAAM;AACf,YAAM;AAEN,UAAI,OAAO,GAAG,UAAU,SAAS,GAAG,YAAY,EAAG;AAEnD,YAAM,QAAQ,KAAK;AACnB,MAAAA,YAAW,KAAK;AAAA,IACjB,CAAC;AAAA,EACF,OAAO;AACN,UAAM;AAAA,EACP;AAEA,QAAM,OAAO;AAKb,MAAIH,iBAAgB,EAAG,CAAAC,gBAAe,OAAO;AAE7C,SAAO;AACR;","names":["getCurrentScope","onScopeDispose","shallowRef","triggerRef"]}
@@ -0,0 +1,43 @@
1
+ import { RenderOptions, RenderResult } from '@testing-library/vue';
2
+ import { Component } from 'vue';
3
+ import { IPrepareLankaRenderOptions } from '@lankajs/tool-testing';
4
+ import { ILankaInstance } from 'lanka';
5
+
6
+ interface IRenderWithLankaOptions extends Omit<RenderOptions<Component>, "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 Vue 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: Component, options?: IRenderWithLankaOptions) => RenderResult & IRenderWithLankaResult;
42
+
43
+ export { type IRenderWithLankaOptions, type IRenderWithLankaResult, renderWithLanka };