@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,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
|
+
}
|