@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.
Files changed (79) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +104 -0
  3. package/package.json +99 -0
  4. package/src/build/build.ts +244 -0
  5. package/src/build/head.ts +43 -0
  6. package/src/build/index.ts +10 -0
  7. package/src/build/sync.ts +65 -0
  8. package/src/client/bind.ts +47 -0
  9. package/src/client/client-id.ts +10 -0
  10. package/src/client/dispatch.ts +62 -0
  11. package/src/client/element.ts +156 -0
  12. package/src/client/index.ts +13 -0
  13. package/src/client/inspector.ts +237 -0
  14. package/src/client/machine.ts +39 -0
  15. package/src/client/runtime.ts +246 -0
  16. package/src/client/use.ts +119 -0
  17. package/src/compiler/client-emit.ts +180 -0
  18. package/src/compiler/client-script.ts +256 -0
  19. package/src/compiler/compile.ts +459 -0
  20. package/src/compiler/diagnostics.ts +65 -0
  21. package/src/compiler/dts.ts +87 -0
  22. package/src/compiler/hash.ts +8 -0
  23. package/src/compiler/index.ts +48 -0
  24. package/src/compiler/lower.ts +0 -0
  25. package/src/compiler/regions.ts +79 -0
  26. package/src/compiler/split.ts +168 -0
  27. package/src/compiler/styles.ts +200 -0
  28. package/src/compiler/virtual-code.ts +184 -0
  29. package/src/components/index.ts +2 -0
  30. package/src/components/json-ld.ts +85 -0
  31. package/src/engine/actor.ts +248 -0
  32. package/src/engine/define-machine.ts +113 -0
  33. package/src/engine/index.ts +37 -0
  34. package/src/engine/types.ts +236 -0
  35. package/src/server/api-route.ts +149 -0
  36. package/src/server/app-dispatch.ts +52 -0
  37. package/src/server/app-store.ts +35 -0
  38. package/src/server/cached-store.ts +117 -0
  39. package/src/server/create-app.ts +83 -0
  40. package/src/server/define-machine.ts +10 -0
  41. package/src/server/dev.ts +255 -0
  42. package/src/server/discovery.ts +111 -0
  43. package/src/server/dispatch-context.ts +39 -0
  44. package/src/server/effects.ts +120 -0
  45. package/src/server/http.ts +475 -0
  46. package/src/server/index.ts +101 -0
  47. package/src/server/instance-proxy.ts +95 -0
  48. package/src/server/logger.ts +52 -0
  49. package/src/server/machine-store.ts +355 -0
  50. package/src/server/reads-helpers.ts +40 -0
  51. package/src/server/recompute.ts +287 -0
  52. package/src/server/redis-store.ts +116 -0
  53. package/src/server/render-context.ts +228 -0
  54. package/src/server/render.ts +111 -0
  55. package/src/server/route-discovery.ts +226 -0
  56. package/src/server/route-request.ts +36 -0
  57. package/src/server/routing.ts +175 -0
  58. package/src/server/session-lock.ts +33 -0
  59. package/src/server/session-runtime.ts +242 -0
  60. package/src/server/session.ts +29 -0
  61. package/src/server/sse.ts +175 -0
  62. package/src/server/store.ts +95 -0
  63. package/src/stator-modules.d.ts +12 -0
  64. package/src/template/client-shell.ts +65 -0
  65. package/src/template/conditional.ts +166 -0
  66. package/src/template/directives/core.ts +58 -0
  67. package/src/template/directives/list-attr.ts +183 -0
  68. package/src/template/directives/on.ts +22 -0
  69. package/src/template/each.ts +295 -0
  70. package/src/template/html.ts +180 -0
  71. package/src/template/index.ts +29 -0
  72. package/src/template/parser.ts +341 -0
  73. package/src/template/read.ts +50 -0
  74. package/src/template/types.ts +33 -0
  75. package/src/vite/index.ts +6 -0
  76. package/src/vite/plugin.ts +151 -0
  77. package/src/vite/stub.ts +79 -0
  78. package/src/wire/apply.ts +103 -0
  79. 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
+ }