@abide/abide 0.41.1 → 0.43.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 +234 -294
- package/CHANGELOG.md +82 -0
- package/README.md +51 -95
- package/package.json +3 -2
- package/src/abideLsp.ts +46 -8
- package/src/abideResolverPlugin.ts +107 -19
- 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/devClientFingerprint.ts +2 -1
- 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/escapeRegex.ts +9 -0
- package/src/lib/shared/extraForwardHeaders.ts +1 -1
- package/src/lib/shared/fileName.ts +9 -0
- package/src/lib/shared/fileStem.ts +3 -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/programNameForPackage.ts +3 -1
- 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 +6 -4
- 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/ABIDE_SEMANTIC_TOKENS_LEGEND.ts +45 -0
- package/src/lib/ui/compile/abideUiPlugin.ts +2 -1
- package/src/lib/ui/compile/awaitPlan.ts +48 -0
- package/src/lib/ui/compile/compileShadow.ts +117 -31
- package/src/lib/ui/compile/createShadowLanguageService.ts +48 -0
- package/src/lib/ui/compile/desugarSignals.ts +42 -6
- package/src/lib/ui/compile/encodeSemanticTokens.ts +37 -0
- package/src/lib/ui/compile/generateBuild.ts +45 -62
- package/src/lib/ui/compile/generateSSR.ts +90 -60
- package/src/lib/ui/compile/ifPlan.ts +30 -0
- package/src/lib/ui/compile/makeVarNamer.ts +10 -0
- package/src/lib/ui/compile/offsetToPosition.ts +9 -0
- package/src/lib/ui/compile/parseTemplate.ts +565 -115
- package/src/lib/ui/compile/stripEffects.ts +17 -9
- package/src/lib/ui/compile/structuralBlockTokens.ts +101 -0
- package/src/lib/ui/compile/switchPlan.ts +22 -0
- package/src/lib/ui/compile/tryPlan.ts +29 -0
- package/src/lib/ui/compile/types/SemanticToken.ts +11 -0
- package/src/lib/ui/compile/types/TemplateAttr.ts +8 -2
- package/src/lib/ui/compile/types/TemplateNode.ts +15 -2
- package/src/lib/ui/createScope.ts +20 -2
- package/src/lib/ui/dom/appendText.ts +1 -5
- package/src/lib/ui/dom/applyResolved.ts +1 -5
- package/src/lib/ui/dom/disposeRange.ts +6 -10
- package/src/lib/ui/dom/hydrate.ts +2 -5
- package/src/lib/ui/dom/isComment.ts +6 -0
- 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 +3 -3
- package/src/lib/ui/rpcOutbox/createOutboxQueue.ts +281 -0
- package/src/lib/ui/rpcOutbox/outboxRegistry.ts +69 -0
- package/src/lib/ui/runtime/createDoc.ts +3 -3
- package/src/lib/ui/runtime/pathSegments.ts +10 -0
- package/src/lib/ui/socketChannel.ts +7 -1
- package/src/lib/ui/types/Scope.ts +20 -5
- package/template/.zed/settings.json +4 -0
- package/template/CLAUDE.md +1 -1
- package/template/src/server/rpc/getHello.ts +3 -3
- package/template/src/ui/pages/page.abide +4 -4
- 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
|
@@ -91,6 +91,10 @@ export function compileShadow(source: string): CompiledShadow {
|
|
|
91
91
|
pre-assembled scope line carrying its own embedded segments. */
|
|
92
92
|
type Builder = {
|
|
93
93
|
raw: (text: string) => void
|
|
94
|
+
/* Appends `text` verbatim and maps it back to source, WITHOUT the `expr`
|
|
95
|
+
parens — for binding names and other spans that must stay bare TS syntax
|
|
96
|
+
(`for (const NAME of …)`). `text` must equal the source at `sourceLoc`. */
|
|
97
|
+
mapped: (text: string, sourceLoc: number | undefined) => void
|
|
94
98
|
expr: (code: string, sourceLoc: number | undefined) => void
|
|
95
99
|
stmt: (code: string, sourceLoc: number | undefined) => void
|
|
96
100
|
flush: (line: ScopeLine) => void
|
|
@@ -115,6 +119,16 @@ function createBuilder(): Builder {
|
|
|
115
119
|
raw(text) {
|
|
116
120
|
code += text
|
|
117
121
|
},
|
|
122
|
+
mapped(text, sourceLoc) {
|
|
123
|
+
if (sourceLoc !== undefined) {
|
|
124
|
+
mappings.push({
|
|
125
|
+
shadowStart: code.length,
|
|
126
|
+
sourceStart: sourceLoc,
|
|
127
|
+
length: text.length,
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
code += text
|
|
131
|
+
},
|
|
118
132
|
expr(exprCode, sourceLoc) {
|
|
119
133
|
code += '('
|
|
120
134
|
if (sourceLoc !== undefined) {
|
|
@@ -268,6 +282,14 @@ function scopeLineFor(
|
|
|
268
282
|
propsShapes.push(shape === undefined ? 'Record<string, any>' : verbatim(shape))
|
|
269
283
|
return { text: `const ${verbatim(declaration)};`, segments: [span(declaration, 6)] }
|
|
270
284
|
}
|
|
285
|
+
/* The rewrite drops the callee from the line, so hovering `state`/`computed`/
|
|
286
|
+
`linked` at its call site has nothing to resolve. Append the callee as a
|
|
287
|
+
trailing reference statement (`…; state;`), mapped back to its source span,
|
|
288
|
+
so it resolves to the same primitive the destructure does. */
|
|
289
|
+
const withCalleeRef = (line: ScopeLine): ScopeLine => ({
|
|
290
|
+
text: `${line.text} ${verbatim(call.expression)};`,
|
|
291
|
+
segments: [...line.segments, span(call.expression, line.text.length + 1)],
|
|
292
|
+
})
|
|
271
293
|
if (callee === 'state') {
|
|
272
294
|
/* state<T>(initial): T is the value type — carry it onto the `let` so an
|
|
273
295
|
explicit annotation isn't lost to `any`/`any[]` inference of the initial. */
|
|
@@ -283,13 +305,16 @@ function scopeLineFor(
|
|
|
283
305
|
then correctly flagged possibly-undefined; a guard narrows cleanly. */
|
|
284
306
|
const valueType = annotation === '' ? ': unknown' : `${annotation} | undefined`
|
|
285
307
|
/* map the binding name (offset 4, past `let `) so hover/go-to resolve on it */
|
|
286
|
-
return {
|
|
308
|
+
return withCalleeRef({
|
|
309
|
+
text: `let ${name}!${valueType};`,
|
|
310
|
+
segments: [span(declaration.name, 4)],
|
|
311
|
+
})
|
|
287
312
|
}
|
|
288
313
|
const prefix = `let ${name}${annotation} = (`
|
|
289
|
-
return {
|
|
314
|
+
return withCalleeRef({
|
|
290
315
|
text: `${prefix}${verbatim(init)});`,
|
|
291
316
|
segments: [span(declaration.name, 4), span(init, prefix.length)],
|
|
292
|
-
}
|
|
317
|
+
})
|
|
293
318
|
}
|
|
294
319
|
/* computed<T>(compute) / linked<T>(seed) — the only callees left: T is the value
|
|
295
320
|
type — the call's first arg is a thunk, so invoking it yields the value. Annotate
|
|
@@ -303,16 +328,16 @@ function scopeLineFor(
|
|
|
303
328
|
/* binding-name map offset = past the keyword + space (`let ` = 4, `const ` = 6) */
|
|
304
329
|
const keywordOffset = keyword.length + 1
|
|
305
330
|
if (fn === undefined) {
|
|
306
|
-
return {
|
|
331
|
+
return withCalleeRef({
|
|
307
332
|
text: `${keyword} ${name} = undefined;`,
|
|
308
333
|
segments: [span(declaration.name, keywordOffset)],
|
|
309
|
-
}
|
|
334
|
+
})
|
|
310
335
|
}
|
|
311
336
|
const prefix = `${keyword} ${name}${annotation} = (`
|
|
312
|
-
return {
|
|
337
|
+
return withCalleeRef({
|
|
313
338
|
text: `${prefix}${verbatim(fn)})();`,
|
|
314
339
|
segments: [span(declaration.name, keywordOffset), span(fn, prefix.length)],
|
|
315
|
-
}
|
|
340
|
+
})
|
|
316
341
|
}
|
|
317
342
|
|
|
318
343
|
/* Emits a sibling list — each node standalone via `emitNode`. */
|
|
@@ -345,23 +370,29 @@ function emitNode(node: TemplateNode, builder: Builder): void {
|
|
|
345
370
|
emitNodes(node.children, builder)
|
|
346
371
|
return
|
|
347
372
|
case 'component': {
|
|
348
|
-
/*
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
373
|
+
/* The imported tag resolves (via the shadow host) to the child's
|
|
374
|
+
`(props: Props) => …` default, so `Parameters<typeof Child>[0]` is its prop
|
|
375
|
+
shape. `on*` / `bind:` / `attach` props are framework-handled passthrough
|
|
376
|
+
(not part of the declared shape), so they are checked leniently — each value
|
|
377
|
+
against its own declared key type — never flagged as excess. */
|
|
378
|
+
const handled = (prop: { name: string; spread?: boolean }): boolean =>
|
|
379
|
+
prop.spread === true ||
|
|
380
|
+
prop.name.startsWith('on') ||
|
|
381
|
+
prop.name.startsWith('bind:') ||
|
|
382
|
+
prop.name.startsWith('class:') ||
|
|
383
|
+
prop.name.startsWith('style:') ||
|
|
384
|
+
prop.name === 'attach'
|
|
385
|
+
const hasSpread = node.props.some((prop) => prop.spread)
|
|
386
|
+
for (const prop of node.props.filter(handled)) {
|
|
387
|
+
/* Lead with a defensive `;`: an IIFE/object-literal arg starts with `(` or
|
|
388
|
+
`{`, so without it a preceding scope statement left unterminated (a script
|
|
389
|
+
ending in a call with no trailing semicolon, e.g. `effect(() => …)`)
|
|
390
|
+
merges across the newline into `effect(…)(…)` — a spurious "not callable". */
|
|
360
391
|
if (prop.spread) {
|
|
361
392
|
/* A `{...expr}` spread contributes a SUBSET of the props (required ones
|
|
362
393
|
may come from another spread/explicit prop), so check it against
|
|
363
|
-
`Partial<Props>` — every key it does carry must match
|
|
364
|
-
|
|
394
|
+
`Partial<Props>` — every key it does carry must match, without
|
|
395
|
+
demanding completeness. */
|
|
365
396
|
builder.raw(`;((__spread: Partial<Parameters<typeof ${node.name}>[0]>) => {})(`)
|
|
366
397
|
} else {
|
|
367
398
|
builder.raw(
|
|
@@ -371,6 +402,43 @@ function emitNode(node: TemplateNode, builder: Builder): void {
|
|
|
371
402
|
builder.expr(prop.code, prop.loc)
|
|
372
403
|
builder.raw(');\n')
|
|
373
404
|
}
|
|
405
|
+
/* The plain data props as one object-literal argument typed to the child's whole
|
|
406
|
+
prop shape: a missing required prop errors on the literal (anchored at the tag
|
|
407
|
+
via the mapped `{`), an unknown prop errors on its key, a wrong type on its
|
|
408
|
+
value. Skipped when a spread is present — a spread may supply required props,
|
|
409
|
+
so completeness can't be demanded; the data props fall back to lenient per-key
|
|
410
|
+
checks instead. */
|
|
411
|
+
const dataProps = node.props.filter((prop) => !handled(prop))
|
|
412
|
+
if (hasSpread) {
|
|
413
|
+
for (const prop of dataProps) {
|
|
414
|
+
builder.raw(
|
|
415
|
+
`;((__prop: Parameters<typeof ${node.name}>[0][${JSON.stringify(prop.name)}]) => {})(`,
|
|
416
|
+
)
|
|
417
|
+
builder.expr(prop.code, prop.loc)
|
|
418
|
+
builder.raw(');\n')
|
|
419
|
+
}
|
|
420
|
+
} else {
|
|
421
|
+
builder.raw(`;((__c: Parameters<typeof ${node.name}>[0]): void => { void __c })({`)
|
|
422
|
+
/* A zero-length anchor right after `{`, pointing at the tag: a missing-
|
|
423
|
+
required-prop error spans the literal from `{`, so it overlaps this and
|
|
424
|
+
maps to the tag (an empty span trivially satisfies the source-text ==
|
|
425
|
+
shadow-text invariant). */
|
|
426
|
+
builder.mapped('', node.loc)
|
|
427
|
+
builder.raw('\n')
|
|
428
|
+
for (const prop of dataProps) {
|
|
429
|
+
/* The key mapped to its source name (excess-prop errors land on the key);
|
|
430
|
+
the value verbatim-mapped (wrong-type errors land on the value). */
|
|
431
|
+
if (prop.nameLoc !== undefined) {
|
|
432
|
+
builder.mapped(prop.name, prop.nameLoc)
|
|
433
|
+
} else {
|
|
434
|
+
builder.raw(prop.name)
|
|
435
|
+
}
|
|
436
|
+
builder.raw(': ')
|
|
437
|
+
builder.expr(prop.code, prop.loc)
|
|
438
|
+
builder.raw(',\n')
|
|
439
|
+
}
|
|
440
|
+
builder.raw('});\n')
|
|
441
|
+
}
|
|
374
442
|
emitNodes(node.children, builder)
|
|
375
443
|
return
|
|
376
444
|
}
|
|
@@ -408,19 +476,24 @@ function emitNode(node: TemplateNode, builder: Builder): void {
|
|
|
408
476
|
}
|
|
409
477
|
case 'each':
|
|
410
478
|
/* `for await` over an async each's AsyncIterable, plain `for…of` otherwise —
|
|
411
|
-
so the item binds to the element type under either iteration protocol.
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
)
|
|
479
|
+
so the item binds to the element type under either iteration protocol. The
|
|
480
|
+
binding name is `mapped` (not `raw`) so hover/highlighting land on it. */
|
|
481
|
+
builder.raw(node.async ? 'for await (const ' : 'for (const ')
|
|
482
|
+
builder.mapped(node.as, node.asLoc)
|
|
483
|
+
builder.raw(' of ')
|
|
415
484
|
builder.expr(node.items, node.loc)
|
|
416
485
|
builder.raw(') {\n')
|
|
417
486
|
if (node.key !== undefined) {
|
|
418
|
-
builder.raw(
|
|
487
|
+
builder.raw('void ')
|
|
488
|
+
builder.expr(node.key, node.keyLoc)
|
|
489
|
+
builder.raw(';\n')
|
|
419
490
|
}
|
|
420
491
|
/* `index="i"` binds the row's position — always a number (the row ordinal,
|
|
421
492
|
or an async stream's arrival count). Declare it so body references check. */
|
|
422
493
|
if (node.index !== undefined) {
|
|
423
|
-
builder.raw(
|
|
494
|
+
builder.raw('const ')
|
|
495
|
+
builder.mapped(node.index, node.indexLoc)
|
|
496
|
+
builder.raw(': number = 0;\n')
|
|
424
497
|
}
|
|
425
498
|
emitNodes(node.children, builder)
|
|
426
499
|
builder.raw('}\n')
|
|
@@ -439,7 +512,9 @@ function emitNode(node: TemplateNode, builder: Builder): void {
|
|
|
439
512
|
const pending = node.children.filter((child) => child.kind !== 'branch')
|
|
440
513
|
const branches = node.children.filter((child) => child.kind === 'branch')
|
|
441
514
|
if (node.blocking && node.as !== undefined) {
|
|
442
|
-
builder.raw(
|
|
515
|
+
builder.raw('{\nconst ')
|
|
516
|
+
builder.mapped(node.as, node.asLoc)
|
|
517
|
+
builder.raw(` = ${resolved};\n`)
|
|
443
518
|
emitNodes(pending, builder)
|
|
444
519
|
builder.raw('}\n')
|
|
445
520
|
} else {
|
|
@@ -451,9 +526,13 @@ function emitNode(node: TemplateNode, builder: Builder): void {
|
|
|
451
526
|
}
|
|
452
527
|
builder.raw('{\n')
|
|
453
528
|
if (branch.branch === 'then' && branch.as !== undefined) {
|
|
454
|
-
builder.raw(
|
|
529
|
+
builder.raw('const ')
|
|
530
|
+
builder.mapped(branch.as, branch.asLoc)
|
|
531
|
+
builder.raw(` = ${resolved};\n`)
|
|
455
532
|
} else if (branch.branch === 'catch' && branch.as !== undefined) {
|
|
456
|
-
builder.raw(
|
|
533
|
+
builder.raw('const ')
|
|
534
|
+
builder.mapped(branch.as, branch.asLoc)
|
|
535
|
+
builder.raw(' = undefined as any;\n')
|
|
457
536
|
}
|
|
458
537
|
emitNodes(branch.children, builder)
|
|
459
538
|
builder.raw('}\n')
|
|
@@ -508,7 +587,14 @@ function emitNode(node: TemplateNode, builder: Builder): void {
|
|
|
508
587
|
builder.raw('}\n')
|
|
509
588
|
return
|
|
510
589
|
case 'snippet':
|
|
511
|
-
|
|
590
|
+
/* `args={…}` is the parameter list; `mapped` (not `raw`) so hover/highlighting
|
|
591
|
+
land on the binding. `loc` is the `args` expression's offset (see
|
|
592
|
+
toSnippetOrTemplate). The name is a static attribute with no tracked offset. */
|
|
593
|
+
builder.raw(`const ${node.name} = (`)
|
|
594
|
+
if (node.params !== undefined) {
|
|
595
|
+
builder.mapped(node.params, node.loc)
|
|
596
|
+
}
|
|
597
|
+
builder.raw(') => {\n')
|
|
512
598
|
emitNodes(node.children, builder)
|
|
513
599
|
builder.raw('};\n')
|
|
514
600
|
return
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { resolve } from 'node:path'
|
|
2
2
|
import ts from 'typescript'
|
|
3
3
|
import { messageFromError } from '../../shared/messageFromError.ts'
|
|
4
|
+
import { mapTsClassification } from './ABIDE_SEMANTIC_TOKENS_LEGEND.ts'
|
|
4
5
|
import { assetModulesFile } from './assetModulesFile.ts'
|
|
5
6
|
import { compileShadow } from './compileShadow.ts'
|
|
6
7
|
import { loadShadowTsConfig } from './loadShadowTsConfig.ts'
|
|
@@ -10,6 +11,7 @@ import { shadowNaming } from './shadowNaming.ts'
|
|
|
10
11
|
import { sourceToShadowOffset } from './sourceToShadowOffset.ts'
|
|
11
12
|
import type { AbideDiagnostic } from './types/AbideDiagnostic.ts'
|
|
12
13
|
import type { CompiledShadow } from './types/CompiledShadow.ts'
|
|
14
|
+
import type { SemanticToken } from './types/SemanticToken.ts'
|
|
13
15
|
|
|
14
16
|
const { suffixed, isShadow, sourceOf } = shadowNaming
|
|
15
17
|
|
|
@@ -27,6 +29,8 @@ export type ShadowLanguageService = {
|
|
|
27
29
|
/* Hover info at a source offset, or undefined if the offset isn't a checked
|
|
28
30
|
expression (markup, whitespace) or TypeScript has nothing to report. */
|
|
29
31
|
quickInfo: (abidePath: string, sourceOffset: number) => ShadowQuickInfo | undefined
|
|
32
|
+
/* Type-aware semantic tokens for every checked expression, in source coords. */
|
|
33
|
+
semanticClassifications: (abidePath: string) => SemanticToken[]
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
/*
|
|
@@ -194,5 +198,49 @@ export function createShadowLanguageService(cwd: string): ShadowLanguageService
|
|
|
194
198
|
length: span?.length ?? 1,
|
|
195
199
|
}
|
|
196
200
|
},
|
|
201
|
+
semanticClassifications(abidePath) {
|
|
202
|
+
const fileName = suffixed(abidePath)
|
|
203
|
+
/* Compile first so the mappings cache is current. */
|
|
204
|
+
shadowText(abidePath)
|
|
205
|
+
const shadow = shadows.get(abidePath)
|
|
206
|
+
if (shadow === undefined) {
|
|
207
|
+
return []
|
|
208
|
+
}
|
|
209
|
+
const { spans } = service.getEncodedSemanticClassifications(
|
|
210
|
+
fileName,
|
|
211
|
+
{ start: 0, length: shadow.code.length },
|
|
212
|
+
ts.SemanticClassificationFormat.TwentyTwenty,
|
|
213
|
+
)
|
|
214
|
+
/* `spans` is flat triples [start, length, classification, …] in shadow
|
|
215
|
+
coords; keep only those overlapping a mapped expression segment. */
|
|
216
|
+
const tokens: SemanticToken[] = []
|
|
217
|
+
for (let index = 0; index + 2 < spans.length; index += 3) {
|
|
218
|
+
const spanStart = spans[index]
|
|
219
|
+
const spanLength = spans[index + 1]
|
|
220
|
+
const classification = spans[index + 2]
|
|
221
|
+
if (
|
|
222
|
+
spanStart === undefined ||
|
|
223
|
+
spanLength === undefined ||
|
|
224
|
+
classification === undefined
|
|
225
|
+
) {
|
|
226
|
+
continue
|
|
227
|
+
}
|
|
228
|
+
const mapped = mapTsClassification(classification)
|
|
229
|
+
if (mapped === undefined) {
|
|
230
|
+
continue
|
|
231
|
+
}
|
|
232
|
+
const located = remapShadowDiagnostic(shadow.mappings, spanStart, spanLength)
|
|
233
|
+
if (located === undefined) {
|
|
234
|
+
continue
|
|
235
|
+
}
|
|
236
|
+
tokens.push({
|
|
237
|
+
start: located.start,
|
|
238
|
+
length: located.length,
|
|
239
|
+
type: mapped.type,
|
|
240
|
+
modifiers: mapped.modifiers,
|
|
241
|
+
})
|
|
242
|
+
}
|
|
243
|
+
return tokens
|
|
244
|
+
},
|
|
197
245
|
}
|
|
198
246
|
}
|
|
@@ -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') {
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { ABIDE_SEMANTIC_TOKENS_LEGEND } from './ABIDE_SEMANTIC_TOKENS_LEGEND.ts'
|
|
2
|
+
import { offsetToPosition } from './offsetToPosition.ts'
|
|
3
|
+
import type { SemanticToken } from './types/SemanticToken.ts'
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
Encodes source-coordinate tokens into the LSP semantic-tokens `data` array: five
|
|
7
|
+
integers per token (deltaLine, deltaStartChar, length, tokenTypeIndex,
|
|
8
|
+
modifierBitset), each position relative to the previous token. Tokens are sorted
|
|
9
|
+
by start; tokens with an unknown legend type, and any token overlapping the one
|
|
10
|
+
before it, are dropped — the protocol requires a strictly non-overlapping,
|
|
11
|
+
positionally-ordered stream.
|
|
12
|
+
*/
|
|
13
|
+
export function encodeSemanticTokens(text: string, tokens: SemanticToken[]): number[] {
|
|
14
|
+
const sorted = [...tokens].sort((a, b) => a.start - b.start || a.length - b.length)
|
|
15
|
+
const data: number[] = []
|
|
16
|
+
let previousLine = 0
|
|
17
|
+
let previousCharacter = 0
|
|
18
|
+
let previousEnd = -1
|
|
19
|
+
for (const token of sorted) {
|
|
20
|
+
const typeIndex = ABIDE_SEMANTIC_TOKENS_LEGEND.tokenTypes.indexOf(token.type)
|
|
21
|
+
if (typeIndex < 0 || token.start < previousEnd) {
|
|
22
|
+
continue
|
|
23
|
+
}
|
|
24
|
+
const modifierBitset = token.modifiers.reduce((bits, name) => {
|
|
25
|
+
const bit = ABIDE_SEMANTIC_TOKENS_LEGEND.tokenModifiers.indexOf(name)
|
|
26
|
+
return bit < 0 ? bits : bits | (1 << bit)
|
|
27
|
+
}, 0)
|
|
28
|
+
const { line, character } = offsetToPosition(text, token.start)
|
|
29
|
+
const deltaLine = line - previousLine
|
|
30
|
+
const deltaCharacter = deltaLine === 0 ? character - previousCharacter : character
|
|
31
|
+
data.push(deltaLine, deltaCharacter, token.length, typeIndex, modifierBitset)
|
|
32
|
+
previousLine = line
|
|
33
|
+
previousCharacter = character
|
|
34
|
+
previousEnd = token.start + token.length
|
|
35
|
+
}
|
|
36
|
+
return data
|
|
37
|
+
}
|
|
@@ -3,19 +3,24 @@ 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'
|
|
15
|
+
import { makeVarNamer } from './makeVarNamer.ts'
|
|
13
16
|
import { resolveBranches } from './resolveBranches.ts'
|
|
14
17
|
import { scopeAttr } from './scopeAttr.ts'
|
|
15
18
|
import { skeletonContext } from './skeletonContext.ts'
|
|
16
19
|
import { spreadExcludedNames } from './spreadExcludedNames.ts'
|
|
17
20
|
import { staticAttr } from './staticAttr.ts'
|
|
18
21
|
import { staticTextPart } from './staticTextPart.ts'
|
|
22
|
+
import { switchPlan } from './switchPlan.ts'
|
|
23
|
+
import { tryPlan } from './tryPlan.ts'
|
|
19
24
|
import type { TemplateNode } from './types/TemplateNode.ts'
|
|
20
25
|
import { VOID_TAGS } from './VOID_TAGS.ts'
|
|
21
26
|
|
|
@@ -61,8 +66,7 @@ export function generateBuild(
|
|
|
61
66
|
computedNames: ReadonlySet<string>,
|
|
62
67
|
isLayout = false,
|
|
63
68
|
): string {
|
|
64
|
-
|
|
65
|
-
const nextVar = (prefix: string): string => `${prefix}${counter++}`
|
|
69
|
+
const nextVar = makeVarNamer()
|
|
66
70
|
|
|
67
71
|
/* In a layout, `<slot/>` outlets are rewritten to `OUTLET_TAG` elements up front
|
|
68
72
|
(`asOutlet`) so the static-clone path carries them as ordinary structure. `asOutlet`
|
|
@@ -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)} }`,
|