@statorjs/stator 1.4.0 → 1.4.1
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@statorjs/stator",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.1",
|
|
4
4
|
"description": "Server-canonical web framework: business logic in composable state machines, UI as a thin renderer binding machine outputs to DOM positions. Ships TypeScript source (Vite/tsx-native by design).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
"pino-pretty": "^13.1.3",
|
|
89
89
|
"vite": "^6.0.0",
|
|
90
90
|
"vitest": "^2.1.0",
|
|
91
|
-
"@statorjs/stator": "1.4.
|
|
91
|
+
"@statorjs/stator": "1.4.1"
|
|
92
92
|
},
|
|
93
93
|
"scripts": {
|
|
94
94
|
"typecheck": "tsc --noEmit",
|
|
@@ -21,6 +21,15 @@ import { type DirectiveInvocation, defineDirective, invoke } from './core.ts'
|
|
|
21
21
|
|
|
22
22
|
type ConditionalEntry<TValue> = TValue | ReadResult<TValue>
|
|
23
23
|
|
|
24
|
+
/** Resolve the machine instance a spec read evaluates against. During a
|
|
25
|
+
* fan-out recompute this returns the CURRENT proxy — the ReadResult's captured
|
|
26
|
+
* `.instance` was frozen at first render, and `rehydrate()` has since replaced
|
|
27
|
+
* the connection's actor (FINDINGS #3, one composition boundary further out:
|
|
28
|
+
* the spec object outlives the render that built it). Null runtime (initial
|
|
29
|
+
* render, request-scoped recompute) falls back to the captured instance,
|
|
30
|
+
* which is current there by construction. */
|
|
31
|
+
type InstanceResolver = (r: ReadResult<unknown>) => unknown
|
|
32
|
+
|
|
24
33
|
/** Recursive shape accepted by class:list. */
|
|
25
34
|
export type ClassListSpec =
|
|
26
35
|
| string
|
|
@@ -41,8 +50,8 @@ export type StyleListSpec =
|
|
|
41
50
|
| Record<string, ConditionalEntry<string | number | null | undefined>>
|
|
42
51
|
| StyleListSpec[]
|
|
43
52
|
|
|
44
|
-
function evalRead<T>(value: ConditionalEntry<T
|
|
45
|
-
return isReadResult(value) ? (value.selector(value
|
|
53
|
+
function evalRead<T>(value: ConditionalEntry<T>, resolve: InstanceResolver): T {
|
|
54
|
+
return isReadResult(value) ? (value.selector(resolve(value)) as T) : value
|
|
46
55
|
}
|
|
47
56
|
|
|
48
57
|
/** Backstop for hand-written templates (the compiler rejects this at build
|
|
@@ -78,42 +87,42 @@ function collectMachines(spec: unknown, out: Set<string>): void {
|
|
|
78
87
|
}
|
|
79
88
|
}
|
|
80
89
|
|
|
81
|
-
function composeClass(spec: ClassListSpec): string {
|
|
90
|
+
function composeClass(spec: ClassListSpec, resolve: InstanceResolver): string {
|
|
82
91
|
const parts: string[] = []
|
|
83
|
-
walkClass(spec, parts)
|
|
92
|
+
walkClass(spec, parts, resolve)
|
|
84
93
|
return parts.join(' ')
|
|
85
94
|
}
|
|
86
95
|
|
|
87
|
-
function walkClass(spec: ClassListSpec, out: string[]): void {
|
|
96
|
+
function walkClass(spec: ClassListSpec, out: string[], resolve: InstanceResolver): void {
|
|
88
97
|
if (spec == null || spec === false) return
|
|
89
98
|
if (typeof spec === 'string') {
|
|
90
99
|
if (spec.length > 0) out.push(spec)
|
|
91
100
|
return
|
|
92
101
|
}
|
|
93
102
|
if (isReadResult(spec)) {
|
|
94
|
-
const v = spec.selector(spec
|
|
103
|
+
const v = spec.selector(resolve(spec))
|
|
95
104
|
if (typeof v === 'string' && v.length > 0) out.push(v)
|
|
96
105
|
return
|
|
97
106
|
}
|
|
98
107
|
if (Array.isArray(spec)) {
|
|
99
|
-
for (const item of spec) walkClass(item, out)
|
|
108
|
+
for (const item of spec) walkClass(item, out, resolve)
|
|
100
109
|
return
|
|
101
110
|
}
|
|
102
111
|
if (typeof spec === 'object') {
|
|
103
112
|
for (const [name, cond] of Object.entries(spec)) {
|
|
104
|
-
const truthy = evalRead(cond)
|
|
113
|
+
const truthy = evalRead(cond, resolve)
|
|
105
114
|
if (truthy) out.push(name)
|
|
106
115
|
}
|
|
107
116
|
}
|
|
108
117
|
}
|
|
109
118
|
|
|
110
|
-
function composeStyle(spec: StyleListSpec): string {
|
|
119
|
+
function composeStyle(spec: StyleListSpec, resolve: InstanceResolver): string {
|
|
111
120
|
const decls: string[] = []
|
|
112
|
-
walkStyle(spec, decls)
|
|
121
|
+
walkStyle(spec, decls, resolve)
|
|
113
122
|
return decls.join('; ')
|
|
114
123
|
}
|
|
115
124
|
|
|
116
|
-
function walkStyle(spec: StyleListSpec, out: string[]): void {
|
|
125
|
+
function walkStyle(spec: StyleListSpec, out: string[], resolve: InstanceResolver): void {
|
|
117
126
|
if (spec == null || spec === false) return
|
|
118
127
|
if (typeof spec === 'string') {
|
|
119
128
|
const trimmed = spec.trim()
|
|
@@ -121,19 +130,19 @@ function walkStyle(spec: StyleListSpec, out: string[]): void {
|
|
|
121
130
|
return
|
|
122
131
|
}
|
|
123
132
|
if (isReadResult(spec)) {
|
|
124
|
-
const v = spec.selector(spec
|
|
133
|
+
const v = spec.selector(resolve(spec))
|
|
125
134
|
if (typeof v === 'string' && v.trim().length > 0) {
|
|
126
135
|
out.push(v.trim().replace(/;\s*$/, ''))
|
|
127
136
|
}
|
|
128
137
|
return
|
|
129
138
|
}
|
|
130
139
|
if (Array.isArray(spec)) {
|
|
131
|
-
for (const item of spec) walkStyle(item, out)
|
|
140
|
+
for (const item of spec) walkStyle(item, out, resolve)
|
|
132
141
|
return
|
|
133
142
|
}
|
|
134
143
|
if (typeof spec === 'object') {
|
|
135
144
|
for (const [prop, raw] of Object.entries(spec)) {
|
|
136
|
-
const value = evalRead(raw)
|
|
145
|
+
const value = evalRead(raw, resolve)
|
|
137
146
|
if (value == null || value === '') continue
|
|
138
147
|
// A single property's value must not carry `;` — a `read()`-sourced value
|
|
139
148
|
// like `red; position: fixed; …` would otherwise inject extra
|
|
@@ -156,26 +165,32 @@ function cssValue(v: string): string {
|
|
|
156
165
|
/**
|
|
157
166
|
* Register one attr-binding per unique machine the spec depends on. Each
|
|
158
167
|
* binding's selector ignores its arg and recomputes the entire attribute
|
|
159
|
-
* by re-walking the spec — every ReadResult inside re-evaluates its
|
|
160
|
-
*
|
|
168
|
+
* by re-walking the spec — every ReadResult inside re-evaluates its selector
|
|
169
|
+
* against the RESOLVED instance (current proxy under a fan-out recompute,
|
|
170
|
+
* the captured one otherwise), returning fresh state.
|
|
161
171
|
*/
|
|
162
172
|
function bindListAttr<TSpec>(
|
|
163
173
|
state: RenderState,
|
|
164
174
|
spec: TSpec,
|
|
165
175
|
attrName: string,
|
|
166
176
|
elementId: string,
|
|
167
|
-
compose: (spec: TSpec) => string,
|
|
177
|
+
compose: (spec: TSpec, resolve: InstanceResolver) => string,
|
|
168
178
|
): string {
|
|
169
179
|
const machines = new Set<string>()
|
|
170
180
|
collectMachines(spec, machines)
|
|
171
181
|
|
|
172
|
-
|
|
182
|
+
// Resolution is per-evaluation, through the render state the binding was
|
|
183
|
+
// registered under — recompute sets `currentRuntime` for the fan-out pass.
|
|
184
|
+
const resolve: InstanceResolver = (r) =>
|
|
185
|
+
state.currentRuntime?.proxyFor(r.machineName) ?? r.instance
|
|
186
|
+
|
|
187
|
+
const initial = compose(spec, resolve)
|
|
173
188
|
for (const machineName of machines) {
|
|
174
189
|
const slotId = allocSlotId(state)
|
|
175
190
|
registerBinding(state, {
|
|
176
191
|
slotId,
|
|
177
192
|
machineName,
|
|
178
|
-
selector: () => compose(spec),
|
|
193
|
+
selector: () => compose(spec, resolve),
|
|
179
194
|
lastValue: initial,
|
|
180
195
|
kind: 'attr',
|
|
181
196
|
attrName,
|