@abide/abide 0.43.0 → 0.44.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +293 -219
- package/CHANGELOG.md +46 -0
- package/README.md +191 -67
- package/package.json +2 -2
- package/src/abideLsp.ts +8 -4
- package/src/abideResolverPlugin.ts +11 -1
- package/src/build.ts +9 -0
- package/src/buildDisconnected.ts +2 -0
- package/src/devEntry.ts +1 -0
- package/src/lib/bundle/probeAbideServer.ts +1 -1
- package/src/lib/cli/parseArgvForRpc.ts +4 -2
- package/src/lib/cli/types/CliManifest.ts +6 -4
- package/src/lib/mcp/annotationsForMethod.ts +1 -1
- package/src/lib/mcp/toolResultFromResponse.ts +1 -1
- package/src/lib/mcp/types/JsonRpcRequest.ts +3 -3
- package/src/lib/server/AppModule.ts +1 -1
- package/src/lib/server/cli/handleCliDownload.ts +7 -5
- package/src/lib/server/cli/installScript.ts +3 -3
- package/src/lib/server/prompts/promptRegistry.ts +1 -1
- package/src/lib/server/prompts/types/Prompt.ts +2 -1
- package/src/lib/server/rpc/dispatchRpcInProcess.ts +2 -2
- package/src/lib/server/rpc/types/RpcHelper.ts +1 -1
- package/src/lib/server/runtime/createPublicAssetServer.ts +2 -2
- package/src/lib/server/runtime/createRouteDispatcher.ts +1 -1
- package/src/lib/server/runtime/createServer.ts +21 -0
- package/src/lib/server/runtime/createUiPageRenderer.ts +7 -11
- package/src/lib/server/runtime/crossOriginForbidden.ts +2 -2
- package/src/lib/server/runtime/crossOriginGate.ts +1 -1
- package/src/lib/server/runtime/devHotModuleResponse.ts +2 -2
- package/src/lib/server/runtime/disableIdleTimeoutForStream.ts +1 -1
- package/src/lib/server/runtime/logExposedSurfaces.ts +3 -4
- package/src/lib/server/runtime/serializeCacheSnapshot.ts +3 -8
- package/src/lib/server/runtime/snapshotEntryFromCache.ts +10 -7
- package/src/lib/server/runtime/streamFromIterator.ts +2 -2
- package/src/lib/server/runtime/types/InspectorRpc.ts +2 -2
- package/src/lib/server/socket.ts +1 -1
- package/src/lib/server/sockets/createSocketDispatcher.ts +2 -2
- package/src/lib/server/sockets/socketRegistry.ts +2 -2
- package/src/lib/server/sockets/types/SocketClientFrame.ts +3 -3
- package/src/lib/shared/CACHE_WRAPPED.ts +3 -2
- package/src/lib/shared/REMOTE_FUNCTION.ts +1 -1
- package/src/lib/shared/STREAMING_CONTENT_TYPES.ts +3 -2
- package/src/lib/shared/bodyValueForKind.ts +35 -0
- package/src/lib/shared/cache.ts +1 -1
- package/src/lib/shared/cacheEntryFromSnapshot.ts +23 -18
- package/src/lib/shared/createPushIterator.ts +1 -1
- package/src/lib/shared/decodeResponse.ts +11 -8
- package/src/lib/shared/hasReplayableRequest.ts +17 -0
- package/src/lib/shared/html.ts +7 -26
- package/src/lib/shared/manifestModule.ts +2 -2
- package/src/lib/shared/online.ts +1 -1
- package/src/lib/shared/outboxProbeSlot.ts +1 -1
- package/src/lib/shared/pageUrlForFile.ts +1 -1
- package/src/lib/shared/routeParamsShape.ts +20 -0
- package/src/lib/shared/snapshotShippable.ts +22 -0
- package/src/lib/shared/types/CacheEntry.ts +1 -1
- package/src/lib/shared/types/CacheInvalidation.ts +3 -3
- package/src/lib/shared/types/ClientFlags.ts +5 -3
- package/src/lib/shared/types/LastConnection.ts +1 -1
- package/src/lib/shared/url.ts +4 -1
- package/src/lib/shared/withBase.ts +1 -2
- package/src/lib/shared/writeDts.ts +2 -2
- package/src/lib/shared/writeRoutesDts.ts +3 -31
- package/src/lib/test/createTestSocketChannel.ts +3 -3
- package/src/lib/ui/COMPONENT_WRAPPER_PREFIX.ts +1 -1
- package/src/lib/ui/compile/ABIDE_SEMANTIC_TOKENS_LEGEND.ts +40 -6
- package/src/lib/ui/compile/REACTIVE_CALLEES.ts +1 -1
- package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +49 -37
- package/src/lib/ui/compile/abideUiPlugin.ts +1 -1
- package/src/lib/ui/compile/assertRuntimeHelpersBound.ts +3 -1
- package/src/lib/ui/compile/awaitPlan.ts +14 -2
- package/src/lib/ui/compile/catchBinding.ts +8 -0
- package/src/lib/ui/compile/classStyleMergePlan.ts +108 -0
- package/src/lib/ui/compile/collectAbideDiagnostics.ts +11 -0
- package/src/lib/ui/compile/compileComponent.ts +4 -4
- package/src/lib/ui/compile/compileModule.ts +20 -6
- package/src/lib/ui/compile/compileSSR.ts +12 -10
- package/src/lib/ui/compile/compileShadow.ts +107 -16
- package/src/lib/ui/compile/composeProps.ts +3 -3
- package/src/lib/ui/compile/createShadowLanguageService.ts +30 -2
- package/src/lib/ui/compile/createShadowProgram.ts +2 -1
- package/src/lib/ui/compile/createShadowScope.ts +42 -0
- package/src/lib/ui/compile/decodeHtmlEntities.ts +4 -0
- package/src/lib/ui/compile/desugarSignals.ts +12 -7
- package/src/lib/ui/compile/eachPlan.ts +61 -0
- package/src/lib/ui/compile/elementPlan.ts +55 -0
- package/src/lib/ui/compile/encodeSemanticTokens.ts +24 -1
- package/src/lib/ui/compile/generateBuild.ts +150 -127
- package/src/lib/ui/compile/generateSSR.ts +112 -73
- package/src/lib/ui/compile/ifPlan.ts +7 -1
- package/src/lib/ui/compile/interpolatedTemplateLiteral.ts +25 -0
- package/src/lib/ui/compile/isPlainIdentifier.ts +7 -0
- package/src/lib/ui/compile/isTextLeaf.ts +8 -7
- package/src/lib/ui/compile/lowerContext.ts +36 -46
- package/src/lib/ui/compile/lowerDocAccess.ts +2 -2
- package/src/lib/ui/compile/lowerScript.ts +1 -1
- package/src/lib/ui/compile/markupTokens.ts +313 -0
- package/src/lib/ui/compile/pagePropsType.ts +23 -0
- package/src/lib/ui/compile/parseTemplate.ts +131 -61
- package/src/lib/ui/compile/reactiveBinding.ts +47 -0
- package/src/lib/ui/compile/renameSignalRefs.ts +44 -10
- package/src/lib/ui/compile/skeletonable.ts +5 -5
- package/src/lib/ui/compile/snippetPlan.ts +28 -0
- package/src/lib/ui/compile/spreadExcludedNames.ts +4 -4
- package/src/lib/ui/compile/structuralBlockTokens.ts +3 -4
- package/src/lib/ui/compile/switchPlan.ts +7 -1
- package/src/lib/ui/compile/tryPlan.ts +8 -1
- package/src/lib/ui/compile/types/AnalyzedComponent.ts +2 -2
- package/src/lib/ui/compile/types/Binding.ts +24 -0
- package/src/lib/ui/compile/types/CompiledShadow.ts +13 -1
- package/src/lib/ui/compile/types/ElementPlan.ts +43 -0
- package/src/lib/ui/compile/types/ShadowKind.ts +9 -0
- package/src/lib/ui/compile/types/ShadowScope.ts +19 -0
- package/src/lib/ui/compile/types/TemplateAttr.ts +7 -0
- package/src/lib/ui/compile/types/TemplateNode.ts +8 -8
- package/src/lib/ui/compile/withBindings.ts +45 -0
- package/src/lib/ui/createScope.ts +2 -2
- package/src/lib/ui/dom/MATHML_NAMESPACE.ts +2 -2
- package/src/lib/ui/dom/SVG_NAMESPACE.ts +3 -2
- package/src/lib/ui/dom/appendText.ts +1 -1
- package/src/lib/ui/dom/applyResolved.ts +1 -1
- package/src/lib/ui/dom/buildDetachedRange.ts +4 -3
- package/src/lib/ui/dom/cloneStatic.ts +3 -2
- package/src/lib/ui/dom/each.ts +26 -7
- package/src/lib/ui/dom/foreignWrapperTag.ts +2 -2
- package/src/lib/ui/dom/mountChild.ts +8 -1
- package/src/lib/ui/dom/readCall.ts +6 -4
- package/src/lib/ui/dom/scopeLabel.ts +3 -3
- package/src/lib/ui/dom/switchBlock.ts +3 -1
- package/src/lib/ui/dom/types/EachRow.ts +4 -0
- package/src/lib/ui/dom/types/SkeletonHoles.ts +4 -2
- package/src/lib/ui/history.ts +1 -2
- package/src/lib/ui/html.ts +28 -0
- package/src/lib/ui/installHotBridge.ts +0 -2
- package/src/lib/ui/renderChain.ts +16 -3
- package/src/lib/ui/router.ts +66 -15
- package/src/lib/ui/rpcOutbox/createOutboxQueue.ts +1 -1
- package/src/lib/ui/runtime/CHILD_PRESENT.ts +8 -0
- package/src/lib/ui/runtime/OUTLET_MARKER.ts +2 -2
- package/src/lib/ui/runtime/RANGE_MARKER.ts +1 -1
- package/src/lib/ui/runtime/REACTIVE_CONTEXT.ts +9 -3
- package/src/lib/ui/runtime/captureModelDoc.ts +1 -1
- package/src/lib/ui/runtime/createDoc.ts +5 -1
- package/src/lib/ui/runtime/createEffectNode.ts +5 -5
- package/src/lib/ui/runtime/flushEffects.ts +22 -9
- package/src/lib/ui/runtime/historyEntries.ts +1 -1
- package/src/lib/ui/runtime/track.ts +4 -2
- package/src/lib/ui/runtime/trigger.ts +34 -20
- package/src/lib/ui/runtime/types/Route.ts +2 -2
- package/src/lib/ui/runtime/types/UiComponent.ts +3 -2
- package/src/lib/ui/runtime/types/UiProps.ts +9 -12
- package/src/lib/ui/runtime/walkPath.ts +15 -3
- package/src/lib/ui/startClient.ts +41 -14
- package/src/lib/ui/state.ts +3 -3
- package/src/lib/ui/types/Scope.ts +3 -3
- package/src/serverBuildPlugins.ts +11 -5
- package/src/zodCjsPlugin.ts +36 -0
- package/template/.zed/settings.json +7 -1
- package/template/package.json +3 -0
- package/template/src/ui/pages/layout.abide +3 -5
|
@@ -6,9 +6,9 @@ import type { AnalyzedComponent } from './types/AnalyzedComponent.ts'
|
|
|
6
6
|
/*
|
|
7
7
|
Compiles a single-file abide component into the body of a client build function.
|
|
8
8
|
Runs the shared front-end (`analyzeComponent`), generates the DOM build from the
|
|
9
|
-
template, and hoists static paths to cells. The returned body
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
template, and hoists static paths to cells. The returned body operates on `host`
|
|
10
|
+
with the `$$`-prefixed dom bindings and `$$model` in scope (the latter emitted by
|
|
11
|
+
`desugarSignals`). `compileModule` wraps it (and the SSR body) into
|
|
12
12
|
a real module; tests wrap it with `new Function`.
|
|
13
13
|
|
|
14
14
|
`analyzed` is a lazy default: a direct caller (tests) omits it and the front-end
|
|
@@ -26,5 +26,5 @@ export function compileComponent(
|
|
|
26
26
|
/* The scoped CSS is bundled into the entry stylesheet (see `abideUiPlugin`), not
|
|
27
27
|
injected at runtime; the build only needs the `data-a-…` scope attributes on
|
|
28
28
|
elements, which `generateBuild` reads from each node's annotated `scopes`. */
|
|
29
|
-
return `${script}\n${hoistCells(build, 'model')}`
|
|
29
|
+
return `${script}\n${hoistCells(build, '$$model')}`
|
|
30
30
|
}
|
|
@@ -44,7 +44,13 @@ export function compileModule(
|
|
|
44
44
|
leaf components today; one importing children falls back to a reload (the dev
|
|
45
45
|
layer decides what to hot-swap). */
|
|
46
46
|
if (options.hot) {
|
|
47
|
-
|
|
47
|
+
/* Bridge keys are the bare source names; bind each to its emitted local (`$$`
|
|
48
|
+
alias when set), so the hot body's `$$mountChild(...)` resolves. */
|
|
49
|
+
const names = UI_RUNTIME_IMPORTS.map((entry) =>
|
|
50
|
+
entry.alias === undefined || entry.alias === entry.name
|
|
51
|
+
? entry.name
|
|
52
|
+
: `${entry.name}: ${entry.alias}`,
|
|
53
|
+
).join(', ')
|
|
48
54
|
const id = JSON.stringify(options.moduleId)
|
|
49
55
|
return `const { ${names}, hotReplace } = window.__abide
|
|
50
56
|
${userImports}
|
|
@@ -52,7 +58,7 @@ function build(host, $props) {
|
|
|
52
58
|
${body}
|
|
53
59
|
}
|
|
54
60
|
function component(host, $props) {
|
|
55
|
-
return mount(host, build, $props)
|
|
61
|
+
return $$mount(host, build, $props)
|
|
56
62
|
}
|
|
57
63
|
component.build = build
|
|
58
64
|
component.__abideId = ${id}
|
|
@@ -66,12 +72,12 @@ ${body}
|
|
|
66
72
|
}
|
|
67
73
|
|
|
68
74
|
export default function component(host, $props) {
|
|
69
|
-
return mount(host, build, $props)
|
|
75
|
+
return $$mount(host, build, $props)
|
|
70
76
|
}
|
|
71
77
|
|
|
72
78
|
/* Adopt the server-rendered DOM in place instead of rebuilding it. */
|
|
73
79
|
export function hydrateInto(host, $props) {
|
|
74
|
-
return hydrate(host, build, $props)
|
|
80
|
+
return $$hydrate(host, build, $props)
|
|
75
81
|
}
|
|
76
82
|
|
|
77
83
|
export function render($props, $ctx) {
|
|
@@ -96,8 +102,16 @@ ${options.moduleId === undefined ? '' : `component.__abideId = ${JSON.stringify(
|
|
|
96
102
|
forces a spurious import, so no per-surface scoping is needed: a client-only helper
|
|
97
103
|
simply doesn't appear as an identifier in the SSR body. */
|
|
98
104
|
const referenced = collectIdentifiers(`${userImports}\n${body}\n${ssrBody}\n${moduleBody}`)
|
|
99
|
-
const importBlock = UI_RUNTIME_IMPORTS.filter((entry) =>
|
|
100
|
-
.
|
|
105
|
+
const importBlock = UI_RUNTIME_IMPORTS.filter((entry) =>
|
|
106
|
+
referenced.has(entry.alias ?? entry.name),
|
|
107
|
+
)
|
|
108
|
+
.map((entry) => {
|
|
109
|
+
const local =
|
|
110
|
+
entry.alias === undefined || entry.alias === entry.name
|
|
111
|
+
? entry.name
|
|
112
|
+
: `${entry.name} as ${entry.alias}`
|
|
113
|
+
return `import { ${local} } from '${ABIDE_PACKAGE_NAME}/${entry.specifier}'`
|
|
114
|
+
})
|
|
101
115
|
.join('\n')
|
|
102
116
|
const module = `${importBlock}
|
|
103
117
|
${userImports}
|
|
@@ -5,17 +5,20 @@ import type { AnalyzedComponent } from './types/AnalyzedComponent.ts'
|
|
|
5
5
|
|
|
6
6
|
/*
|
|
7
7
|
Compiles a component into the body of a server render function. Runs the shared
|
|
8
|
-
front-end, then the SSR back-end, and returns `{ html, state, awaits }`:
|
|
8
|
+
front-end, then the SSR back-end, and returns `{ html, state, awaits, resume }`:
|
|
9
9
|
|
|
10
10
|
- `html` — server-rendered markup (await blocks render their pending shell);
|
|
11
11
|
- `state` — the document snapshot the client adopts on resume;
|
|
12
12
|
- `awaits` — pending await blocks (id + promise + resolved/error renderers) that
|
|
13
|
-
`renderToStream` flushes out of order; empty for a fully synchronous component
|
|
13
|
+
`renderToStream` flushes out of order; empty for a fully synchronous component;
|
|
14
|
+
- `resume` — blocking await values rendered inline, keyed by boundary id, seeded
|
|
15
|
+
into the hydration manifest.
|
|
14
16
|
|
|
15
17
|
Effects are stripped — they are client lifecycle and emit no HTML, so the server
|
|
16
18
|
render is a snapshot of the markup before any effect runs.
|
|
17
19
|
|
|
18
|
-
Defines
|
|
20
|
+
Defines `$$model` via the lowered script; the reactive scope is entered via `$$enterScope`
|
|
21
|
+
and exited in a `finally` block. The block-id counter is
|
|
19
22
|
the request-local `$ctx` (threaded into child renders), not a module global — a blocking
|
|
20
23
|
`await` block awaits its promise at its structural position and renders inline, so ids
|
|
21
24
|
allocate depth-first like the client; render yields at that `await`, and a shared global
|
|
@@ -29,10 +32,9 @@ purely synchronous / streaming-await / try-only component returns `SsrRender` di
|
|
|
29
32
|
framework always `await`s `render()`, so production is uniform either way; the sync return
|
|
30
33
|
keeps static pages off the microtask queue and leaves the bulk of the SSR tests synchronous.
|
|
31
34
|
|
|
32
|
-
The reactive scope brackets the body
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
the awaits).
|
|
35
|
+
The reactive scope brackets the body: `$$enterScope()` runs synchronously before any inline
|
|
36
|
+
`await`, and `$$model` is captured for the rest of the render (its reads are object-method
|
|
37
|
+
calls, not scope lookups, so they stay correct across the awaits).
|
|
36
38
|
|
|
37
39
|
`analyzed` is a lazy default: a direct caller (tests) omits it and the front-end
|
|
38
40
|
runs here, but `compileModule` shares one analysis across both back-ends.
|
|
@@ -51,9 +53,9 @@ export function compileSSR(
|
|
|
51
53
|
/* `typeof model` guards a component with no reactive state (a pure-async or
|
|
52
54
|
static component declares no `model`); its snapshot is then empty. */
|
|
53
55
|
const body =
|
|
54
|
-
`const $scope = enterScope();\ntry {\n${lowered}\n${SSR_ESCAPE}\nconst $out = [];\nconst $awaits = [];\nconst $resume = {};\n${ssr}` +
|
|
55
|
-
`return { html: $out.join(''), state: (typeof model !== 'undefined' ? model.snapshot() : {}), awaits: $awaits, resume: $resume };\n` +
|
|
56
|
-
`} finally { exitScope($scope); }`
|
|
56
|
+
`const $scope = $$enterScope();\ntry {\n${lowered}\n${SSR_ESCAPE}\nconst $out = [];\nconst $awaits = [];\nconst $resume = {};\n${ssr}` +
|
|
57
|
+
`return { html: $out.join(''), state: (typeof $$model !== 'undefined' ? $$model.snapshot() : {}), awaits: $awaits, resume: $resume };\n` +
|
|
58
|
+
`} finally { $$exitScope($scope); }`
|
|
57
59
|
/* An inline `await` — a blocking await block, a child render, a slot read, or a
|
|
58
60
|
top-level `await` in the author script — forces an async wrapper. Match `await` as a
|
|
59
61
|
standalone token (operator), so a no-space form (`await(x)`, `await[i]`, `` await`t` ``)
|
|
@@ -2,12 +2,24 @@ import ts from 'typescript'
|
|
|
2
2
|
import { ABIDE_PACKAGE_NAME } from '../../shared/ABIDE_PACKAGE_NAME.ts'
|
|
3
3
|
import { parseTemplate } from './parseTemplate.ts'
|
|
4
4
|
import { REACTIVE_CALLEES } from './REACTIVE_CALLEES.ts'
|
|
5
|
-
import type { CompiledShadow, ShadowMapping } from './types/CompiledShadow.ts'
|
|
5
|
+
import type { CompiledShadow, ShadowDiagnostic, ShadowMapping } from './types/CompiledShadow.ts'
|
|
6
6
|
import type { TemplateNode } from './types/TemplateNode.ts'
|
|
7
7
|
|
|
8
|
+
/*
|
|
9
|
+
Runtime helpers published as package subpaths solely so generated component code
|
|
10
|
+
can import them; an author must reach them through their namespaced surface
|
|
11
|
+
instead. The module resolver can't tell codegen from user source (same specifier),
|
|
12
|
+
so the ban can only live in the check — map each forbidden specifier to the
|
|
13
|
+
guidance shown when an author imports it.
|
|
14
|
+
*/
|
|
15
|
+
const INTERNAL_ONLY_IMPORTS: Record<string, string> = {
|
|
16
|
+
[`${ABIDE_PACKAGE_NAME}/ui/effect`]:
|
|
17
|
+
'`effect` is compiler-internal — author it as `scope().effect(...)`',
|
|
18
|
+
}
|
|
19
|
+
|
|
8
20
|
/*
|
|
9
21
|
Framework callables the `.abide` loader injects into a component's scope. `effect`,
|
|
10
|
-
`
|
|
22
|
+
`snippet`, and `scope` keep their real published types via imports so author
|
|
11
23
|
calls type-check — `scope()` is the authored reactive surface (`scope().state(...)` /
|
|
12
24
|
`.computed(...)` / `.undo()` …), so it must resolve like any import. `state`/`linked`/
|
|
13
25
|
`computed` are ALSO declared ambiently as a fallback for the rare bare/nested use the
|
|
@@ -18,14 +30,13 @@ argument (default `Record<string, any>`) so each binding inherits its prop type
|
|
|
18
30
|
`= default` narrows.
|
|
19
31
|
*/
|
|
20
32
|
const SHADOW_PREAMBLE = `import { effect } from '${ABIDE_PACKAGE_NAME}/ui/effect'
|
|
21
|
-
import { html } from '${ABIDE_PACKAGE_NAME}/shared/html'
|
|
22
33
|
import { snippet } from '${ABIDE_PACKAGE_NAME}/shared/snippet'
|
|
23
34
|
import { scope } from '${ABIDE_PACKAGE_NAME}/ui/scope'
|
|
24
35
|
declare function state<T>(initial?: T, transform?: (next: T, previous: T) => T): { value: T }
|
|
25
36
|
declare function linked<T>(seed: () => T, transform?: (next: T, previous: T) => T): { value: T }
|
|
26
37
|
declare function computed<T>(compute: () => T): { readonly value: T }
|
|
27
|
-
declare
|
|
28
|
-
void [effect,
|
|
38
|
+
declare const children: (() => void) | undefined
|
|
39
|
+
void [effect, snippet, scope]
|
|
29
40
|
`
|
|
30
41
|
|
|
31
42
|
/*
|
|
@@ -43,7 +54,7 @@ The script's signal surface is rewritten to value types:
|
|
|
43
54
|
Everything else (functions, plain consts, imports) is emitted verbatim, so
|
|
44
55
|
expressions inside it (e.g. a computed's compute body) are checked and mapped too.
|
|
45
56
|
*/
|
|
46
|
-
export function compileShadow(source: string): CompiledShadow {
|
|
57
|
+
export function compileShadow(source: string, propsType = 'Record<string, any>'): CompiledShadow {
|
|
47
58
|
const builder = createBuilder()
|
|
48
59
|
const leadingScript = source.match(/^\s*<script[^>]*>([\s\S]*?)<\/script>/)
|
|
49
60
|
const scriptBody = leadingScript?.[1] ?? ''
|
|
@@ -51,8 +62,14 @@ export function compileShadow(source: string): CompiledShadow {
|
|
|
51
62
|
const scriptStart = leadingScript ? source.indexOf('>', leadingScript.index) + 1 : 0
|
|
52
63
|
const templateStart = leadingScript ? (leadingScript.index ?? 0) + leadingScript[0].length : 0
|
|
53
64
|
|
|
54
|
-
const { imports, types, scope, propsShapes } = analyzeScript(
|
|
65
|
+
const { imports, types, scope, propsShapes, diagnostics } = analyzeScript(
|
|
66
|
+
scriptBody,
|
|
67
|
+
scriptStart,
|
|
68
|
+
)
|
|
55
69
|
builder.raw(SHADOW_PREAMBLE)
|
|
70
|
+
/* `props()` defaults to the file's type — a page/layout's route param shape, else
|
|
71
|
+
`Record<string, any>` — so `const { id } = props()` infers from the route. */
|
|
72
|
+
builder.raw(`declare function props<T = ${propsType}>(): T\n`)
|
|
56
73
|
for (const line of imports) {
|
|
57
74
|
builder.flush(line)
|
|
58
75
|
}
|
|
@@ -82,7 +99,7 @@ export function compileShadow(source: string): CompiledShadow {
|
|
|
82
99
|
}
|
|
83
100
|
emitNodes(parseTemplate(source.slice(templateStart), templateStart).nodes, builder)
|
|
84
101
|
builder.raw('}\n')
|
|
85
|
-
return builder.result()
|
|
102
|
+
return { ...builder.result(), diagnostics }
|
|
86
103
|
}
|
|
87
104
|
|
|
88
105
|
/* The shadow text builder: `raw` appends synthesised scaffolding (no mapping),
|
|
@@ -162,6 +179,39 @@ type ScriptAnalysis = {
|
|
|
162
179
|
types: ScopeLine[]
|
|
163
180
|
scope: ScopeLine[]
|
|
164
181
|
propsShapes: string[]
|
|
182
|
+
diagnostics: ShadowDiagnostic[]
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/* Pushes a diagnostic for every author binding whose name starts with the reserved `$$`
|
|
186
|
+
prefix — variable/function/class declarations, parameters, and destructuring leaves.
|
|
187
|
+
Read structurally off the parsed script; the message points at the offending name. */
|
|
188
|
+
function collectReservedNameDiagnostics(
|
|
189
|
+
file: ts.SourceFile,
|
|
190
|
+
scriptStart: number,
|
|
191
|
+
diagnostics: ShadowDiagnostic[],
|
|
192
|
+
): void {
|
|
193
|
+
const flag = (name: ts.Node | undefined): void => {
|
|
194
|
+
if (name !== undefined && ts.isIdentifier(name) && name.text.startsWith('$$')) {
|
|
195
|
+
diagnostics.push({
|
|
196
|
+
start: scriptStart + name.getStart(file),
|
|
197
|
+
length: name.getEnd() - name.getStart(file),
|
|
198
|
+
message: `\`${name.text}\` is reserved — the \`$$\` prefix is the compiler's injected runtime namespace; rename this binding.`,
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const visit = (node: ts.Node): void => {
|
|
203
|
+
if (
|
|
204
|
+
ts.isVariableDeclaration(node) ||
|
|
205
|
+
ts.isFunctionDeclaration(node) ||
|
|
206
|
+
ts.isClassDeclaration(node) ||
|
|
207
|
+
ts.isParameter(node) ||
|
|
208
|
+
ts.isBindingElement(node)
|
|
209
|
+
) {
|
|
210
|
+
flag(node.name)
|
|
211
|
+
}
|
|
212
|
+
ts.forEachChild(node, visit)
|
|
213
|
+
}
|
|
214
|
+
visit(file)
|
|
165
215
|
}
|
|
166
216
|
|
|
167
217
|
/* Walks the leading `<script>` and produces the shadow's module imports, the
|
|
@@ -173,10 +223,15 @@ function analyzeScript(scriptBody: string, scriptStart: number): ScriptAnalysis
|
|
|
173
223
|
const types: ScopeLine[] = []
|
|
174
224
|
const scope: ScopeLine[] = []
|
|
175
225
|
const propsShapes: string[] = []
|
|
226
|
+
const diagnostics: ShadowDiagnostic[] = []
|
|
176
227
|
if (scriptBody.trim() === '') {
|
|
177
|
-
return { imports, types, scope, propsShapes }
|
|
228
|
+
return { imports, types, scope, propsShapes, diagnostics }
|
|
178
229
|
}
|
|
179
230
|
const file = ts.createSourceFile('script.ts', scriptBody, ts.ScriptTarget.Latest, true)
|
|
231
|
+
/* The `$$` prefix is reserved for the compiler's injected runtime (`$$each`, `$$model`,
|
|
232
|
+
`$$scope`, …), so an author binding may not start with it — that's the contract that
|
|
233
|
+
lets a user freely name a variable after any helper. Flag every such declaration. */
|
|
234
|
+
collectReservedNameDiagnostics(file, scriptStart, diagnostics)
|
|
180
235
|
/* A verbatim span: original text + the segment mapping it back, relative to the
|
|
181
236
|
line start (the caller rebases shadowStart onto the running shadow length). */
|
|
182
237
|
const span = (node: ts.Node, prefixLength: number): ScopeLine['segments'][number] => ({
|
|
@@ -188,6 +243,20 @@ function analyzeScript(scriptBody: string, scriptStart: number): ScriptAnalysis
|
|
|
188
243
|
|
|
189
244
|
for (const statement of file.statements) {
|
|
190
245
|
if (ts.isImportDeclaration(statement)) {
|
|
246
|
+
const specifier = ts.isStringLiteral(statement.moduleSpecifier)
|
|
247
|
+
? statement.moduleSpecifier.text
|
|
248
|
+
: undefined
|
|
249
|
+
const banned = specifier !== undefined ? INTERNAL_ONLY_IMPORTS[specifier] : undefined
|
|
250
|
+
if (banned !== undefined) {
|
|
251
|
+
/* Flag and drop it — leaving it out keeps `effect` resolving via the
|
|
252
|
+
preamble (no duplicate-identifier noise on top of our message). */
|
|
253
|
+
diagnostics.push({
|
|
254
|
+
start: scriptStart + statement.getStart(file),
|
|
255
|
+
length: statement.getEnd() - statement.getStart(file),
|
|
256
|
+
message: banned,
|
|
257
|
+
})
|
|
258
|
+
continue
|
|
259
|
+
}
|
|
191
260
|
/* Emit verbatim with a span so hover/go-to resolve on the imported names. */
|
|
192
261
|
imports.push({ text: verbatim(statement), segments: [span(statement, 0)] })
|
|
193
262
|
continue
|
|
@@ -207,7 +276,7 @@ function analyzeScript(scriptBody: string, scriptStart: number): ScriptAnalysis
|
|
|
207
276
|
scope.push(scopeLineFor(declaration, propsShapes, verbatim, span))
|
|
208
277
|
}
|
|
209
278
|
}
|
|
210
|
-
return { imports, types, scope, propsShapes }
|
|
279
|
+
return { imports, types, scope, propsShapes, diagnostics }
|
|
211
280
|
}
|
|
212
281
|
|
|
213
282
|
/* Value-projects a nested control-flow `<script>` body the way `analyzeScript`
|
|
@@ -363,7 +432,15 @@ function emitNode(node: TemplateNode, builder: Builder): void {
|
|
|
363
432
|
return
|
|
364
433
|
case 'element':
|
|
365
434
|
for (const attr of node.attrs) {
|
|
366
|
-
|
|
435
|
+
/* An interpolated value checks each `{expr}` part on its own offset; every
|
|
436
|
+
other dynamic attribute checks its single `code`. */
|
|
437
|
+
if (attr.kind === 'interpolated') {
|
|
438
|
+
for (const part of attr.parts) {
|
|
439
|
+
if (part.kind === 'expression') {
|
|
440
|
+
builder.stmt(part.code, part.loc)
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
} else if (attr.kind !== 'static') {
|
|
367
444
|
builder.stmt(attr.code, attr.loc)
|
|
368
445
|
}
|
|
369
446
|
}
|
|
@@ -372,12 +449,15 @@ function emitNode(node: TemplateNode, builder: Builder): void {
|
|
|
372
449
|
case 'component': {
|
|
373
450
|
/* The imported tag resolves (via the shadow host) to the child's
|
|
374
451
|
`(props: Props) => …` default, so `Parameters<typeof Child>[0]` is its prop
|
|
375
|
-
shape. `
|
|
376
|
-
(not part of the declared shape), so they are checked
|
|
377
|
-
against its own declared key type — never flagged as
|
|
452
|
+
shape. `bind:` / `class:` / `style:` / `attach` and `{...spread}` are
|
|
453
|
+
framework directives (not part of the declared shape), so they are checked
|
|
454
|
+
leniently — each value against its own declared key type — never flagged as
|
|
455
|
+
excess. `on*` callbacks ARE ordinary declared props on a component (abide does
|
|
456
|
+
not auto-forward events to the DOM), so they go through the completeness check
|
|
457
|
+
with the rest — otherwise a required `onsave`/`oncancel` reads as missing even
|
|
458
|
+
when passed, and an undeclared handler slips by unflagged. */
|
|
378
459
|
const handled = (prop: { name: string; spread?: boolean }): boolean =>
|
|
379
460
|
prop.spread === true ||
|
|
380
|
-
prop.name.startsWith('on') ||
|
|
381
461
|
prop.name.startsWith('bind:') ||
|
|
382
462
|
prop.name.startsWith('class:') ||
|
|
383
463
|
prop.name.startsWith('style:') ||
|
|
@@ -427,12 +507,23 @@ function emitNode(node: TemplateNode, builder: Builder): void {
|
|
|
427
507
|
builder.raw('\n')
|
|
428
508
|
for (const prop of dataProps) {
|
|
429
509
|
/* 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).
|
|
510
|
+
the value verbatim-mapped (wrong-type errors land on the value).
|
|
511
|
+
Hyphenated names (`aria-label`, `data-*`) aren't valid identifiers, so
|
|
512
|
+
they're wrapped in raw quotes — a string-literal key the parser accepts.
|
|
513
|
+
The quotes are unmapped; an excess-prop error on the literal starts at the
|
|
514
|
+
opening quote and overlaps the mapped name, clamping back onto it. */
|
|
515
|
+
const needsQuoting = !/^[A-Za-z_$][\w$]*$/.test(prop.name)
|
|
516
|
+
if (needsQuoting) {
|
|
517
|
+
builder.raw('"')
|
|
518
|
+
}
|
|
431
519
|
if (prop.nameLoc !== undefined) {
|
|
432
520
|
builder.mapped(prop.name, prop.nameLoc)
|
|
433
521
|
} else {
|
|
434
522
|
builder.raw(prop.name)
|
|
435
523
|
}
|
|
524
|
+
if (needsQuoting) {
|
|
525
|
+
builder.raw('"')
|
|
526
|
+
}
|
|
436
527
|
builder.raw(': ')
|
|
437
528
|
builder.expr(prop.code, prop.loc)
|
|
438
529
|
builder.raw(',\n')
|
|
@@ -9,7 +9,7 @@ The props-bag source expression a child mount/render receives, shared by the bui
|
|
|
9
9
|
and SSR back-ends so their last-wins layering stays byte-identical — the invariant
|
|
10
10
|
SSR/client prop congruence rests on. No spread → the plain object literal of named
|
|
11
11
|
value thunks (+ the trailing slot). With a `{...expr}` spread → a `mergeProps` of
|
|
12
|
-
ordered layers — explicit-prop runs,
|
|
12
|
+
ordered layers — explicit-prop runs, `$$spreadProps(expr)` spreads, the slot —
|
|
13
13
|
resolved last-wins per key, so source order decides overrides (like JSX).
|
|
14
14
|
`lowerExpression` is the caller's expression lowering; `slotPart` is its `$children`
|
|
15
15
|
layer (a host-taking builder for the client, a string-returning thunk for SSR) or
|
|
@@ -40,7 +40,7 @@ export function composeProps(
|
|
|
40
40
|
for (const prop of props) {
|
|
41
41
|
if (prop.spread) {
|
|
42
42
|
flushRun()
|
|
43
|
-
layers.push(
|
|
43
|
+
layers.push(`$$spreadProps(() => (${lowerExpression(prop.code)}))`)
|
|
44
44
|
} else {
|
|
45
45
|
run.push(propThunk(prop))
|
|
46
46
|
}
|
|
@@ -49,5 +49,5 @@ export function composeProps(
|
|
|
49
49
|
if (slotPart !== undefined) {
|
|
50
50
|
layers.push(`{ ${slotPart} }`)
|
|
51
51
|
}
|
|
52
|
-
return
|
|
52
|
+
return `$$mergeProps([${layers.join(', ')}])`
|
|
53
53
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
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
|
+
import { mapSyntacticClassification, mapTsClassification } from './ABIDE_SEMANTIC_TOKENS_LEGEND.ts'
|
|
5
5
|
import { assetModulesFile } from './assetModulesFile.ts'
|
|
6
6
|
import { compileShadow } from './compileShadow.ts'
|
|
7
7
|
import { loadShadowTsConfig } from './loadShadowTsConfig.ts'
|
|
8
|
+
import { pagePropsType } from './pagePropsType.ts'
|
|
8
9
|
import { remapShadowDiagnostic } from './remapShadowDiagnostic.ts'
|
|
9
10
|
import { resolveAbideImports } from './resolveAbideImports.ts'
|
|
10
11
|
import { shadowNaming } from './shadowNaming.ts'
|
|
@@ -58,7 +59,7 @@ export function createShadowLanguageService(cwd: string): ShadowLanguageService
|
|
|
58
59
|
const shadowText = (abidePath: string): string => {
|
|
59
60
|
const source = overlays.get(abidePath) ?? ts.sys.readFile(abidePath) ?? ''
|
|
60
61
|
try {
|
|
61
|
-
const compiled = compileShadow(source)
|
|
62
|
+
const compiled = compileShadow(source, pagePropsType(abidePath))
|
|
62
63
|
shadows.set(abidePath, compiled)
|
|
63
64
|
parseErrors.delete(abidePath)
|
|
64
65
|
return compiled.code
|
|
@@ -240,6 +241,33 @@ export function createShadowLanguageService(cwd: string): ShadowLanguageService
|
|
|
240
241
|
modifiers: mapped.modifiers,
|
|
241
242
|
})
|
|
242
243
|
}
|
|
244
|
+
/* Literal syntactic tokens (string/number/regex) the semantic classifier
|
|
245
|
+
never emits — so a template-literal string inside `{…}` gets colored. */
|
|
246
|
+
const syntactic = service.getEncodedSyntacticClassifications(fileName, {
|
|
247
|
+
start: 0,
|
|
248
|
+
length: shadow.code.length,
|
|
249
|
+
})
|
|
250
|
+
for (let index = 0; index + 2 < syntactic.spans.length; index += 3) {
|
|
251
|
+
const spanStart = syntactic.spans[index]
|
|
252
|
+
const spanLength = syntactic.spans[index + 1]
|
|
253
|
+
const classification = syntactic.spans[index + 2]
|
|
254
|
+
if (
|
|
255
|
+
spanStart === undefined ||
|
|
256
|
+
spanLength === undefined ||
|
|
257
|
+
classification === undefined
|
|
258
|
+
) {
|
|
259
|
+
continue
|
|
260
|
+
}
|
|
261
|
+
const type = mapSyntacticClassification(classification)
|
|
262
|
+
if (type === undefined) {
|
|
263
|
+
continue
|
|
264
|
+
}
|
|
265
|
+
const located = remapShadowDiagnostic(shadow.mappings, spanStart, spanLength)
|
|
266
|
+
if (located === undefined) {
|
|
267
|
+
continue
|
|
268
|
+
}
|
|
269
|
+
tokens.push({ start: located.start, length: located.length, type, modifiers: [] })
|
|
270
|
+
}
|
|
243
271
|
return tokens
|
|
244
272
|
},
|
|
245
273
|
}
|
|
@@ -4,6 +4,7 @@ import { messageFromError } from '../../shared/messageFromError.ts'
|
|
|
4
4
|
import { assetModulesFile } from './assetModulesFile.ts'
|
|
5
5
|
import { compileShadow } from './compileShadow.ts'
|
|
6
6
|
import { loadShadowTsConfig } from './loadShadowTsConfig.ts'
|
|
7
|
+
import { pagePropsType } from './pagePropsType.ts'
|
|
7
8
|
import { resolveAbideImports } from './resolveAbideImports.ts'
|
|
8
9
|
import { shadowNaming } from './shadowNaming.ts'
|
|
9
10
|
import type { CompiledShadow } from './types/CompiledShadow.ts'
|
|
@@ -46,7 +47,7 @@ export function createShadowProgram(cwd: string, abidePaths?: string[]): ShadowP
|
|
|
46
47
|
const shadowText = (abidePath: string): string => {
|
|
47
48
|
const source = ts.sys.readFile(abidePath) ?? ''
|
|
48
49
|
try {
|
|
49
|
-
const compiled = compileShadow(source)
|
|
50
|
+
const compiled = compileShadow(source, pagePropsType(abidePath))
|
|
50
51
|
shadows.set(abidePath, compiled)
|
|
51
52
|
parseErrors.delete(abidePath)
|
|
52
53
|
return compiled.code
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ShadowKind } from './types/ShadowKind.ts'
|
|
2
|
+
import type { ShadowScope } from './types/ShadowScope.ts'
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
Builds a fresh `ShadowScope` — the typed, auto-popping branch-local shadow stack. One
|
|
6
|
+
`Set` per `ShadowKind`, reached only through `withShadow` (push on entry, pop in a
|
|
7
|
+
`finally`), never directly. The `finally` is the safety move: the old hand-written
|
|
8
|
+
pop ran after the body returned, so any throw inside the body (e.g. SSR's `await then`
|
|
9
|
+
TDZ path) leaked the branch's shadows into every later sibling. Here the pop is
|
|
10
|
+
unconditional, so a branch's shadows cannot outlive the branch.
|
|
11
|
+
*/
|
|
12
|
+
export function createShadowScope(): ShadowScope {
|
|
13
|
+
const byKind: Record<ShadowKind, Set<string>> = {
|
|
14
|
+
derived: new Set<string>(),
|
|
15
|
+
plain: new Set<string>(),
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function withShadow<T>(names: Iterable<string>, kind: ShadowKind, body: () => T): T {
|
|
19
|
+
const scope = byKind[kind]
|
|
20
|
+
/* Track only the names this call newly added, so a name already shadowing from an
|
|
21
|
+
outer branch survives this branch's pop. */
|
|
22
|
+
const added: string[] = []
|
|
23
|
+
for (const name of names) {
|
|
24
|
+
if (!scope.has(name)) {
|
|
25
|
+
scope.add(name)
|
|
26
|
+
added.push(name)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
return body()
|
|
31
|
+
} finally {
|
|
32
|
+
for (const name of added) {
|
|
33
|
+
scope.delete(name)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
withShadow,
|
|
40
|
+
names: (kind) => byKind[kind],
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -15,6 +15,10 @@ const NAMED_ENTITIES: Record<string, string> = {
|
|
|
15
15
|
gt: '>',
|
|
16
16
|
quot: '"',
|
|
17
17
|
apos: "'",
|
|
18
|
+
/* A literal brace where `{` would otherwise open an interpolation (text or
|
|
19
|
+
attribute value); written `{` / `}`. */
|
|
20
|
+
lbrace: '{',
|
|
21
|
+
rbrace: '}',
|
|
18
22
|
nbsp: ' ',
|
|
19
23
|
copy: '©',
|
|
20
24
|
reg: '®',
|
|
@@ -152,11 +152,12 @@ export function desugarSignals(source: ts.SourceFile): {
|
|
|
152
152
|
return root
|
|
153
153
|
}
|
|
154
154
|
const statements: ts.Statement[] = []
|
|
155
|
-
/* A shared
|
|
155
|
+
/* A shared `$$model = scope()` host for the state slots, prepended once. The doc
|
|
156
|
+
base is `$$`-reserved so a user variable named `model` can never collide. */
|
|
156
157
|
if (stateNames.size > 0) {
|
|
157
158
|
statements.push(
|
|
158
159
|
constDeclaration(
|
|
159
|
-
'model',
|
|
160
|
+
'$$model',
|
|
160
161
|
factory.createCallExpression(factory.createIdentifier('scope'), undefined, []),
|
|
161
162
|
),
|
|
162
163
|
)
|
|
@@ -425,10 +426,14 @@ function propsStatements(statement: ts.Statement): ts.Statement[] | undefined {
|
|
|
425
426
|
statements.push(
|
|
426
427
|
constDeclaration(
|
|
427
428
|
rest,
|
|
428
|
-
factory.createCallExpression(
|
|
429
|
-
factory.createIdentifier('
|
|
430
|
-
|
|
431
|
-
|
|
429
|
+
factory.createCallExpression(
|
|
430
|
+
factory.createIdentifier('$$restProps'),
|
|
431
|
+
undefined,
|
|
432
|
+
[
|
|
433
|
+
factory.createIdentifier('$props'),
|
|
434
|
+
factory.createArrayLiteralExpression(consumed),
|
|
435
|
+
],
|
|
436
|
+
),
|
|
432
437
|
),
|
|
433
438
|
)
|
|
434
439
|
}
|
|
@@ -457,7 +462,7 @@ function stateAssignmentStatements(statement: ts.Statement): ts.Statement[] | un
|
|
|
457
462
|
factory.createExpressionStatement(
|
|
458
463
|
factory.createAssignment(
|
|
459
464
|
factory.createPropertyAccessExpression(
|
|
460
|
-
factory.createIdentifier('model'),
|
|
465
|
+
factory.createIdentifier('$$model'),
|
|
461
466
|
declaration.name.text,
|
|
462
467
|
),
|
|
463
468
|
initial,
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { catchBinding } from './catchBinding.ts'
|
|
2
|
+
import { resolveBranches } from './resolveBranches.ts'
|
|
3
|
+
import type { Binding } from './types/Binding.ts'
|
|
4
|
+
import type { TemplateNode } from './types/TemplateNode.ts'
|
|
5
|
+
|
|
6
|
+
/* The structural shape of an `each`/`for` loop, resolved once so the build and SSR back-ends
|
|
7
|
+
share one reading of it and only own emission. Both previously read the item/index/key
|
|
8
|
+
names, the `async` split, and the row children straight off the node; build additionally
|
|
9
|
+
resolved the async-each `catch` branch. The names the row introduces are now classified
|
|
10
|
+
ONCE here as `bindings` (item + index, both `reactive`) and `catchBindings` (the async-each
|
|
11
|
+
`catch` error, `plain`) — the single source both back-ends register through `withBindings`. */
|
|
12
|
+
export type EachPlan = {
|
|
13
|
+
/* The list expression — an Array (sync) or AsyncIterable (`async`). */
|
|
14
|
+
items: string
|
|
15
|
+
/* The item binding pattern (`node.as`): a plain identifier or a destructuring pattern. */
|
|
16
|
+
as: string
|
|
17
|
+
/* `by={k}` key expression, or undefined → key on the item's own identity. */
|
|
18
|
+
key: string | undefined
|
|
19
|
+
/* `index="i"` → the row's position bound to this name; undefined → unbound. */
|
|
20
|
+
index: string | undefined
|
|
21
|
+
/* `await` on the tag → `items` is an AsyncIterable, drained on the client. */
|
|
22
|
+
async: boolean
|
|
23
|
+
/* The row content. */
|
|
24
|
+
children: TemplateNode[]
|
|
25
|
+
/* The async-each `catch` content + its bound name (`_error` default); empty when absent.
|
|
26
|
+
Build renders it after the streamed rows on iterator rejection; SSR renders no async
|
|
27
|
+
rows so it ignores this. */
|
|
28
|
+
catchChildren: TemplateNode[]
|
|
29
|
+
catchAs: string
|
|
30
|
+
/* No catch → the rejection surfaces instead of rendering an empty branch. */
|
|
31
|
+
hasCatch: boolean
|
|
32
|
+
/* The row body's bindings, classified once: the item (`as`) and, when present, the
|
|
33
|
+
`index`, both `reactive` (a `.value` cell on the client). */
|
|
34
|
+
bindings: Binding[]
|
|
35
|
+
/* The async-each `catch` branch's binding (`catchAs`, `plain`); empty when no catch. */
|
|
36
|
+
catchBindings: Binding[]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/* Resolves an `each` node's structure into the shared structural plan. */
|
|
40
|
+
export function eachPlan(node: Extract<TemplateNode, { kind: 'each' }>): EachPlan {
|
|
41
|
+
const [catchBranch] = resolveBranches(node, 'catch')
|
|
42
|
+
const catchAs = catchBranch?.as ?? '_error'
|
|
43
|
+
return {
|
|
44
|
+
items: node.items,
|
|
45
|
+
as: node.as,
|
|
46
|
+
key: node.key,
|
|
47
|
+
index: node.index,
|
|
48
|
+
async: node.async,
|
|
49
|
+
children: node.children,
|
|
50
|
+
catchChildren: catchBranch?.children ?? [],
|
|
51
|
+
catchAs,
|
|
52
|
+
hasCatch: catchBranch !== undefined,
|
|
53
|
+
bindings: [
|
|
54
|
+
{ name: node.as, classification: 'reactive' },
|
|
55
|
+
...(node.index === undefined
|
|
56
|
+
? []
|
|
57
|
+
: [{ name: node.index, classification: 'reactive' } as const]),
|
|
58
|
+
],
|
|
59
|
+
catchBindings: catchBinding(catchAs, catchBranch !== undefined),
|
|
60
|
+
}
|
|
61
|
+
}
|