@statorjs/stator 1.0.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 +104 -0
- package/package.json +99 -0
- package/src/build/build.ts +244 -0
- package/src/build/head.ts +43 -0
- package/src/build/index.ts +10 -0
- package/src/build/sync.ts +65 -0
- package/src/client/bind.ts +47 -0
- package/src/client/client-id.ts +10 -0
- package/src/client/dispatch.ts +62 -0
- package/src/client/element.ts +156 -0
- package/src/client/index.ts +13 -0
- package/src/client/inspector.ts +237 -0
- package/src/client/machine.ts +39 -0
- package/src/client/runtime.ts +246 -0
- package/src/client/use.ts +119 -0
- package/src/compiler/client-emit.ts +180 -0
- package/src/compiler/client-script.ts +256 -0
- package/src/compiler/compile.ts +459 -0
- package/src/compiler/diagnostics.ts +65 -0
- package/src/compiler/dts.ts +87 -0
- package/src/compiler/hash.ts +8 -0
- package/src/compiler/index.ts +48 -0
- package/src/compiler/lower.ts +0 -0
- package/src/compiler/regions.ts +79 -0
- package/src/compiler/split.ts +168 -0
- package/src/compiler/styles.ts +200 -0
- package/src/compiler/virtual-code.ts +184 -0
- package/src/components/index.ts +2 -0
- package/src/components/json-ld.ts +85 -0
- package/src/engine/actor.ts +248 -0
- package/src/engine/define-machine.ts +113 -0
- package/src/engine/index.ts +37 -0
- package/src/engine/types.ts +236 -0
- package/src/server/api-route.ts +149 -0
- package/src/server/app-dispatch.ts +52 -0
- package/src/server/app-store.ts +35 -0
- package/src/server/cached-store.ts +117 -0
- package/src/server/create-app.ts +83 -0
- package/src/server/define-machine.ts +10 -0
- package/src/server/dev.ts +255 -0
- package/src/server/discovery.ts +111 -0
- package/src/server/dispatch-context.ts +39 -0
- package/src/server/effects.ts +120 -0
- package/src/server/http.ts +475 -0
- package/src/server/index.ts +101 -0
- package/src/server/instance-proxy.ts +95 -0
- package/src/server/logger.ts +52 -0
- package/src/server/machine-store.ts +355 -0
- package/src/server/reads-helpers.ts +40 -0
- package/src/server/recompute.ts +287 -0
- package/src/server/redis-store.ts +116 -0
- package/src/server/render-context.ts +228 -0
- package/src/server/render.ts +111 -0
- package/src/server/route-discovery.ts +226 -0
- package/src/server/route-request.ts +36 -0
- package/src/server/routing.ts +175 -0
- package/src/server/session-lock.ts +33 -0
- package/src/server/session-runtime.ts +242 -0
- package/src/server/session.ts +29 -0
- package/src/server/sse.ts +175 -0
- package/src/server/store.ts +95 -0
- package/src/stator-modules.d.ts +12 -0
- package/src/template/client-shell.ts +65 -0
- package/src/template/conditional.ts +166 -0
- package/src/template/directives/core.ts +58 -0
- package/src/template/directives/list-attr.ts +183 -0
- package/src/template/directives/on.ts +22 -0
- package/src/template/each.ts +295 -0
- package/src/template/html.ts +180 -0
- package/src/template/index.ts +29 -0
- package/src/template/parser.ts +341 -0
- package/src/template/read.ts +50 -0
- package/src/template/types.ts +33 -0
- package/src/vite/index.ts +6 -0
- package/src/vite/plugin.ts +151 -0
- package/src/vite/stub.ts +79 -0
- package/src/wire/apply.ts +103 -0
- package/src/wire/index.ts +67 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side appliers for the wire protocol — the one implementation of
|
|
3
|
+
* "given patches/directives, mutate the DOM". Used by both the page runtime
|
|
4
|
+
* (client/runtime.ts) and island dispatch (client/dispatch.ts), so the two
|
|
5
|
+
* paths can't drift.
|
|
6
|
+
*
|
|
7
|
+
* Observability: every applied patch/directive dispatches a `stator:*`
|
|
8
|
+
* CustomEvent on `window` (the inspector's contract), regardless of which
|
|
9
|
+
* path applied it.
|
|
10
|
+
*/
|
|
11
|
+
import type { Directive, Patch } from './index.ts'
|
|
12
|
+
|
|
13
|
+
function emit(name: string, detail: unknown): void {
|
|
14
|
+
window.dispatchEvent(new CustomEvent(name, { detail }))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function resolveTarget(target: { kind: 'slot' | 'element'; id: string }): Element | null {
|
|
18
|
+
if (target.kind === 'slot') {
|
|
19
|
+
return document.querySelector(`[data-slot="${target.id}"]`)
|
|
20
|
+
}
|
|
21
|
+
return document.querySelector(`[data-stator-id="${target.id}"]`)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function applyPatches(patches: Patch[]): void {
|
|
25
|
+
for (const patch of patches) {
|
|
26
|
+
const element = resolveTarget(patch.target)
|
|
27
|
+
if (!element) {
|
|
28
|
+
// A missing target means this DOM diverged from server truth (stale
|
|
29
|
+
// non-live page, or client code removed server-owned nodes). Skipping
|
|
30
|
+
// is safe — arm/key-scoped slot ids guarantee a patch can't land on
|
|
31
|
+
// the wrong content — but the divergence is worth surfacing.
|
|
32
|
+
console.warn(
|
|
33
|
+
`stator: patch target ${patch.target.kind} "${patch.target.id}" not in DOM — skipped`,
|
|
34
|
+
)
|
|
35
|
+
continue
|
|
36
|
+
}
|
|
37
|
+
if (element) {
|
|
38
|
+
if (patch.op === 'text') element.textContent = patch.value
|
|
39
|
+
else if (patch.op === 'html') element.innerHTML = patch.value
|
|
40
|
+
else if (patch.op === 'attr') {
|
|
41
|
+
if (patch.value === null) element.removeAttribute(patch.name)
|
|
42
|
+
else element.setAttribute(patch.name, patch.value)
|
|
43
|
+
}
|
|
44
|
+
// Keyed-list ops address element children by index, sequentially: each
|
|
45
|
+
// op sees the DOM as left by the previous one (see wire/index.ts).
|
|
46
|
+
else if (patch.op === 'insert') {
|
|
47
|
+
const tpl = document.createElement('template')
|
|
48
|
+
tpl.innerHTML = patch.value
|
|
49
|
+
element.insertBefore(tpl.content, element.children[patch.index] ?? null)
|
|
50
|
+
} else if (patch.op === 'remove') {
|
|
51
|
+
element.children[patch.index]?.remove()
|
|
52
|
+
} else if (patch.op === 'move') {
|
|
53
|
+
const node = element.children[patch.from]
|
|
54
|
+
if (node) {
|
|
55
|
+
node.remove()
|
|
56
|
+
element.insertBefore(node, element.children[patch.to] ?? null)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
emit('stator:patch-applied', { patch, element, timestamp: Date.now() })
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function applyDirectives(directives: Directive[]): void {
|
|
65
|
+
for (const directive of directives) {
|
|
66
|
+
emit('stator:directive-applied', { directive, timestamp: Date.now() })
|
|
67
|
+
switch (directive.type) {
|
|
68
|
+
case 'navigate':
|
|
69
|
+
location.href = directive.to
|
|
70
|
+
return // stop processing further directives; we're leaving
|
|
71
|
+
case 'reload':
|
|
72
|
+
location.reload()
|
|
73
|
+
return
|
|
74
|
+
case 'push-url':
|
|
75
|
+
history.pushState({}, '', directive.to)
|
|
76
|
+
break
|
|
77
|
+
case 'replace-url':
|
|
78
|
+
history.replaceState({}, '', directive.to)
|
|
79
|
+
break
|
|
80
|
+
case 'focus': {
|
|
81
|
+
const el = resolveTarget(directive.target)
|
|
82
|
+
if (el && 'focus' in el && typeof (el as HTMLElement).focus === 'function') {
|
|
83
|
+
;(el as HTMLElement).focus()
|
|
84
|
+
}
|
|
85
|
+
break
|
|
86
|
+
}
|
|
87
|
+
case 'scroll': {
|
|
88
|
+
const el = resolveTarget(directive.target)
|
|
89
|
+
if (el && 'scrollIntoView' in el) {
|
|
90
|
+
;(el as HTMLElement).scrollIntoView({
|
|
91
|
+
behavior: directive.behavior ?? 'auto',
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
break
|
|
95
|
+
}
|
|
96
|
+
case 'event':
|
|
97
|
+
emit(directive.name, directive.detail)
|
|
98
|
+
break
|
|
99
|
+
default:
|
|
100
|
+
console.error('stator: unknown directive type', directive)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wire protocol — the single source of truth for every shape that crosses
|
|
3
|
+
* the server/client boundary. Documented in WIRE.md; produced by
|
|
4
|
+
* `server/recompute.ts` and the API-route envelope; consumed by the client
|
|
5
|
+
* runtime and island dispatch via `wire/apply.ts`.
|
|
6
|
+
*
|
|
7
|
+
* This module is types-only (no DOM, no server imports) so both sides can
|
|
8
|
+
* import it. Keep it that way: a new patch op or directive is added HERE and
|
|
9
|
+
* nowhere else — producers and appliers then fail to typecheck until they
|
|
10
|
+
* handle it.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** Slot positions (data-slot) vs element identities (data-stator-id) — the
|
|
14
|
+
* two addressing dimensions, orthogonal to the op. */
|
|
15
|
+
export type SlotTarget = { kind: 'slot'; id: string }
|
|
16
|
+
export type ElementTarget = { kind: 'element'; id: string }
|
|
17
|
+
export type PatchTarget = SlotTarget | ElementTarget
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Wire patch. Addressing is a discriminated `target` and the op describes
|
|
21
|
+
* what to do at that target.
|
|
22
|
+
*
|
|
23
|
+
* The keyed-list ops (`insert`/`remove`/`move`) address element children of a
|
|
24
|
+
* list slot **by index, sequentially**: each op's indices refer to the DOM
|
|
25
|
+
* state after every preceding op in the batch has been applied. The server
|
|
26
|
+
* emits them from a replay simulation, so a batch is deterministic.
|
|
27
|
+
*
|
|
28
|
+
* Reserved future ops (not yet emitted, documented for the wire spec):
|
|
29
|
+
* - 'attr-add' / 'attr-remove' on element targets (per-class toggles)
|
|
30
|
+
* - 'prop' on element targets (IDL property writes that have no attr)
|
|
31
|
+
*/
|
|
32
|
+
export type Patch =
|
|
33
|
+
| { target: SlotTarget; op: 'text'; value: string }
|
|
34
|
+
| { target: SlotTarget; op: 'html'; value: string }
|
|
35
|
+
/** `value: null` removes the attribute — boolean attributes (disabled,
|
|
36
|
+
* checked, open, …) toggle by presence, so the wire must express absence. */
|
|
37
|
+
| { target: ElementTarget; op: 'attr'; name: string; value: string | null }
|
|
38
|
+
| { target: SlotTarget; op: 'insert'; index: number; value: string }
|
|
39
|
+
| { target: SlotTarget; op: 'remove'; index: number }
|
|
40
|
+
| { target: SlotTarget; op: 'move'; from: number; to: number }
|
|
41
|
+
|
|
42
|
+
/** A client directive describing a side effect the client should perform
|
|
43
|
+
* after applying patches. See the response-directives spec for the full list. */
|
|
44
|
+
export type Directive =
|
|
45
|
+
| { type: 'navigate'; to: string }
|
|
46
|
+
| { type: 'reload' }
|
|
47
|
+
| { type: 'push-url'; to: string }
|
|
48
|
+
| { type: 'replace-url'; to: string }
|
|
49
|
+
| { type: 'focus'; target: { kind: 'slot' | 'element'; id: string } }
|
|
50
|
+
| {
|
|
51
|
+
type: 'scroll'
|
|
52
|
+
target: { kind: 'slot' | 'element'; id: string }
|
|
53
|
+
behavior?: 'smooth' | 'auto'
|
|
54
|
+
}
|
|
55
|
+
| { type: 'event'; name: string; detail?: unknown }
|
|
56
|
+
|
|
57
|
+
/** The JSON envelope carried by `/__events` responses, API-route responses,
|
|
58
|
+
* and SSE messages. */
|
|
59
|
+
export interface WireEnvelope {
|
|
60
|
+
patches?: Patch[]
|
|
61
|
+
directives?: Directive[]
|
|
62
|
+
/** POST /__events only: whether the event committed a transition (any
|
|
63
|
+
* machine touched). Distinguishes a guard-dropped event (HTTP 200, zero
|
|
64
|
+
* patches, committed: false) from a committed one that happened to patch
|
|
65
|
+
* nothing on this page. */
|
|
66
|
+
committed?: boolean
|
|
67
|
+
}
|