@abide/abide 0.43.0 → 0.44.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/AGENTS.md +293 -219
- package/CHANGELOG.md +46 -0
- package/README.md +191 -67
- package/package.json +2 -2
- package/src/abideLsp.ts +8 -4
- package/src/abideResolverPlugin.ts +11 -1
- package/src/build.ts +9 -0
- package/src/buildDisconnected.ts +2 -0
- package/src/devEntry.ts +1 -0
- package/src/lib/bundle/probeAbideServer.ts +1 -1
- package/src/lib/cli/parseArgvForRpc.ts +4 -2
- package/src/lib/cli/types/CliManifest.ts +6 -4
- package/src/lib/mcp/annotationsForMethod.ts +1 -1
- package/src/lib/mcp/toolResultFromResponse.ts +1 -1
- package/src/lib/mcp/types/JsonRpcRequest.ts +3 -3
- package/src/lib/server/AppModule.ts +1 -1
- package/src/lib/server/cli/handleCliDownload.ts +7 -5
- package/src/lib/server/cli/installScript.ts +3 -3
- package/src/lib/server/prompts/promptRegistry.ts +1 -1
- package/src/lib/server/prompts/types/Prompt.ts +2 -1
- package/src/lib/server/rpc/dispatchRpcInProcess.ts +2 -2
- package/src/lib/server/rpc/types/RpcHelper.ts +1 -1
- package/src/lib/server/runtime/createPublicAssetServer.ts +2 -2
- package/src/lib/server/runtime/createRouteDispatcher.ts +1 -1
- package/src/lib/server/runtime/createServer.ts +21 -0
- package/src/lib/server/runtime/createUiPageRenderer.ts +7 -11
- package/src/lib/server/runtime/crossOriginForbidden.ts +2 -2
- package/src/lib/server/runtime/crossOriginGate.ts +1 -1
- package/src/lib/server/runtime/devHotModuleResponse.ts +2 -2
- package/src/lib/server/runtime/disableIdleTimeoutForStream.ts +1 -1
- package/src/lib/server/runtime/logExposedSurfaces.ts +3 -4
- package/src/lib/server/runtime/serializeCacheSnapshot.ts +3 -8
- package/src/lib/server/runtime/snapshotEntryFromCache.ts +10 -7
- package/src/lib/server/runtime/streamFromIterator.ts +2 -2
- package/src/lib/server/runtime/types/InspectorRpc.ts +2 -2
- package/src/lib/server/socket.ts +1 -1
- package/src/lib/server/sockets/createSocketDispatcher.ts +2 -2
- package/src/lib/server/sockets/socketRegistry.ts +2 -2
- package/src/lib/server/sockets/types/SocketClientFrame.ts +3 -3
- package/src/lib/shared/CACHE_WRAPPED.ts +3 -2
- package/src/lib/shared/REMOTE_FUNCTION.ts +1 -1
- package/src/lib/shared/STREAMING_CONTENT_TYPES.ts +3 -2
- package/src/lib/shared/bodyValueForKind.ts +35 -0
- package/src/lib/shared/cache.ts +1 -1
- package/src/lib/shared/cacheEntryFromSnapshot.ts +23 -18
- package/src/lib/shared/createPushIterator.ts +1 -1
- package/src/lib/shared/decodeResponse.ts +11 -8
- package/src/lib/shared/hasReplayableRequest.ts +17 -0
- package/src/lib/shared/html.ts +7 -26
- package/src/lib/shared/manifestModule.ts +2 -2
- package/src/lib/shared/online.ts +1 -1
- package/src/lib/shared/outboxProbeSlot.ts +1 -1
- package/src/lib/shared/pageUrlForFile.ts +1 -1
- package/src/lib/shared/routeParamsShape.ts +20 -0
- package/src/lib/shared/snapshotShippable.ts +22 -0
- package/src/lib/shared/types/CacheEntry.ts +1 -1
- package/src/lib/shared/types/CacheInvalidation.ts +3 -3
- package/src/lib/shared/types/ClientFlags.ts +5 -3
- package/src/lib/shared/types/LastConnection.ts +1 -1
- package/src/lib/shared/url.ts +4 -1
- package/src/lib/shared/withBase.ts +1 -2
- package/src/lib/shared/writeDts.ts +2 -2
- package/src/lib/shared/writeRoutesDts.ts +3 -31
- package/src/lib/test/createTestSocketChannel.ts +3 -3
- package/src/lib/ui/COMPONENT_WRAPPER_PREFIX.ts +1 -1
- package/src/lib/ui/compile/ABIDE_SEMANTIC_TOKENS_LEGEND.ts +40 -6
- package/src/lib/ui/compile/REACTIVE_CALLEES.ts +1 -1
- package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +49 -37
- package/src/lib/ui/compile/abideUiPlugin.ts +1 -1
- package/src/lib/ui/compile/assertRuntimeHelpersBound.ts +3 -1
- package/src/lib/ui/compile/awaitPlan.ts +14 -2
- package/src/lib/ui/compile/catchBinding.ts +8 -0
- package/src/lib/ui/compile/classStyleMergePlan.ts +108 -0
- package/src/lib/ui/compile/collectAbideDiagnostics.ts +11 -0
- package/src/lib/ui/compile/compileComponent.ts +4 -4
- package/src/lib/ui/compile/compileModule.ts +20 -6
- package/src/lib/ui/compile/compileSSR.ts +12 -10
- package/src/lib/ui/compile/compileShadow.ts +107 -16
- package/src/lib/ui/compile/composeProps.ts +3 -3
- package/src/lib/ui/compile/createShadowLanguageService.ts +30 -2
- package/src/lib/ui/compile/createShadowProgram.ts +2 -1
- package/src/lib/ui/compile/createShadowScope.ts +42 -0
- package/src/lib/ui/compile/decodeHtmlEntities.ts +4 -0
- package/src/lib/ui/compile/desugarSignals.ts +12 -7
- package/src/lib/ui/compile/eachPlan.ts +61 -0
- package/src/lib/ui/compile/elementPlan.ts +55 -0
- package/src/lib/ui/compile/encodeSemanticTokens.ts +24 -1
- package/src/lib/ui/compile/generateBuild.ts +150 -127
- package/src/lib/ui/compile/generateSSR.ts +112 -73
- package/src/lib/ui/compile/ifPlan.ts +7 -1
- package/src/lib/ui/compile/interpolatedTemplateLiteral.ts +25 -0
- package/src/lib/ui/compile/isPlainIdentifier.ts +7 -0
- package/src/lib/ui/compile/isTextLeaf.ts +8 -7
- package/src/lib/ui/compile/lowerContext.ts +36 -46
- package/src/lib/ui/compile/lowerDocAccess.ts +2 -2
- package/src/lib/ui/compile/lowerScript.ts +1 -1
- package/src/lib/ui/compile/markupTokens.ts +313 -0
- package/src/lib/ui/compile/pagePropsType.ts +23 -0
- package/src/lib/ui/compile/parseTemplate.ts +131 -61
- package/src/lib/ui/compile/reactiveBinding.ts +47 -0
- package/src/lib/ui/compile/renameSignalRefs.ts +44 -10
- package/src/lib/ui/compile/skeletonable.ts +5 -5
- package/src/lib/ui/compile/snippetPlan.ts +28 -0
- package/src/lib/ui/compile/spreadExcludedNames.ts +4 -4
- package/src/lib/ui/compile/structuralBlockTokens.ts +3 -4
- package/src/lib/ui/compile/switchPlan.ts +7 -1
- package/src/lib/ui/compile/tryPlan.ts +8 -1
- package/src/lib/ui/compile/types/AnalyzedComponent.ts +2 -2
- package/src/lib/ui/compile/types/Binding.ts +24 -0
- package/src/lib/ui/compile/types/CompiledShadow.ts +13 -1
- package/src/lib/ui/compile/types/ElementPlan.ts +43 -0
- package/src/lib/ui/compile/types/ShadowKind.ts +9 -0
- package/src/lib/ui/compile/types/ShadowScope.ts +19 -0
- package/src/lib/ui/compile/types/TemplateAttr.ts +7 -0
- package/src/lib/ui/compile/types/TemplateNode.ts +8 -8
- package/src/lib/ui/compile/withBindings.ts +45 -0
- package/src/lib/ui/createScope.ts +2 -2
- package/src/lib/ui/dom/MATHML_NAMESPACE.ts +2 -2
- package/src/lib/ui/dom/SVG_NAMESPACE.ts +3 -2
- package/src/lib/ui/dom/appendText.ts +1 -1
- package/src/lib/ui/dom/applyResolved.ts +1 -1
- package/src/lib/ui/dom/buildDetachedRange.ts +4 -3
- package/src/lib/ui/dom/cloneStatic.ts +3 -2
- package/src/lib/ui/dom/each.ts +26 -7
- package/src/lib/ui/dom/foreignWrapperTag.ts +2 -2
- package/src/lib/ui/dom/mountChild.ts +8 -1
- package/src/lib/ui/dom/readCall.ts +6 -4
- package/src/lib/ui/dom/scopeLabel.ts +3 -3
- package/src/lib/ui/dom/switchBlock.ts +3 -1
- package/src/lib/ui/dom/types/EachRow.ts +4 -0
- package/src/lib/ui/dom/types/SkeletonHoles.ts +4 -2
- package/src/lib/ui/history.ts +1 -2
- package/src/lib/ui/html.ts +28 -0
- package/src/lib/ui/installHotBridge.ts +0 -2
- package/src/lib/ui/renderChain.ts +16 -3
- package/src/lib/ui/router.ts +66 -15
- package/src/lib/ui/rpcOutbox/createOutboxQueue.ts +1 -1
- package/src/lib/ui/runtime/CHILD_PRESENT.ts +8 -0
- package/src/lib/ui/runtime/OUTLET_MARKER.ts +2 -2
- package/src/lib/ui/runtime/RANGE_MARKER.ts +1 -1
- package/src/lib/ui/runtime/REACTIVE_CONTEXT.ts +9 -3
- package/src/lib/ui/runtime/captureModelDoc.ts +1 -1
- package/src/lib/ui/runtime/createDoc.ts +5 -1
- package/src/lib/ui/runtime/createEffectNode.ts +5 -5
- package/src/lib/ui/runtime/flushEffects.ts +22 -9
- package/src/lib/ui/runtime/historyEntries.ts +1 -1
- package/src/lib/ui/runtime/track.ts +4 -2
- package/src/lib/ui/runtime/trigger.ts +34 -20
- package/src/lib/ui/runtime/types/Route.ts +2 -2
- package/src/lib/ui/runtime/types/UiComponent.ts +3 -2
- package/src/lib/ui/runtime/types/UiProps.ts +9 -12
- package/src/lib/ui/runtime/walkPath.ts +15 -3
- package/src/lib/ui/startClient.ts +41 -14
- package/src/lib/ui/state.ts +3 -3
- package/src/lib/ui/types/Scope.ts +3 -3
- package/src/serverBuildPlugins.ts +11 -5
- package/src/zodCjsPlugin.ts +36 -0
- package/template/.zed/settings.json +7 -1
- package/template/package.json +3 -0
- package/template/src/ui/pages/layout.abide +3 -5
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
import { decodeHtmlEntities } from './decodeHtmlEntities.ts'
|
|
2
|
+
import { interpolatedTemplateLiteral } from './interpolatedTemplateLiteral.ts'
|
|
2
3
|
import { isWhitespaceText } from './isWhitespaceText.ts'
|
|
3
4
|
import type { TemplateAttr } from './types/TemplateAttr.ts'
|
|
4
5
|
import type { TemplateNode } from './types/TemplateNode.ts'
|
|
5
6
|
import type { TextPart } from './types/TextPart.ts'
|
|
6
7
|
import { VOID_TAGS } from './VOID_TAGS.ts'
|
|
7
8
|
|
|
9
|
+
/* Compiled once — the parser tests these per source character, so a per-call literal
|
|
10
|
+
would recompile them across the whole template scan. */
|
|
11
|
+
const WHITESPACE = /\s/
|
|
12
|
+
const WHITESPACE_OR_GT = /[\s>]/
|
|
13
|
+
const ATTR_NAME_END = /[\s=>/]/
|
|
14
|
+
const TAG_NAME_END = /[\s>/]/
|
|
15
|
+
const UPPERCASE_START = /^[A-Z]/
|
|
16
|
+
const AWAIT_PREFIX = /^await\b/
|
|
17
|
+
const LEADING_KEYWORD = /^\s*(\S+)/
|
|
18
|
+
|
|
8
19
|
/*
|
|
9
20
|
A minimal compile-time parser for the abide template subset: elements, text with
|
|
10
21
|
`{expr}` interpolation, static/`{expr}`/`on<event>={expr}` attributes, and
|
|
11
|
-
|
|
22
|
+
`{#if}/{#for}/{#await}/{#switch}/{#try}` block control flow. Not a full HTML parser — it covers what
|
|
12
23
|
components need and reads brace expressions with quote/nesting awareness so an
|
|
13
24
|
expression containing `<`, `>`, or `}` parses intact. Void elements self-close.
|
|
14
25
|
`<!-- … -->` comments are dropped (no node emitted) so they leave no trace in the
|
|
@@ -73,7 +84,9 @@ export function parseTemplate(source: string, baseOffset = 0): { nodes: Template
|
|
|
73
84
|
/* True when `cursor` is on a `<style>` open tag — read raw (`readStyle`) so its
|
|
74
85
|
CSS braces never misparse as `{expr}` interpolations. */
|
|
75
86
|
function atStyleTag(): boolean {
|
|
76
|
-
return
|
|
87
|
+
return (
|
|
88
|
+
source.startsWith('<style', cursor) && WHITESPACE_OR_GT.test(source.charAt(cursor + 6))
|
|
89
|
+
)
|
|
77
90
|
}
|
|
78
91
|
|
|
79
92
|
/* Reads a `<style>…</style>` block into a `style` node carrying its CSS body; an
|
|
@@ -97,6 +110,19 @@ export function parseTemplate(source: string, baseOffset = 0): { nodes: Template
|
|
|
97
110
|
return source.charAt(cursor) === '{' && source.charAt(cursor + 1) === '#'
|
|
98
111
|
}
|
|
99
112
|
|
|
113
|
+
/* `{children()}` is the slot fill point — the content a parent passed (a component)
|
|
114
|
+
or the route chain below (a layout). It parses to the SAME node the retired `<slot>`
|
|
115
|
+
element produced, so every downstream helper that branches on `tag === 'slot'` is
|
|
116
|
+
unchanged. */
|
|
117
|
+
const CHILDREN_CALL = /^\{\s*children\s*\(\s*\)\s*\}/
|
|
118
|
+
function atChildrenCall(): boolean {
|
|
119
|
+
return source.charAt(cursor) === '{' && CHILDREN_CALL.test(source.slice(cursor))
|
|
120
|
+
}
|
|
121
|
+
function readChildrenCall(): TemplateNode {
|
|
122
|
+
cursor = source.indexOf('}', cursor) + 1 // consume `{children()}`
|
|
123
|
+
return { kind: 'element', tag: 'slot', attrs: [], children: [] }
|
|
124
|
+
}
|
|
125
|
+
|
|
100
126
|
/* Reads a `{#…}` / `{:…}` / `{/…}` token starting on `{`. Tracks string literals
|
|
101
127
|
and nested braces (same scan as readBracedExpression) so a brace/quote inside an
|
|
102
128
|
expression (`{#if obj.has('}')}`) doesn't end the token early. Returns the sigil,
|
|
@@ -132,13 +158,14 @@ export function parseTemplate(source: string, baseOffset = 0): { nodes: Template
|
|
|
132
158
|
/* The leading keyword of a directive body (`if`, `for`, `await`, `switch`, `try`,
|
|
133
159
|
`else`, `then`, `catch`, `finally`, `case`, `default`). */
|
|
134
160
|
function headKeyword(body: string): string {
|
|
135
|
-
const match = body.match(
|
|
161
|
+
const match = body.match(LEADING_KEYWORD)
|
|
136
162
|
return match?.[1] ?? ''
|
|
137
163
|
}
|
|
138
164
|
|
|
139
165
|
/* Reads a `{#…}` control block: the open token, its children up to a continuation
|
|
140
|
-
`{:…}` (a branch) or close `{/…}`, recursing. Emits
|
|
141
|
-
|
|
166
|
+
`{:…}` (a branch) or close `{/…}`, recursing. Emits `if`/`each`/`await`/`switch`/`try`
|
|
167
|
+
nodes with their `case`/`branch` children — the same TemplateNode kinds the back-ends
|
|
168
|
+
and `skeletonContext` consume. */
|
|
142
169
|
function readBlock(): TemplateNode {
|
|
143
170
|
const open = readBlockToken() // sigil is '#'
|
|
144
171
|
const keyword = headKeyword(open.body)
|
|
@@ -207,8 +234,32 @@ export function parseTemplate(source: string, baseOffset = 0): { nodes: Template
|
|
|
207
234
|
const children = readBlockChildren('try')
|
|
208
235
|
return { kind: 'try', children }
|
|
209
236
|
}
|
|
237
|
+
if (keyword === 'snippet') {
|
|
238
|
+
const head = open.body.slice(open.body.indexOf('snippet') + 'snippet'.length).trim()
|
|
239
|
+
const parenAt = head.indexOf('(')
|
|
240
|
+
const name = (parenAt === -1 ? head : head.slice(0, parenAt)).trim()
|
|
241
|
+
if (name === '') {
|
|
242
|
+
throw new Error('[abide] {#snippet} requires a name, e.g. {#snippet row(item)}')
|
|
243
|
+
}
|
|
244
|
+
/* Params ride the parens: `{#snippet row({ item })}` → `{ item }`. */
|
|
245
|
+
const params =
|
|
246
|
+
parenAt === -1
|
|
247
|
+
? undefined
|
|
248
|
+
: head.slice(parenAt + 1, head.lastIndexOf(')')).trim() || undefined
|
|
249
|
+
const children = readBlockChildren('snippet')
|
|
250
|
+
return {
|
|
251
|
+
kind: 'snippet',
|
|
252
|
+
name,
|
|
253
|
+
params,
|
|
254
|
+
children,
|
|
255
|
+
loc:
|
|
256
|
+
parenAt === -1
|
|
257
|
+
? undefined
|
|
258
|
+
: exprLoc(open.loc, open.body, open.body.indexOf('(') + 1),
|
|
259
|
+
}
|
|
260
|
+
}
|
|
210
261
|
throw new Error(
|
|
211
|
-
`[abide] unknown control block {#${keyword}} — expected if/for/await/switch/try`,
|
|
262
|
+
`[abide] unknown control block {#${keyword}} — expected if/for/await/switch/try/snippet`,
|
|
212
263
|
)
|
|
213
264
|
}
|
|
214
265
|
|
|
@@ -227,6 +278,8 @@ export function parseTemplate(source: string, baseOffset = 0): { nodes: Template
|
|
|
227
278
|
}
|
|
228
279
|
if (atBlock()) {
|
|
229
280
|
nodes.push(readBlock())
|
|
281
|
+
} else if (atChildrenCall()) {
|
|
282
|
+
nodes.push(readChildrenCall())
|
|
230
283
|
} else if (source.startsWith('<!--', cursor)) {
|
|
231
284
|
skipComment()
|
|
232
285
|
} else if (atStyleTag()) {
|
|
@@ -243,7 +296,7 @@ export function parseTemplate(source: string, baseOffset = 0): { nodes: Template
|
|
|
243
296
|
`{:…}` ends the current branch's children and starts a new `case`/`branch` node
|
|
244
297
|
(per construct). The leading children (before the first `{:…}`) are the block's
|
|
245
298
|
own children (the `if`/`await`/`try` then-content). Returns the full children list
|
|
246
|
-
INCLUDING the case/branch nodes,
|
|
299
|
+
INCLUDING the case/branch nodes, producing the same TemplateNode tree the back-ends consume. */
|
|
247
300
|
function readBlockChildren(keyword: string): TemplateNode[] {
|
|
248
301
|
const nodes: TemplateNode[] = []
|
|
249
302
|
while (cursor < source.length) {
|
|
@@ -403,6 +456,9 @@ export function parseTemplate(source: string, baseOffset = 0): { nodes: Template
|
|
|
403
456
|
let literal = ''
|
|
404
457
|
while (cursor < source.length && source.charAt(cursor) !== '<') {
|
|
405
458
|
if (source.charAt(cursor) === '{') {
|
|
459
|
+
if (atChildrenCall()) {
|
|
460
|
+
break // a slot fill point — handled by the enclosing scan loop
|
|
461
|
+
}
|
|
406
462
|
const next = source.charAt(cursor + 1)
|
|
407
463
|
if (next === '#' || next === ':' || next === '/') {
|
|
408
464
|
break // a block/continuation/close token — not interpolation
|
|
@@ -427,7 +483,7 @@ export function parseTemplate(source: string, baseOffset = 0): { nodes: Template
|
|
|
427
483
|
function readAttributes(): TemplateAttr[] {
|
|
428
484
|
const attrs: TemplateAttr[] = []
|
|
429
485
|
while (cursor < source.length) {
|
|
430
|
-
while (
|
|
486
|
+
while (WHITESPACE.test(source.charAt(cursor))) {
|
|
431
487
|
cursor += 1
|
|
432
488
|
}
|
|
433
489
|
const char = source.charAt(cursor)
|
|
@@ -454,11 +510,11 @@ export function parseTemplate(source: string, baseOffset = 0): { nodes: Template
|
|
|
454
510
|
}
|
|
455
511
|
let name = ''
|
|
456
512
|
const nameLoc = baseOffset + cursor // absolute offset of the attribute name
|
|
457
|
-
while (cursor < source.length &&
|
|
513
|
+
while (cursor < source.length && !ATTR_NAME_END.test(source.charAt(cursor))) {
|
|
458
514
|
name += source.charAt(cursor)
|
|
459
515
|
cursor += 1
|
|
460
516
|
}
|
|
461
|
-
while (
|
|
517
|
+
while (WHITESPACE.test(source.charAt(cursor))) {
|
|
462
518
|
cursor += 1
|
|
463
519
|
}
|
|
464
520
|
if (source.charAt(cursor) !== '=') {
|
|
@@ -466,7 +522,7 @@ export function parseTemplate(source: string, baseOffset = 0): { nodes: Template
|
|
|
466
522
|
continue
|
|
467
523
|
}
|
|
468
524
|
cursor += 1 // past '='
|
|
469
|
-
while (
|
|
525
|
+
while (WHITESPACE.test(source.charAt(cursor))) {
|
|
470
526
|
cursor += 1
|
|
471
527
|
}
|
|
472
528
|
if (source.charAt(cursor) === '{') {
|
|
@@ -487,18 +543,44 @@ export function parseTemplate(source: string, baseOffset = 0): { nodes: Template
|
|
|
487
543
|
} else if (source.charAt(cursor) === '"' || source.charAt(cursor) === "'") {
|
|
488
544
|
const quote = source.charAt(cursor)
|
|
489
545
|
cursor += 1
|
|
490
|
-
|
|
546
|
+
/* Scan the quoted span into static/`{expr}` parts (the text-node model),
|
|
547
|
+
reusing the brace/quote-aware expression reader so a `}` inside an
|
|
548
|
+
expression doesn't close the value early. */
|
|
549
|
+
const parts: TextPart[] = []
|
|
550
|
+
let literal = ''
|
|
491
551
|
while (cursor < source.length && source.charAt(cursor) !== quote) {
|
|
492
|
-
|
|
493
|
-
|
|
552
|
+
if (source.charAt(cursor) === '{') {
|
|
553
|
+
if (literal !== '') {
|
|
554
|
+
parts.push({ kind: 'static', value: decodeHtmlEntities(literal) })
|
|
555
|
+
literal = ''
|
|
556
|
+
}
|
|
557
|
+
const { code, loc } = readBracedExpression()
|
|
558
|
+
parts.push({ kind: 'expression', code, loc })
|
|
559
|
+
} else {
|
|
560
|
+
literal += source.charAt(cursor)
|
|
561
|
+
cursor += 1
|
|
562
|
+
}
|
|
494
563
|
}
|
|
495
564
|
cursor += 1 // past closing quote
|
|
496
|
-
|
|
565
|
+
if (literal !== '') {
|
|
566
|
+
parts.push({ kind: 'static', value: decodeHtmlEntities(literal) })
|
|
567
|
+
}
|
|
568
|
+
/* No `{expr}` → a plain static attribute; its literal is entity-decoded
|
|
569
|
+
like a text node (SSR/clone re-escape it), so a value such as `&`
|
|
570
|
+
round-trips instead of double-escaping. Otherwise an interpolated value. */
|
|
571
|
+
if (parts.every((part) => part.kind === 'static')) {
|
|
572
|
+
const value = parts
|
|
573
|
+
.map((part) => (part.kind === 'static' ? part.value : ''))
|
|
574
|
+
.join('')
|
|
575
|
+
attrs.push({ kind: 'static', name, value, nameLoc })
|
|
576
|
+
} else {
|
|
577
|
+
attrs.push({ kind: 'interpolated', name, parts, nameLoc })
|
|
578
|
+
}
|
|
497
579
|
} else {
|
|
498
580
|
/* Unquoted value (`<input type=text>`): runs to the next whitespace or
|
|
499
581
|
`>`, per the HTML unquoted-attribute rule. No delimiter to consume. */
|
|
500
582
|
let value = ''
|
|
501
|
-
while (cursor < source.length &&
|
|
583
|
+
while (cursor < source.length && !WHITESPACE_OR_GT.test(source.charAt(cursor))) {
|
|
502
584
|
/* Stop before a `/` that closes the tag (`<Comp x=y/>`) so the value
|
|
503
585
|
doesn't swallow the self-closing slash and defeat detection; a `/`
|
|
504
586
|
elsewhere (e.g. a URL `href=/a/b`) stays part of the value. */
|
|
@@ -518,7 +600,7 @@ export function parseTemplate(source: string, baseOffset = 0): { nodes: Template
|
|
|
518
600
|
cursor += 1 // past '<'
|
|
519
601
|
let tag = ''
|
|
520
602
|
const tagStart = cursor // absolute (baseOffset-relative) offset of the tag name
|
|
521
|
-
while (cursor < source.length &&
|
|
603
|
+
while (cursor < source.length && !TAG_NAME_END.test(source.charAt(cursor))) {
|
|
522
604
|
tag += source.charAt(cursor)
|
|
523
605
|
cursor += 1
|
|
524
606
|
}
|
|
@@ -553,7 +635,7 @@ export function parseTemplate(source: string, baseOffset = 0): { nodes: Template
|
|
|
553
635
|
}
|
|
554
636
|
/* A capitalised tag is a child component; its attributes become props and
|
|
555
637
|
its children become slot content (rendered where the child puts <slot>). */
|
|
556
|
-
if (
|
|
638
|
+
if (UPPERCASE_START.test(tag)) {
|
|
557
639
|
const slotted = selfClosing ? [] : readChildren(tag)
|
|
558
640
|
return {
|
|
559
641
|
kind: 'component',
|
|
@@ -569,6 +651,11 @@ export function parseTemplate(source: string, baseOffset = 0): { nodes: Template
|
|
|
569
651
|
throw new Error('[abide] {...expr} spread is not supported on a <template> directive')
|
|
570
652
|
}
|
|
571
653
|
const children = selfClosing || VOID_TAGS.has(tag) ? [] : readChildren(tag)
|
|
654
|
+
if (tag === 'slot') {
|
|
655
|
+
throw new Error(
|
|
656
|
+
'[abide] the <slot> element was removed — render passed content with {children()} (with {#if children}{children()}{:else}…{/if} for a fallback)',
|
|
657
|
+
)
|
|
658
|
+
}
|
|
572
659
|
if (tag === 'template') {
|
|
573
660
|
return toSnippetOrTemplate(attrs, children)
|
|
574
661
|
}
|
|
@@ -601,6 +688,8 @@ export function parseTemplate(source: string, baseOffset = 0): { nodes: Template
|
|
|
601
688
|
nodes.push(readStyle())
|
|
602
689
|
} else if (atBlock()) {
|
|
603
690
|
nodes.push(readBlock())
|
|
691
|
+
} else if (atChildrenCall()) {
|
|
692
|
+
nodes.push(readChildrenCall())
|
|
604
693
|
} else if (source.charAt(cursor) === '<') {
|
|
605
694
|
nodes.push(readElement())
|
|
606
695
|
} else {
|
|
@@ -619,6 +708,8 @@ export function parseTemplate(source: string, baseOffset = 0): { nodes: Template
|
|
|
619
708
|
roots.push(readStyle())
|
|
620
709
|
} else if (atBlock()) {
|
|
621
710
|
roots.push(readBlock())
|
|
711
|
+
} else if (atChildrenCall()) {
|
|
712
|
+
roots.push(readChildrenCall())
|
|
622
713
|
} else if (source.charAt(cursor) === '<') {
|
|
623
714
|
roots.push(readElement())
|
|
624
715
|
} else {
|
|
@@ -682,8 +773,8 @@ function keywordAtDepthZero(text: string, word: string): number {
|
|
|
682
773
|
depth === 0 &&
|
|
683
774
|
text.startsWith(word, i) &&
|
|
684
775
|
i > 0 &&
|
|
685
|
-
|
|
686
|
-
(i + word.length === text.length ||
|
|
776
|
+
WHITESPACE.test(text.charAt(i - 1)) &&
|
|
777
|
+
(i + word.length === text.length || WHITESPACE.test(text.charAt(i + word.length)))
|
|
687
778
|
) {
|
|
688
779
|
return i
|
|
689
780
|
}
|
|
@@ -740,13 +831,13 @@ function parseForHead(
|
|
|
740
831
|
const leadingSpace = (text: string): number => text.length - text.trimStart().length
|
|
741
832
|
const skipSpace = (i: number): number => {
|
|
742
833
|
let at = i
|
|
743
|
-
while (at < body.length &&
|
|
834
|
+
while (at < body.length && WHITESPACE.test(body.charAt(at))) {
|
|
744
835
|
at += 1
|
|
745
836
|
}
|
|
746
837
|
return at
|
|
747
838
|
}
|
|
748
839
|
let bindingStart = skipSpace(body.indexOf('for') + 3)
|
|
749
|
-
const isAsync =
|
|
840
|
+
const isAsync = AWAIT_PREFIX.test(body.slice(bindingStart))
|
|
750
841
|
if (isAsync) {
|
|
751
842
|
bindingStart = skipSpace(bindingStart + 'await'.length)
|
|
752
843
|
}
|
|
@@ -903,6 +994,15 @@ function toProps(
|
|
|
903
994
|
nameLoc: attr.nameLoc,
|
|
904
995
|
}
|
|
905
996
|
}
|
|
997
|
+
/* `label="hi {name}"` — a string-valued prop, the template-literal concatenation
|
|
998
|
+
of its parts (lowered downstream like any other prop value). */
|
|
999
|
+
if (attr.kind === 'interpolated') {
|
|
1000
|
+
return {
|
|
1001
|
+
name: attr.name,
|
|
1002
|
+
code: interpolatedTemplateLiteral(attr.parts),
|
|
1003
|
+
nameLoc: attr.nameLoc,
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
906
1006
|
/* Every non-static kind keeps its `code`/`loc`; only the prop name differs —
|
|
907
1007
|
a directive (`event`/`bind`/`class`/`style`/`attach`) round-trips to its written
|
|
908
1008
|
name as a passthrough prop. */
|
|
@@ -925,24 +1025,6 @@ function toProps(
|
|
|
925
1025
|
})
|
|
926
1026
|
}
|
|
927
1027
|
|
|
928
|
-
/* The literal text of an attribute (a static value or an expression's code);
|
|
929
|
-
undefined for event/bind attributes, which a directive never is. */
|
|
930
|
-
function attrText(attr: TemplateAttr): string | undefined {
|
|
931
|
-
if (attr.kind === 'static') {
|
|
932
|
-
return attr.value
|
|
933
|
-
}
|
|
934
|
-
if (attr.kind === 'expression') {
|
|
935
|
-
return attr.code
|
|
936
|
-
}
|
|
937
|
-
return undefined
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
/* The source offset of an attribute expression's code (see TextPart.loc); only
|
|
941
|
-
expression attributes carry one — a static value isn't a checkable expression. */
|
|
942
|
-
function attrLoc(attr: TemplateAttr | undefined): number | undefined {
|
|
943
|
-
return attr !== undefined && attr.kind === 'expression' ? attr.loc : undefined
|
|
944
|
-
}
|
|
945
|
-
|
|
946
1028
|
/* The attribute's source name (`on<event>`/`bind:<property>` reconstructed). */
|
|
947
1029
|
function attrName(attr: TemplateAttr): string {
|
|
948
1030
|
if (attr.kind === 'event') {
|
|
@@ -970,7 +1052,6 @@ function attrName(attr: TemplateAttr): string {
|
|
|
970
1052
|
return attr.name
|
|
971
1053
|
}
|
|
972
1054
|
|
|
973
|
-
/* Turns a `<template>` directive into a control node (if/each/await + then/catch). */
|
|
974
1055
|
/* The control-flow attribute names that used to drive `<template>` directives — now
|
|
975
1056
|
moved to `{#…}` blocks. A `<template>` carrying one is a migration error. */
|
|
976
1057
|
const CONTROL_DIRECTIVES = new Set([
|
|
@@ -988,10 +1069,9 @@ const CONTROL_DIRECTIVES = new Set([
|
|
|
988
1069
|
'try',
|
|
989
1070
|
])
|
|
990
1071
|
|
|
991
|
-
/* A `<template>` is now ONLY a
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
makes the element callable; without it, it stays an inert reusable fragment. */
|
|
1072
|
+
/* A `<template>` is now ONLY a plain inert element (e.g. client-side cloning).
|
|
1073
|
+
Snippet declarations moved to `{#snippet name(args)}…{/snippet}` blocks; control
|
|
1074
|
+
flow moved to `{#…}` blocks — both throw migration errors here. */
|
|
995
1075
|
function toSnippetOrTemplate(attrs: TemplateAttr[], children: TemplateNode[]): TemplateNode {
|
|
996
1076
|
const find = (name: string) => attrs.find((attr) => attrName(attr) === name)
|
|
997
1077
|
const directive = attrs.find((attr) => CONTROL_DIRECTIVES.has(attrName(attr)))
|
|
@@ -1002,22 +1082,12 @@ function toSnippetOrTemplate(attrs: TemplateAttr[], children: TemplateNode[]): T
|
|
|
1002
1082
|
`[abide] <template ${name}> control flow was removed — use the {#${block}…} block instead`,
|
|
1003
1083
|
)
|
|
1004
1084
|
}
|
|
1005
|
-
/* `<template name
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
throw new Error('[abide] <template name> requires a snippet name')
|
|
1012
|
-
}
|
|
1013
|
-
const params = find('args')
|
|
1014
|
-
return {
|
|
1015
|
-
kind: 'snippet',
|
|
1016
|
-
name,
|
|
1017
|
-
params: params === undefined ? undefined : attrText(params),
|
|
1018
|
-
children,
|
|
1019
|
-
loc: attrLoc(params),
|
|
1020
|
-
}
|
|
1085
|
+
/* `<template name>` snippet declarations were retired for the `{#snippet name(args)}`
|
|
1086
|
+
block — reject with a migration error. */
|
|
1087
|
+
if (find('name') !== undefined) {
|
|
1088
|
+
throw new Error(
|
|
1089
|
+
'[abide] <template name> snippet declarations were removed — use a {#snippet name(args)}…{/snippet} block',
|
|
1090
|
+
)
|
|
1021
1091
|
}
|
|
1022
1092
|
/* A plain inert `<template>` element (e.g. client-side cloning) — keep as an element. */
|
|
1023
1093
|
return { kind: 'element', tag: 'template', attrs, children }
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { destructureBindingNames } from './destructureBindingNames.ts'
|
|
2
|
+
import { isPlainIdentifier } from './isPlainIdentifier.ts'
|
|
3
|
+
import type { Binding } from './types/Binding.ts'
|
|
4
|
+
|
|
5
|
+
/* How the client back-end binds a `reactive` Binding (an `await` `then` value, a keyed
|
|
6
|
+
`each` item / index): the value arrives as a `.value` cell the runtime can update in
|
|
7
|
+
place, so the consuming branch/row re-runs in place on a re-settle/re-key rather than
|
|
8
|
+
being rebuilt. THE RENDERER of a `reactive` classification — it takes the Binding the
|
|
9
|
+
plan declared, never a raw author param, so a back-end can only render what the plan
|
|
10
|
+
classified. `param` is the thunk's value parameter (the cell), `prefix` declares any
|
|
11
|
+
per-leaf readers, `localNames` (the Binding's leaf names) enter the deref scope for the
|
|
12
|
+
body. A plain identifier reads the cell directly (`item` → `item.value`); a destructure
|
|
13
|
+
re-applies over the cell per read so each leaf stays reactive (JS handles
|
|
14
|
+
defaults/rest/rename/nesting). */
|
|
15
|
+
export type ReactiveBindingWiring = {
|
|
16
|
+
param: string
|
|
17
|
+
prefix: string
|
|
18
|
+
localNames: string[]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/* Renders a `reactive` Binding's cell wiring for the client back-end. `nextVar` mints the
|
|
22
|
+
synthetic cell/derive vars; `lowerStatement` lowers the destructure declaration so a
|
|
23
|
+
default/computed-key initializer referencing a component signal is rewritten to its
|
|
24
|
+
`model`/cell form (the bound leaf names are name-slots, untouched). */
|
|
25
|
+
export function reactiveBinding(
|
|
26
|
+
binding: Binding,
|
|
27
|
+
nextVar: (hint: string) => string,
|
|
28
|
+
lowerStatement: (code: string) => string,
|
|
29
|
+
): ReactiveBindingWiring {
|
|
30
|
+
const authorParam = binding.name
|
|
31
|
+
if (isPlainIdentifier(authorParam)) {
|
|
32
|
+
return { param: authorParam, prefix: '', localNames: [authorParam] }
|
|
33
|
+
}
|
|
34
|
+
const cellParam = nextVar('aw')
|
|
35
|
+
const deriveVar = nextVar('ad')
|
|
36
|
+
const leaves = destructureBindingNames(authorParam)
|
|
37
|
+
const declaration = lowerStatement(`const ${authorParam} = ${cellParam}.value`)
|
|
38
|
+
const prefix =
|
|
39
|
+
`const ${deriveVar} = { get value() { ${declaration} return { ${leaves.join(', ')} }; } };\n` +
|
|
40
|
+
leaves
|
|
41
|
+
.map(
|
|
42
|
+
(leaf) =>
|
|
43
|
+
`const ${leaf} = { get value() { return ${deriveVar}.value.${leaf}; } };\n`,
|
|
44
|
+
)
|
|
45
|
+
.join('')
|
|
46
|
+
return { param: cellParam, prefix, localNames: leaves }
|
|
47
|
+
}
|
|
@@ -3,12 +3,12 @@ import { TS_PRINTER } from './TS_PRINTER.ts'
|
|
|
3
3
|
|
|
4
4
|
/*
|
|
5
5
|
Rewrites references to a component's signal bindings into the document form the
|
|
6
|
-
rest of the pipeline understands: a `state` binding `count` becomes
|
|
6
|
+
rest of the pipeline understands: a `state` binding `count` becomes `$$model.count`
|
|
7
7
|
(data access `lowerDocAccess` then lowers to a patch/read), and a `computed`
|
|
8
8
|
binding `total` becomes `total.value`. Only value-position identifiers are
|
|
9
9
|
touched — declaration names, parameter names, and property names are collected
|
|
10
10
|
into a skip set first, and object shorthand (`{ count }`) is expanded to
|
|
11
|
-
`{ count: model.count }`. This is the bridge from the signal surface the author
|
|
11
|
+
`{ count: $$model.count }`. This is the bridge from the signal surface the author
|
|
12
12
|
writes to the patch substrate underneath.
|
|
13
13
|
|
|
14
14
|
The rewrite is lexically scope-aware: a reference whose name is re-bound by an
|
|
@@ -37,7 +37,7 @@ export function renameSignalRefs(
|
|
|
37
37
|
The signal-rewrite as a `ts.TransformerFactory`, so the script pipeline can chain it
|
|
38
38
|
with `docAccessTransformer` over a SINGLE parsed tree (see `lowerScript`) instead of
|
|
39
39
|
print-then-reparse between passes. `renameSignalRefs` is the standalone string wrapper
|
|
40
|
-
kept for
|
|
40
|
+
kept for test and one-off callers.
|
|
41
41
|
*/
|
|
42
42
|
export function signalRefsTransformer(
|
|
43
43
|
stateNames: ReadonlySet<string>,
|
|
@@ -47,6 +47,12 @@ export function signalRefsTransformer(
|
|
|
47
47
|
`<script>` declaration) — a nearer lexical scope than the component signals, so a
|
|
48
48
|
name here shadows a same-named `state`/`computed`/`derived` and derefs as a cell. */
|
|
49
49
|
blockLocal: ReadonlySet<string> = new Set(),
|
|
50
|
+
/* Block-local PLAIN bindings — an SSR `await` `then` value bound as a real JS local
|
|
51
|
+
holding the plain resolved value, not a reactive cell. Like `blockLocal` it shadows
|
|
52
|
+
a same-named component signal (it is a nearer lexical scope), but it derefs as the
|
|
53
|
+
bare identifier, not `.value`: SSR declares `const foo = <resolved>`, so a reference
|
|
54
|
+
reads the local directly. Seeded into the root shadow set so it is left untouched. */
|
|
55
|
+
blockLocalPlain: ReadonlySet<string> = new Set(),
|
|
50
56
|
): ts.TransformerFactory<ts.SourceFile> {
|
|
51
57
|
/* The signal names that a nested binding can shadow — only these matter for
|
|
52
58
|
scope tracking, so we ignore every other local binding. */
|
|
@@ -55,6 +61,16 @@ export function signalRefsTransformer(
|
|
|
55
61
|
...derivedNames,
|
|
56
62
|
...computedNames,
|
|
57
63
|
...blockLocal,
|
|
64
|
+
/* The reserved slot reader (`{#if children}` → `$props?.$children`) is rewritten
|
|
65
|
+
through `referenceFor` like a signal, so it inherits the same lexical-scope
|
|
66
|
+
tracking: a `{#snippet row(children)}` arg, a `{#for children of …}` item, a
|
|
67
|
+
script param named `children` re-binds it for that subtree and is left untouched. */
|
|
68
|
+
'children',
|
|
69
|
+
/* `scope` is the author-facing reactive entry (`scope().state(...)`), lowered to the
|
|
70
|
+
reserved `$$scope` import so a user variable named `scope` can never collide — but
|
|
71
|
+
shadowably: a `const scope`/param `scope` re-binds it for that subtree and is left
|
|
72
|
+
untouched, exactly like a signal name. */
|
|
73
|
+
'scope',
|
|
58
74
|
])
|
|
59
75
|
return (context) => (root) => {
|
|
60
76
|
/* The identifier nodes that are names, not value reads — collected by walking
|
|
@@ -89,7 +105,7 @@ export function signalRefsTransformer(
|
|
|
89
105
|
if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) {
|
|
90
106
|
return node
|
|
91
107
|
}
|
|
92
|
-
/* Shorthand `{ count }` → `{ count: model.count }` / `{ total: total.value }`,
|
|
108
|
+
/* Shorthand `{ count }` → `{ count: $$model.count }` / `{ total: total.value }`,
|
|
93
109
|
unless a nearer scope shadows the name. */
|
|
94
110
|
if (ts.isShorthandPropertyAssignment(node) && !shadowed.has(node.name.text)) {
|
|
95
111
|
const replacement = referenceFor(
|
|
@@ -126,7 +142,7 @@ export function signalRefsTransformer(
|
|
|
126
142
|
}
|
|
127
143
|
return visit
|
|
128
144
|
}
|
|
129
|
-
return ts.visitNode(root, makeVisitor(new Set())) as ts.SourceFile
|
|
145
|
+
return ts.visitNode(root, makeVisitor(new Set(blockLocalPlain))) as ts.SourceFile
|
|
130
146
|
}
|
|
131
147
|
}
|
|
132
148
|
|
|
@@ -314,11 +330,12 @@ function functionParameters(node: ts.Node): ts.NodeArray<ts.ParameterDeclaration
|
|
|
314
330
|
return undefined
|
|
315
331
|
}
|
|
316
332
|
|
|
317
|
-
/*
|
|
333
|
+
/* `$$model.<name>` for a state binding, `<name>()` for a computed doc-slot (the
|
|
318
334
|
string-free reader `scope().derive` returns), `<name>.value` for a runtime cell
|
|
319
|
-
(linked / lens / transform-state),
|
|
320
|
-
any same-named component signal — it is
|
|
321
|
-
cell (`<name>.value`) regardless of a
|
|
335
|
+
(linked / lens / transform-state), `$props?.$children` for the reserved slot reader,
|
|
336
|
+
else undefined. A `blockLocal` binding shadows any same-named component signal — it is
|
|
337
|
+
a nearer lexical scope — so it derefs as a cell (`<name>.value`) regardless of a
|
|
338
|
+
colliding `state`/`computed`/`derived`, and ahead of the `children` slot reader. */
|
|
322
339
|
function referenceFor(
|
|
323
340
|
name: string,
|
|
324
341
|
stateNames: ReadonlySet<string>,
|
|
@@ -329,8 +346,25 @@ function referenceFor(
|
|
|
329
346
|
if (blockLocal.has(name)) {
|
|
330
347
|
return ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier(name), 'value')
|
|
331
348
|
}
|
|
349
|
+
/* `{children()}` is a slot NODE the parser handles; any other bare `children` read
|
|
350
|
+
(notably `{#if children}`) is the reserved reader. `?.` guards a propless mount. */
|
|
351
|
+
if (name === 'children') {
|
|
352
|
+
return ts.factory.createPropertyAccessChain(
|
|
353
|
+
ts.factory.createIdentifier('$props'),
|
|
354
|
+
ts.factory.createToken(ts.SyntaxKind.QuestionDotToken),
|
|
355
|
+
ts.factory.createIdentifier('$children'),
|
|
356
|
+
)
|
|
357
|
+
}
|
|
358
|
+
/* The author-facing reactive entry `scope` lowers to its reserved `$$scope` import (the
|
|
359
|
+
value read only — a `.scope` member or a shadowing local is never reached here). */
|
|
360
|
+
if (name === 'scope') {
|
|
361
|
+
return ts.factory.createIdentifier('$$scope')
|
|
362
|
+
}
|
|
332
363
|
if (stateNames.has(name)) {
|
|
333
|
-
return ts.factory.createPropertyAccessExpression(
|
|
364
|
+
return ts.factory.createPropertyAccessExpression(
|
|
365
|
+
ts.factory.createIdentifier('$$model'),
|
|
366
|
+
name,
|
|
367
|
+
)
|
|
334
368
|
}
|
|
335
369
|
if (computedNames.has(name)) {
|
|
336
370
|
return ts.factory.createCallExpression(ts.factory.createIdentifier(name), undefined, [])
|
|
@@ -4,11 +4,11 @@ import type { TemplateNode } from './types/TemplateNode.ts'
|
|
|
4
4
|
|
|
5
5
|
/* Whether a subtree is skeleton STRUCTURE — anything the parser-backed clone can carry with
|
|
6
6
|
its holes mounted in place: elements, `<style>`, text (static OR reactive), child
|
|
7
|
-
components (
|
|
8
|
-
`<slot>` outlets (each anchor-mounted at its position), and emit-nothing nodes
|
|
9
|
-
`<script>` (a scoped reactive block) and a
|
|
10
|
-
both of which the skeleton's bind list runs in document order, scoped like
|
|
11
|
-
`
|
|
7
|
+
components (anchor-positioned via an `<!--a-->` marker in the skeleton), control-flow
|
|
8
|
+
blocks and `<slot>` outlets (each anchor-mounted at its position), and emit-nothing nodes
|
|
9
|
+
— a nested `<script>` (a scoped reactive block) and a `{#snippet}` block (a hoisted
|
|
10
|
+
builder), both of which the skeleton's bind list runs in document order, scoped like
|
|
11
|
+
`generateChild` does. Only a standalone `branch`/`case` (consumed by its block, never a
|
|
12
12
|
loose child) disqualifies. */
|
|
13
13
|
function skeletonStructure(node: TemplateNode): boolean {
|
|
14
14
|
if (isControlFlow(node) || node.kind === 'component') {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Binding } from './types/Binding.ts'
|
|
2
|
+
import type { TemplateNode } from './types/TemplateNode.ts'
|
|
3
|
+
|
|
4
|
+
/* The structural shape of a `snippet` declaration (`<template name="row" args={item}>`),
|
|
5
|
+
resolved once so the build and SSR back-ends share one reading of it and only own emission.
|
|
6
|
+
Both previously read the name, the raw `args` source, and the body straight off the node.
|
|
7
|
+
The `args` params are now classified ONCE here as `bindings` (`plain` — real call
|
|
8
|
+
parameters, not cells), the single source both back-ends register through `withBindings`. */
|
|
9
|
+
export type SnippetPlan = {
|
|
10
|
+
/* The snippet's name — the hoisted builder function's identifier and call name. */
|
|
11
|
+
name: string
|
|
12
|
+
/* The raw `args` source spliced into the builder's parameter list, or undefined. */
|
|
13
|
+
params: string | undefined
|
|
14
|
+
/* The snippet body. */
|
|
15
|
+
children: TemplateNode[]
|
|
16
|
+
/* The body's bindings: the `args` params (`plain`); empty when the snippet takes none. */
|
|
17
|
+
bindings: Binding[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/* Reads a `snippet` node's structure into the shared structural plan. */
|
|
21
|
+
export function snippetPlan(node: Extract<TemplateNode, { kind: 'snippet' }>): SnippetPlan {
|
|
22
|
+
return {
|
|
23
|
+
name: node.name,
|
|
24
|
+
params: node.params,
|
|
25
|
+
children: node.children,
|
|
26
|
+
bindings: node.params === undefined ? [] : [{ name: node.params, classification: 'plain' }],
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -7,13 +7,13 @@ both back-ends pass this set to skip those keys from the spread — keeping the
|
|
|
7
7
|
server string and the client DOM congruent (no duplicate attribute, and no
|
|
8
8
|
SSR-first-wins vs client-spread-wins divergence). Excluding by NAME (not by a
|
|
9
9
|
runtime present/absent check) means the two sides agree regardless of a dynamic
|
|
10
|
-
attribute's runtime value. Static
|
|
11
|
-
name, a `bind:` its property, an
|
|
12
|
-
carry no attribute name.
|
|
10
|
+
attribute's runtime value. Static, dynamic (`{expr}`), and interpolated
|
|
11
|
+
(`"… {expr} …"`) attrs contribute their name, a `bind:` its property, an
|
|
12
|
+
`on<event>` its `on…` form; `attach`/`spread` carry no attribute name.
|
|
13
13
|
*/
|
|
14
14
|
export function spreadExcludedNames(attrs: TemplateAttr[]): string[] {
|
|
15
15
|
return attrs.flatMap((attr) => {
|
|
16
|
-
if (attr.kind === 'static' || attr.kind === 'expression') {
|
|
16
|
+
if (attr.kind === 'static' || attr.kind === 'expression' || attr.kind === 'interpolated') {
|
|
17
17
|
return [attr.name]
|
|
18
18
|
}
|
|
19
19
|
if (attr.kind === 'bind') {
|
|
@@ -6,10 +6,9 @@ HTML grammar sees only as text and the shadow program lowers away. A pure scan o
|
|
|
6
6
|
raw source (independent of a successful parse, so it survives mid-edit), it emits
|
|
7
7
|
an `operator` token for the `{`+sigil opener and a `keyword` token for the block
|
|
8
8
|
word, plus the `of`/`by` connectors inside a `{#for …}` head. Expression interiors
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
and `else if` beats `else`.
|
|
9
|
+
and bare `{expr}` interpolations are NOT touched here — those are the shadow's job.
|
|
10
|
+
A keyword allowlist after the sigil prevents matching arbitrary `{:foo}` text.
|
|
11
|
+
Longest phrases first so `for await` beats `for` and `else if` beats `else`.
|
|
13
12
|
*/
|
|
14
13
|
const BLOCK_KEYWORDS = [
|
|
15
14
|
'for await',
|
|
@@ -1,15 +1,20 @@
|
|
|
1
|
+
import type { Binding } from './types/Binding.ts'
|
|
1
2
|
import type { TemplateNode } from './types/TemplateNode.ts'
|
|
2
3
|
|
|
3
4
|
type Case = Extract<TemplateNode, { kind: 'case' }>
|
|
4
5
|
|
|
5
6
|
/* The structural shape of a `switch`, resolved once so the build and SSR back-ends share one
|
|
6
7
|
reading of its cases and only own emission. Both previously recomputed the `case` filter;
|
|
7
|
-
SSR additionally located the match-less default.
|
|
8
|
+
SSR additionally located the match-less default. A `switch` introduces no names, so
|
|
9
|
+
`bindings` is empty — present so every block plan answers the single binding source
|
|
10
|
+
uniformly. */
|
|
8
11
|
export type SwitchPlan = {
|
|
9
12
|
/* The `case` branches in source order (`match` set; the default leaves it unset). */
|
|
10
13
|
cases: Case[]
|
|
11
14
|
/* The match-less default branch, if present. */
|
|
12
15
|
fallback: Case | undefined
|
|
16
|
+
/* A `switch` binds no names. */
|
|
17
|
+
bindings: Binding[]
|
|
13
18
|
}
|
|
14
19
|
|
|
15
20
|
/* Resolves a `switch` node's cases into the shared structural plan. */
|
|
@@ -18,5 +23,6 @@ export function switchPlan(node: Extract<TemplateNode, { kind: 'switch' }>): Swi
|
|
|
18
23
|
return {
|
|
19
24
|
cases,
|
|
20
25
|
fallback: cases.find((branch) => branch.match === undefined),
|
|
26
|
+
bindings: [],
|
|
21
27
|
}
|
|
22
28
|
}
|