@abide/abide 0.43.1 → 0.44.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +293 -219
- package/CHANGELOG.md +39 -0
- package/README.md +191 -67
- package/package.json +2 -2
- package/src/abideLsp.ts +8 -4
- package/src/abideResolverPlugin.ts +11 -1
- package/src/build.ts +9 -0
- package/src/buildDisconnected.ts +2 -0
- package/src/devEntry.ts +1 -0
- package/src/lib/bundle/probeAbideServer.ts +1 -1
- package/src/lib/cli/parseArgvForRpc.ts +4 -2
- package/src/lib/cli/types/CliManifest.ts +6 -4
- package/src/lib/mcp/annotationsForMethod.ts +1 -1
- package/src/lib/mcp/toolResultFromResponse.ts +1 -1
- package/src/lib/mcp/types/JsonRpcRequest.ts +3 -3
- package/src/lib/server/AppModule.ts +1 -1
- package/src/lib/server/cli/handleCliDownload.ts +7 -5
- package/src/lib/server/cli/installScript.ts +3 -3
- package/src/lib/server/prompts/promptRegistry.ts +1 -1
- package/src/lib/server/prompts/types/Prompt.ts +2 -1
- package/src/lib/server/rpc/dispatchRpcInProcess.ts +2 -2
- package/src/lib/server/rpc/types/RpcHelper.ts +1 -1
- package/src/lib/server/runtime/createPublicAssetServer.ts +2 -2
- package/src/lib/server/runtime/createRouteDispatcher.ts +1 -1
- package/src/lib/server/runtime/createServer.ts +21 -0
- package/src/lib/server/runtime/createUiPageRenderer.ts +7 -11
- package/src/lib/server/runtime/crossOriginForbidden.ts +2 -2
- package/src/lib/server/runtime/crossOriginGate.ts +1 -1
- package/src/lib/server/runtime/devHotModuleResponse.ts +2 -2
- package/src/lib/server/runtime/disableIdleTimeoutForStream.ts +1 -1
- package/src/lib/server/runtime/logExposedSurfaces.ts +3 -4
- package/src/lib/server/runtime/serializeCacheSnapshot.ts +3 -8
- package/src/lib/server/runtime/snapshotEntryFromCache.ts +10 -7
- package/src/lib/server/runtime/streamFromIterator.ts +2 -2
- package/src/lib/server/runtime/types/InspectorRpc.ts +2 -2
- package/src/lib/server/socket.ts +1 -1
- package/src/lib/server/sockets/createSocketDispatcher.ts +2 -2
- package/src/lib/server/sockets/socketRegistry.ts +2 -2
- package/src/lib/server/sockets/types/SocketClientFrame.ts +3 -3
- package/src/lib/shared/CACHE_WRAPPED.ts +3 -2
- package/src/lib/shared/REMOTE_FUNCTION.ts +1 -1
- package/src/lib/shared/STREAMING_CONTENT_TYPES.ts +3 -2
- package/src/lib/shared/bodyValueForKind.ts +35 -0
- package/src/lib/shared/cache.ts +1 -1
- package/src/lib/shared/cacheEntryFromSnapshot.ts +23 -18
- package/src/lib/shared/createPushIterator.ts +1 -1
- package/src/lib/shared/decodeResponse.ts +11 -8
- package/src/lib/shared/hasReplayableRequest.ts +17 -0
- package/src/lib/shared/html.ts +7 -26
- package/src/lib/shared/manifestModule.ts +2 -2
- package/src/lib/shared/online.ts +1 -1
- package/src/lib/shared/outboxProbeSlot.ts +1 -1
- package/src/lib/shared/pageUrlForFile.ts +1 -1
- package/src/lib/shared/routeParamsShape.ts +20 -0
- package/src/lib/shared/snapshotShippable.ts +22 -0
- package/src/lib/shared/types/CacheEntry.ts +1 -1
- package/src/lib/shared/types/CacheInvalidation.ts +3 -3
- package/src/lib/shared/types/ClientFlags.ts +5 -3
- package/src/lib/shared/types/LastConnection.ts +1 -1
- package/src/lib/shared/url.ts +4 -1
- package/src/lib/shared/withBase.ts +1 -2
- package/src/lib/shared/writeDts.ts +2 -2
- package/src/lib/shared/writeRoutesDts.ts +3 -31
- package/src/lib/test/createTestSocketChannel.ts +3 -3
- package/src/lib/ui/COMPONENT_WRAPPER_PREFIX.ts +1 -1
- package/src/lib/ui/compile/ABIDE_SEMANTIC_TOKENS_LEGEND.ts +40 -6
- package/src/lib/ui/compile/REACTIVE_CALLEES.ts +1 -1
- package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +49 -37
- package/src/lib/ui/compile/abideUiPlugin.ts +1 -1
- package/src/lib/ui/compile/assertRuntimeHelpersBound.ts +3 -1
- package/src/lib/ui/compile/awaitPlan.ts +14 -2
- package/src/lib/ui/compile/catchBinding.ts +8 -0
- package/src/lib/ui/compile/classStyleMergePlan.ts +108 -0
- package/src/lib/ui/compile/collectAbideDiagnostics.ts +11 -0
- package/src/lib/ui/compile/compileComponent.ts +4 -4
- package/src/lib/ui/compile/compileModule.ts +20 -6
- package/src/lib/ui/compile/compileSSR.ts +12 -10
- package/src/lib/ui/compile/compileShadow.ts +88 -11
- package/src/lib/ui/compile/composeProps.ts +3 -3
- package/src/lib/ui/compile/createShadowLanguageService.ts +30 -2
- package/src/lib/ui/compile/createShadowProgram.ts +2 -1
- package/src/lib/ui/compile/createShadowScope.ts +42 -0
- package/src/lib/ui/compile/decodeHtmlEntities.ts +4 -0
- package/src/lib/ui/compile/desugarSignals.ts +12 -7
- package/src/lib/ui/compile/eachPlan.ts +61 -0
- package/src/lib/ui/compile/elementPlan.ts +55 -0
- package/src/lib/ui/compile/encodeSemanticTokens.ts +24 -1
- package/src/lib/ui/compile/generateBuild.ts +150 -127
- package/src/lib/ui/compile/generateSSR.ts +112 -73
- package/src/lib/ui/compile/ifPlan.ts +7 -1
- package/src/lib/ui/compile/interpolatedTemplateLiteral.ts +25 -0
- package/src/lib/ui/compile/isPlainIdentifier.ts +7 -0
- package/src/lib/ui/compile/isTextLeaf.ts +8 -7
- package/src/lib/ui/compile/lowerContext.ts +36 -46
- package/src/lib/ui/compile/lowerDocAccess.ts +2 -2
- package/src/lib/ui/compile/lowerScript.ts +1 -1
- package/src/lib/ui/compile/markupTokens.ts +313 -0
- package/src/lib/ui/compile/pagePropsType.ts +23 -0
- package/src/lib/ui/compile/parseTemplate.ts +131 -61
- package/src/lib/ui/compile/reactiveBinding.ts +47 -0
- package/src/lib/ui/compile/renameSignalRefs.ts +44 -10
- package/src/lib/ui/compile/skeletonable.ts +5 -5
- package/src/lib/ui/compile/snippetPlan.ts +28 -0
- package/src/lib/ui/compile/spreadExcludedNames.ts +4 -4
- package/src/lib/ui/compile/structuralBlockTokens.ts +3 -4
- package/src/lib/ui/compile/switchPlan.ts +7 -1
- package/src/lib/ui/compile/tryPlan.ts +8 -1
- package/src/lib/ui/compile/types/AnalyzedComponent.ts +2 -2
- package/src/lib/ui/compile/types/Binding.ts +24 -0
- package/src/lib/ui/compile/types/CompiledShadow.ts +13 -1
- package/src/lib/ui/compile/types/ElementPlan.ts +43 -0
- package/src/lib/ui/compile/types/ShadowKind.ts +9 -0
- package/src/lib/ui/compile/types/ShadowScope.ts +19 -0
- package/src/lib/ui/compile/types/TemplateAttr.ts +7 -0
- package/src/lib/ui/compile/types/TemplateNode.ts +8 -8
- package/src/lib/ui/compile/withBindings.ts +45 -0
- package/src/lib/ui/createScope.ts +2 -2
- package/src/lib/ui/dom/MATHML_NAMESPACE.ts +2 -2
- package/src/lib/ui/dom/SVG_NAMESPACE.ts +3 -2
- package/src/lib/ui/dom/appendText.ts +1 -1
- package/src/lib/ui/dom/applyResolved.ts +1 -1
- package/src/lib/ui/dom/buildDetachedRange.ts +4 -3
- package/src/lib/ui/dom/cloneStatic.ts +3 -2
- package/src/lib/ui/dom/each.ts +26 -7
- package/src/lib/ui/dom/foreignWrapperTag.ts +2 -2
- package/src/lib/ui/dom/mountChild.ts +8 -1
- package/src/lib/ui/dom/readCall.ts +6 -4
- package/src/lib/ui/dom/scopeLabel.ts +3 -3
- package/src/lib/ui/dom/switchBlock.ts +3 -1
- package/src/lib/ui/dom/types/EachRow.ts +4 -0
- package/src/lib/ui/dom/types/SkeletonHoles.ts +4 -2
- package/src/lib/ui/history.ts +1 -2
- package/src/lib/ui/html.ts +28 -0
- package/src/lib/ui/installHotBridge.ts +0 -2
- package/src/lib/ui/renderChain.ts +16 -3
- package/src/lib/ui/router.ts +24 -3
- package/src/lib/ui/rpcOutbox/createOutboxQueue.ts +1 -1
- package/src/lib/ui/runtime/CHILD_PRESENT.ts +8 -0
- package/src/lib/ui/runtime/OUTLET_MARKER.ts +2 -2
- package/src/lib/ui/runtime/RANGE_MARKER.ts +1 -1
- package/src/lib/ui/runtime/REACTIVE_CONTEXT.ts +9 -3
- package/src/lib/ui/runtime/captureModelDoc.ts +1 -1
- package/src/lib/ui/runtime/createDoc.ts +5 -1
- package/src/lib/ui/runtime/createEffectNode.ts +5 -5
- package/src/lib/ui/runtime/flushEffects.ts +22 -9
- package/src/lib/ui/runtime/historyEntries.ts +1 -1
- package/src/lib/ui/runtime/track.ts +4 -2
- package/src/lib/ui/runtime/trigger.ts +34 -20
- package/src/lib/ui/runtime/types/Route.ts +2 -2
- package/src/lib/ui/runtime/types/UiComponent.ts +3 -2
- package/src/lib/ui/runtime/types/UiProps.ts +9 -12
- package/src/lib/ui/runtime/walkPath.ts +15 -3
- package/src/lib/ui/startClient.ts +41 -14
- package/src/lib/ui/state.ts +3 -3
- package/src/lib/ui/types/Scope.ts +3 -3
- package/src/serverBuildPlugins.ts +11 -5
- package/src/zodCjsPlugin.ts +36 -0
- package/template/.zed/settings.json +7 -1
- package/template/package.json +3 -0
- package/template/src/ui/pages/layout.abide +3 -5
|
@@ -10,22 +10,27 @@ import {
|
|
|
10
10
|
import { asOutlet } from './asOutlet.ts'
|
|
11
11
|
import { awaitPlan } from './awaitPlan.ts'
|
|
12
12
|
import { composeProps } from './composeProps.ts'
|
|
13
|
+
import { eachPlan } from './eachPlan.ts'
|
|
14
|
+
import { elementPlan } from './elementPlan.ts'
|
|
13
15
|
import { groupBindParts } from './groupBindParts.ts'
|
|
14
16
|
import { ifPlan } from './ifPlan.ts'
|
|
17
|
+
import { interpolatedTemplateLiteral } from './interpolatedTemplateLiteral.ts'
|
|
15
18
|
import { isAnchorPositioned } from './isAnchorPositioned.ts'
|
|
16
19
|
import { lowerContext } from './lowerContext.ts'
|
|
17
20
|
import { makeVarNamer } from './makeVarNamer.ts'
|
|
18
21
|
import { scopeAttr } from './scopeAttr.ts'
|
|
19
22
|
import { skeletonContext } from './skeletonContext.ts'
|
|
23
|
+
import { snippetPlan } from './snippetPlan.ts'
|
|
20
24
|
import { spreadExcludedNames } from './spreadExcludedNames.ts'
|
|
21
25
|
import { staticAttr } from './staticAttr.ts'
|
|
22
26
|
import { staticTextPart } from './staticTextPart.ts'
|
|
23
27
|
import { stripEffects } from './stripEffects.ts'
|
|
24
28
|
import { switchPlan } from './switchPlan.ts'
|
|
25
29
|
import { tryPlan } from './tryPlan.ts'
|
|
26
|
-
import type {
|
|
30
|
+
import type { Binding } from './types/Binding.ts'
|
|
31
|
+
import type { ShadowKind } from './types/ShadowKind.ts'
|
|
27
32
|
import type { TemplateNode } from './types/TemplateNode.ts'
|
|
28
|
-
import {
|
|
33
|
+
import { withBindings } from './withBindings.ts'
|
|
29
34
|
|
|
30
35
|
/* The range boundary comments a control-flow block emits around its content. Sourced
|
|
31
36
|
from the SAME wire-alphabet constants the client's `document.createComment` markers
|
|
@@ -70,9 +75,15 @@ export function generateSSR(
|
|
|
70
75
|
expression: lowerExpression,
|
|
71
76
|
statement,
|
|
72
77
|
withNestedScripts,
|
|
78
|
+
withShadow,
|
|
73
79
|
bindRead,
|
|
74
80
|
} = lowerContext(stateNames, derivedNames, computedNames)
|
|
75
81
|
|
|
82
|
+
/* SSR has no cells, so every plan `Binding` — `reactive` or `plain` — renders as a `plain`
|
|
83
|
+
shadow (a real JS local / loop var / arrow param, read as the bare identifier). The one
|
|
84
|
+
mapping `withBindings` registers every binding through on this back-end. */
|
|
85
|
+
const ssrBindingKind = (_binding: Binding): ShadowKind => 'plain'
|
|
86
|
+
|
|
76
87
|
/* A scoped-script body for SSR: the shared lowering, then strip effects
|
|
77
88
|
(client-only lifecycle that emits no HTML) — the one SSR-side asymmetry. */
|
|
78
89
|
const lowerScript = (code: string): string => stripEffects(statement(code))
|
|
@@ -248,14 +259,19 @@ export function generateSSR(
|
|
|
248
259
|
return ''
|
|
249
260
|
}
|
|
250
261
|
if (node.kind === 'snippet') {
|
|
262
|
+
const plan = snippetPlan(node)
|
|
251
263
|
/* A hoisted function returning the snippet's `$snip`-branded HTML string;
|
|
252
|
-
`{name(args)}` pushes it via `$text`, which wraps it in markers.
|
|
253
|
-
|
|
264
|
+
`{name(args)}` pushes it via `$text`, which wraps it in markers. `args` are plain
|
|
265
|
+
call parameters — `withBindings` registers the plan's `plain` bindings so the body
|
|
266
|
+
reads the bare local, shadowing a same-named component signal rather than reading it. */
|
|
267
|
+
const body = withBindings(withShadow, plan.bindings, ssrBindingKind, () =>
|
|
268
|
+
generateInto(plan.children, '$o'),
|
|
269
|
+
)
|
|
254
270
|
/* `async` only when the body produces an `await` (it inlines a child component) — then
|
|
255
271
|
call sites `await` it (`$text(await frag())`). A sync snippet stays a plain function
|
|
256
272
|
called inline, preserving the sync render contract. */
|
|
257
|
-
const keyword = asyncSnippets.has(
|
|
258
|
-
return `${keyword} ${
|
|
273
|
+
const keyword = asyncSnippets.has(plan.name) ? 'async function' : 'function'
|
|
274
|
+
return `${keyword} ${plan.name}(${plan.params ?? ''}) {\nconst $o = [];\n${body}return $snip($o.join(''));\n}\n`
|
|
259
275
|
}
|
|
260
276
|
if (node.kind === 'script') {
|
|
261
277
|
/* A scoped reactive block: re-seed its local signals (lowered, in scope)
|
|
@@ -268,22 +284,35 @@ export function generateSSR(
|
|
|
268
284
|
return ''
|
|
269
285
|
}
|
|
270
286
|
if (node.kind === 'each') {
|
|
287
|
+
const plan = eachPlan(node)
|
|
271
288
|
/* Async each (`await`) is drained on the client — render no rows on the
|
|
272
289
|
server (an infinite stream would hang SSR); the client inserts its anchor
|
|
273
290
|
before the next sibling during hydration, like an empty sync each. In a
|
|
274
291
|
skeleton the `<!--a-->` anchor still marks its position (the client mounts
|
|
275
292
|
there); no range markers, since there are no server rows to claim. */
|
|
276
|
-
if (
|
|
293
|
+
if (plan.async) {
|
|
277
294
|
return anchor
|
|
278
295
|
}
|
|
279
|
-
|
|
296
|
+
/* The row item (and index) are real `for`-loop locals, so the body must lower
|
|
297
|
+
references to them as the bare identifier — `withBindings` registers the plan's
|
|
298
|
+
row bindings (under `plain`, SSR's only kind) so a row binding that shadows a
|
|
299
|
+
same-named component signal reads the loop value, not the (whole-list) signal it
|
|
300
|
+
shadows. The names come straight from `plan.bindings` (the single source the
|
|
301
|
+
client also reads); the items expression stays outside the shadow. */
|
|
302
|
+
const rowBody = withBindings(
|
|
303
|
+
withShadow,
|
|
304
|
+
plan.bindings,
|
|
305
|
+
ssrBindingKind,
|
|
306
|
+
() =>
|
|
307
|
+
`${openRange(target)}${branchContent(plan.children, target)}${closeRange(target)}`,
|
|
308
|
+
)
|
|
280
309
|
/* `index="i"` binds the row position. SSR reads it as a plain number from
|
|
281
310
|
`entries()` over a materialized array; the client reads the same number from a
|
|
282
311
|
cell, so first paint is congruent. No index → a plain `for…of` over the items. */
|
|
283
312
|
const header =
|
|
284
|
-
|
|
285
|
-
? `for (const ${
|
|
286
|
-
: `for (const [${
|
|
313
|
+
plan.index === undefined
|
|
314
|
+
? `for (const ${plan.as} of (${lowerExpression(plan.items)}))`
|
|
315
|
+
: `for (const [${plan.index}, ${plan.as}] of [...(${lowerExpression(plan.items)})].entries())`
|
|
287
316
|
return `${anchor}${header} {\n${rowBody}}\n`
|
|
288
317
|
}
|
|
289
318
|
if (node.kind === 'await') {
|
|
@@ -369,30 +398,33 @@ export function generateSSR(
|
|
|
369
398
|
for (const scope of node.scopes ?? []) {
|
|
370
399
|
code += push(target, scopeAttr(scope))
|
|
371
400
|
}
|
|
372
|
-
/*
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
401
|
+
/* The shared per-element emission DECISION (one site, consulted by the build back-end
|
|
402
|
+
too): each attribute classified + tagged with its merge status, the class/style/
|
|
403
|
+
directive merge folded in, and void-tag status. `class:`/`style:` directives collapse
|
|
404
|
+
with any static/interpolated `class`/`style` base into a SINGLE merged attribute (a
|
|
405
|
+
duplicate attribute would be invalid and ignored). SSR must always emit one attribute
|
|
406
|
+
string, so it merges whenever a directive exists (`mergeClass`/`mergeStyle`); those
|
|
407
|
+
attrs are tagged `mergedSSR` and skipped below. The client's classList.toggle /
|
|
408
|
+
style.setProperty effects re-apply the same values on hydrate, so the merged SSR value
|
|
409
|
+
already matches the post-mount DOM — no flash, no desync. SSR RENDERS each kind below
|
|
410
|
+
as an escaped string. */
|
|
411
|
+
const plan = elementPlan(node, lowerExpression)
|
|
412
|
+
const merge = plan.merge
|
|
413
|
+
for (const { attr, mergedSSR } of plan.attrs) {
|
|
414
|
+
/* Skip attrs folded into the merged class/style string below. */
|
|
415
|
+
if (mergedSSR) {
|
|
416
|
+
continue
|
|
417
|
+
}
|
|
383
418
|
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
|
-
}
|
|
391
419
|
code += push(target, staticAttr(attr.name, attr.value))
|
|
392
420
|
} else if (attr.kind === 'expression') {
|
|
393
421
|
/* present/absent semantics matching the client `attr` binding:
|
|
394
422
|
false/null/undefined drops it, true emits the bare attribute. */
|
|
395
423
|
code += `${target}.push($attr(${JSON.stringify(attr.name)}, ${lowerExpression(attr.code)}));\n`
|
|
424
|
+
} else if (attr.kind === 'interpolated') {
|
|
425
|
+
/* A string-valued attribute, always present (a merged class/style was skipped
|
|
426
|
+
above). */
|
|
427
|
+
code += `${target}.push($attr(${JSON.stringify(attr.name)}, ${lowerExpression(interpolatedTemplateLiteral(attr.parts))}));\n`
|
|
396
428
|
} else if (attr.kind === 'spread') {
|
|
397
429
|
/* `{...expr}` element spread: each key as an attribute, functions (event
|
|
398
430
|
handlers) skipped — the client `spreadAttrs` wires those on hydrate. Keys
|
|
@@ -415,38 +447,16 @@ export function generateSSR(
|
|
|
415
447
|
code += `${target}.push(${JSON.stringify(` ${attr.property}="`)} + $esc(${bindRead(attr.code)}) + '"');\n`
|
|
416
448
|
}
|
|
417
449
|
}
|
|
418
|
-
/* Merged class:
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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`
|
|
450
|
+
/* Merged class/style: the same composed parts the build effect uses (from the plan),
|
|
451
|
+
joined and escaped into one attribute string. */
|
|
452
|
+
if (merge.mergeClass) {
|
|
453
|
+
code += `${target}.push(' class="' + $esc([${merge.classParts.join(', ')}].filter(Boolean).join(' ')) + '"');\n`
|
|
431
454
|
}
|
|
432
|
-
|
|
433
|
-
|
|
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`
|
|
455
|
+
if (merge.mergeStyle) {
|
|
456
|
+
code += `${target}.push(' style="' + $esc([${merge.styleParts.join(', ')}].filter(Boolean).join(';')) + '"');\n`
|
|
447
457
|
}
|
|
448
458
|
code += push(target, '>')
|
|
449
|
-
if (!
|
|
459
|
+
if (!plan.isVoid) {
|
|
450
460
|
/* Each child's skeleton position (whether its reactive text interleaves into an
|
|
451
461
|
anchor, whether a nested block anchors) is already recorded by `skeletonContext`
|
|
452
462
|
— read per node, not tracked here. A `<script>` child scopes its bindings to
|
|
@@ -470,15 +480,13 @@ export function generateSSR(
|
|
|
470
480
|
anchor: string,
|
|
471
481
|
): string {
|
|
472
482
|
const wrap = inSkeleton.get(node)
|
|
473
|
-
const fallback = generateInto(node.children, target)
|
|
474
483
|
/* `$children` is an async builder the parent passes lazily; `await` it here so the
|
|
475
|
-
slot content's block ids allocate AT the
|
|
484
|
+
slot content's block ids allocate AT the slot position — the same order the
|
|
476
485
|
client builds slot content — keeping hydration congruent. The `await` makes a
|
|
477
|
-
component with a
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
: `if ($props && $props.$children) { ${target}.push(await $props.$children()); } else {\n${fallback}}\n`
|
|
486
|
+
component with a slot an async render (its caller already `await`s `render()`).
|
|
487
|
+
A fallback is now an authored `{#if children}…{:else}…{/if}`, so the slot node
|
|
488
|
+
carries no children. */
|
|
489
|
+
const body = `if ($props && $props.$children) { ${target}.push(await $props.$children()); }\n`
|
|
482
490
|
if (!wrap) {
|
|
483
491
|
return body
|
|
484
492
|
}
|
|
@@ -511,19 +519,37 @@ export function generateSSR(
|
|
|
511
519
|
): string {
|
|
512
520
|
const plan = awaitPlan(node)
|
|
513
521
|
const id = nextVar('$aid')
|
|
522
|
+
/* The resolved value lands in a synthetic var FIRST, then the author binding is
|
|
523
|
+
declared from it inside a nested block. Two reasons: the promise expression is
|
|
524
|
+
lowered in the outer scope where the binding name is not yet declared, so
|
|
525
|
+
`{#await foo then foo}` reads the outer `foo` (no temporal-dead-zone crash on a
|
|
526
|
+
same-named binding); and the nested block is the lexical scope the branch's
|
|
527
|
+
`withBindings` shadow models, so a reference to the binding reads the plain
|
|
528
|
+
local rather than the (unresolved) component signal it shadows. */
|
|
529
|
+
const resolved = nextVar('$av')
|
|
514
530
|
let code = `const ${id} = $ctx.next++;\n`
|
|
515
531
|
code += `${target}.push("<!--abide:await:" + ${id} + "-->");\n`
|
|
516
532
|
code += `try {\n`
|
|
517
|
-
code += `const ${
|
|
518
|
-
code +=
|
|
533
|
+
code += `const ${resolved} = await (${lowerExpression(node.promise)});\n`
|
|
534
|
+
code += `{\n`
|
|
535
|
+
code += `const ${plan.resolvedAs} = ${resolved};\n`
|
|
536
|
+
code += withBindings(withShadow, plan.resolvedBindings, ssrBindingKind, () =>
|
|
537
|
+
branchContent(plan.resolvedChildren, target),
|
|
538
|
+
)
|
|
539
|
+
/* `finally` does not bind the resolved value, so it is lowered OUTSIDE the `then`
|
|
540
|
+
shadow — matching the catch branch below, so a finally expression naming the
|
|
541
|
+
same identifier as the `then` binding reads the component signal, not the local. */
|
|
519
542
|
code += branchContent(plan.finallyChildren, target)
|
|
520
543
|
code += `$resume[${id}] = { ok: true, value: ${plan.resolvedAs} };\n`
|
|
544
|
+
code += `}\n`
|
|
521
545
|
if (plan.surfaceRejection) {
|
|
522
546
|
/* No catch/finally → let the rejection surface instead of an empty branch. */
|
|
523
547
|
code += `} catch (_error) { throw _error; }\n`
|
|
524
548
|
} else {
|
|
525
549
|
code += `} catch (${plan.catchAs}) {\n`
|
|
526
|
-
code +=
|
|
550
|
+
code += withBindings(withShadow, plan.catchBindings, ssrBindingKind, () =>
|
|
551
|
+
branchContent(plan.catchChildren, target),
|
|
552
|
+
)
|
|
527
553
|
code += branchContent(plan.finallyChildren, target)
|
|
528
554
|
code += `$resume[${id}] = { ok: false, error: String(${plan.catchAs}) };\n`
|
|
529
555
|
code += `}\n`
|
|
@@ -549,15 +575,26 @@ export function generateSSR(
|
|
|
549
575
|
code += `${target}.push("<!--abide:await:" + ${id} + "-->");\n`
|
|
550
576
|
code += branchContent(plan.pending, target)
|
|
551
577
|
code += `${target}.push("<!--/abide:await:" + ${id} + "-->");\n`
|
|
552
|
-
|
|
553
|
-
|
|
578
|
+
/* The settled renderer takes the resolved/error value as a real arrow parameter
|
|
579
|
+
(`async (foo) => …`), so its body must lower references to that name as the plain
|
|
580
|
+
local — `withBindings` registers the plan's bindings (under `plain`, SSR's only kind)
|
|
581
|
+
so a binding that shadows a same-named component signal reads the value, not the
|
|
582
|
+
signal. The arrow param is the binding's author name; the names enter scope from the
|
|
583
|
+
same `plan.*Bindings` the client reads. */
|
|
584
|
+
const settled = (param: string, bindings: Binding[], children: TemplateNode[]) =>
|
|
585
|
+
`async (${param}) => { const $o = []; ${withBindings(
|
|
586
|
+
withShadow,
|
|
587
|
+
bindings,
|
|
588
|
+
ssrBindingKind,
|
|
589
|
+
() => branchContent(children, '$o'),
|
|
590
|
+
)}${branchContent(plan.finallyChildren, '$o')}return $o.join(''); }`
|
|
554
591
|
const catchProp = plan.surfaceRejection
|
|
555
592
|
? ''
|
|
556
|
-
: `catch: ${settled(plan.catchAs, plan.catchChildren)} `
|
|
593
|
+
: `catch: ${settled(plan.catchAs, plan.catchBindings, plan.catchChildren)} `
|
|
557
594
|
code +=
|
|
558
595
|
`$awaits.push({ id: ${id}, ` +
|
|
559
596
|
`promise: () => (${lowerExpression(node.promise)}), ` +
|
|
560
|
-
`then: ${settled(plan.resolvedAs, plan.resolvedChildren)}, ` +
|
|
597
|
+
`then: ${settled(plan.resolvedAs, plan.resolvedBindings, plan.resolvedChildren)}, ` +
|
|
561
598
|
`${catchProp}});\n`
|
|
562
599
|
return code
|
|
563
600
|
}
|
|
@@ -580,7 +617,9 @@ export function generateSSR(
|
|
|
580
617
|
code += branchContent(plan.finallyChildren, target)
|
|
581
618
|
code += `} catch (${plan.catchAs}) {\n${target}.length = ${mark};\n`
|
|
582
619
|
if (plan.hasCatch) {
|
|
583
|
-
code +=
|
|
620
|
+
code += withBindings(withShadow, plan.catchBindings, ssrBindingKind, () =>
|
|
621
|
+
branchContent(plan.catchChildren, target),
|
|
622
|
+
)
|
|
584
623
|
code += branchContent(plan.finallyChildren, target)
|
|
585
624
|
} else {
|
|
586
625
|
code += `throw ${plan.catchAs};\n`
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
import type { Binding } from './types/Binding.ts'
|
|
1
2
|
import type { TemplateNode } from './types/TemplateNode.ts'
|
|
2
3
|
|
|
3
4
|
type Case = Extract<TemplateNode, { kind: 'case' }>
|
|
4
5
|
|
|
5
6
|
/* The structural shape of an `if` chain, resolved once so the build and SSR back-ends share
|
|
6
7
|
one partition of its children and only own emission. Both previously recomputed the
|
|
7
|
-
then-content / branch split and the elseif/else classification.
|
|
8
|
+
then-content / branch split and the elseif/else classification. An `if`/`elseif`/`else`
|
|
9
|
+
introduces no names, so `bindings` is empty — present so every block plan answers the
|
|
10
|
+
single binding source uniformly. */
|
|
8
11
|
export type IfPlan = {
|
|
9
12
|
/* The `then` content — every child that is not an `elseif`/`else` branch. */
|
|
10
13
|
thenChildren: TemplateNode[]
|
|
@@ -16,6 +19,8 @@ export type IfPlan = {
|
|
|
16
19
|
hasElseif: boolean
|
|
17
20
|
/* The match-less `else` branch, if present. */
|
|
18
21
|
elseBranch: Case | undefined
|
|
22
|
+
/* An `if` chain binds no names. */
|
|
23
|
+
bindings: Binding[]
|
|
19
24
|
}
|
|
20
25
|
|
|
21
26
|
/* Partitions an `if` node's children into the shared structural plan. */
|
|
@@ -26,5 +31,6 @@ export function ifPlan(node: Extract<TemplateNode, { kind: 'if' }>): IfPlan {
|
|
|
26
31
|
branches,
|
|
27
32
|
hasElseif: branches.some((branch) => branch.condition !== undefined),
|
|
28
33
|
elseBranch: branches.find((branch) => branch.condition === undefined),
|
|
34
|
+
bindings: [],
|
|
29
35
|
}
|
|
30
36
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { TextPart } from './types/TextPart.ts'
|
|
2
|
+
|
|
3
|
+
/* Escapes a literal attribute chunk for embedding in a JS template literal: a
|
|
4
|
+
backslash, backtick, or `${` would otherwise start an escape/expression. */
|
|
5
|
+
function escapeTemplateChunk(value: string): string {
|
|
6
|
+
return value.replace(/[\\`]/g, '\\$&').replace(/\$\{/g, '\\${')
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/*
|
|
10
|
+
Builds the RAW (un-lowered) template-literal source for an interpolated attribute
|
|
11
|
+
or prop value — static parts escaped, each `{expr}` spliced as `${code}`. The caller
|
|
12
|
+
lowers the whole result through its signal transformer, so the embedded expressions
|
|
13
|
+
are rewritten exactly like any other template expression. Both back-ends and the
|
|
14
|
+
component-prop path share this one builder, so server markup, client binding, and
|
|
15
|
+
prop value can't diverge on the concatenation. Always yields a string, so an
|
|
16
|
+
interpolated attribute is always present.
|
|
17
|
+
*/
|
|
18
|
+
export function interpolatedTemplateLiteral(parts: TextPart[]): string {
|
|
19
|
+
const body = parts
|
|
20
|
+
.map((part) =>
|
|
21
|
+
part.kind === 'static' ? escapeTemplateChunk(part.value) : `\${${part.code}}`,
|
|
22
|
+
)
|
|
23
|
+
.join('')
|
|
24
|
+
return `\`${body}\``
|
|
25
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/* Compiled once at module load — re-evaluating the literal per call would recompile it. */
|
|
2
|
+
const PLAIN_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/
|
|
3
|
+
|
|
4
|
+
/* Matches a JS identifier — a plain binding `as`/index that reads/keys directly, vs a
|
|
5
|
+
destructuring pattern that re-applies per read. Shared by `reactiveBinding` (cell deref)
|
|
6
|
+
and `each` key derivation so the one identifier test stays single-source. */
|
|
7
|
+
export const isPlainIdentifier = (name: string): boolean => PLAIN_IDENTIFIER.test(name)
|
|
@@ -2,13 +2,14 @@ import type { TemplateNode } from './types/TemplateNode.ts'
|
|
|
2
2
|
|
|
3
3
|
/*
|
|
4
4
|
An element whose children are ONLY text and `<style>` — the common `<h1>{x}</h1>` shape.
|
|
5
|
-
The
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
the
|
|
5
|
+
The anchor-placement decision for reactive text: `skeletonContext` consults this once and
|
|
6
|
+
records the result in `markText`; both back-ends read that map so they agree on where a
|
|
7
|
+
text anchor goes. A text-leaf binds its reactive text marker-free on the located element
|
|
8
|
+
(`generateBuild`) / emits it without an `<!--a-->` prefix (`generateSSR`); reactive text
|
|
9
|
+
interleaved with element children is anchor-positioned instead. Computing it once keeps
|
|
10
|
+
the SSR string and the client skeleton from disagreeing about a `<!--a-->` — the first
|
|
11
|
+
slice of lifting the positional model out of the two parallel traversals, alongside the
|
|
12
|
+
already-shared `isControlFlow` and `skeletonable`.
|
|
12
13
|
*/
|
|
13
14
|
export function isTextLeaf(node: Extract<TemplateNode, { kind: 'element' }>): boolean {
|
|
14
15
|
return node.children.every((child) => child.kind === 'text' || child.kind === 'style')
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import ts from 'typescript'
|
|
2
|
+
import { createShadowScope } from './createShadowScope.ts'
|
|
2
3
|
import { docAccessTransformer } from './lowerDocAccess.ts'
|
|
3
4
|
import { nestedBindingNames } from './prepareNestedScript.ts'
|
|
4
5
|
import { signalRefsTransformer } from './renameSignalRefs.ts'
|
|
@@ -6,6 +7,9 @@ import { TS_PRINTER } from './TS_PRINTER.ts'
|
|
|
6
7
|
import type { TemplateNode } from './types/TemplateNode.ts'
|
|
7
8
|
import { unwrapParens } from './unwrapParens.ts'
|
|
8
9
|
|
|
10
|
+
/* Compiled once — strips the trailing `;` off every lowered expression. */
|
|
11
|
+
const TRAILING_SEMICOLON = /;$/
|
|
12
|
+
|
|
9
13
|
/*
|
|
10
14
|
The shared expression-lowering context both back-ends build on: the signal→`model`
|
|
11
15
|
rewrite and doc-access lowering that turns the signal surface the author writes
|
|
@@ -22,12 +26,14 @@ export function lowerContext(
|
|
|
22
26
|
derivedNames: ReadonlySet<string>,
|
|
23
27
|
computedNames: ReadonlySet<string> = new Set(),
|
|
24
28
|
) {
|
|
25
|
-
/*
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
/* The typed branch-local shadow stack: one auto-popping value owning both kinds.
|
|
30
|
+
`derived` names deref to `.value` like a `computed` (block value params the client
|
|
31
|
+
binds as a reactive cell, and nested-`<script>` bindings); `plain` names deref as
|
|
32
|
+
the bare identifier (block value params SSR binds as a real JS local). Both, as a
|
|
33
|
+
nearer lexical scope, SHADOW a same-named component signal. `withShadow` pushes on
|
|
34
|
+
entry and pops in a `finally`, so a branch's shadows cannot outlive the branch even
|
|
35
|
+
if its body throws — the leak the old hand-written pop allowed. */
|
|
36
|
+
const scope = createShadowScope()
|
|
31
37
|
|
|
32
38
|
/* Parse `code` once and chain the reference rename and doc-access lowering over the
|
|
33
39
|
one tree — the two string passes would each parse + reprint. `localDerived` is
|
|
@@ -36,8 +42,14 @@ export function lowerContext(
|
|
|
36
42
|
function lowerOnce(code: string): string {
|
|
37
43
|
const source = ts.createSourceFile('expr.ts', code, ts.ScriptTarget.Latest, true)
|
|
38
44
|
const result = ts.transform(source, [
|
|
39
|
-
signalRefsTransformer(
|
|
40
|
-
|
|
45
|
+
signalRefsTransformer(
|
|
46
|
+
stateNames,
|
|
47
|
+
derivedNames,
|
|
48
|
+
computedNames,
|
|
49
|
+
new Set(scope.names('derived')),
|
|
50
|
+
new Set(scope.names('plain')),
|
|
51
|
+
),
|
|
52
|
+
docAccessTransformer('$$model'),
|
|
41
53
|
])
|
|
42
54
|
const output = TS_PRINTER.printFile(result.transformed[0] as ts.SourceFile).trim()
|
|
43
55
|
result.dispose()
|
|
@@ -48,7 +60,7 @@ export function lowerContext(
|
|
|
48
60
|
literal (`{ a: 1 }`) parses as an expression, not a block of labeled statements,
|
|
49
61
|
through the rewrite; the wrapper is then peeled back off. */
|
|
50
62
|
function expression(code: string): string {
|
|
51
|
-
return unwrapParens(lowerOnce(`(${code})`).replace(
|
|
63
|
+
return unwrapParens(lowerOnce(`(${code})`).replace(TRAILING_SEMICOLON, ''))
|
|
52
64
|
}
|
|
53
65
|
|
|
54
66
|
/* As above but keeps the trailing `;` for a statement/handler body. */
|
|
@@ -93,45 +105,23 @@ export function lowerContext(
|
|
|
93
105
|
}
|
|
94
106
|
|
|
95
107
|
/* Adds any `<script>` children's binding names to the deref scope (so the script
|
|
96
|
-
bodies and the branch's markup auto-deref them)
|
|
97
|
-
then pops the names it added. Returns whatever `body` produces. */
|
|
108
|
+
bodies and the branch's markup auto-deref them) for the duration of `body`. */
|
|
98
109
|
function withNestedScripts<T>(children: TemplateNode[], body: () => T): T {
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (!localDerived.has(name)) {
|
|
104
|
-
localDerived.add(name)
|
|
105
|
-
added.push(name)
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
const result = body()
|
|
111
|
-
for (const name of added) {
|
|
112
|
-
localDerived.delete(name)
|
|
113
|
-
}
|
|
114
|
-
return result
|
|
110
|
+
const names = children.flatMap((child) =>
|
|
111
|
+
child.kind === 'script' ? [...nestedBindingNames(child.code)] : [],
|
|
112
|
+
)
|
|
113
|
+
return scope.withShadow(names, 'derived', body)
|
|
115
114
|
}
|
|
116
115
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
const result = body()
|
|
130
|
-
for (const name of added) {
|
|
131
|
-
localDerived.delete(name)
|
|
132
|
-
}
|
|
133
|
-
return result
|
|
116
|
+
return {
|
|
117
|
+
expression,
|
|
118
|
+
statement,
|
|
119
|
+
withNestedScripts,
|
|
120
|
+
/* The raw branch-local shadow registration both back-ends drive `withBindings`
|
|
121
|
+
through: a block's bindings flow to a `ShadowKind` only via that one shared loop,
|
|
122
|
+
never through a per-block `scope.withShadow` call here. */
|
|
123
|
+
withShadow: scope.withShadow,
|
|
124
|
+
bindRead,
|
|
125
|
+
bindWrite,
|
|
134
126
|
}
|
|
135
|
-
|
|
136
|
-
return { expression, statement, withNestedScripts, withLocalDerived, bindRead, bindWrite }
|
|
137
127
|
}
|
|
@@ -191,7 +191,7 @@ export function docAccessTransformer(docName: string): ts.TransformerFactory<ts.
|
|
|
191
191
|
naming the authored scope path and member — the key and member are both
|
|
192
192
|
in hand here, the `.abide` location comes free off the mapped stack. */
|
|
193
193
|
return ts.factory.createCallExpression(
|
|
194
|
-
ts.factory.createIdentifier('readCall'),
|
|
194
|
+
ts.factory.createIdentifier('$$readCall'),
|
|
195
195
|
undefined,
|
|
196
196
|
[
|
|
197
197
|
read,
|
|
@@ -268,7 +268,7 @@ function buildPath(segments: Segment[]): ts.Expression {
|
|
|
268
268
|
key holding `/`|`~` (a date, a composite id) addresses one segment, not many. */
|
|
269
269
|
fragments.push(
|
|
270
270
|
ts.factory.createCallExpression(
|
|
271
|
-
ts.factory.createIdentifier('escapeKey'),
|
|
271
|
+
ts.factory.createIdentifier('$$escapeKey'),
|
|
272
272
|
undefined,
|
|
273
273
|
[segment.node],
|
|
274
274
|
),
|
|
@@ -34,7 +34,7 @@ export function lowerScript(scriptBody: string): {
|
|
|
34
34
|
const result = ts.transform(source, [
|
|
35
35
|
transformer,
|
|
36
36
|
signalRefsTransformer(stateNames, derivedNames, computedNames),
|
|
37
|
-
docAccessTransformer('model'),
|
|
37
|
+
docAccessTransformer('$$model'),
|
|
38
38
|
])
|
|
39
39
|
const transformed = result.transformed[0] as ts.SourceFile
|
|
40
40
|
/* Top-level imports must live at module scope, not inside the mount/render
|