@abide/abide 0.39.0 → 0.40.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/AGENTS.md +318 -146
- package/CHANGELOG.md +89 -0
- package/README.md +77 -79
- package/package.json +1 -1
- package/src/lib/server/rpc/parseArgs.ts +10 -1
- package/src/lib/server/rpc/runWithVerbTimeout.ts +7 -9
- package/src/lib/server/rpc/types/VerbHelper.ts +18 -21
- package/src/lib/server/runtime/SSR_SWAP_SCRIPT.ts +8 -7
- package/src/lib/server/sockets/createSocketDispatcher.ts +12 -3
- package/src/lib/server/sockets/defineSocket.ts +3 -1
- package/src/lib/shared/REF_JSON_HEADER.ts +9 -0
- package/src/lib/shared/REF_JSON_TAGS.ts +31 -0
- package/src/lib/shared/assertExhaustive.ts +14 -0
- package/src/lib/shared/buildRpcRequest.ts +8 -1
- package/src/lib/shared/decodeRefJson.ts +110 -0
- package/src/lib/shared/encodeRefJson.ts +106 -0
- package/src/lib/test/createTestSocketChannel.ts +6 -2
- package/src/lib/ui/compile/TS_PRINTER.ts +7 -0
- package/src/lib/ui/compile/analyzeComponent.ts +15 -16
- package/src/lib/ui/compile/assertRuntimeHelpersBound.ts +66 -0
- package/src/lib/ui/compile/assertTranspiles.ts +19 -0
- package/src/lib/ui/compile/compileModule.ts +56 -34
- package/src/lib/ui/compile/compileSSR.ts +1 -3
- package/src/lib/ui/compile/compileShadow.ts +14 -7
- package/src/lib/ui/compile/desugarSignals.ts +168 -107
- package/src/lib/ui/compile/generateBuild.ts +49 -59
- package/src/lib/ui/compile/generateSSR.ts +30 -21
- package/src/lib/ui/compile/hoistCells.ts +2 -2
- package/src/lib/ui/compile/isWhitespaceText.ts +11 -0
- package/src/lib/ui/compile/lowerContext.ts +23 -10
- package/src/lib/ui/compile/lowerDocAccess.ts +29 -3
- package/src/lib/ui/compile/lowerScript.ts +64 -0
- package/src/lib/ui/compile/parseTemplate.ts +74 -0
- package/src/lib/ui/compile/renameSignalRefs.ts +160 -90
- package/src/lib/ui/compile/resolveBranches.ts +21 -0
- package/src/lib/ui/compile/stripEffects.ts +22 -17
- package/src/lib/ui/compile/types/AnalyzedComponent.ts +5 -0
- package/src/lib/ui/compile/types/TemplateNode.ts +12 -2
- package/src/lib/ui/createScope.ts +14 -0
- package/src/lib/ui/dom/appendText.ts +2 -3
- package/src/lib/ui/dom/appendTextAt.ts +2 -3
- package/src/lib/ui/dom/applyResolved.ts +5 -8
- package/src/lib/ui/dom/awaitBlock.ts +22 -2
- package/src/lib/ui/dom/each.ts +4 -0
- package/src/lib/ui/dom/on.ts +7 -0
- package/src/lib/ui/dom/parseRawNodes.ts +17 -0
- package/src/lib/ui/dom/switchBlock.ts +4 -0
- package/src/lib/ui/dom/when.ts +4 -0
- package/src/lib/ui/installInspectorBridge.ts +3 -1
- package/src/lib/ui/navigate.ts +28 -3
- package/src/lib/ui/renderToStream.ts +17 -9
- package/src/lib/ui/resumeSeedScript.ts +16 -6
- package/src/lib/ui/router.ts +108 -15
- package/src/lib/ui/runtime/NODE_STATE.ts +22 -0
- package/src/lib/ui/runtime/RESUME.ts +13 -6
- package/src/lib/ui/runtime/clientPage.ts +114 -9
- package/src/lib/ui/runtime/createComputedNode.ts +5 -3
- package/src/lib/ui/runtime/createEffectNode.ts +3 -1
- package/src/lib/ui/runtime/createSignalNode.ts +4 -2
- package/src/lib/ui/runtime/flushEffects.ts +8 -5
- package/src/lib/ui/runtime/historyEntries.ts +39 -1
- package/src/lib/ui/runtime/localStoragePersistence.ts +14 -8
- package/src/lib/ui/runtime/readNode.ts +8 -7
- package/src/lib/ui/runtime/runNode.ts +18 -2
- package/src/lib/ui/runtime/scope.ts +12 -1
- package/src/lib/ui/runtime/trigger.ts +40 -24
- package/src/lib/ui/runtime/types/ReactiveNode.ts +4 -1
- package/src/lib/ui/runtime/updateIfNecessary.ts +40 -0
- package/src/lib/ui/socketChannel.ts +5 -2
- package/src/lib/ui/tryEncodeResume.ts +20 -0
- package/src/lib/ui/types/Scope.ts +9 -3
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import ts from 'typescript'
|
|
2
2
|
import { REACTIVE_CALLEES } from './REACTIVE_CALLEES.ts'
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
const factory = ts.factory
|
|
4
5
|
|
|
5
6
|
/* The reactive primitives that must be reached through a scope. A bare call to one of
|
|
6
7
|
these is a compile error: reactive state is owned by a scope and the surface must show
|
|
@@ -34,28 +35,28 @@ function assertScopedPrimitives(source: ts.SourceFile): void {
|
|
|
34
35
|
Desugars the signal surface into the document form. A component's `<script>`
|
|
35
36
|
declares reactive state as signals:
|
|
36
37
|
|
|
37
|
-
let count = state(0)
|
|
38
|
-
let items = state([])
|
|
39
|
-
const total = computed(() => count + items.length)
|
|
38
|
+
let count = scope().state(0)
|
|
39
|
+
let items = scope().state([])
|
|
40
|
+
const total = scope().computed(() => count() + items.length)
|
|
40
41
|
|
|
41
|
-
This
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
42
|
+
This walks the already-parsed script once to collect the binding names, then returns
|
|
43
|
+
a `ts.TransformerFactory` that rewrites the declarations onto a shared `model`
|
|
44
|
+
document: each plain `state(initial)` becomes an initialising assignment (`model.x =
|
|
45
|
+
initial`, in source order so a later state can read an earlier one), each `computed`
|
|
46
|
+
and destructured prop becomes a `scope().derive` slot, and `linked` /
|
|
47
|
+
`state(initial, transform)` stay `.value` cells routed onto the scope. Returning a
|
|
48
|
+
TRANSFORMER (not rebuilt source text) lets the caller (`lowerScript`) chain it with
|
|
49
|
+
reference renaming and doc-access lowering over ONE parsed tree — no print-then-reparse
|
|
50
|
+
between passes. Plain `state` becomes `model.x` access that `lowerDocAccess` lowers to
|
|
51
|
+
patches/reads. No reactive declarations → an identity transformer (the explicit
|
|
52
|
+
`const model = doc(...)` form still works).
|
|
51
53
|
*/
|
|
52
|
-
export function desugarSignals(
|
|
53
|
-
|
|
54
|
+
export function desugarSignals(source: ts.SourceFile): {
|
|
55
|
+
transformer: ts.TransformerFactory<ts.SourceFile>
|
|
54
56
|
stateNames: Set<string>
|
|
55
57
|
derivedNames: Set<string>
|
|
56
58
|
computedNames: Set<string>
|
|
57
59
|
} {
|
|
58
|
-
const source = ts.createSourceFile('script.ts', scriptBody, ts.ScriptTarget.Latest, true)
|
|
59
60
|
assertScopedPrimitives(source)
|
|
60
61
|
const stateNames = new Set<string>()
|
|
61
62
|
const derivedNames = new Set<string>()
|
|
@@ -103,43 +104,70 @@ export function desugarSignals(scriptBody: string): {
|
|
|
103
104
|
}
|
|
104
105
|
}
|
|
105
106
|
}
|
|
106
|
-
if (
|
|
107
|
-
stateNames.size === 0 &&
|
|
108
|
-
derivedNames.size === 0 &&
|
|
109
|
-
computedNames.size === 0 &&
|
|
110
|
-
!hasPropsDestructure
|
|
111
|
-
) {
|
|
112
|
-
return { code: scriptBody, stateNames, derivedNames, computedNames }
|
|
113
|
-
}
|
|
114
107
|
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const cellDeclarations = cellDeclarationLines(statement, printer, source)
|
|
125
|
-
if (stateAssignments !== undefined) {
|
|
126
|
-
lines.push(...stateAssignments)
|
|
127
|
-
} else if (computedDeclarations !== undefined) {
|
|
128
|
-
lines.push(...computedDeclarations)
|
|
129
|
-
} else if (propsDestructure !== undefined) {
|
|
130
|
-
lines.push(...propsDestructure)
|
|
131
|
-
} else if (cellDeclarations !== undefined) {
|
|
132
|
-
lines.push(...cellDeclarations)
|
|
133
|
-
} else {
|
|
134
|
-
lines.push(printer.printNode(ts.EmitHint.Unspecified, statement, source))
|
|
108
|
+
const hasReactive =
|
|
109
|
+
stateNames.size > 0 ||
|
|
110
|
+
derivedNames.size > 0 ||
|
|
111
|
+
computedNames.size > 0 ||
|
|
112
|
+
hasPropsDestructure
|
|
113
|
+
|
|
114
|
+
const transformer: ts.TransformerFactory<ts.SourceFile> = () => (root) => {
|
|
115
|
+
if (!hasReactive) {
|
|
116
|
+
return root
|
|
135
117
|
}
|
|
118
|
+
const statements: ts.Statement[] = []
|
|
119
|
+
/* A shared `model = scope()` host for the state slots, prepended once. */
|
|
120
|
+
if (stateNames.size > 0) {
|
|
121
|
+
statements.push(
|
|
122
|
+
constDeclaration(
|
|
123
|
+
'model',
|
|
124
|
+
factory.createCallExpression(factory.createIdentifier('scope'), undefined, []),
|
|
125
|
+
),
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
for (const statement of root.statements) {
|
|
129
|
+
statements.push(...loweredStatement(statement))
|
|
130
|
+
}
|
|
131
|
+
return factory.updateSourceFile(root, statements)
|
|
136
132
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
133
|
+
|
|
134
|
+
return { transformer, stateNames, derivedNames, computedNames }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* The lowered form of a top-level statement: state slots → `model.x = init`
|
|
138
|
+
assignments, computed → `scope().derive` consts, props → derive consts (+ restProps),
|
|
139
|
+
cells → `scope().<callee>(...)` consts; anything else passes through verbatim. The
|
|
140
|
+
per-statement dispatch mirrors the name-collection pass. */
|
|
141
|
+
function loweredStatement(statement: ts.Statement): ts.Statement[] {
|
|
142
|
+
return (
|
|
143
|
+
stateAssignmentStatements(statement) ??
|
|
144
|
+
computedStatements(statement) ??
|
|
145
|
+
propsStatements(statement) ??
|
|
146
|
+
cellStatements(statement) ?? [statement]
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/* `const NAME = <init>` — a fresh const declaration reusing the original initializer node. */
|
|
151
|
+
function constDeclaration(name: string, initializer: ts.Expression): ts.VariableStatement {
|
|
152
|
+
return factory.createVariableStatement(
|
|
153
|
+
undefined,
|
|
154
|
+
factory.createVariableDeclarationList(
|
|
155
|
+
[factory.createVariableDeclaration(name, undefined, undefined, initializer)],
|
|
156
|
+
ts.NodeFlags.Const,
|
|
157
|
+
),
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/* `scope().<method>(...args)` — the reactive method routed onto the ambient scope. */
|
|
162
|
+
function scopeMethodCall(method: string, args: readonly ts.Expression[]): ts.CallExpression {
|
|
163
|
+
return factory.createCallExpression(
|
|
164
|
+
factory.createPropertyAccessExpression(
|
|
165
|
+
factory.createCallExpression(factory.createIdentifier('scope'), undefined, []),
|
|
166
|
+
method,
|
|
167
|
+
),
|
|
168
|
+
undefined,
|
|
169
|
+
args,
|
|
170
|
+
)
|
|
143
171
|
}
|
|
144
172
|
|
|
145
173
|
/* True for a read-only computed slot — `computed(compute)` with no write-through
|
|
@@ -159,15 +187,11 @@ function isComputedSlot(declaration: ts.VariableDeclaration): boolean {
|
|
|
159
187
|
routed onto the scope: `linked(seed)` → `const x = scope().linked(seed)`. The cell
|
|
160
188
|
stays a standalone non-serializing cell (its refs stay `name.value`); the rewrite only
|
|
161
189
|
removes the bare runtime import so the sole reactive surface is `scope()`. */
|
|
162
|
-
function
|
|
163
|
-
statement: ts.Statement,
|
|
164
|
-
printer: ts.Printer,
|
|
165
|
-
source: ts.SourceFile,
|
|
166
|
-
): string[] | undefined {
|
|
190
|
+
function cellStatements(statement: ts.Statement): ts.Statement[] | undefined {
|
|
167
191
|
if (!ts.isVariableStatement(statement)) {
|
|
168
192
|
return undefined
|
|
169
193
|
}
|
|
170
|
-
const
|
|
194
|
+
const statements: ts.Statement[] = []
|
|
171
195
|
for (const declaration of statement.declarationList.declarations) {
|
|
172
196
|
const callee = signalCallee(declaration)
|
|
173
197
|
if (
|
|
@@ -178,24 +202,18 @@ function cellDeclarationLines(
|
|
|
178
202
|
return undefined
|
|
179
203
|
}
|
|
180
204
|
const args = (declaration.initializer as ts.CallExpression).arguments
|
|
181
|
-
|
|
182
|
-
.join(', ')
|
|
183
|
-
lines.push(`const ${declaration.name.text} = scope().${callee}(${args})`)
|
|
205
|
+
statements.push(constDeclaration(declaration.name.text, scopeMethodCall(callee, args)))
|
|
184
206
|
}
|
|
185
|
-
return
|
|
207
|
+
return statements
|
|
186
208
|
}
|
|
187
209
|
|
|
188
210
|
/* `let total = computed(compute)` → `const total = scope().derive("total", compute)`
|
|
189
211
|
— a computed doc slot whose reader the references lower to `total()`. */
|
|
190
|
-
function
|
|
191
|
-
statement: ts.Statement,
|
|
192
|
-
printer: ts.Printer,
|
|
193
|
-
source: ts.SourceFile,
|
|
194
|
-
): string[] | undefined {
|
|
212
|
+
function computedStatements(statement: ts.Statement): ts.Statement[] | undefined {
|
|
195
213
|
if (!ts.isVariableStatement(statement)) {
|
|
196
214
|
return undefined
|
|
197
215
|
}
|
|
198
|
-
const
|
|
216
|
+
const statements: ts.Statement[] = []
|
|
199
217
|
for (const declaration of statement.declarationList.declarations) {
|
|
200
218
|
if (
|
|
201
219
|
!ts.isIdentifier(declaration.name) ||
|
|
@@ -204,16 +222,25 @@ function computedDeclarationLines(
|
|
|
204
222
|
) {
|
|
205
223
|
return undefined
|
|
206
224
|
}
|
|
207
|
-
const
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
225
|
+
const name = declaration.name.text
|
|
226
|
+
const compute =
|
|
227
|
+
(declaration.initializer as ts.CallExpression).arguments[0] ??
|
|
228
|
+
factory.createArrowFunction(
|
|
229
|
+
undefined,
|
|
230
|
+
undefined,
|
|
231
|
+
[],
|
|
232
|
+
undefined,
|
|
233
|
+
factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
|
|
234
|
+
factory.createIdentifier('undefined'),
|
|
235
|
+
)
|
|
236
|
+
statements.push(
|
|
237
|
+
constDeclaration(
|
|
238
|
+
name,
|
|
239
|
+
scopeMethodCall('derive', [factory.createStringLiteral(name), compute]),
|
|
240
|
+
),
|
|
214
241
|
)
|
|
215
242
|
}
|
|
216
|
-
return
|
|
243
|
+
return statements
|
|
217
244
|
}
|
|
218
245
|
|
|
219
246
|
/* True for `state(initial, transform)` — the write-coercion transform forces a
|
|
@@ -308,65 +335,99 @@ function propsBindingKey(element: ts.BindingElement): string {
|
|
|
308
335
|
|
|
309
336
|
/* If `statement` is a `const {…, ...rest} = props()` destructure, returns one reactive
|
|
310
337
|
computed per named binding — `scope().derive("name", () => $props["key"]?.() ?? default)`,
|
|
311
|
-
read as `name()` — plus a `const rest = restProps($props, [consumed])`
|
|
312
|
-
binding; otherwise undefined. The `?? default` applies the binding's `= default`
|
|
313
|
-
when the prop is absent. */
|
|
314
|
-
function
|
|
315
|
-
statement: ts.Statement,
|
|
316
|
-
printer: ts.Printer,
|
|
317
|
-
source: ts.SourceFile,
|
|
318
|
-
): string[] | undefined {
|
|
338
|
+
read as `name()` — plus a `const rest = restProps($props, [consumed])` for a rest
|
|
339
|
+
binding; otherwise undefined. The `?? default` applies the binding's `= default`
|
|
340
|
+
fallback when the prop is absent. */
|
|
341
|
+
function propsStatements(statement: ts.Statement): ts.Statement[] | undefined {
|
|
319
342
|
if (!ts.isVariableStatement(statement)) {
|
|
320
343
|
return undefined
|
|
321
344
|
}
|
|
322
|
-
const
|
|
345
|
+
const statements: ts.Statement[] = []
|
|
323
346
|
for (const declaration of statement.declarationList.declarations) {
|
|
324
347
|
if (signalCallee(declaration) !== 'props' || !ts.isObjectBindingPattern(declaration.name)) {
|
|
325
348
|
return undefined
|
|
326
349
|
}
|
|
327
350
|
const { bindings, rest } = propsDestructure(declaration)
|
|
328
351
|
for (const { local, key, initializer } of bindings) {
|
|
329
|
-
|
|
352
|
+
/* `$props["key"]?.()` — the parent thunk, optionally called. */
|
|
353
|
+
const read = factory.createCallChain(
|
|
354
|
+
factory.createElementAccessExpression(
|
|
355
|
+
factory.createIdentifier('$props'),
|
|
356
|
+
factory.createStringLiteral(key),
|
|
357
|
+
),
|
|
358
|
+
factory.createToken(ts.SyntaxKind.QuestionDotToken),
|
|
359
|
+
undefined,
|
|
360
|
+
[],
|
|
361
|
+
)
|
|
362
|
+
/* `?? default` only when the binding declared a `= default` fallback. */
|
|
363
|
+
const body =
|
|
330
364
|
initializer === undefined
|
|
331
|
-
?
|
|
332
|
-
:
|
|
333
|
-
|
|
334
|
-
|
|
365
|
+
? read
|
|
366
|
+
: factory.createBinaryExpression(
|
|
367
|
+
read,
|
|
368
|
+
factory.createToken(ts.SyntaxKind.QuestionQuestionToken),
|
|
369
|
+
factory.createParenthesizedExpression(initializer),
|
|
370
|
+
)
|
|
371
|
+
const thunk = factory.createArrowFunction(
|
|
372
|
+
undefined,
|
|
373
|
+
undefined,
|
|
374
|
+
[],
|
|
375
|
+
undefined,
|
|
376
|
+
factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
|
|
377
|
+
body,
|
|
378
|
+
)
|
|
379
|
+
statements.push(
|
|
380
|
+
constDeclaration(
|
|
381
|
+
local,
|
|
382
|
+
scopeMethodCall('derive', [factory.createStringLiteral(local), thunk]),
|
|
383
|
+
),
|
|
335
384
|
)
|
|
336
385
|
}
|
|
337
386
|
/* The rest bag gathers every prop not named above (and not `$children`). */
|
|
338
387
|
if (rest !== undefined) {
|
|
339
|
-
const consumed = bindings.map((binding) => binding.key)
|
|
340
|
-
|
|
388
|
+
const consumed = bindings.map((binding) => factory.createStringLiteral(binding.key))
|
|
389
|
+
statements.push(
|
|
390
|
+
constDeclaration(
|
|
391
|
+
rest,
|
|
392
|
+
factory.createCallExpression(factory.createIdentifier('restProps'), undefined, [
|
|
393
|
+
factory.createIdentifier('$props'),
|
|
394
|
+
factory.createArrayLiteralExpression(consumed),
|
|
395
|
+
]),
|
|
396
|
+
),
|
|
397
|
+
)
|
|
341
398
|
}
|
|
342
399
|
}
|
|
343
|
-
return
|
|
400
|
+
return statements
|
|
344
401
|
}
|
|
345
402
|
|
|
346
403
|
/* If `statement` declares `state(...)` bindings, returns `model.<name> = <init>`
|
|
347
|
-
assignment
|
|
348
|
-
function
|
|
349
|
-
statement: ts.Statement,
|
|
350
|
-
printer: ts.Printer,
|
|
351
|
-
source: ts.SourceFile,
|
|
352
|
-
): string[] | undefined {
|
|
404
|
+
assignment statements (one per declaration); otherwise undefined. */
|
|
405
|
+
function stateAssignmentStatements(statement: ts.Statement): ts.Statement[] | undefined {
|
|
353
406
|
if (!ts.isVariableStatement(statement)) {
|
|
354
407
|
return undefined
|
|
355
408
|
}
|
|
356
|
-
const
|
|
409
|
+
const statements: ts.Statement[] = []
|
|
357
410
|
for (const declaration of statement.declarationList.declarations) {
|
|
358
411
|
if (!ts.isIdentifier(declaration.name) || !isPlainStateSlot(declaration)) {
|
|
359
412
|
/* Only a plain `state(initial)` becomes a slot; `state(initial, transform)`
|
|
360
|
-
(and everything else) is a `.value` cell —
|
|
413
|
+
(and everything else) is a `.value` cell — pass it through so the
|
|
361
414
|
runtime call (and its transform) survives. */
|
|
362
415
|
return undefined
|
|
363
416
|
}
|
|
364
|
-
const initial =
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
417
|
+
const initial =
|
|
418
|
+
(declaration.initializer as ts.CallExpression).arguments[0] ??
|
|
419
|
+
factory.createIdentifier('undefined')
|
|
420
|
+
statements.push(
|
|
421
|
+
factory.createExpressionStatement(
|
|
422
|
+
factory.createAssignment(
|
|
423
|
+
factory.createPropertyAccessExpression(
|
|
424
|
+
factory.createIdentifier('model'),
|
|
425
|
+
declaration.name.text,
|
|
426
|
+
),
|
|
427
|
+
initial,
|
|
428
|
+
),
|
|
429
|
+
),
|
|
430
|
+
)
|
|
370
431
|
}
|
|
371
|
-
return
|
|
432
|
+
return statements
|
|
372
433
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assertExhaustive } from '../../shared/assertExhaustive.ts'
|
|
1
2
|
import { HOLE_ATTRIBUTE } from '../runtime/HOLE_ATTRIBUTE.ts'
|
|
2
3
|
import { OUTLET_TAG } from '../runtime/OUTLET_TAG.ts'
|
|
3
4
|
import { asOutlet } from './asOutlet.ts'
|
|
@@ -5,7 +6,9 @@ import { bindListenEvent } from './bindListenEvent.ts'
|
|
|
5
6
|
import { composeProps } from './composeProps.ts'
|
|
6
7
|
import { groupBindParts } from './groupBindParts.ts'
|
|
7
8
|
import { isControlFlow } from './isControlFlow.ts'
|
|
9
|
+
import { isWhitespaceText } from './isWhitespaceText.ts'
|
|
8
10
|
import { lowerContext } from './lowerContext.ts'
|
|
11
|
+
import { resolveBranches } from './resolveBranches.ts'
|
|
9
12
|
import { scopeAttr } from './scopeAttr.ts'
|
|
10
13
|
import { skeletonContext } from './skeletonContext.ts'
|
|
11
14
|
import { spreadExcludedNames } from './spreadExcludedNames.ts'
|
|
@@ -369,7 +372,13 @@ export function generateBuild(
|
|
|
369
372
|
if (node.kind === 'snippet') {
|
|
370
373
|
return generateSnippet(node)
|
|
371
374
|
}
|
|
372
|
-
|
|
375
|
+
if (node.kind === 'each') {
|
|
376
|
+
return generateEach(node, parentVar, before)
|
|
377
|
+
}
|
|
378
|
+
/* Every TemplateNode kind is handled above; `node` is `never` here. A new kind
|
|
379
|
+
reaching this point is a compiler gap — fail loud instead of silently routing
|
|
380
|
+
it to the wrong branch. */
|
|
381
|
+
return assertExhaustive(node, 'template node kind')
|
|
373
382
|
}
|
|
374
383
|
|
|
375
384
|
/* Builds a sibling list, coalescing maximal runs of fully-static element subtrees
|
|
@@ -477,27 +486,25 @@ export function generateBuild(
|
|
|
477
486
|
parentVar: string,
|
|
478
487
|
before: string,
|
|
479
488
|
): string {
|
|
480
|
-
const
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
489
|
+
const [thenBranch, catchBranch, finallyBranch] = resolveBranches(
|
|
490
|
+
node,
|
|
491
|
+
'then',
|
|
492
|
+
'catch',
|
|
493
|
+
'finally',
|
|
494
|
+
)
|
|
495
|
+
const finallyChildren = finallyBranch?.children ?? []
|
|
484
496
|
/* Blocking: no pending, the children are the resolved branch bound to `node.as`.
|
|
485
497
|
Streaming: pending is the non-branch children, resolved is the `then` child. */
|
|
486
498
|
const pending = node.blocking
|
|
487
499
|
? []
|
|
488
500
|
: node.children.filter((child) => child.kind !== 'branch')
|
|
489
|
-
const thenBranch = node.children.find(isBranch('then'))
|
|
490
501
|
const thenThunk = node.blocking
|
|
491
502
|
? branchThunk(
|
|
492
503
|
node.children.filter((child) => child.kind !== 'branch'),
|
|
493
504
|
node.as ?? '_value',
|
|
494
505
|
finallyChildren,
|
|
495
506
|
)
|
|
496
|
-
: branchThunk(
|
|
497
|
-
branchChildren(thenBranch),
|
|
498
|
-
branchVar(thenBranch) ?? '_value',
|
|
499
|
-
finallyChildren,
|
|
500
|
-
)
|
|
507
|
+
: branchThunk(thenBranch?.children ?? [], thenBranch?.as ?? '_value', finallyChildren)
|
|
501
508
|
/* Neither catch nor finally → pass `undefined` so awaitBlock re-throws the
|
|
502
509
|
rejection (surfacing it) instead of rendering an empty branch. A finally-only
|
|
503
510
|
block keeps a catch thunk that renders just finally. */
|
|
@@ -505,8 +512,8 @@ export function generateBuild(
|
|
|
505
512
|
catchBranch === undefined && finallyChildren.length === 0
|
|
506
513
|
? 'undefined'
|
|
507
514
|
: branchThunk(
|
|
508
|
-
|
|
509
|
-
|
|
515
|
+
catchBranch?.children ?? [],
|
|
516
|
+
catchBranch?.as ?? '_error',
|
|
510
517
|
finallyChildren,
|
|
511
518
|
)
|
|
512
519
|
const pendingThunk = hasRenderableContent(pending) ? branchThunk(pending) : 'undefined'
|
|
@@ -518,16 +525,6 @@ export function generateBuild(
|
|
|
518
525
|
)
|
|
519
526
|
}
|
|
520
527
|
|
|
521
|
-
/* Children of a branch node (then/catch), or [] when the branch is absent. */
|
|
522
|
-
function branchChildren(branch: TemplateNode | undefined): TemplateNode[] {
|
|
523
|
-
return branch !== undefined && branch.kind === 'branch' ? branch.children : []
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
/* The value/error variable name a branch binds, if any. */
|
|
527
|
-
function branchVar(branch: TemplateNode | undefined): string | undefined {
|
|
528
|
-
return branch !== undefined && branch.kind === 'branch' ? branch.as : undefined
|
|
529
|
-
}
|
|
530
|
-
|
|
531
528
|
/* A branch's content as a void render thunk `(parent[, value]) => void` that
|
|
532
529
|
builds its children — and an optional trailing `finally` branch — into
|
|
533
530
|
`parent`. The full-range model tracks the built content between markers, so a
|
|
@@ -566,17 +563,6 @@ export function generateBuild(
|
|
|
566
563
|
)
|
|
567
564
|
}
|
|
568
565
|
|
|
569
|
-
/* The branch child of a control block matching `which` (then/catch/finally). */
|
|
570
|
-
function findBranch(
|
|
571
|
-
children: TemplateNode[],
|
|
572
|
-
which: 'then' | 'catch' | 'finally',
|
|
573
|
-
): Extract<TemplateNode, { kind: 'branch' }> | undefined {
|
|
574
|
-
return children.find(
|
|
575
|
-
(child): child is Extract<TemplateNode, { kind: 'branch' }> =>
|
|
576
|
-
child.kind === 'branch' && child.branch === which,
|
|
577
|
-
)
|
|
578
|
-
}
|
|
579
|
-
|
|
580
566
|
/* A sync error boundary: build the guarded subtree (++ finally); a throw while
|
|
581
567
|
building swaps to the catch branch (++ finally). No catch → `undefined`, which
|
|
582
568
|
makes the runtime re-throw to the nearest enclosing boundary. */
|
|
@@ -585,18 +571,14 @@ export function generateBuild(
|
|
|
585
571
|
parentVar: string,
|
|
586
572
|
before: string,
|
|
587
573
|
): string {
|
|
588
|
-
const catchBranch =
|
|
589
|
-
const finallyChildren =
|
|
574
|
+
const [catchBranch, finallyBranch] = resolveBranches(node, 'catch', 'finally')
|
|
575
|
+
const finallyChildren = finallyBranch?.children ?? []
|
|
590
576
|
const guarded = node.children.filter((child) => child.kind !== 'branch')
|
|
591
577
|
const tryThunk = branchThunk(guarded, undefined, finallyChildren)
|
|
592
578
|
const catchThunk =
|
|
593
579
|
catchBranch === undefined
|
|
594
580
|
? 'undefined'
|
|
595
|
-
: branchThunk(
|
|
596
|
-
branchChildren(catchBranch),
|
|
597
|
-
branchVar(catchBranch) ?? '_error',
|
|
598
|
-
finallyChildren,
|
|
599
|
-
)
|
|
581
|
+
: branchThunk(catchBranch.children, catchBranch.as ?? '_error', finallyChildren)
|
|
600
582
|
return `tryBlock(${parentVar}, nextBlockId(), ${tryThunk}, ${catchThunk}, ${before});\n`
|
|
601
583
|
}
|
|
602
584
|
|
|
@@ -607,13 +589,33 @@ export function generateBuild(
|
|
|
607
589
|
parentVar: string,
|
|
608
590
|
before: string,
|
|
609
591
|
): string {
|
|
610
|
-
|
|
592
|
+
/* The `case` children are the chain's `elseif`/`else` branches in source order;
|
|
593
|
+
the rest are the `then` content. */
|
|
594
|
+
const branches = node.children.filter(
|
|
611
595
|
(child): child is Extract<TemplateNode, { kind: 'case' }> => child.kind === 'case',
|
|
612
596
|
)
|
|
613
597
|
const thenChildren = node.children.filter((child) => child.kind !== 'case')
|
|
614
|
-
const
|
|
615
|
-
|
|
616
|
-
|
|
598
|
+
const hasElseif = branches.some((branch) => branch.condition !== undefined)
|
|
599
|
+
/* Fast path: a plain `if` (with optional `else`) is the binary `when` runtime. */
|
|
600
|
+
if (!hasElseif) {
|
|
601
|
+
const elseBranch = branches.find((branch) => branch.condition === undefined)
|
|
602
|
+
const thenThunk = branchThunk(thenChildren)
|
|
603
|
+
const elseThunk =
|
|
604
|
+
elseBranch === undefined ? 'undefined' : branchThunk(elseBranch.children)
|
|
605
|
+
return `when(${parentVar}, () => (${lowerExpression(node.condition)}), ${thenThunk}, ${elseThunk}, ${before});\n`
|
|
606
|
+
}
|
|
607
|
+
/* if/elseif/else is a cond-chain — reuse `switchBlock` over a constant `true`
|
|
608
|
+
subject with `Boolean`-coerced match thunks, so the first truthy branch wins
|
|
609
|
+
(`else` is the match-less default). */
|
|
610
|
+
const entries = [
|
|
611
|
+
`{ match: () => Boolean(${lowerExpression(node.condition)}), render: ${branchThunk(thenChildren)} }`,
|
|
612
|
+
...branches.map((branch) =>
|
|
613
|
+
branch.condition !== undefined
|
|
614
|
+
? `{ match: () => Boolean(${lowerExpression(branch.condition)}), render: ${branchThunk(branch.children)} }`
|
|
615
|
+
: `{ match: undefined, render: ${branchThunk(branch.children)} }`,
|
|
616
|
+
),
|
|
617
|
+
]
|
|
618
|
+
return `switchBlock(${parentVar}, () => true, [${entries.join(', ')}], ${before});\n`
|
|
617
619
|
}
|
|
618
620
|
|
|
619
621
|
/* A keyed each. Each row is a content RANGE (any content, tracked between the
|
|
@@ -635,11 +637,9 @@ export function generateBuild(
|
|
|
635
637
|
optional `<template catch>` branch rendered (after the streamed rows) when the
|
|
636
638
|
iterator rejects. Absent → `undefined`, so the rejection surfaces instead. */
|
|
637
639
|
const fn = node.async ? 'eachAsync' : 'each'
|
|
638
|
-
const catchBranch = node
|
|
639
|
-
(child) => child.kind === 'branch' && child.branch === 'catch',
|
|
640
|
-
)
|
|
640
|
+
const [catchBranch] = resolveBranches(node, 'catch')
|
|
641
641
|
const catchArg = node.async
|
|
642
|
-
? `, ${catchBranch === undefined ? 'undefined' : branchThunk(
|
|
642
|
+
? `, ${catchBranch === undefined ? 'undefined' : branchThunk(catchBranch.children, catchBranch.as ?? '_error')}`
|
|
643
643
|
: ''
|
|
644
644
|
return (
|
|
645
645
|
`${fn}(${parentVar}, () => (${lowerExpression(node.items)}), ` +
|
|
@@ -650,16 +650,6 @@ export function generateBuild(
|
|
|
650
650
|
return generateChildren(rootNodes, hostVar)
|
|
651
651
|
}
|
|
652
652
|
|
|
653
|
-
/* A text node that is purely whitespace (no interpolation, only blank static
|
|
654
|
-
parts). Both back-ends drop it, so it neither contributes markup nor breaks a
|
|
655
|
-
static clone run — it stays transparent so `<a/>\n<b/>` still coalesces. */
|
|
656
|
-
function isWhitespaceText(node: TemplateNode): boolean {
|
|
657
|
-
return (
|
|
658
|
-
node.kind === 'text' &&
|
|
659
|
-
node.parts.every((part) => part.kind === 'static' && part.value.trim() === '')
|
|
660
|
-
)
|
|
661
|
-
}
|
|
662
|
-
|
|
663
653
|
/*
|
|
664
654
|
Whether an element subtree is fully static — no reactive/event/bind attributes,
|
|
665
655
|
no nested `<script>`, and every descendant likewise static (static text, static
|