@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,3 +1,4 @@
|
|
|
1
|
+
import { assertExhaustive } from '../../shared/assertExhaustive.ts'
|
|
1
2
|
import { OUTLET_CLOSE, OUTLET_OPEN } from '../runtime/OUTLET_MARKER.ts'
|
|
2
3
|
import { OUTLET_TAG } from '../runtime/OUTLET_TAG.ts'
|
|
3
4
|
import { asOutlet } from './asOutlet.ts'
|
|
@@ -5,6 +6,7 @@ import { composeProps } from './composeProps.ts'
|
|
|
5
6
|
import { groupBindParts } from './groupBindParts.ts'
|
|
6
7
|
import { isAnchorPositioned } from './isAnchorPositioned.ts'
|
|
7
8
|
import { lowerContext } from './lowerContext.ts'
|
|
9
|
+
import { resolveBranches } from './resolveBranches.ts'
|
|
8
10
|
import { scopeAttr } from './scopeAttr.ts'
|
|
9
11
|
import { skeletonContext } from './skeletonContext.ts'
|
|
10
12
|
import { spreadExcludedNames } from './spreadExcludedNames.ts'
|
|
@@ -21,17 +23,6 @@ import { VOID_TAGS } from './VOID_TAGS.ts'
|
|
|
21
23
|
const RANGE_OPEN = '<!--[-->'
|
|
22
24
|
const RANGE_CLOSE = '<!--]-->'
|
|
23
25
|
|
|
24
|
-
/* The `then`/`catch`/`finally` branch child of an await/try block, or undefined. */
|
|
25
|
-
function branchNamed(
|
|
26
|
-
children: TemplateNode[],
|
|
27
|
-
which: 'then' | 'catch' | 'finally',
|
|
28
|
-
): Extract<TemplateNode, { kind: 'branch' }> | undefined {
|
|
29
|
-
return children.find(
|
|
30
|
-
(child): child is Extract<TemplateNode, { kind: 'branch' }> =>
|
|
31
|
-
child.kind === 'branch' && child.branch === which,
|
|
32
|
-
)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
26
|
/*
|
|
36
27
|
Server code generator: turns the parsed template into statements that push HTML
|
|
37
28
|
fragments onto an output array, reading the document synchronously (no DOM, no
|
|
@@ -212,11 +203,19 @@ export function generateSSR(
|
|
|
212
203
|
.join('')
|
|
213
204
|
}
|
|
214
205
|
if (node.kind === 'if') {
|
|
215
|
-
|
|
206
|
+
/* `case` children are the `elseif`/`else` branches in source order; the rest are
|
|
207
|
+
the `then` content. Each `elseif` becomes an `else if`, the match-less `else`
|
|
208
|
+
the trailing `else`. */
|
|
209
|
+
const branches = node.children.filter(
|
|
210
|
+
(child): child is Extract<TemplateNode, { kind: 'case' }> => child.kind === 'case',
|
|
211
|
+
)
|
|
216
212
|
const thenChildren = node.children.filter((child) => child.kind !== 'case')
|
|
217
213
|
let code = `if (${lowerExpression(node.condition)}) {\n${branchContent(thenChildren, target)}}`
|
|
218
|
-
|
|
219
|
-
code +=
|
|
214
|
+
for (const branch of branches) {
|
|
215
|
+
code +=
|
|
216
|
+
branch.condition !== undefined
|
|
217
|
+
? ` else if (${lowerExpression(branch.condition)}) {\n${branchContent(branch.children, target)}}`
|
|
218
|
+
: ` else {\n${branchContent(branch.children, target)}}`
|
|
220
219
|
}
|
|
221
220
|
return `${anchor}${openRange(target)}${code}\n${closeRange(target)}`
|
|
222
221
|
}
|
|
@@ -343,6 +342,12 @@ export function generateSSR(
|
|
|
343
342
|
}
|
|
344
343
|
return generateSlot(node, target, anchor)
|
|
345
344
|
}
|
|
345
|
+
/* Every non-element kind returned above; a kind reaching the element builder
|
|
346
|
+
that isn't an element is a compiler gap — fail loud rather than emit it as a
|
|
347
|
+
bogus `<undefined>` tag. (`node` narrows to `never` in this branch.) */
|
|
348
|
+
if (node.kind !== 'element') {
|
|
349
|
+
return assertExhaustive(node, 'template node kind')
|
|
350
|
+
}
|
|
346
351
|
let code = push(target, `<${node.tag}`)
|
|
347
352
|
/* Every `<style>` active at this element (own siblings + ancestors) — same set
|
|
348
353
|
the client stamps, so server and client markup carry identical attributes. */
|
|
@@ -442,8 +447,8 @@ export function generateSSR(
|
|
|
442
447
|
node: Extract<TemplateNode, { kind: 'await' }>,
|
|
443
448
|
target: string,
|
|
444
449
|
): string {
|
|
445
|
-
const catchBranch =
|
|
446
|
-
const finallyChildren =
|
|
450
|
+
const [catchBranch, finallyBranch] = resolveBranches(node, 'catch', 'finally')
|
|
451
|
+
const finallyChildren = finallyBranch?.children ?? []
|
|
447
452
|
const resolvedChildren = node.children.filter((child) => child.kind !== 'branch')
|
|
448
453
|
const resolvedAs = node.as ?? '_value'
|
|
449
454
|
const errorAs = catchBranch?.as ?? '_error'
|
|
@@ -480,9 +485,13 @@ export function generateSSR(
|
|
|
480
485
|
node: Extract<TemplateNode, { kind: 'await' }>,
|
|
481
486
|
target: string,
|
|
482
487
|
): string {
|
|
483
|
-
const catchBranch =
|
|
484
|
-
|
|
485
|
-
|
|
488
|
+
const [thenBranch, catchBranch, finallyBranch] = resolveBranches(
|
|
489
|
+
node,
|
|
490
|
+
'then',
|
|
491
|
+
'catch',
|
|
492
|
+
'finally',
|
|
493
|
+
)
|
|
494
|
+
const finallyChildren = finallyBranch?.children ?? []
|
|
486
495
|
const pending = node.children.filter((child) => child.kind !== 'branch')
|
|
487
496
|
const id = nextVar('$aid')
|
|
488
497
|
let code = `const ${id} = $ctx.next++;\n`
|
|
@@ -510,8 +519,8 @@ export function generateSSR(
|
|
|
510
519
|
an enclosing boundary / the 500 / the stream). Boundary comments let hydration
|
|
511
520
|
discard the server content if the client adoption fails. */
|
|
512
521
|
function generateTry(node: Extract<TemplateNode, { kind: 'try' }>, target: string): string {
|
|
513
|
-
const catchBranch =
|
|
514
|
-
const finallyChildren =
|
|
522
|
+
const [catchBranch, finallyBranch] = resolveBranches(node, 'catch', 'finally')
|
|
523
|
+
const finallyChildren = finallyBranch?.children ?? []
|
|
515
524
|
const guarded = node.children.filter((child) => child.kind !== 'branch')
|
|
516
525
|
const errName = catchBranch?.as ?? '_error'
|
|
517
526
|
const id = nextVar('$tid')
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import ts from 'typescript'
|
|
2
|
+
import { TS_PRINTER } from './TS_PRINTER.ts'
|
|
2
3
|
|
|
3
4
|
/*
|
|
4
5
|
The perf pass that follows `lowerDocAccess`. Within a render scope, a static path
|
|
@@ -35,8 +36,7 @@ export function hoistCells(code: string, docName: string): string {
|
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
const result = ts.transform(source, [hoistTransformer(docName, cellIdForPath)])
|
|
38
|
-
const
|
|
39
|
-
const output = printer.printFile(result.transformed[0] as ts.SourceFile)
|
|
39
|
+
const output = TS_PRINTER.printFile(result.transformed[0] as ts.SourceFile)
|
|
40
40
|
result.dispose()
|
|
41
41
|
return output
|
|
42
42
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { TemplateNode } from './types/TemplateNode.ts'
|
|
2
|
+
|
|
3
|
+
/* A text node that is purely whitespace (no interpolation, only blank static
|
|
4
|
+
parts). Both back-ends drop it, so it neither contributes markup nor breaks a
|
|
5
|
+
static clone run — it stays transparent so `<a/>\n<b/>` still coalesces. */
|
|
6
|
+
export function isWhitespaceText(node: TemplateNode): boolean {
|
|
7
|
+
return (
|
|
8
|
+
node.kind === 'text' &&
|
|
9
|
+
node.parts.every((part) => part.kind === 'static' && part.value.trim() === '')
|
|
10
|
+
)
|
|
11
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import ts from 'typescript'
|
|
2
|
+
import { docAccessTransformer } from './lowerDocAccess.ts'
|
|
2
3
|
import { nestedBindingNames } from './prepareNestedScript.ts'
|
|
3
|
-
import {
|
|
4
|
+
import { signalRefsTransformer } from './renameSignalRefs.ts'
|
|
5
|
+
import { TS_PRINTER } from './TS_PRINTER.ts'
|
|
4
6
|
import type { TemplateNode } from './types/TemplateNode.ts'
|
|
5
7
|
import { unwrapParens } from './unwrapParens.ts'
|
|
6
8
|
|
|
@@ -27,19 +29,30 @@ export function lowerContext(
|
|
|
27
29
|
const derefScope = (): ReadonlySet<string> =>
|
|
28
30
|
localDerived.size === 0 ? derivedNames : new Set([...derivedNames, ...localDerived])
|
|
29
31
|
|
|
30
|
-
/*
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
/* Parse `code` once and chain the reference rename and doc-access lowering over the
|
|
33
|
+
one tree — the two string passes would each parse + reprint. `derefScope()` is read
|
|
34
|
+
per call so a nested-`<script>` binding pushed mid-compile is honoured. */
|
|
35
|
+
function lowerOnce(code: string): string {
|
|
36
|
+
const source = ts.createSourceFile('expr.ts', code, ts.ScriptTarget.Latest, true)
|
|
37
|
+
const result = ts.transform(source, [
|
|
38
|
+
signalRefsTransformer(stateNames, derefScope(), computedNames),
|
|
39
|
+
docAccessTransformer('model'),
|
|
40
|
+
])
|
|
41
|
+
const output = TS_PRINTER.printFile(result.transformed[0] as ts.SourceFile).trim()
|
|
42
|
+
result.dispose()
|
|
43
|
+
return output
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* Lowers a single expression (no trailing `;`). Wrapped in parens so a bare object
|
|
47
|
+
literal (`{ a: 1 }`) parses as an expression, not a block of labeled statements,
|
|
48
|
+
through the rewrite; the wrapper is then peeled back off. */
|
|
34
49
|
function expression(code: string): string {
|
|
35
|
-
|
|
36
|
-
return unwrapParens(lowerDocAccess(renamed, 'model').trim().replace(/;$/, ''))
|
|
50
|
+
return unwrapParens(lowerOnce(`(${code})`).replace(/;$/, ''))
|
|
37
51
|
}
|
|
38
52
|
|
|
39
53
|
/* As above but keeps the trailing `;` for a statement/handler body. */
|
|
40
54
|
function statement(code: string): string {
|
|
41
|
-
|
|
42
|
-
return lowerDocAccess(renamed, 'model').trim()
|
|
55
|
+
return lowerOnce(code)
|
|
43
56
|
}
|
|
44
57
|
|
|
45
58
|
/* A two-way bind target is either an LVALUE (`count`, `model.lines[i]`) — reads as
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import ts from 'typescript'
|
|
2
2
|
import { escapeKey } from '../runtime/escapeKey.ts'
|
|
3
|
+
import { TS_PRINTER } from './TS_PRINTER.ts'
|
|
3
4
|
|
|
4
5
|
/*
|
|
5
6
|
The linchpin compiler pass. Rewrites idiomatic data access on a reactive document
|
|
@@ -27,8 +28,7 @@ used as an index lowers too.
|
|
|
27
28
|
export function lowerDocAccess(code: string, docName: string): string {
|
|
28
29
|
const source = ts.createSourceFile('component.ts', code, ts.ScriptTarget.Latest, true)
|
|
29
30
|
const result = ts.transform(source, [docAccessTransformer(docName)])
|
|
30
|
-
const
|
|
31
|
-
const output = printer.printFile(result.transformed[0] as ts.SourceFile)
|
|
31
|
+
const output = TS_PRINTER.printFile(result.transformed[0] as ts.SourceFile)
|
|
32
32
|
result.dispose()
|
|
33
33
|
return output
|
|
34
34
|
}
|
|
@@ -49,7 +49,7 @@ const COMPOUND_OPERATORS = new Map<ts.SyntaxKind, ts.BinaryOperator>([
|
|
|
49
49
|
[ts.SyntaxKind.QuestionQuestionEqualsToken, ts.SyntaxKind.QuestionQuestionToken],
|
|
50
50
|
])
|
|
51
51
|
|
|
52
|
-
function docAccessTransformer(docName: string): ts.TransformerFactory<ts.SourceFile> {
|
|
52
|
+
export function docAccessTransformer(docName: string): ts.TransformerFactory<ts.SourceFile> {
|
|
53
53
|
return (context) => (root) => {
|
|
54
54
|
/* Collects the path of an access chain rooted at `docName`, visiting any
|
|
55
55
|
dynamic index so reads inside it lower too. undefined if not our doc. */
|
|
@@ -107,6 +107,32 @@ function docAccessTransformer(docName: string): ts.TransformerFactory<ts.SourceF
|
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
|
+
/* `path++` / `++path` / `path--` on a doc path → a replace patch (the same
|
|
111
|
+
shape as `+= 1`). A bare `++` would otherwise survive onto the lowered read
|
|
112
|
+
(`model.read("n")++` / `cell.get()++`) — invalid, since a call result is not
|
|
113
|
+
an lvalue. abide handlers are statements, so the postfix/prefix value is
|
|
114
|
+
discarded and both lower identically. Only `++`/`--` are handled here; other
|
|
115
|
+
unary operators (`!path`, `-path`) fall through so their operand lowers as a
|
|
116
|
+
normal read. */
|
|
117
|
+
if (
|
|
118
|
+
(ts.isPostfixUnaryExpression(node) || ts.isPrefixUnaryExpression(node)) &&
|
|
119
|
+
(node.operator === ts.SyntaxKind.PlusPlusToken ||
|
|
120
|
+
node.operator === ts.SyntaxKind.MinusMinusToken)
|
|
121
|
+
) {
|
|
122
|
+
const segments = pathSegments(node.operand)
|
|
123
|
+
if (segments) {
|
|
124
|
+
const step =
|
|
125
|
+
node.operator === ts.SyntaxKind.PlusPlusToken
|
|
126
|
+
? ts.SyntaxKind.PlusToken
|
|
127
|
+
: ts.SyntaxKind.MinusToken
|
|
128
|
+
const next = ts.factory.createBinaryExpression(
|
|
129
|
+
docCall(docName, 'read', [buildPath(segments)]),
|
|
130
|
+
step,
|
|
131
|
+
ts.factory.createNumericLiteral(1),
|
|
132
|
+
)
|
|
133
|
+
return docCall(docName, 'replace', [buildPath(segments), next])
|
|
134
|
+
}
|
|
135
|
+
}
|
|
110
136
|
/* doc array `.push(a, b, …)` → one `add` patch per argument at the array's `-`
|
|
111
137
|
slot, matching native multi-arg push (each lands at the end in order). */
|
|
112
138
|
if (
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import ts from 'typescript'
|
|
2
|
+
import { assertTranspiles } from './assertTranspiles.ts'
|
|
3
|
+
import { desugarSignals } from './desugarSignals.ts'
|
|
4
|
+
import { docAccessTransformer } from './lowerDocAccess.ts'
|
|
5
|
+
import { signalRefsTransformer } from './renameSignalRefs.ts'
|
|
6
|
+
import { stripEffectsTransformer } from './stripEffects.ts'
|
|
7
|
+
import { TS_PRINTER } from './TS_PRINTER.ts'
|
|
8
|
+
|
|
9
|
+
/*
|
|
10
|
+
The component-script lowering, done in ONE parse. The script is parsed once, then
|
|
11
|
+
`desugarSignals` (signal declarations → `model` slots / `scope().derive`), reference
|
|
12
|
+
renaming (`count` → `model.count`), and doc-access lowering (`model.count` → patch/read)
|
|
13
|
+
run as a chained `ts.transform` over the SAME tree — each as a standalone string pass
|
|
14
|
+
would parse + reprint. desugar returns a transformer rather than rebuilt source text
|
|
15
|
+
precisely so it can chain here; the name sets it collects feed the rename transformer.
|
|
16
|
+
|
|
17
|
+
Imports are partitioned off the transformed tree structurally (`ts.isImportDeclaration`),
|
|
18
|
+
not by regex, so a multi-line import hoists correctly regardless of formatting. The
|
|
19
|
+
reassembled output is transpiled as a fail-loud guard: if a future un-handled rewrite
|
|
20
|
+
position corrupts the script (the failure mode the syntax fuzz corpus also guards), it
|
|
21
|
+
surfaces here as a located compile error instead of shipping a broken bundle.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
export function lowerScript(scriptBody: string): {
|
|
25
|
+
body: string
|
|
26
|
+
imports: string
|
|
27
|
+
ssrBody: string
|
|
28
|
+
stateNames: Set<string>
|
|
29
|
+
derivedNames: Set<string>
|
|
30
|
+
computedNames: Set<string>
|
|
31
|
+
} {
|
|
32
|
+
const source = ts.createSourceFile('component.ts', scriptBody, ts.ScriptTarget.Latest, true)
|
|
33
|
+
const { transformer, stateNames, derivedNames, computedNames } = desugarSignals(source)
|
|
34
|
+
const result = ts.transform(source, [
|
|
35
|
+
transformer,
|
|
36
|
+
signalRefsTransformer(stateNames, derivedNames, computedNames),
|
|
37
|
+
docAccessTransformer('model'),
|
|
38
|
+
])
|
|
39
|
+
const transformed = result.transformed[0] as ts.SourceFile
|
|
40
|
+
/* Top-level imports must live at module scope, not inside the mount/render
|
|
41
|
+
function the script body becomes — hoist them off the tree. */
|
|
42
|
+
const importStatements = transformed.statements.filter(ts.isImportDeclaration)
|
|
43
|
+
const bodyStatements = transformed.statements.filter(
|
|
44
|
+
(statement) => !ts.isImportDeclaration(statement),
|
|
45
|
+
)
|
|
46
|
+
const imports = importStatements
|
|
47
|
+
.map((statement) => TS_PRINTER.printNode(ts.EmitHint.Unspecified, statement, transformed))
|
|
48
|
+
.join('\n')
|
|
49
|
+
const bodyFile = ts.factory.updateSourceFile(transformed, bodyStatements)
|
|
50
|
+
const body = TS_PRINTER.printFile(bodyFile).trim()
|
|
51
|
+
/* The SSR variant strips client-only `effect(...)` calls — run over the SAME lowered
|
|
52
|
+
tree (one extra transform + print, no reparse) instead of re-parsing the printed
|
|
53
|
+
script downstream. */
|
|
54
|
+
const ssrResult = ts.transform(bodyFile, [stripEffectsTransformer()])
|
|
55
|
+
const ssrBody = TS_PRINTER.printFile(ssrResult.transformed[0] as ts.SourceFile).trim()
|
|
56
|
+
ssrResult.dispose()
|
|
57
|
+
result.dispose()
|
|
58
|
+
|
|
59
|
+
assertTranspiles(
|
|
60
|
+
[imports, body].filter((part) => part !== '').join('\n'),
|
|
61
|
+
'component script lowering',
|
|
62
|
+
)
|
|
63
|
+
return { body, imports, ssrBody, stateNames, derivedNames, computedNames }
|
|
64
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { decodeHtmlEntities } from './decodeHtmlEntities.ts'
|
|
2
|
+
import { isWhitespaceText } from './isWhitespaceText.ts'
|
|
2
3
|
import type { TemplateAttr } from './types/TemplateAttr.ts'
|
|
3
4
|
import type { TemplateNode } from './types/TemplateNode.ts'
|
|
4
5
|
import type { TextPart } from './types/TextPart.ts'
|
|
@@ -303,6 +304,13 @@ function rejectStrayBranches(
|
|
|
303
304
|
'[abide] <template else>/<template case> must be nested inside its <template if>/<template switch> — a sibling branch is not supported',
|
|
304
305
|
)
|
|
305
306
|
}
|
|
307
|
+
/* `elseif` is an `if`-chain branch; inside a `switch` it would silently read as the
|
|
308
|
+
default (match-less), so reject the cross-construct mix. */
|
|
309
|
+
if (node.kind === 'case' && node.condition !== undefined && parentKind === 'switch') {
|
|
310
|
+
throw new Error(
|
|
311
|
+
'[abide] <template elseif> belongs to a <template if> chain, not a <template switch> — use <template case>',
|
|
312
|
+
)
|
|
313
|
+
}
|
|
306
314
|
if (
|
|
307
315
|
node.kind === 'branch' &&
|
|
308
316
|
parentKind !== 'await' &&
|
|
@@ -313,12 +321,62 @@ function rejectStrayBranches(
|
|
|
313
321
|
'[abide] <template then>/<template catch>/<template finally> must be nested inside its <template await>/<template try>/<template each await>',
|
|
314
322
|
)
|
|
315
323
|
}
|
|
324
|
+
if (node.kind === 'if' || node.kind === 'switch') {
|
|
325
|
+
rejectMisplacedBranchContent(node)
|
|
326
|
+
}
|
|
316
327
|
if ('children' in node) {
|
|
317
328
|
rejectStrayBranches(node.children, node.kind)
|
|
318
329
|
}
|
|
319
330
|
}
|
|
320
331
|
}
|
|
321
332
|
|
|
333
|
+
/* A `<template if>` chain's `then` content precedes its first branch tag
|
|
334
|
+
(`elseif`/`else`); a `<template switch>` renders only its branch tags. So rendered
|
|
335
|
+
content sitting AFTER the first branch in an `if` — or ANYWHERE in a `switch` —
|
|
336
|
+
belongs to no branch: today it silently folds into `then` / is dropped. Reject it so
|
|
337
|
+
the misplacement surfaces. Whitespace stays transparent, and `<script>`/`<style>` are
|
|
338
|
+
directives (scoping, not rendered output), so both remain legal anywhere. */
|
|
339
|
+
function rejectMisplacedBranchContent(
|
|
340
|
+
node: Extract<TemplateNode, { kind: 'if' | 'switch' }>,
|
|
341
|
+
): void {
|
|
342
|
+
const firstBranch = node.children.findIndex((child) => child.kind === 'case')
|
|
343
|
+
node.children.forEach((child, index) => {
|
|
344
|
+
const isRenderedContent =
|
|
345
|
+
child.kind !== 'case' &&
|
|
346
|
+
child.kind !== 'script' &&
|
|
347
|
+
child.kind !== 'style' &&
|
|
348
|
+
!isWhitespaceText(child)
|
|
349
|
+
/* In a switch nothing but branches renders, so every position is illegal; in an if
|
|
350
|
+
only content past the first branch is (the leading then-content is legal). */
|
|
351
|
+
const illegalPosition =
|
|
352
|
+
node.kind === 'switch' || (firstBranch !== -1 && index > firstBranch)
|
|
353
|
+
if (isRenderedContent && illegalPosition) {
|
|
354
|
+
throw new Error(
|
|
355
|
+
node.kind === 'switch'
|
|
356
|
+
? '[abide] a <template switch> renders only its <template case>/<template default> branches — move stray content into a branch'
|
|
357
|
+
: '[abide] content after a <template elseif>/<template else> belongs to no branch — the then-content must precede the first branch tag',
|
|
358
|
+
)
|
|
359
|
+
}
|
|
360
|
+
})
|
|
361
|
+
/* In an `if` chain the match-less `<template else>` is the trailing block, so any
|
|
362
|
+
branch after it (a second `else`, or an `elseif`) compiles to invalid `} else {…}
|
|
363
|
+
else if {…}` (SSR/type-shadow) or a silently-shadowed branch (the `switchBlock`
|
|
364
|
+
default wins). Reject so the misordering surfaces here, not as opaque codegen. */
|
|
365
|
+
if (node.kind === 'if') {
|
|
366
|
+
const branches = node.children.filter(
|
|
367
|
+
(child): child is Extract<TemplateNode, { kind: 'case' }> => child.kind === 'case',
|
|
368
|
+
)
|
|
369
|
+
const elseIndex = branches.findIndex(
|
|
370
|
+
(branch) => branch.match === undefined && branch.condition === undefined,
|
|
371
|
+
)
|
|
372
|
+
if (elseIndex !== -1 && elseIndex < branches.length - 1) {
|
|
373
|
+
throw new Error(
|
|
374
|
+
'[abide] <template else> must be the last branch of its <template if> chain — no <template elseif>/<template else> may follow it',
|
|
375
|
+
)
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
322
380
|
/* Turns a component's attributes into props. A component has no directives —
|
|
323
381
|
every attribute is a prop under its written name, so `on*`/`bind:`/`attach`
|
|
324
382
|
round-trip to their original names (the kinds the tag-blind attribute parser
|
|
@@ -468,6 +526,22 @@ function toControlFlow(attrs: TemplateAttr[], children: TemplateNode[]): Templat
|
|
|
468
526
|
}
|
|
469
527
|
return { kind: 'case', match: matchCode, children, loc: attrLoc(caseAttr) }
|
|
470
528
|
}
|
|
529
|
+
/* `<template elseif={c}>` is a match-less case carrying a condition — a branch of the
|
|
530
|
+
enclosing `<template if>` chain, truthy-tested in source order. */
|
|
531
|
+
const elseif = find('elseif')
|
|
532
|
+
if (elseif !== undefined) {
|
|
533
|
+
const conditionCode = attrText(elseif)
|
|
534
|
+
if (conditionCode === undefined) {
|
|
535
|
+
throw new Error('[abide] <template elseif> requires a condition expression')
|
|
536
|
+
}
|
|
537
|
+
return {
|
|
538
|
+
kind: 'case',
|
|
539
|
+
match: undefined,
|
|
540
|
+
condition: conditionCode,
|
|
541
|
+
children,
|
|
542
|
+
loc: attrLoc(elseif),
|
|
543
|
+
}
|
|
544
|
+
}
|
|
471
545
|
if (find('default') !== undefined || find('else') !== undefined) {
|
|
472
546
|
return { kind: 'case', match: undefined, children } // default (switch) / else (if)
|
|
473
547
|
}
|