@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,256 @@
1
+ import ts from 'typescript'
2
+ import { CompileError, type DiagnosticLocation } from './diagnostics.ts'
3
+
4
+ /**
5
+ * Phase 3b — analyze a component's client `<script>` and its template's
6
+ * custom-element tags, and validate the name-match binding:
7
+ *
8
+ * <quantity-stepper> ↔ export class QuantityStepper extends StatorElement
9
+ *
10
+ * The kebab-case tag binds to the PascalCase class of the same name. Checked
11
+ * both directions: a tag with no class, or a class with no tag, is an error.
12
+ * Custom-element names must contain a hyphen (the platform's rule), so a
13
+ * single-word class name is rejected.
14
+ */
15
+
16
+ export interface ClientElement {
17
+ /** Kebab-case custom-element tag, e.g. `quantity-stepper`. */
18
+ tag: string
19
+ /** PascalCase class name, e.g. `QuantityStepper`. */
20
+ className: string
21
+ }
22
+
23
+ export interface ClientAnalysis {
24
+ /** Custom elements defined by this file (tag ↔ class, name-matched). */
25
+ elements: ClientElement[]
26
+ }
27
+
28
+ /** kebab-case → PascalCase: `quantity-stepper` → `QuantityStepper`. */
29
+ export function kebabToPascal(tag: string): string {
30
+ return tag
31
+ .split('-')
32
+ .map((p) => p.charAt(0).toUpperCase() + p.slice(1))
33
+ .join('')
34
+ }
35
+
36
+ /** PascalCase → kebab-case: `QuantityStepper` → `quantity-stepper`. */
37
+ export function pascalToKebab(name: string): string {
38
+ return name
39
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
40
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2')
41
+ .toLowerCase()
42
+ }
43
+
44
+ /** A tag is a custom-element tag if it's lowercase and contains a hyphen. */
45
+ export function isCustomElementTag(tag: string): boolean {
46
+ return /^[a-z][a-z0-9]*-[a-z0-9-]*$/.test(tag)
47
+ }
48
+
49
+ /**
50
+ * Validate the name-match between custom-element tags used in the template and
51
+ * the classes exported from the `<script>`. Returns the matched elements.
52
+ *
53
+ * `script` is the raw `<script>` source; `tags` is the set of custom-element
54
+ * tag names found in the template. `locForTag`/`locForClass` provide diagnostics
55
+ * locations when available.
56
+ */
57
+ export function analyzeClient(
58
+ script: string,
59
+ tags: Set<string>,
60
+ opts: { file?: string } = {},
61
+ ): ClientAnalysis {
62
+ const exportedClasses = extractExportedClasses(script)
63
+ const classByName = new Map(exportedClasses.map((c) => [c.name, c]))
64
+
65
+ const elements: ClientElement[] = []
66
+
67
+ // Each custom-element tag must have a same-named exported class.
68
+ for (const tag of tags) {
69
+ const className = kebabToPascal(tag)
70
+ if (!classByName.has(className)) {
71
+ throw new CompileError(
72
+ `stator: <${tag}> has no matching client class. Add ` +
73
+ `\`export class ${className} extends StatorElement { ... }\` to the <script>.`,
74
+ loc(opts.file),
75
+ )
76
+ }
77
+ elements.push({ tag, className })
78
+ }
79
+
80
+ // Each exported class must have a same-named custom-element tag in the template.
81
+ for (const cls of exportedClasses) {
82
+ const tag = pascalToKebab(cls.name)
83
+ if (!tag.includes('-')) {
84
+ throw new CompileError(
85
+ `stator: client class "${cls.name}" maps to <${tag}>, which is not a valid ` +
86
+ `custom-element name (it needs a hyphen). Use a multi-word name, e.g. ` +
87
+ `"${cls.name}Widget" → <${tag}-widget>.`,
88
+ loc(opts.file),
89
+ )
90
+ }
91
+ if (!tags.has(tag)) {
92
+ throw new CompileError(
93
+ `stator: client class "${cls.name}" has no matching <${tag}> tag in the ` +
94
+ `template. Add the element, or remove the class.`,
95
+ loc(opts.file),
96
+ )
97
+ }
98
+ }
99
+
100
+ return { elements }
101
+ }
102
+
103
+ interface ExportedClass {
104
+ name: string
105
+ }
106
+
107
+ /** Find `export class Foo ...` declarations in the script. */
108
+ function extractExportedClasses(script: string): ExportedClass[] {
109
+ return analyzeScriptClasses(script).map((c) => ({ name: c.name }))
110
+ }
111
+
112
+ /** Per-island analysis used by client codegen: which fields are `use()` client
113
+ * actors (field name → the machine identifier they instantiate) and the class's
114
+ * method names (so `on:click={inc}` can resolve `inc` to a method). */
115
+ export interface ScriptClass {
116
+ name: string
117
+ /** field name → machine identifier (e.g. `qty` → `Qty`). These are the
118
+ * reactive dependencies a `bind:`/`on:` expression can reference. */
119
+ useFields: Map<string, string>
120
+ /** method names declared on the class. */
121
+ methods: Set<string>
122
+ /** every member name (fields + methods) — for rewriting a template
123
+ * expression's class-member references to `this.<member>`. */
124
+ members: Set<string>
125
+ /** declared attribute surface from `static attrs = { unitPrice: Number, ... }`:
126
+ * camelCase prop name → value kind (drives server prop→attr rendering + types). */
127
+ staticAttrs: Map<string, AttrKind>
128
+ }
129
+
130
+ export type AttrKind = 'number' | 'string' | 'boolean'
131
+
132
+ function attrKindOf(coercer: ts.Expression): AttrKind {
133
+ if (ts.isIdentifier(coercer)) {
134
+ if (coercer.text === 'Number') return 'number'
135
+ if (coercer.text === 'Boolean') return 'boolean'
136
+ }
137
+ return 'string' // String, or any custom coercer → serialize as a string attribute
138
+ }
139
+
140
+ export function analyzeScriptClasses(script: string): ScriptClass[] {
141
+ const sf = ts.createSourceFile(
142
+ 'script.ts',
143
+ script,
144
+ ts.ScriptTarget.Latest,
145
+ true,
146
+ ts.ScriptKind.TS,
147
+ )
148
+ const out: ScriptClass[] = []
149
+ for (const stmt of sf.statements) {
150
+ if (
151
+ !ts.isClassDeclaration(stmt) ||
152
+ !stmt.name ||
153
+ !stmt.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)
154
+ ) {
155
+ continue
156
+ }
157
+ const useFields = new Map<string, string>()
158
+ const methods = new Set<string>()
159
+ const members = new Set<string>()
160
+ const staticAttrs = new Map<string, AttrKind>()
161
+ for (const member of stmt.members) {
162
+ // `static attrs = { unitPrice: Number, selected: Boolean }`
163
+ if (
164
+ ts.isPropertyDeclaration(member) &&
165
+ member.modifiers?.some((m) => m.kind === ts.SyntaxKind.StaticKeyword) &&
166
+ ts.isIdentifier(member.name) &&
167
+ member.name.text === 'attrs' &&
168
+ member.initializer &&
169
+ ts.isObjectLiteralExpression(member.initializer)
170
+ ) {
171
+ for (const p of member.initializer.properties) {
172
+ if (ts.isPropertyAssignment(p) && ts.isIdentifier(p.name)) {
173
+ staticAttrs.set(p.name.text, attrKindOf(p.initializer))
174
+ }
175
+ }
176
+ continue
177
+ }
178
+ if (ts.isPropertyDeclaration(member) && ts.isIdentifier(member.name)) {
179
+ members.add(member.name.text)
180
+ const init = member.initializer
181
+ // `field = use(Machine, ...)` → reactive client actor.
182
+ if (
183
+ init &&
184
+ ts.isCallExpression(init) &&
185
+ ts.isIdentifier(init.expression) &&
186
+ init.expression.text === 'use' &&
187
+ init.arguments[0] &&
188
+ ts.isIdentifier(init.arguments[0])
189
+ ) {
190
+ useFields.set(member.name.text, (init.arguments[0] as ts.Identifier).text)
191
+ }
192
+ } else if (ts.isMethodDeclaration(member) && ts.isIdentifier(member.name)) {
193
+ methods.add(member.name.text)
194
+ members.add(member.name.text)
195
+ }
196
+ }
197
+ out.push({
198
+ name: stmt.name.text,
199
+ useFields,
200
+ methods,
201
+ members,
202
+ staticAttrs,
203
+ })
204
+ }
205
+ return out
206
+ }
207
+
208
+ function loc(file?: string): DiagnosticLocation | undefined {
209
+ return file ? { file, line: 1, column: 1, frame: '' } : undefined
210
+ }
211
+
212
+ /**
213
+ * A `bind:` / `on:` directive collected from a client component's template
214
+ * (Phase 3b stage 4). The marker addresses the node at runtime
215
+ * (`data-b="<marker>"` ↔ `this.querySelector('[data-b="<marker>"]')`); stage 5
216
+ * emits the wiring from this.
217
+ */
218
+ export interface ClientDirective {
219
+ /** Unique node marker, e.g. `b0`. The element gets `data-b="b0"`. */
220
+ marker: string
221
+ kind: 'on' | 'bind'
222
+ /** on: the DOM event name (`click`). */
223
+ event?: string
224
+ /** bind: the target (`text` / `html` / `value` / `checked` / `disabled` / attr). */
225
+ target?: string
226
+ /** The author expression (handler for on:, value for bind:). */
227
+ expr: string
228
+ /** Reactive client-actor deps referenced by the expression (`use()` fields).
229
+ * Only meaningful for `bind:`; `on:` handlers fire on the event, not on state. */
230
+ deps: string[]
231
+ }
232
+
233
+ /**
234
+ * Infer which `use()` client actors an expression depends on: the identifiers it
235
+ * references whose name is a known use-field, excluding the property-name side of
236
+ * member access (so `qty.count` → `qty`, `qty.count + other.x` → `qty, other`).
237
+ */
238
+ export function inferDeps(expr: string, useFields: Set<string>): string[] {
239
+ const sf = ts.createSourceFile(
240
+ 'e.ts',
241
+ `(${expr})`,
242
+ ts.ScriptTarget.Latest,
243
+ true,
244
+ ts.ScriptKind.TS,
245
+ )
246
+ const found = new Set<string>()
247
+ const visit = (n: ts.Node): void => {
248
+ if (ts.isIdentifier(n)) {
249
+ const isPropName = n.parent && ts.isPropertyAccessExpression(n.parent) && n.parent.name === n
250
+ if (!isPropName && useFields.has(n.text)) found.add(n.text)
251
+ }
252
+ ts.forEachChild(n, visit)
253
+ }
254
+ visit(sf)
255
+ return [...found]
256
+ }