@abide/abide 0.42.0 → 0.43.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 +234 -300
- package/CHANGELOG.md +41 -0
- package/README.md +50 -93
- package/package.json +3 -2
- package/src/abideResolverPlugin.ts +104 -14
- package/src/buildCli.ts +1 -1
- package/src/discoveryEntry.ts +3 -3
- package/src/lib/bundle/BundleMenuItem.ts +1 -1
- package/src/lib/cli/createClient.ts +12 -12
- package/src/lib/cli/dispatchCommand.ts +1 -1
- package/src/lib/cli/printSessionStatus.ts +1 -1
- package/src/lib/cli/resolveCliTarget.ts +1 -1
- package/src/lib/cli/runCli.ts +1 -1
- package/src/lib/cli/types/CliManifest.ts +1 -1
- package/src/lib/cli/types/CliManifestEntry.ts +2 -2
- package/src/lib/mcp/annotationsForMethod.ts +5 -5
- package/src/lib/mcp/createMcpServer.ts +3 -3
- package/src/lib/mcp/mcpSurface.ts +14 -14
- package/src/lib/mcp/types/McpServerOptions.ts +1 -1
- package/src/lib/server/DELETE.ts +4 -4
- package/src/lib/server/GET.ts +4 -4
- package/src/lib/server/HEAD.ts +4 -4
- package/src/lib/server/PATCH.ts +4 -4
- package/src/lib/server/POST.ts +4 -4
- package/src/lib/server/PUT.ts +4 -4
- package/src/lib/server/agent.ts +4 -4
- package/src/lib/server/env.ts +1 -1
- package/src/lib/server/error.ts +49 -7
- package/src/lib/server/json.ts +1 -1
- package/src/lib/server/rpc/buildErrorConstructors.ts +19 -0
- package/src/lib/server/rpc/{defineVerb.ts → defineRpc.ts} +52 -34
- package/src/lib/server/rpc/{dispatchVerbInProcess.ts → dispatchRpcInProcess.ts} +3 -3
- package/src/lib/server/rpc/fieldErrorsFromIssues.ts +23 -0
- package/src/lib/server/rpc/{findVerbByCommandName.ts → findRpcByCommandName.ts} +5 -5
- package/src/lib/server/rpc/parseArgs.ts +6 -6
- package/src/lib/server/rpc/readBodyWithinLimit.ts +2 -2
- package/src/lib/server/rpc/registerRpc.ts +6 -0
- package/src/lib/server/rpc/{verbRegistry.ts → rpcRegistry.ts} +4 -4
- package/src/lib/server/rpc/{runWithVerbTimeout.ts → runWithRpcTimeout.ts} +4 -4
- package/src/lib/server/rpc/types/RemoteHandler.ts +9 -3
- package/src/lib/server/rpc/types/{VerbHelper.ts → RpcHelper.ts} +57 -23
- package/src/lib/server/rpc/types/{VerbRegistryEntry.ts → RpcRegistryEntry.ts} +3 -3
- package/src/lib/server/rpc/types/TypedResponse.ts +2 -2
- package/src/lib/server/rpc/unprocessed.ts +6 -7
- package/src/lib/server/rpc/validationError.ts +17 -0
- package/src/lib/server/runtime/buildInspectorSurface.ts +7 -7
- package/src/lib/server/runtime/buildOpenApiSpec.ts +6 -6
- package/src/lib/server/runtime/createRouteDispatcher.ts +7 -7
- package/src/lib/server/runtime/createServer.ts +4 -4
- package/src/lib/server/runtime/crossOriginForbidden.ts +1 -1
- package/src/lib/server/runtime/crossOriginGate.ts +4 -4
- package/src/lib/server/runtime/logExposedSurfaces.ts +4 -4
- package/src/lib/server/runtime/registryManifests.ts +2 -2
- package/src/lib/server/runtime/types/InspectorCacheEntry.ts +1 -1
- package/src/lib/server/runtime/types/InspectorRpc.ts +27 -0
- package/src/lib/server/runtime/types/InspectorSurface.ts +4 -4
- package/src/lib/server/runtime/types/RequestStore.ts +1 -1
- package/src/lib/server/runtime/warnUnguardedMcp.ts +2 -2
- package/src/lib/server/sockets/createSocketDispatcher.ts +15 -14
- package/src/lib/server/sockets/types/SocketOperation.ts +2 -2
- package/src/lib/shared/HttpError.ts +15 -1
- package/src/lib/shared/UNREACHABLE_STATUSES.ts +13 -0
- package/src/lib/shared/buildRpcRequest.ts +5 -5
- package/src/lib/shared/cache.ts +50 -168
- package/src/lib/shared/carriesBodyArgs.ts +4 -4
- package/src/lib/shared/createCacheStore.ts +12 -0
- package/src/lib/shared/createRemoteFunction.ts +11 -6
- package/src/lib/shared/decodeResponse.ts +2 -2
- package/src/lib/shared/detectRpcMethod.ts +17 -0
- package/src/lib/shared/emitLogRecord.ts +3 -3
- package/src/lib/shared/extraForwardHeaders.ts +1 -1
- package/src/lib/shared/findExportCallSite.ts +30 -273
- package/src/lib/shared/forwardHeaders.ts +2 -2
- package/src/lib/shared/httpErrorFor.ts +26 -0
- package/src/lib/shared/isAsciiWhitespace.ts +5 -0
- package/src/lib/shared/isIdentPart.ts +9 -0
- package/src/lib/shared/isIdentStart.ts +8 -0
- package/src/lib/shared/isReadOnlyMethod.ts +4 -4
- package/src/lib/shared/keyForRemoteCall.ts +4 -4
- package/src/lib/shared/log.ts +1 -1
- package/src/lib/shared/outboxProbeSlot.ts +20 -0
- package/src/lib/shared/pending.ts +4 -1
- package/src/lib/shared/prepareRpcModule.ts +96 -14
- package/src/lib/shared/probeRegistries.ts +17 -4
- package/src/lib/shared/queryStringFromArgs.ts +1 -1
- package/src/lib/shared/remoteMetaStore.ts +2 -2
- package/src/lib/shared/resolveClientFlags.ts +1 -1
- package/src/lib/shared/skipNonCode.ts +230 -0
- package/src/lib/shared/streamResponse.ts +9 -6
- package/src/lib/shared/stripImport.ts +3 -3
- package/src/lib/shared/types/CacheEntry.ts +2 -4
- package/src/lib/shared/types/CacheOnContext.ts +1 -11
- package/src/lib/shared/types/CacheStore.ts +13 -7
- package/src/lib/shared/types/ClientFlags.ts +1 -1
- package/src/lib/shared/types/ErrorConstructors.ts +17 -0
- package/src/lib/shared/types/ErrorDescriptor.ts +10 -0
- package/src/lib/shared/types/ErrorSpec.ts +11 -0
- package/src/lib/shared/types/HttpMethod.ts +1 -0
- package/src/lib/shared/types/Outbox.ts +9 -0
- package/src/lib/shared/types/OutboxEntry.ts +27 -0
- package/src/lib/shared/types/RawRemoteFunction.ts +2 -2
- package/src/lib/shared/types/RemoteCallable.ts +2 -2
- package/src/lib/shared/types/RemoteFunction.ts +21 -6
- package/src/lib/shared/types/RequestScopeInfo.ts +1 -1
- package/src/lib/shared/types/RpcErrorGuard.ts +40 -0
- package/src/lib/shared/types/RpcInvoker.ts +1 -1
- package/src/lib/shared/types/StandardSchemaV1.ts +1 -1
- package/src/lib/shared/types/ValidationErrorData.ts +20 -0
- package/src/lib/shared/url.ts +3 -3
- package/src/lib/shared/writeRpcDts.ts +7 -7
- package/src/lib/shared/writeTestRpcDts.ts +3 -3
- package/src/lib/test/createTestApp.ts +10 -10
- package/src/lib/ui/compile/awaitPlan.ts +48 -0
- package/src/lib/ui/compile/compileShadow.ts +89 -21
- package/src/lib/ui/compile/desugarSignals.ts +42 -6
- package/src/lib/ui/compile/generateBuild.ts +43 -60
- package/src/lib/ui/compile/generateSSR.ts +84 -54
- package/src/lib/ui/compile/ifPlan.ts +30 -0
- package/src/lib/ui/compile/parseTemplate.ts +41 -11
- package/src/lib/ui/compile/stripEffects.ts +17 -9
- package/src/lib/ui/compile/switchPlan.ts +22 -0
- package/src/lib/ui/compile/tryPlan.ts +29 -0
- package/src/lib/ui/compile/types/TemplateAttr.ts +8 -2
- package/src/lib/ui/compile/types/TemplateNode.ts +6 -2
- package/src/lib/ui/createScope.ts +20 -2
- package/src/lib/ui/dom/disposeRange.ts +6 -10
- package/src/lib/ui/dom/hydrate.ts +2 -5
- package/src/lib/ui/dom/mount.ts +1 -2
- package/src/lib/ui/dom/withScope.ts +6 -9
- package/src/lib/ui/effect.ts +4 -2
- package/src/lib/ui/navigate.ts +40 -13
- package/src/lib/ui/outbox.ts +30 -110
- package/src/lib/ui/remoteProxy.ts +160 -10
- package/src/lib/ui/router.ts +45 -15
- package/src/lib/ui/rpcOutbox/createOutboxQueue.ts +281 -0
- package/src/lib/ui/rpcOutbox/outboxRegistry.ts +69 -0
- package/src/lib/ui/socketChannel.ts +7 -1
- package/src/lib/ui/types/Scope.ts +20 -5
- package/template/CLAUDE.md +1 -1
- package/template/src/server/rpc/getHello.ts +3 -3
- package/template/test/app.test.ts +2 -2
- package/src/lib/server/rpc/registerVerb.ts +0 -6
- package/src/lib/server/runtime/types/InspectorVerb.ts +0 -27
- package/src/lib/shared/detectVerbMethod.ts +0 -17
- package/src/lib/shared/types/HttpVerb.ts +0 -1
- package/src/lib/ui/types/Outbox.ts +0 -14
|
@@ -4,20 +4,56 @@ import { REACTIVE_CALLEES } from './REACTIVE_CALLEES.ts'
|
|
|
4
4
|
const factory = ts.factory
|
|
5
5
|
|
|
6
6
|
/* The reactive primitives that must be reached through a scope. A bare call to one of
|
|
7
|
-
these is a compile error: reactive
|
|
8
|
-
it (`scope().state(...)`), so a reader always sees the scope interaction.
|
|
9
|
-
|
|
7
|
+
these is a compile error: a reactive primitive is owned by a scope and the surface must
|
|
8
|
+
show it (`scope().state(...)`), so a reader always sees the scope interaction. `effect`
|
|
9
|
+
is here too — a reaction is scope-owned (it tears down with the scope), so it joins the
|
|
10
|
+
one surface; unlike the cells it stays a runtime call (`scope().effect(...)` passes
|
|
11
|
+
through to the `effect` helper), not a doc slot. */
|
|
12
|
+
const SCOPE_PRIMITIVES: ReadonlySet<string> = new Set(['state', 'linked', 'computed', 'effect'])
|
|
13
|
+
|
|
14
|
+
/* The primitive names a top-level `const { state, computed } = scope()` destructure binds.
|
|
15
|
+
Such a name is scope-bound — its bare call below is the destructured method, not a stray
|
|
16
|
+
global — so it is exempt from the bare-primitive error. Only a destructure of a `scope()`
|
|
17
|
+
call counts (receiver-agnostic on the callee name, matching signalCallee); an aliased
|
|
18
|
+
binding (`{ state: s }`) is not recognised, so the canonical name must be kept. */
|
|
19
|
+
function scopeDestructuredPrimitives(source: ts.SourceFile): Set<string> {
|
|
20
|
+
const bound = new Set<string>()
|
|
21
|
+
for (const statement of source.statements) {
|
|
22
|
+
if (!ts.isVariableStatement(statement)) {
|
|
23
|
+
continue
|
|
24
|
+
}
|
|
25
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
26
|
+
if (
|
|
27
|
+
signalCallee(declaration) === 'scope' &&
|
|
28
|
+
ts.isObjectBindingPattern(declaration.name)
|
|
29
|
+
) {
|
|
30
|
+
for (const element of declaration.name.elements) {
|
|
31
|
+
if (
|
|
32
|
+
element.propertyName === undefined &&
|
|
33
|
+
ts.isIdentifier(element.name) &&
|
|
34
|
+
SCOPE_PRIMITIVES.has(element.name.text)
|
|
35
|
+
) {
|
|
36
|
+
bound.add(element.name.text)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return bound
|
|
43
|
+
}
|
|
10
44
|
|
|
11
45
|
/* Throws on a bare scope primitive (`state(0)` instead of `scope().state(0)`) or on the
|
|
12
46
|
removed `prop(...)` reader — props are now read by destructuring `props()`. Walks all
|
|
13
|
-
calls, so a stray one nested in a function is caught too, not just top-level declarations.
|
|
47
|
+
calls, so a stray one nested in a function is caught too, not just top-level declarations.
|
|
48
|
+
A primitive destructured from `scope()` at the top is scope-bound and exempt. */
|
|
14
49
|
function assertScopedPrimitives(source: ts.SourceFile): void {
|
|
50
|
+
const scopeBound = scopeDestructuredPrimitives(source)
|
|
15
51
|
const visit = (node: ts.Node): void => {
|
|
16
52
|
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
|
|
17
53
|
const name = node.expression.text
|
|
18
|
-
if (SCOPE_PRIMITIVES.has(name)) {
|
|
54
|
+
if (SCOPE_PRIMITIVES.has(name) && !scopeBound.has(name)) {
|
|
19
55
|
throw new Error(
|
|
20
|
-
`abide: bare \`${name}(...)\` is not allowed — reactive
|
|
56
|
+
`abide: bare \`${name}(...)\` is not allowed — a reactive primitive lives on a scope. Use \`scope().${name}(...)\` (or a captured handle: \`const s = scope(); s.${name}(...)\`).`,
|
|
21
57
|
)
|
|
22
58
|
}
|
|
23
59
|
if (name === 'prop') {
|
|
@@ -3,10 +3,12 @@ import { HOLE_ATTRIBUTE } from '../runtime/HOLE_ATTRIBUTE.ts'
|
|
|
3
3
|
import { OUTLET_TAG } from '../runtime/OUTLET_TAG.ts'
|
|
4
4
|
import { ANCHOR } from '../runtime/RANGE_MARKER.ts'
|
|
5
5
|
import { asOutlet } from './asOutlet.ts'
|
|
6
|
+
import { awaitPlan } from './awaitPlan.ts'
|
|
6
7
|
import { bindListenEvent } from './bindListenEvent.ts'
|
|
7
8
|
import { composeProps } from './composeProps.ts'
|
|
8
9
|
import { destructureBindingNames } from './destructureBindingNames.ts'
|
|
9
10
|
import { groupBindParts } from './groupBindParts.ts'
|
|
11
|
+
import { ifPlan } from './ifPlan.ts'
|
|
10
12
|
import { isControlFlow } from './isControlFlow.ts'
|
|
11
13
|
import { isWhitespaceText } from './isWhitespaceText.ts'
|
|
12
14
|
import { lowerContext } from './lowerContext.ts'
|
|
@@ -17,6 +19,8 @@ import { skeletonContext } from './skeletonContext.ts'
|
|
|
17
19
|
import { spreadExcludedNames } from './spreadExcludedNames.ts'
|
|
18
20
|
import { staticAttr } from './staticAttr.ts'
|
|
19
21
|
import { staticTextPart } from './staticTextPart.ts'
|
|
22
|
+
import { switchPlan } from './switchPlan.ts'
|
|
23
|
+
import { tryPlan } from './tryPlan.ts'
|
|
20
24
|
import type { TemplateNode } from './types/TemplateNode.ts'
|
|
21
25
|
import { VOID_TAGS } from './VOID_TAGS.ts'
|
|
22
26
|
|
|
@@ -143,7 +147,7 @@ export function generateBuild(
|
|
|
143
147
|
node: Extract<TemplateNode, { kind: 'element' }>,
|
|
144
148
|
attr: Extract<
|
|
145
149
|
(typeof node.attrs)[number],
|
|
146
|
-
{ kind: 'expression' | 'event' | 'attach' | 'bind' }
|
|
150
|
+
{ kind: 'expression' | 'event' | 'attach' | 'bind' | 'class' | 'style' }
|
|
147
151
|
>,
|
|
148
152
|
varName: string,
|
|
149
153
|
): string {
|
|
@@ -156,6 +160,15 @@ export function generateBuild(
|
|
|
156
160
|
if (attr.kind === 'attach') {
|
|
157
161
|
return `attach(${varName}, (${lowerExpression(attr.code)}));\n`
|
|
158
162
|
}
|
|
163
|
+
/* `class:<name>` — toggle the class by truthiness; surgical, no element re-render.
|
|
164
|
+
Layers on top of any static `class="…"` in the skeleton (classList is additive). */
|
|
165
|
+
if (attr.kind === 'class') {
|
|
166
|
+
return `effect(${namedThunk(`class_${attr.name}`, `${varName}.classList.toggle(${JSON.stringify(attr.name)}, !!(${lowerExpression(attr.code)}));`)});\n`
|
|
167
|
+
}
|
|
168
|
+
/* `style:<property>` — write one inline style / custom property reactively. */
|
|
169
|
+
if (attr.kind === 'style') {
|
|
170
|
+
return `effect(${namedThunk(`style_${attr.property}`, `${varName}.style.setProperty(${JSON.stringify(attr.property)}, String(${lowerExpression(attr.code)}));`)});\n`
|
|
171
|
+
}
|
|
159
172
|
if (attr.property === 'group') {
|
|
160
173
|
/* Grouped two-way: radio binds the path to the single checked `value`;
|
|
161
174
|
checkbox treats the path as an array, adding/removing `value` on toggle.
|
|
@@ -471,11 +484,8 @@ export function generateBuild(
|
|
|
471
484
|
parentVar: string,
|
|
472
485
|
before: string,
|
|
473
486
|
): string {
|
|
474
|
-
const cases = node
|
|
475
|
-
.
|
|
476
|
-
(child): child is Extract<TemplateNode, { kind: 'case' }> => child.kind === 'case',
|
|
477
|
-
)
|
|
478
|
-
.map((branch) => {
|
|
487
|
+
const cases = switchPlan(node)
|
|
488
|
+
.cases.map((branch) => {
|
|
479
489
|
const match =
|
|
480
490
|
branch.match === undefined
|
|
481
491
|
? 'undefined'
|
|
@@ -534,45 +544,26 @@ export function generateBuild(
|
|
|
534
544
|
parentVar: string,
|
|
535
545
|
before: string,
|
|
536
546
|
): string {
|
|
537
|
-
const
|
|
538
|
-
node,
|
|
539
|
-
'then',
|
|
540
|
-
'catch',
|
|
541
|
-
'finally',
|
|
542
|
-
)
|
|
543
|
-
const finallyChildren = finallyBranch?.children ?? []
|
|
544
|
-
/* Blocking: no pending, the children are the resolved branch bound to `node.as`.
|
|
545
|
-
Streaming: pending is the non-branch children, resolved is the `then` child. */
|
|
546
|
-
const pending = node.blocking
|
|
547
|
-
? []
|
|
548
|
-
: node.children.filter((child) => child.kind !== 'branch')
|
|
547
|
+
const plan = awaitPlan(node)
|
|
549
548
|
/* The resolved value is reactive: a re-settle updates it in place rather than
|
|
550
|
-
rebuilding the branch (see awaitBlock). The branch reads it as a `.value` cell.
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
thenBranch?.children ?? [],
|
|
560
|
-
thenBranch?.as ?? '_value',
|
|
561
|
-
finallyChildren,
|
|
562
|
-
true,
|
|
563
|
-
)
|
|
549
|
+
rebuilding the branch (see awaitBlock). The branch reads it as a `.value` cell.
|
|
550
|
+
Build settles blocking and streaming uniformly through `awaitBlock`, so the plan's
|
|
551
|
+
resolved content/binding feeds one thunk regardless of mode. */
|
|
552
|
+
const thenThunk = branchThunk(
|
|
553
|
+
plan.resolvedChildren,
|
|
554
|
+
plan.resolvedAs,
|
|
555
|
+
plan.finallyChildren,
|
|
556
|
+
true,
|
|
557
|
+
)
|
|
564
558
|
/* Neither catch nor finally → pass `undefined` so awaitBlock re-throws the
|
|
565
559
|
rejection (surfacing it) instead of rendering an empty branch. A finally-only
|
|
566
560
|
block keeps a catch thunk that renders just finally. */
|
|
567
|
-
const catchThunk =
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
finallyChildren,
|
|
574
|
-
)
|
|
575
|
-
const pendingThunk = hasRenderableContent(pending) ? branchThunk(pending) : 'undefined'
|
|
561
|
+
const catchThunk = plan.surfaceRejection
|
|
562
|
+
? 'undefined'
|
|
563
|
+
: branchThunk(plan.catchChildren, plan.catchAs, plan.finallyChildren)
|
|
564
|
+
const pendingThunk = hasRenderableContent(plan.pending)
|
|
565
|
+
? branchThunk(plan.pending)
|
|
566
|
+
: 'undefined'
|
|
576
567
|
return (
|
|
577
568
|
`awaitBlock(${parentVar}, nextBlockId(), () => (${lowerExpression(node.promise)}), ` +
|
|
578
569
|
`${pendingThunk}, ` +
|
|
@@ -638,14 +629,11 @@ export function generateBuild(
|
|
|
638
629
|
parentVar: string,
|
|
639
630
|
before: string,
|
|
640
631
|
): string {
|
|
641
|
-
const
|
|
642
|
-
const
|
|
643
|
-
const
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
catchBranch === undefined
|
|
647
|
-
? 'undefined'
|
|
648
|
-
: branchThunk(catchBranch.children, catchBranch.as ?? '_error', finallyChildren)
|
|
632
|
+
const plan = tryPlan(node)
|
|
633
|
+
const tryThunk = branchThunk(plan.guarded, undefined, plan.finallyChildren)
|
|
634
|
+
const catchThunk = plan.hasCatch
|
|
635
|
+
? branchThunk(plan.catchChildren, plan.catchAs, plan.finallyChildren)
|
|
636
|
+
: 'undefined'
|
|
649
637
|
return `tryBlock(${parentVar}, nextBlockId(), ${tryThunk}, ${catchThunk}, ${before});\n`
|
|
650
638
|
}
|
|
651
639
|
|
|
@@ -658,25 +646,20 @@ export function generateBuild(
|
|
|
658
646
|
): string {
|
|
659
647
|
/* The `case` children are the chain's `elseif`/`else` branches in source order;
|
|
660
648
|
the rest are the `then` content. */
|
|
661
|
-
const
|
|
662
|
-
(child): child is Extract<TemplateNode, { kind: 'case' }> => child.kind === 'case',
|
|
663
|
-
)
|
|
664
|
-
const thenChildren = node.children.filter((child) => child.kind !== 'case')
|
|
665
|
-
const hasElseif = branches.some((branch) => branch.condition !== undefined)
|
|
649
|
+
const plan = ifPlan(node)
|
|
666
650
|
/* Fast path: a plain `if` (with optional `else`) is the binary `when` runtime. */
|
|
667
|
-
if (!hasElseif) {
|
|
668
|
-
const
|
|
669
|
-
const thenThunk = branchThunk(thenChildren)
|
|
651
|
+
if (!plan.hasElseif) {
|
|
652
|
+
const thenThunk = branchThunk(plan.thenChildren)
|
|
670
653
|
const elseThunk =
|
|
671
|
-
elseBranch === undefined ? 'undefined' : branchThunk(elseBranch.children)
|
|
654
|
+
plan.elseBranch === undefined ? 'undefined' : branchThunk(plan.elseBranch.children)
|
|
672
655
|
return `when(${parentVar}, () => (${lowerExpression(node.condition)}), ${thenThunk}, ${elseThunk}, ${before});\n`
|
|
673
656
|
}
|
|
674
657
|
/* if/elseif/else is a cond-chain — reuse `switchBlock` over a constant `true`
|
|
675
658
|
subject with `Boolean`-coerced match thunks, so the first truthy branch wins
|
|
676
659
|
(`else` is the match-less default). */
|
|
677
660
|
const entries = [
|
|
678
|
-
`{ match: () => Boolean(${lowerExpression(node.condition)}), render: ${branchThunk(thenChildren)} }`,
|
|
679
|
-
...branches.map((branch) =>
|
|
661
|
+
`{ match: () => Boolean(${lowerExpression(node.condition)}), render: ${branchThunk(plan.thenChildren)} }`,
|
|
662
|
+
...plan.branches.map((branch) =>
|
|
680
663
|
branch.condition !== undefined
|
|
681
664
|
? `{ match: () => Boolean(${lowerExpression(branch.condition)}), render: ${branchThunk(branch.children)} }`
|
|
682
665
|
: `{ match: undefined, render: ${branchThunk(branch.children)} }`,
|
|
@@ -8,18 +8,22 @@ import {
|
|
|
8
8
|
RANGE_OPEN as RANGE_OPEN_DATA,
|
|
9
9
|
} from '../runtime/RANGE_MARKER.ts'
|
|
10
10
|
import { asOutlet } from './asOutlet.ts'
|
|
11
|
+
import { awaitPlan } from './awaitPlan.ts'
|
|
11
12
|
import { composeProps } from './composeProps.ts'
|
|
12
13
|
import { groupBindParts } from './groupBindParts.ts'
|
|
14
|
+
import { ifPlan } from './ifPlan.ts'
|
|
13
15
|
import { isAnchorPositioned } from './isAnchorPositioned.ts'
|
|
14
16
|
import { lowerContext } from './lowerContext.ts'
|
|
15
17
|
import { makeVarNamer } from './makeVarNamer.ts'
|
|
16
|
-
import { resolveBranches } from './resolveBranches.ts'
|
|
17
18
|
import { scopeAttr } from './scopeAttr.ts'
|
|
18
19
|
import { skeletonContext } from './skeletonContext.ts'
|
|
19
20
|
import { spreadExcludedNames } from './spreadExcludedNames.ts'
|
|
20
21
|
import { staticAttr } from './staticAttr.ts'
|
|
21
22
|
import { staticTextPart } from './staticTextPart.ts'
|
|
22
23
|
import { stripEffects } from './stripEffects.ts'
|
|
24
|
+
import { switchPlan } from './switchPlan.ts'
|
|
25
|
+
import { tryPlan } from './tryPlan.ts'
|
|
26
|
+
import type { TemplateAttr } from './types/TemplateAttr.ts'
|
|
23
27
|
import type { TemplateNode } from './types/TemplateNode.ts'
|
|
24
28
|
import { VOID_TAGS } from './VOID_TAGS.ts'
|
|
25
29
|
|
|
@@ -215,12 +219,9 @@ export function generateSSR(
|
|
|
215
219
|
/* `case` children are the `elseif`/`else` branches in source order; the rest are
|
|
216
220
|
the `then` content. Each `elseif` becomes an `else if`, the match-less `else`
|
|
217
221
|
the trailing `else`. */
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
)
|
|
221
|
-
const thenChildren = node.children.filter((child) => child.kind !== 'case')
|
|
222
|
-
let code = `if (${lowerExpression(node.condition)}) {\n${branchContent(thenChildren, target)}}`
|
|
223
|
-
for (const branch of branches) {
|
|
222
|
+
const plan = ifPlan(node)
|
|
223
|
+
let code = `if (${lowerExpression(node.condition)}) {\n${branchContent(plan.thenChildren, target)}}`
|
|
224
|
+
for (const branch of plan.branches) {
|
|
224
225
|
code +=
|
|
225
226
|
branch.condition !== undefined
|
|
226
227
|
? ` else if (${lowerExpression(branch.condition)}) {\n${branchContent(branch.children, target)}}`
|
|
@@ -229,20 +230,17 @@ export function generateSSR(
|
|
|
229
230
|
return `${anchor}${openRange(target)}${code}\n${closeRange(target)}`
|
|
230
231
|
}
|
|
231
232
|
if (node.kind === 'switch') {
|
|
232
|
-
const
|
|
233
|
-
(child): child is Extract<TemplateNode, { kind: 'case' }> => child.kind === 'case',
|
|
234
|
-
)
|
|
233
|
+
const plan = switchPlan(node)
|
|
235
234
|
let code = `{ const $s = (${lowerExpression(node.subject)});\n`
|
|
236
235
|
let started = false
|
|
237
|
-
for (const branch of cases) {
|
|
236
|
+
for (const branch of plan.cases) {
|
|
238
237
|
if (branch.match !== undefined) {
|
|
239
238
|
code += `${started ? 'else ' : ''}if ($s === (${lowerExpression(branch.match)})) {\n${branchContent(branch.children, target)}}\n`
|
|
240
239
|
started = true
|
|
241
240
|
}
|
|
242
241
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
code += `${started ? 'else ' : ''}{\n${branchContent(fallback.children, target)}}\n`
|
|
242
|
+
if (plan.fallback !== undefined) {
|
|
243
|
+
code += `${started ? 'else ' : ''}{\n${branchContent(plan.fallback.children, target)}}\n`
|
|
246
244
|
}
|
|
247
245
|
return `${anchor}${openRange(target)}${code}}\n${closeRange(target)}`
|
|
248
246
|
}
|
|
@@ -371,8 +369,25 @@ export function generateSSR(
|
|
|
371
369
|
for (const scope of node.scopes ?? []) {
|
|
372
370
|
code += push(target, scopeAttr(scope))
|
|
373
371
|
}
|
|
372
|
+
/* `class:`/`style:` directives collapse with any static `class`/`style` into a SINGLE
|
|
373
|
+
merged attribute (a duplicate attribute would be invalid and ignored). The client's
|
|
374
|
+
classList.toggle / style.setProperty effects re-apply the same values on hydrate, so
|
|
375
|
+
the merged SSR value already matches the post-mount DOM — no flash, no desync. */
|
|
376
|
+
const classDirectives = node.attrs.filter(
|
|
377
|
+
(attr): attr is Extract<TemplateAttr, { kind: 'class' }> => attr.kind === 'class',
|
|
378
|
+
)
|
|
379
|
+
const styleDirectives = node.attrs.filter(
|
|
380
|
+
(attr): attr is Extract<TemplateAttr, { kind: 'style' }> => attr.kind === 'style',
|
|
381
|
+
)
|
|
374
382
|
for (const attr of node.attrs) {
|
|
375
383
|
if (attr.kind === 'static') {
|
|
384
|
+
/* A static class/style is folded into the merge below when directives exist. */
|
|
385
|
+
if (
|
|
386
|
+
(attr.name === 'class' && classDirectives.length > 0) ||
|
|
387
|
+
(attr.name === 'style' && styleDirectives.length > 0)
|
|
388
|
+
) {
|
|
389
|
+
continue
|
|
390
|
+
}
|
|
376
391
|
code += push(target, staticAttr(attr.name, attr.value))
|
|
377
392
|
} else if (attr.kind === 'expression') {
|
|
378
393
|
/* present/absent semantics matching the client `attr` binding:
|
|
@@ -400,6 +415,36 @@ export function generateSSR(
|
|
|
400
415
|
code += `${target}.push(${JSON.stringify(` ${attr.property}="`)} + $esc(${bindRead(attr.code)}) + '"');\n`
|
|
401
416
|
}
|
|
402
417
|
}
|
|
418
|
+
/* Merged class: static value + each directive's name when its expression is truthy. */
|
|
419
|
+
if (classDirectives.length > 0) {
|
|
420
|
+
const staticClass = node.attrs.find(
|
|
421
|
+
(attr): attr is Extract<TemplateAttr, { kind: 'static' }> =>
|
|
422
|
+
attr.kind === 'static' && attr.name === 'class',
|
|
423
|
+
)
|
|
424
|
+
const parts = [
|
|
425
|
+
...(staticClass ? [JSON.stringify(staticClass.value)] : []),
|
|
426
|
+
...classDirectives.map(
|
|
427
|
+
(dir) => `((${lowerExpression(dir.code)}) ? ${JSON.stringify(dir.name)} : "")`,
|
|
428
|
+
),
|
|
429
|
+
]
|
|
430
|
+
code += `${target}.push(' class="' + $esc([${parts.join(', ')}].filter(Boolean).join(' ')) + '"');\n`
|
|
431
|
+
}
|
|
432
|
+
/* Merged style: static value + each directive's `prop:value` (String()-coerced, matching
|
|
433
|
+
the client's style.setProperty). */
|
|
434
|
+
if (styleDirectives.length > 0) {
|
|
435
|
+
const staticStyle = node.attrs.find(
|
|
436
|
+
(attr): attr is Extract<TemplateAttr, { kind: 'static' }> =>
|
|
437
|
+
attr.kind === 'static' && attr.name === 'style',
|
|
438
|
+
)
|
|
439
|
+
const parts = [
|
|
440
|
+
...(staticStyle ? [JSON.stringify(staticStyle.value)] : []),
|
|
441
|
+
...styleDirectives.map(
|
|
442
|
+
(dir) =>
|
|
443
|
+
`(${JSON.stringify(`${dir.property}:`)} + String(${lowerExpression(dir.code)}))`,
|
|
444
|
+
),
|
|
445
|
+
]
|
|
446
|
+
code += `${target}.push(' style="' + $esc([${parts.join(', ')}].filter(Boolean).join(';')) + '"');\n`
|
|
447
|
+
}
|
|
403
448
|
code += push(target, '>')
|
|
404
449
|
if (!VOID_TAGS.has(node.tag)) {
|
|
405
450
|
/* Each child's skeleton position (whether its reactive text interleaves into an
|
|
@@ -464,27 +509,23 @@ export function generateSSR(
|
|
|
464
509
|
node: Extract<TemplateNode, { kind: 'await' }>,
|
|
465
510
|
target: string,
|
|
466
511
|
): string {
|
|
467
|
-
const
|
|
468
|
-
const finallyChildren = finallyBranch?.children ?? []
|
|
469
|
-
const resolvedChildren = node.children.filter((child) => child.kind !== 'branch')
|
|
470
|
-
const resolvedAs = node.as ?? '_value'
|
|
471
|
-
const errorAs = catchBranch?.as ?? '_error'
|
|
512
|
+
const plan = awaitPlan(node)
|
|
472
513
|
const id = nextVar('$aid')
|
|
473
514
|
let code = `const ${id} = $ctx.next++;\n`
|
|
474
515
|
code += `${target}.push("<!--abide:await:" + ${id} + "-->");\n`
|
|
475
516
|
code += `try {\n`
|
|
476
|
-
code += `const ${resolvedAs} = await (${lowerExpression(node.promise)});\n`
|
|
477
|
-
code += branchContent(resolvedChildren, target)
|
|
478
|
-
code += branchContent(finallyChildren, target)
|
|
479
|
-
code += `$resume[${id}] = { ok: true, value: ${resolvedAs} };\n`
|
|
480
|
-
if (
|
|
517
|
+
code += `const ${plan.resolvedAs} = await (${lowerExpression(node.promise)});\n`
|
|
518
|
+
code += branchContent(plan.resolvedChildren, target)
|
|
519
|
+
code += branchContent(plan.finallyChildren, target)
|
|
520
|
+
code += `$resume[${id}] = { ok: true, value: ${plan.resolvedAs} };\n`
|
|
521
|
+
if (plan.surfaceRejection) {
|
|
481
522
|
/* No catch/finally → let the rejection surface instead of an empty branch. */
|
|
482
523
|
code += `} catch (_error) { throw _error; }\n`
|
|
483
524
|
} else {
|
|
484
|
-
code += `} catch (${
|
|
485
|
-
code += branchContent(
|
|
486
|
-
code += branchContent(finallyChildren, target)
|
|
487
|
-
code += `$resume[${id}] = { ok: false, error: String(${
|
|
525
|
+
code += `} catch (${plan.catchAs}) {\n`
|
|
526
|
+
code += branchContent(plan.catchChildren, target)
|
|
527
|
+
code += branchContent(plan.finallyChildren, target)
|
|
528
|
+
code += `$resume[${id}] = { ok: false, error: String(${plan.catchAs}) };\n`
|
|
488
529
|
code += `}\n`
|
|
489
530
|
}
|
|
490
531
|
code += `${target}.push("<!--/abide:await:" + ${id} + "-->");\n`
|
|
@@ -502,29 +543,21 @@ export function generateSSR(
|
|
|
502
543
|
node: Extract<TemplateNode, { kind: 'await' }>,
|
|
503
544
|
target: string,
|
|
504
545
|
): string {
|
|
505
|
-
const
|
|
506
|
-
node,
|
|
507
|
-
'then',
|
|
508
|
-
'catch',
|
|
509
|
-
'finally',
|
|
510
|
-
)
|
|
511
|
-
const finallyChildren = finallyBranch?.children ?? []
|
|
512
|
-
const pending = node.children.filter((child) => child.kind !== 'branch')
|
|
546
|
+
const plan = awaitPlan(node)
|
|
513
547
|
const id = nextVar('$aid')
|
|
514
548
|
let code = `const ${id} = $ctx.next++;\n`
|
|
515
549
|
code += `${target}.push("<!--abide:await:" + ${id} + "-->");\n`
|
|
516
|
-
code += branchContent(pending, target)
|
|
550
|
+
code += branchContent(plan.pending, target)
|
|
517
551
|
code += `${target}.push("<!--/abide:await:" + ${id} + "-->");\n`
|
|
518
552
|
const settled = (binding: string, children: TemplateNode[]) =>
|
|
519
|
-
`async (${binding}) => { const $o = []; ${branchContent(children, '$o')}${branchContent(finallyChildren, '$o')}return $o.join(''); }`
|
|
520
|
-
const catchProp =
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
: `catch: ${settled(catchBranch?.as ?? '_error', catchBranch?.children ?? [])} `
|
|
553
|
+
`async (${binding}) => { const $o = []; ${branchContent(children, '$o')}${branchContent(plan.finallyChildren, '$o')}return $o.join(''); }`
|
|
554
|
+
const catchProp = plan.surfaceRejection
|
|
555
|
+
? ''
|
|
556
|
+
: `catch: ${settled(plan.catchAs, plan.catchChildren)} `
|
|
524
557
|
code +=
|
|
525
558
|
`$awaits.push({ id: ${id}, ` +
|
|
526
559
|
`promise: () => (${lowerExpression(node.promise)}), ` +
|
|
527
|
-
`then: ${settled(
|
|
560
|
+
`then: ${settled(plan.resolvedAs, plan.resolvedChildren)}, ` +
|
|
528
561
|
`${catchProp}});\n`
|
|
529
562
|
return code
|
|
530
563
|
}
|
|
@@ -536,24 +569,21 @@ export function generateSSR(
|
|
|
536
569
|
an enclosing boundary / the 500 / the stream). Boundary comments let hydration
|
|
537
570
|
discard the server content if the client adoption fails. */
|
|
538
571
|
function generateTry(node: Extract<TemplateNode, { kind: 'try' }>, target: string): string {
|
|
539
|
-
const
|
|
540
|
-
const finallyChildren = finallyBranch?.children ?? []
|
|
541
|
-
const guarded = node.children.filter((child) => child.kind !== 'branch')
|
|
542
|
-
const errName = catchBranch?.as ?? '_error'
|
|
572
|
+
const plan = tryPlan(node)
|
|
543
573
|
const id = nextVar('$tid')
|
|
544
574
|
const mark = nextVar('$trim')
|
|
545
575
|
let code = `const ${id} = $ctx.next++;\n`
|
|
546
576
|
code += `${target}.push("<!--abide:try:" + ${id} + "-->");\n`
|
|
547
577
|
code += `const ${mark} = ${target}.length;\n`
|
|
548
578
|
code += `try {\n`
|
|
549
|
-
code += branchContent(guarded, target)
|
|
550
|
-
code += branchContent(finallyChildren, target)
|
|
551
|
-
code += `} catch (${
|
|
552
|
-
if (
|
|
553
|
-
code += branchContent(
|
|
554
|
-
code += branchContent(finallyChildren, target)
|
|
579
|
+
code += branchContent(plan.guarded, target)
|
|
580
|
+
code += branchContent(plan.finallyChildren, target)
|
|
581
|
+
code += `} catch (${plan.catchAs}) {\n${target}.length = ${mark};\n`
|
|
582
|
+
if (plan.hasCatch) {
|
|
583
|
+
code += branchContent(plan.catchChildren, target)
|
|
584
|
+
code += branchContent(plan.finallyChildren, target)
|
|
555
585
|
} else {
|
|
556
|
-
code += `throw ${
|
|
586
|
+
code += `throw ${plan.catchAs};\n`
|
|
557
587
|
}
|
|
558
588
|
code += `}\n`
|
|
559
589
|
code += `${target}.push("<!--/abide:try:" + ${id} + "-->");\n`
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { TemplateNode } from './types/TemplateNode.ts'
|
|
2
|
+
|
|
3
|
+
type Case = Extract<TemplateNode, { kind: 'case' }>
|
|
4
|
+
|
|
5
|
+
/* The structural shape of an `if` chain, resolved once so the build and SSR back-ends share
|
|
6
|
+
one partition of its children and only own emission. Both previously recomputed the
|
|
7
|
+
then-content / branch split and the elseif/else classification. */
|
|
8
|
+
export type IfPlan = {
|
|
9
|
+
/* The `then` content — every child that is not an `elseif`/`else` branch. */
|
|
10
|
+
thenChildren: TemplateNode[]
|
|
11
|
+
/* The `elseif`/`else` branches in source order (each a `case` node: `condition` set for
|
|
12
|
+
`elseif`, both unset for `else`). */
|
|
13
|
+
branches: Case[]
|
|
14
|
+
/* Any branch carries a condition → a cond-chain (the build lowers it through `switchBlock`);
|
|
15
|
+
otherwise a binary `if`/`else`. */
|
|
16
|
+
hasElseif: boolean
|
|
17
|
+
/* The match-less `else` branch, if present. */
|
|
18
|
+
elseBranch: Case | undefined
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/* Partitions an `if` node's children into the shared structural plan. */
|
|
22
|
+
export function ifPlan(node: Extract<TemplateNode, { kind: 'if' }>): IfPlan {
|
|
23
|
+
const branches = node.children.filter((child): child is Case => child.kind === 'case')
|
|
24
|
+
return {
|
|
25
|
+
thenChildren: node.children.filter((child) => child.kind !== 'case'),
|
|
26
|
+
branches,
|
|
27
|
+
hasElseif: branches.some((branch) => branch.condition !== undefined),
|
|
28
|
+
elseBranch: branches.find((branch) => branch.condition === undefined),
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -453,6 +453,7 @@ export function parseTemplate(source: string, baseOffset = 0): { nodes: Template
|
|
|
453
453
|
continue
|
|
454
454
|
}
|
|
455
455
|
let name = ''
|
|
456
|
+
const nameLoc = baseOffset + cursor // absolute offset of the attribute name
|
|
456
457
|
while (cursor < source.length && !/[\s=>/]/.test(source.charAt(cursor))) {
|
|
457
458
|
name += source.charAt(cursor)
|
|
458
459
|
cursor += 1
|
|
@@ -461,7 +462,7 @@ export function parseTemplate(source: string, baseOffset = 0): { nodes: Template
|
|
|
461
462
|
cursor += 1
|
|
462
463
|
}
|
|
463
464
|
if (source.charAt(cursor) !== '=') {
|
|
464
|
-
attrs.push({ kind: 'static', name, value: '', bare: true }) // boolean attribute
|
|
465
|
+
attrs.push({ kind: 'static', name, value: '', bare: true, nameLoc }) // boolean attribute
|
|
465
466
|
continue
|
|
466
467
|
}
|
|
467
468
|
cursor += 1 // past '='
|
|
@@ -474,10 +475,14 @@ export function parseTemplate(source: string, baseOffset = 0): { nodes: Template
|
|
|
474
475
|
attrs.push({ kind: 'event', event: name.slice(2), code, loc })
|
|
475
476
|
} else if (name.startsWith('bind:')) {
|
|
476
477
|
attrs.push({ kind: 'bind', property: name.slice(5), code, loc })
|
|
478
|
+
} else if (name.startsWith('class:')) {
|
|
479
|
+
attrs.push({ kind: 'class', name: name.slice(6), code, loc })
|
|
480
|
+
} else if (name.startsWith('style:')) {
|
|
481
|
+
attrs.push({ kind: 'style', property: name.slice(6), code, loc })
|
|
477
482
|
} else if (name === 'attach') {
|
|
478
483
|
attrs.push({ kind: 'attach', code, loc })
|
|
479
484
|
} else {
|
|
480
|
-
attrs.push({ kind: 'expression', name, code, loc })
|
|
485
|
+
attrs.push({ kind: 'expression', name, code, loc, nameLoc })
|
|
481
486
|
}
|
|
482
487
|
} else if (source.charAt(cursor) === '"' || source.charAt(cursor) === "'") {
|
|
483
488
|
const quote = source.charAt(cursor)
|
|
@@ -488,7 +493,7 @@ export function parseTemplate(source: string, baseOffset = 0): { nodes: Template
|
|
|
488
493
|
cursor += 1
|
|
489
494
|
}
|
|
490
495
|
cursor += 1 // past closing quote
|
|
491
|
-
attrs.push({ kind: 'static', name, value })
|
|
496
|
+
attrs.push({ kind: 'static', name, value, nameLoc })
|
|
492
497
|
} else {
|
|
493
498
|
/* Unquoted value (`<input type=text>`): runs to the next whitespace or
|
|
494
499
|
`>`, per the HTML unquoted-attribute rule. No delimiter to consume. */
|
|
@@ -512,6 +517,7 @@ export function parseTemplate(source: string, baseOffset = 0): { nodes: Template
|
|
|
512
517
|
function readElement(): TemplateNode {
|
|
513
518
|
cursor += 1 // past '<'
|
|
514
519
|
let tag = ''
|
|
520
|
+
const tagStart = cursor // absolute (baseOffset-relative) offset of the tag name
|
|
515
521
|
while (cursor < source.length && !/[\s>/]/.test(source.charAt(cursor))) {
|
|
516
522
|
tag += source.charAt(cursor)
|
|
517
523
|
cursor += 1
|
|
@@ -549,7 +555,13 @@ export function parseTemplate(source: string, baseOffset = 0): { nodes: Template
|
|
|
549
555
|
its children become slot content (rendered where the child puts <slot>). */
|
|
550
556
|
if (/^[A-Z]/.test(tag)) {
|
|
551
557
|
const slotted = selfClosing ? [] : readChildren(tag)
|
|
552
|
-
return {
|
|
558
|
+
return {
|
|
559
|
+
kind: 'component',
|
|
560
|
+
name: tag,
|
|
561
|
+
loc: baseOffset + tagStart,
|
|
562
|
+
props: toProps(attrs),
|
|
563
|
+
children: slotted,
|
|
564
|
+
}
|
|
553
565
|
}
|
|
554
566
|
/* `{...expr}` spreads onto a component (its props) or a native element (its
|
|
555
567
|
attributes), but a `<template>` directive has no such bag — reject it there. */
|
|
@@ -874,7 +886,7 @@ function rejectMisplacedBranchContent(
|
|
|
874
886
|
letting a prop hold any value, functions included (e.g. an `onclick` callback). */
|
|
875
887
|
function toProps(
|
|
876
888
|
attrs: TemplateAttr[],
|
|
877
|
-
): { name: string; code: string; loc?: number; spread?: boolean }[] {
|
|
889
|
+
): { name: string; code: string; loc?: number; nameLoc?: number; spread?: boolean }[] {
|
|
878
890
|
return attrs.map((attr) => {
|
|
879
891
|
/* A `{...expr}` spread carries no name — its keys merge in at runtime
|
|
880
892
|
(`mergeProps`/`spreadProps`); `spread: true` marks it for the back-ends. */
|
|
@@ -885,19 +897,31 @@ function toProps(
|
|
|
885
897
|
/* A bare attribute (`<Toggle on />`) is a boolean flag: coerce it to
|
|
886
898
|
`true` so the prop reads as a boolean, not the empty string a native
|
|
887
899
|
element would serialise. An explicit `on=""` stays the empty string. */
|
|
888
|
-
return {
|
|
900
|
+
return {
|
|
901
|
+
name: attr.name,
|
|
902
|
+
code: attr.bare ? 'true' : JSON.stringify(attr.value),
|
|
903
|
+
nameLoc: attr.nameLoc,
|
|
904
|
+
}
|
|
889
905
|
}
|
|
890
906
|
/* Every non-static kind keeps its `code`/`loc`; only the prop name differs —
|
|
891
|
-
a directive (`event`/`bind`/`attach`) round-trips to its written
|
|
907
|
+
a directive (`event`/`bind`/`class`/`style`/`attach`) round-trips to its written
|
|
908
|
+
name as a passthrough prop. */
|
|
892
909
|
const name =
|
|
893
910
|
attr.kind === 'event'
|
|
894
911
|
? `on${attr.event}`
|
|
895
912
|
: attr.kind === 'bind'
|
|
896
913
|
? `bind:${attr.property}`
|
|
897
|
-
: attr.kind === '
|
|
898
|
-
?
|
|
899
|
-
: attr.
|
|
900
|
-
|
|
914
|
+
: attr.kind === 'class'
|
|
915
|
+
? `class:${attr.name}`
|
|
916
|
+
: attr.kind === 'style'
|
|
917
|
+
? `style:${attr.property}`
|
|
918
|
+
: attr.kind === 'attach'
|
|
919
|
+
? 'attach'
|
|
920
|
+
: attr.name
|
|
921
|
+
/* Only `expression` carries a name offset; event/bind/attach are framework-handled
|
|
922
|
+
passthrough excluded from the strict prop check, so theirs is unused. */
|
|
923
|
+
const nameLoc = attr.kind === 'expression' ? attr.nameLoc : undefined
|
|
924
|
+
return { name, code: attr.code, loc: attr.loc, nameLoc }
|
|
901
925
|
})
|
|
902
926
|
}
|
|
903
927
|
|
|
@@ -930,6 +954,12 @@ function attrName(attr: TemplateAttr): string {
|
|
|
930
954
|
if (attr.kind === 'attach') {
|
|
931
955
|
return 'attach'
|
|
932
956
|
}
|
|
957
|
+
if (attr.kind === 'class') {
|
|
958
|
+
return `class:${attr.name}`
|
|
959
|
+
}
|
|
960
|
+
if (attr.kind === 'style') {
|
|
961
|
+
return `style:${attr.property}`
|
|
962
|
+
}
|
|
933
963
|
/* A spread has no name. `attrName` only feeds `<template>` directive lookups, and a
|
|
934
964
|
spread on a `<template>` is rejected at parse, so this branch is unreachable in
|
|
935
965
|
practice. (Spread itself IS supported — on components as props and on native
|