@abide/abide 0.47.0 → 0.49.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 +26 -13
- package/CHANGELOG.md +53 -0
- package/README.md +9 -3
- package/package.json +3 -1
- package/src/buildCli.ts +3 -5
- package/src/buildDisconnected.ts +2 -3
- package/src/bundleApp.ts +2 -3
- package/src/compile.ts +2 -4
- package/src/lib/bundle/installDownloads.ts +13 -3
- package/src/lib/bundle/installMacMenu.ts +13 -3
- package/src/lib/cli/parseArgvForRpc.ts +36 -9
- package/src/lib/cli/tokenizeArgvFlags.ts +4 -2
- package/src/lib/mcp/createMcpResourceServer.ts +13 -2
- package/src/lib/mcp/toolResultFromResponse.ts +40 -12
- package/src/lib/server/rpc/parseArgs.ts +15 -1
- package/src/lib/server/rpc/runWithRpcTimeout.ts +12 -1
- package/src/lib/server/runtime/buildCacheSnapshot.ts +6 -6
- package/src/lib/server/runtime/finalizeResponse.ts +11 -1
- package/src/lib/server/runtime/snapshotEntryFromCache.ts +10 -2
- package/src/lib/server/runtime/types/InspectorCacheEntry.ts +1 -1
- package/src/lib/server/runtime/types/InspectorCacheSnapshot.ts +4 -4
- package/src/lib/server/runtime/types/InspectorContext.ts +1 -1
- package/src/lib/shared/buildArtifact.ts +17 -0
- package/src/lib/shared/buildRpcRequest.ts +6 -2
- package/src/lib/shared/cache.ts +66 -28
- package/src/lib/shared/cacheStores.ts +3 -3
- package/src/lib/shared/canonicalJson.ts +24 -1
- package/src/lib/shared/createChannelLog.ts +24 -0
- package/src/lib/shared/exitOnBuildFailure.ts +4 -3
- package/src/lib/shared/parseEnv.ts +17 -6
- package/src/lib/shared/refresh.ts +3 -3
- package/src/lib/shared/serializeEnv.ts +18 -6
- package/src/lib/shared/sharedCacheStore.ts +14 -0
- package/src/lib/shared/sharedCacheStoreSlot.ts +10 -0
- package/src/lib/shared/snippet.ts +11 -6
- package/src/lib/shared/types/CacheOptions.ts +15 -11
- package/src/lib/shared/types/CacheStats.ts +1 -1
- package/src/lib/shared/types/SmartReadOptions.ts +15 -11
- package/src/lib/shared/url.ts +5 -0
- package/src/lib/test/createTestApp.ts +6 -6
- package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +1 -0
- package/src/lib/ui/compile/bindListenEvent.ts +5 -6
- package/src/lib/ui/compile/compileSSR.ts +1 -1
- package/src/lib/ui/compile/compileShadow.ts +121 -25
- package/src/lib/ui/compile/composeProps.ts +3 -2
- package/src/lib/ui/compile/desugarSignals.ts +1 -1
- package/src/lib/ui/compile/generateBuild.ts +23 -9
- package/src/lib/ui/compile/generateSSR.ts +49 -37
- package/src/lib/ui/compile/isWhitespaceText.ts +10 -2
- package/src/lib/ui/compile/lowerDocAccess.ts +39 -1
- package/src/lib/ui/compile/parseTemplate.ts +3 -1
- package/src/lib/ui/compile/renameSignalRefs.ts +5 -18
- package/src/lib/ui/compile/resolveReactiveExport.ts +4 -7
- package/src/lib/ui/compile/scopeCss.ts +27 -4
- package/src/lib/ui/dom/awaitBlock.ts +67 -29
- package/src/lib/ui/dom/discardBoundary.ts +24 -6
- package/src/lib/ui/dom/each.ts +15 -8
- package/src/lib/ui/dom/fillBefore.ts +6 -7
- package/src/lib/ui/dom/mergeProps.ts +1 -1
- package/src/lib/ui/dom/mountSlot.ts +1 -1
- package/src/lib/ui/dom/mutateDocArray.ts +38 -0
- package/src/lib/ui/dom/restProps.ts +2 -2
- package/src/lib/ui/dom/spreadProps.ts +3 -4
- package/src/lib/ui/dom/tryBlock.ts +10 -11
- package/src/lib/ui/dom/withScope.ts +7 -0
- package/src/lib/ui/history.ts +14 -7
- package/src/lib/ui/installHotBridge.ts +2 -0
- package/src/lib/ui/props.ts +17 -0
- package/src/lib/ui/renderChain.ts +7 -3
- package/src/lib/ui/router.ts +23 -17
- package/src/lib/ui/runtime/CHILD_PRESENT.ts +2 -2
- package/src/lib/ui/runtime/applyPatchToTree.ts +16 -3
- package/src/lib/ui/runtime/captureModelDoc.ts +22 -7
- package/src/lib/ui/runtime/createDoc.ts +28 -12
- package/src/lib/ui/runtime/flushEffects.ts +23 -1
- package/src/lib/ui/runtime/scope.ts +11 -0
- package/src/lib/ui/runtime/toTeardown.ts +10 -3
- package/src/lib/ui/runtime/types/UiProps.ts +6 -5
- package/src/lib/ui/runtime/withoutHydration.ts +22 -0
- package/src/lib/ui/startClient.ts +3 -3
- package/src/lib/ui/state.ts +9 -3
- package/src/serverEntry.ts +11 -9
- package/src/lib/shared/globalCacheStore.ts +0 -15
- package/src/lib/shared/globalCacheStoreSlot.ts +0 -12
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Per-store cache read tallies, frozen into the request's closing log record at
|
|
3
3
|
settle. hit = a read served from a settled retained entry (including the warm
|
|
4
4
|
SSR sync path); coalesced = a read that joined an in-flight call; miss = a
|
|
5
|
-
read that invoked its producer/remote. Reads against the process-level
|
|
5
|
+
read that invoked its producer/remote. Reads against the process-level shared
|
|
6
6
|
store count into the requesting scope's store, so a request's record reflects
|
|
7
7
|
everything that request asked the cache for.
|
|
8
8
|
*/
|
|
@@ -5,19 +5,23 @@ these govern retention and the refetch clock — NOT transport. Per-call transpo
|
|
|
5
5
|
options (signal/keepalive/priority/cache/headers) live on `.raw(args, init)`.
|
|
6
6
|
|
|
7
7
|
Fetch reads:
|
|
8
|
-
- `ttl` — retention/staleness in ms:
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
- `ttl` — retention/staleness in ms. Default: 0 on the server (coalesce-only —
|
|
9
|
+
the request is the atomic unit, nothing is retained past it), Infinity on the
|
|
10
|
+
client (retain until invalidate/refresh — the tab is the atomic unit). On the
|
|
11
|
+
client, N ms marks a staleness deadline: the retained value goes stale after N
|
|
12
|
+
ms and the next access triggers a background revalidation (stale stays visible,
|
|
13
|
+
`refreshing()` true); the display value is never dropped. On the server, N ms
|
|
14
|
+
is a plain expiry and only takes effect in the shared store (pair with `shared`).
|
|
12
15
|
- `tags` — free-form invalidation-group labels (see the selector grammar).
|
|
13
16
|
- `throttle` / `debounce` — rate-limit the background refetch clock (set one, not
|
|
14
17
|
both). Leading-edge-then-coalesce, or fire-after-quiet respectively.
|
|
15
|
-
- `
|
|
16
|
-
request-scoped one (server)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
- `shared` — opts the entry into the process-level store instead of the default
|
|
19
|
+
request-scoped one (server) — a store that outlives every request. It selects
|
|
20
|
+
the store only; it does NOT retain (pair it with `ttl` to memoise across
|
|
21
|
+
requests). The shared store is keyed by method+url+args, never by user, so do
|
|
22
|
+
not put per-user data in it — it would be served to other users. Omit `shared`
|
|
23
|
+
for per-request data. Write only `shared: true`; there is no `false` form. On
|
|
24
|
+
the client there is a single tab store, so the flag is a no-op there.
|
|
21
25
|
|
|
22
26
|
Streaming reads (jsonl/sse):
|
|
23
27
|
- `n` — how many retained frames to replay before going live.
|
|
@@ -27,6 +31,6 @@ export type SmartReadOptions = {
|
|
|
27
31
|
tags?: string[]
|
|
28
32
|
throttle?: number
|
|
29
33
|
debounce?: number
|
|
30
|
-
|
|
34
|
+
shared?: boolean
|
|
31
35
|
n?: number
|
|
32
36
|
}
|
package/src/lib/shared/url.ts
CHANGED
|
@@ -116,6 +116,11 @@ function interpolate(segments: RouteSegment[], params: Query): string {
|
|
|
116
116
|
if (segment.optional && (value === undefined || value === '')) {
|
|
117
117
|
return undefined
|
|
118
118
|
}
|
|
119
|
+
/* A required `[name]`/`[...rest]` with no value would otherwise stringify to the
|
|
120
|
+
literal "undefined" and silently mis-route; fail loudly instead. */
|
|
121
|
+
if (value === undefined || value === null) {
|
|
122
|
+
throw new Error(`abide url(): missing required param "${segment.name}"`)
|
|
123
|
+
}
|
|
119
124
|
const text = String(value)
|
|
120
125
|
return segment.catchAll
|
|
121
126
|
? text.split('/').map(encodeURIComponent).join('/')
|
|
@@ -37,10 +37,10 @@ import { buildRpcRequest } from '../shared/buildRpcRequest.ts'
|
|
|
37
37
|
import { cacheStoreSlot } from '../shared/cacheStoreSlot.ts'
|
|
38
38
|
import { commandNameForUrl } from '../shared/commandNameForUrl.ts'
|
|
39
39
|
import { createCacheStore } from '../shared/createCacheStore.ts'
|
|
40
|
-
import { globalCacheStoreSlot } from '../shared/globalCacheStoreSlot.ts'
|
|
41
40
|
import { HEALTH_PATH } from '../shared/HEALTH_PATH.ts'
|
|
42
41
|
import { pageSlot } from '../shared/pageSlot.ts'
|
|
43
42
|
import { SOCKETS_PATH } from '../shared/SOCKETS_PATH.ts'
|
|
43
|
+
import { sharedCacheStoreSlot } from '../shared/sharedCacheStoreSlot.ts'
|
|
44
44
|
import type { RemoteFunction } from '../shared/types/RemoteFunction.ts'
|
|
45
45
|
import type { Socket } from '../shared/types/Socket.ts'
|
|
46
46
|
import { createTestSocketChannel } from './createTestSocketChannel.ts'
|
|
@@ -98,15 +98,15 @@ leaking the request-scope/cache/page resolvers into the next test file.
|
|
|
98
98
|
export async function createTestApp(): Promise<TestApp> {
|
|
99
99
|
const previous = {
|
|
100
100
|
cacheResolver: cacheStoreSlot.resolver,
|
|
101
|
-
|
|
101
|
+
sharedResolver: sharedCacheStoreSlot.resolver,
|
|
102
102
|
pageResolver: pageSlot.resolver,
|
|
103
103
|
baseResolver: baseSlot.resolver,
|
|
104
104
|
activeServer: serverSlot.active,
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
107
|
+
const sharedStore = createCacheStore()
|
|
108
|
+
sharedCacheStoreSlot.resolver = () => sharedStore
|
|
109
|
+
cacheStoreSlot.resolver = () => requestContext.getStore()?.cache ?? sharedStore
|
|
110
110
|
pageSlot.resolver = resolvePageSnapshot
|
|
111
111
|
|
|
112
112
|
/* Eager env validation, exactly as serverEntry: a top-level env(schema) in
|
|
@@ -185,7 +185,7 @@ export async function createTestApp(): Promise<TestApp> {
|
|
|
185
185
|
channel?.close()
|
|
186
186
|
server.stop(true)
|
|
187
187
|
cacheStoreSlot.resolver = previous.cacheResolver
|
|
188
|
-
|
|
188
|
+
sharedCacheStoreSlot.resolver = previous.sharedResolver
|
|
189
189
|
pageSlot.resolver = previous.pageResolver
|
|
190
190
|
baseSlot.resolver = previous.baseResolver
|
|
191
191
|
serverSlot.active = previous.activeServer
|
|
@@ -49,6 +49,7 @@ export const UI_RUNTIME_IMPORTS: { name: string; specifier: string; alias?: stri
|
|
|
49
49
|
{ name: 'restProps', specifier: 'ui/dom/restProps', alias: '$$restProps' },
|
|
50
50
|
{ name: 'spreadAttrs', specifier: 'ui/dom/spreadAttrs', alias: '$$spreadAttrs' },
|
|
51
51
|
{ name: 'readCall', specifier: 'ui/dom/readCall', alias: '$$readCall' },
|
|
52
|
+
{ name: 'mutateDocArray', specifier: 'ui/dom/mutateDocArray', alias: '$$mutateDocArray' },
|
|
52
53
|
{ name: 'hydrate', specifier: 'ui/dom/hydrate', alias: '$$hydrate' },
|
|
53
54
|
{ name: 'escapeKey', specifier: 'ui/runtime/escapeKey', alias: '$$escapeKey' },
|
|
54
55
|
{ name: 'nextBlockId', specifier: 'ui/runtime/nextBlockId', alias: '$$nextBlockId' },
|
|
@@ -2,18 +2,17 @@
|
|
|
2
2
|
The DOM event a generic two-way `bind:<property>` listens on to write the bound
|
|
3
3
|
path back. Most form fields report edits via `input`, but a few properties are
|
|
4
4
|
driven by their own event and never fire `input` — `<details open>` fires
|
|
5
|
-
`toggle
|
|
6
|
-
|
|
5
|
+
`toggle` and a checkbox/radio `checked` fires `change`. `<select value>` is NOT
|
|
6
|
+
handled here: generateBuild intercepts it before this function and routes it to
|
|
7
|
+
`bindSelectValue` (a MutationObserver-based helper that re-applies on late
|
|
8
|
+
options), so this function never sees that case.
|
|
7
9
|
*/
|
|
8
|
-
export function bindListenEvent(property: string
|
|
10
|
+
export function bindListenEvent(property: string): string {
|
|
9
11
|
if (property === 'open') {
|
|
10
12
|
return 'toggle'
|
|
11
13
|
}
|
|
12
14
|
if (property === 'checked') {
|
|
13
15
|
return 'change'
|
|
14
16
|
}
|
|
15
|
-
if (property === 'value' && tag === 'select') {
|
|
16
|
-
return 'change'
|
|
17
|
-
}
|
|
18
17
|
return 'input'
|
|
19
18
|
}
|
|
@@ -27,7 +27,7 @@ when a caller (a test, a top-level render) omits it.
|
|
|
27
27
|
|
|
28
28
|
The body is wrapped in an ASYNC IIFE (returns `Promise<SsrRender>`) ONLY when it contains
|
|
29
29
|
an inline `await` — a blocking `{#await … then}` block, a child-component render, a `<slot>`
|
|
30
|
-
read (its
|
|
30
|
+
read (its `children` builder is async), or a top-level `await` in the author script. A
|
|
31
31
|
purely synchronous / streaming-await / try-only component returns `SsrRender` directly. The
|
|
32
32
|
framework always `await`s `render()`, so production is uniform either way; the sync return
|
|
33
33
|
keeps static pages off the microtask queue and leaves the bulk of the SSR tests synchronous.
|
|
@@ -22,8 +22,10 @@ OMITTED (see `shadowPreamble`) to avoid a duplicate identifier. Otherwise
|
|
|
22
22
|
`state`/`linked`/`computed` are declared ambiently and `effect` is imported as a fallback
|
|
23
23
|
for a bare/nested use the top-level rewrite doesn't project (unused when projected — fine,
|
|
24
24
|
the shadow disables noUnusedLocals). `props` is the prop reader — destructured
|
|
25
|
-
(`const { a = 1 } = props<Shape>()`);
|
|
26
|
-
|
|
25
|
+
(`const { a = 1 } = props<Shape>()`); a required import (`abide/ui/props`), so the shadow
|
|
26
|
+
declares it only when imported, returning the file's contextual shape (route params or
|
|
27
|
+
`Record<string, any>`) intersected with its own type argument — additive, so declared props
|
|
28
|
+
like `children: Snippet` layer on top instead of re-spelling the route shape.
|
|
27
29
|
*/
|
|
28
30
|
function shadowPreamble(importedReactives: ReadonlySet<string>): string {
|
|
29
31
|
/* Omit the ambient fallback for a primitive the author imports — its own binding is
|
|
@@ -46,7 +48,6 @@ function shadowPreamble(importedReactives: ReadonlySet<string>): string {
|
|
|
46
48
|
importedReactives.has('computed')
|
|
47
49
|
? undefined
|
|
48
50
|
: 'declare function computed<T>(compute: () => T): { readonly value: T }',
|
|
49
|
-
'declare const children: (() => void) | undefined',
|
|
50
51
|
/* `effect` is defined either way — the preamble import or the author's own import. */
|
|
51
52
|
'void [effect, snippet, scope]',
|
|
52
53
|
]
|
|
@@ -76,15 +77,32 @@ export function compileShadow(source: string, propsType = 'Record<string, any>')
|
|
|
76
77
|
const scriptStart = leadingScript ? source.indexOf('>', leadingScript.index) + 1 : 0
|
|
77
78
|
const templateStart = leadingScript ? (leadingScript.index ?? 0) + leadingScript[0].length : 0
|
|
78
79
|
|
|
79
|
-
const { imports, types, scope, propsShapes, diagnostics, importedReactives } =
|
|
80
|
-
scriptBody,
|
|
81
|
-
scriptStart,
|
|
82
|
-
)
|
|
80
|
+
const { imports, types, scope, propsShapes, diagnostics, importedReactives, propsLocalName } =
|
|
81
|
+
analyzeScript(scriptBody, scriptStart)
|
|
83
82
|
builder.raw(shadowPreamble(importedReactives))
|
|
84
|
-
/* `props
|
|
85
|
-
|
|
86
|
-
|
|
83
|
+
/* `props` is a required import (`abide/ui/props`). The shadow owns its type so the
|
|
84
|
+
return is file-contextual — the route param shape (page/layout) or `Record<string,
|
|
85
|
+
any>` (component), intersected with the author's annotation `T` so declared props
|
|
86
|
+
(notably `children: Snippet`) are ADDITIVE and route params never need re-spelling.
|
|
87
|
+
Emitted only when imported: a missing import surfaces as "Cannot find name 'props'"
|
|
88
|
+
(or the author's local alias), so `abide check` flags it. Declared under the LOCAL
|
|
89
|
+
binding name (alias-safe, like `state`/`effect`) — a `props as p` import must get a
|
|
90
|
+
`p` declare, or `p()` reads as undefined once the import below is stripped. */
|
|
91
|
+
if (propsLocalName !== undefined) {
|
|
92
|
+
builder.raw(`declare function ${propsLocalName}<T = {}>(): (${propsType}) & T\n`)
|
|
93
|
+
}
|
|
94
|
+
const propsSpecifier = `${ABIDE_PACKAGE_NAME}/ui/props`
|
|
87
95
|
for (const line of imports) {
|
|
96
|
+
/* The `props` import is replaced by the contextual `declare function` above;
|
|
97
|
+
emitting it too would be a duplicate-identifier error. Matched by EXACT specifier
|
|
98
|
+
(either quote style, not a loose suffix match) so an unrelated user module merely
|
|
99
|
+
named `.../ui/props` survives verbatim. */
|
|
100
|
+
if (
|
|
101
|
+
line.text.includes(`from '${propsSpecifier}'`) ||
|
|
102
|
+
line.text.includes(`from "${propsSpecifier}"`)
|
|
103
|
+
) {
|
|
104
|
+
continue
|
|
105
|
+
}
|
|
88
106
|
builder.flush(line)
|
|
89
107
|
}
|
|
90
108
|
/* Component-local `type`/`interface` declarations are hoisted to module scope —
|
|
@@ -123,6 +141,19 @@ export function compileShadow(source: string, propsType = 'Record<string, any>')
|
|
|
123
141
|
/* Nested `<script>` blocks inline into the synchronous `build()` too, so a top-level
|
|
124
142
|
await in one is the same build-breaker — flag it, mapped via the node's body offset. */
|
|
125
143
|
collectNestedScriptAwaitDiagnostics(templateNodes, diagnostics)
|
|
144
|
+
/* Emit the DOM-typed attachment aliases only when an element `attach` uses them: they
|
|
145
|
+
reference `HTMLElementTagNameMap`/`Element`, so a template with no element attach never
|
|
146
|
+
forces DOM lib into its shadow. `__ElementFor` maps a tag to its element interface
|
|
147
|
+
(HTML-first; unknown/custom tags fall back to `Element`); `__Attachment` sources its
|
|
148
|
+
return type from the real `attach` runtime signature so it never drifts. */
|
|
149
|
+
if (hasElementAttach(templateNodes)) {
|
|
150
|
+
builder.raw(
|
|
151
|
+
'type __ElementFor<T extends string> = T extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap[T] : T extends keyof SVGElementTagNameMap ? SVGElementTagNameMap[T] : T extends keyof MathMLElementTagNameMap ? MathMLElementTagNameMap[T] : Element;\n',
|
|
152
|
+
)
|
|
153
|
+
builder.raw(
|
|
154
|
+
`type __Attachment<E extends Element> = (node: E) => ReturnType<Parameters<typeof import('${ABIDE_PACKAGE_NAME}/ui/dom/attach').attach>[1]>;\n`,
|
|
155
|
+
)
|
|
156
|
+
}
|
|
126
157
|
emitNodes(templateNodes, builder)
|
|
127
158
|
builder.raw('}\n')
|
|
128
159
|
return { ...builder.result(), diagnostics }
|
|
@@ -209,6 +240,10 @@ type ScriptAnalysis = {
|
|
|
209
240
|
/* The reactive primitives the author imports (`state`/`effect`), so the preamble
|
|
210
241
|
omits the ambient fallback for each and avoids a duplicate-identifier error. */
|
|
211
242
|
importedReactives: Set<string>
|
|
243
|
+
/* The LOCAL binding name the author's `props` import is bound to (alias-safe — `props`
|
|
244
|
+
for the canonical import, `p` for `props as p`), or undefined when not imported. The
|
|
245
|
+
`declare function` for `props` must target this name, not the canonical `'props'`. */
|
|
246
|
+
propsLocalName: string | undefined
|
|
212
247
|
}
|
|
213
248
|
|
|
214
249
|
/* Pushes a diagnostic for every author binding whose name starts with the reserved `$$`
|
|
@@ -321,7 +356,15 @@ function analyzeScript(scriptBody: string, scriptStart: number): ScriptAnalysis
|
|
|
321
356
|
const propsShapes: string[] = []
|
|
322
357
|
const diagnostics: ShadowDiagnostic[] = []
|
|
323
358
|
if (scriptBody.trim() === '') {
|
|
324
|
-
return {
|
|
359
|
+
return {
|
|
360
|
+
imports,
|
|
361
|
+
types,
|
|
362
|
+
scope,
|
|
363
|
+
propsShapes,
|
|
364
|
+
diagnostics,
|
|
365
|
+
importedReactives: new Set(),
|
|
366
|
+
propsLocalName: undefined,
|
|
367
|
+
}
|
|
325
368
|
}
|
|
326
369
|
const file = ts.createSourceFile('script.ts', scriptBody, ts.ScriptTarget.Latest, true)
|
|
327
370
|
/* The author's reactive import bindings (alias-safe) — recognition source for
|
|
@@ -329,6 +372,15 @@ function analyzeScript(scriptBody: string, scriptStart: number): ScriptAnalysis
|
|
|
329
372
|
the preamble omits its ambient fallback for. */
|
|
330
373
|
const bindings = reactiveImportBindings(file)
|
|
331
374
|
const importedReactives = new Set(bindings.direct.values())
|
|
375
|
+
/* The local name bound to `props` (alias-safe): the key whose value is the canonical
|
|
376
|
+
`'props'`. At most one — a file imports `props` from one specifier. */
|
|
377
|
+
let propsLocalName: string | undefined
|
|
378
|
+
for (const [local, canonical] of bindings.direct) {
|
|
379
|
+
if (canonical === 'props') {
|
|
380
|
+
propsLocalName = local
|
|
381
|
+
break
|
|
382
|
+
}
|
|
383
|
+
}
|
|
332
384
|
/* The `$$` prefix is reserved for the compiler's injected runtime (`$$each`, `$$model`,
|
|
333
385
|
`$$scope`, …), so an author binding may not start with it — that's the contract that
|
|
334
386
|
lets a user freely name a variable after any helper. Flag every such declaration. */
|
|
@@ -368,7 +420,7 @@ function analyzeScript(scriptBody: string, scriptStart: number): ScriptAnalysis
|
|
|
368
420
|
scope.push(scopeLineFor(declaration, propsShapes, verbatim, span, bindings))
|
|
369
421
|
}
|
|
370
422
|
}
|
|
371
|
-
return { imports, types, scope, propsShapes, diagnostics, importedReactives }
|
|
423
|
+
return { imports, types, scope, propsShapes, diagnostics, importedReactives, propsLocalName }
|
|
372
424
|
}
|
|
373
425
|
|
|
374
426
|
/* Value-projects a nested control-flow `<script>` body the way `analyzeScript`
|
|
@@ -446,14 +498,22 @@ function scopeLineFor(
|
|
|
446
498
|
})
|
|
447
499
|
if (callee === 'state') {
|
|
448
500
|
/* state<T>(initial): T is the value type — carry it onto the `let` so an
|
|
449
|
-
explicit annotation isn't lost to `any`/`any[]` inference of the initial.
|
|
450
|
-
|
|
501
|
+
explicit annotation isn't lost to `any`/`any[]` inference of the initial. The
|
|
502
|
+
type comes from the generic (`state<T>(v)`), else the binding annotation
|
|
503
|
+
(`let x: T = state(v)`) — either form pins the cell type, so a narrow/`any`
|
|
504
|
+
inference of the initial (`state([])` → `any[]`) can't leak. */
|
|
505
|
+
const typeNode = call.typeArguments?.[0] ?? declaration.type
|
|
451
506
|
const annotation = typeNode === undefined ? '' : `: ${verbatim(typeNode)}`
|
|
452
507
|
const init = call.arguments[0]
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
508
|
+
/* A literal `undefined` initial is the no-arg form spelled out (`state<T>(undefined)`):
|
|
509
|
+
the value is `T | undefined`, never `= (undefined)` checked against a non-optional
|
|
510
|
+
`T` — which would spuriously flag "undefined not assignable to T". */
|
|
511
|
+
const initIsUndefined =
|
|
512
|
+
init !== undefined && ts.isIdentifier(init) && init.text === 'undefined'
|
|
513
|
+
if (init === undefined || initIsUndefined) {
|
|
514
|
+
/* No initial (`state<T>()` / `state<T>(undefined)`): the value is `T | undefined`.
|
|
515
|
+
A definite-assignment assertion (`!`) gives that union without a use-before-
|
|
516
|
+
assign false-positive AND without control-flow narrowing it to just `undefined`
|
|
457
517
|
(an `= undefined` initializer, never reassigned in the shadow, would make
|
|
458
518
|
a guard like `x !== undefined` collapse to `never`). Unguarded access is
|
|
459
519
|
then correctly flagged possibly-undefined; a guard narrows cleanly. */
|
|
@@ -471,9 +531,12 @@ function scopeLineFor(
|
|
|
471
531
|
})
|
|
472
532
|
}
|
|
473
533
|
/* computed<T>(compute) / linked<T>(seed) — the only callees left: T is the value
|
|
474
|
-
type — the call's first arg is a thunk, so invoking it yields the value.
|
|
475
|
-
|
|
476
|
-
|
|
534
|
+
type — the call's first arg is a thunk, so invoking it yields the value. The type
|
|
535
|
+
comes from the generic (`computed<T>(fn)`), else the binding annotation
|
|
536
|
+
(`let x: T = state.computed(fn)`) — same fallback as `state` above, so a binding
|
|
537
|
+
annotation isn't dropped and `abide check` doesn't emit `never`-inference false
|
|
538
|
+
positives on the thunk's return. */
|
|
539
|
+
const typeNode = call.typeArguments?.[0] ?? declaration.type
|
|
477
540
|
const annotation = typeNode === undefined ? '' : `: ${verbatim(typeNode)}`
|
|
478
541
|
const fn = call.arguments[0]
|
|
479
542
|
/* `linked` is a writable `State<T>` at runtime (it reseeds AND accepts `.value =`
|
|
@@ -494,6 +557,28 @@ function scopeLineFor(
|
|
|
494
557
|
})
|
|
495
558
|
}
|
|
496
559
|
|
|
560
|
+
/* Whether any ELEMENT in the tree carries an `attach` — gates emitting the DOM-typed
|
|
561
|
+
attachment aliases. All nested content (block bodies, await/switch branches, snippet
|
|
562
|
+
bodies) routes through `children`, so a recursive `children` walk is complete. */
|
|
563
|
+
function hasElementAttach(nodes: TemplateNode[]): boolean {
|
|
564
|
+
for (const node of nodes) {
|
|
565
|
+
if (node === undefined) {
|
|
566
|
+
continue
|
|
567
|
+
}
|
|
568
|
+
if (node.kind === 'element') {
|
|
569
|
+
for (const attr of node.attrs) {
|
|
570
|
+
if (attr.kind === 'attach') {
|
|
571
|
+
return true
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
if ('children' in node && node.children !== undefined && hasElementAttach(node.children)) {
|
|
576
|
+
return true
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return false
|
|
580
|
+
}
|
|
581
|
+
|
|
497
582
|
/* Emits a sibling list — each node standalone via `emitNode`. */
|
|
498
583
|
function emitNodes(nodes: TemplateNode[], builder: Builder): void {
|
|
499
584
|
for (const node of nodes) {
|
|
@@ -517,15 +602,26 @@ function emitNode(node: TemplateNode, builder: Builder): void {
|
|
|
517
602
|
return
|
|
518
603
|
case 'element':
|
|
519
604
|
for (const attr of node.attrs) {
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
605
|
+
if (attr.kind === 'attach') {
|
|
606
|
+
/* `attach={code}` types its callback's `node` param from the element's tag
|
|
607
|
+
(via `__ElementFor`) and checks the whole value is an attachment: an inline
|
|
608
|
+
arrow's `node` reads the specific DOM interface, and a non-function value is
|
|
609
|
+
rejected. Same IIFE-parameter contextual-typing trick the component-prop
|
|
610
|
+
path uses; the leading `;` guards an unterminated preceding statement. */
|
|
611
|
+
builder.raw(
|
|
612
|
+
`;((__attach: __Attachment<__ElementFor<${JSON.stringify(node.tag)}>>) => {})(`,
|
|
613
|
+
)
|
|
614
|
+
builder.expr(attr.code, attr.loc)
|
|
615
|
+
builder.raw(');\n')
|
|
616
|
+
} else if (attr.kind === 'interpolated') {
|
|
617
|
+
/* An interpolated value checks each `{expr}` part on its own offset. */
|
|
523
618
|
for (const part of attr.parts) {
|
|
524
619
|
if (part.kind === 'expression') {
|
|
525
620
|
builder.stmt(part.code, part.loc)
|
|
526
621
|
}
|
|
527
622
|
}
|
|
528
623
|
} else if (attr.kind !== 'static') {
|
|
624
|
+
/* Every other dynamic attribute checks its single `code`. */
|
|
529
625
|
builder.stmt(attr.code, attr.loc)
|
|
530
626
|
}
|
|
531
627
|
}
|
|
@@ -773,7 +869,7 @@ function emitNode(node: TemplateNode, builder: Builder): void {
|
|
|
773
869
|
builder.mapped(node.params, node.loc)
|
|
774
870
|
}
|
|
775
871
|
/* Wrap the body in `snippet(() => …)` so the shadow types the snippet as
|
|
776
|
-
`(args) =>
|
|
872
|
+
`(args) => SnippetValue` — mirroring the runtime lowering (which returns
|
|
777
873
|
`$$snippet(($host) => …)`) so a snippet passed to a `Snippet`-typed prop
|
|
778
874
|
type-checks instead of reading as `() => void`. */
|
|
779
875
|
builder.raw(') => snippet(() => {\n')
|
|
@@ -11,8 +11,9 @@ SSR/client prop congruence rests on. No spread → the plain object literal of n
|
|
|
11
11
|
value thunks (+ the trailing slot). With a `{...expr}` spread → a `mergeProps` of
|
|
12
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
|
-
`lowerExpression` is the caller's expression lowering; `slotPart` is its
|
|
15
|
-
layer
|
|
14
|
+
`lowerExpression` is the caller's expression lowering; `slotPart` is its `children`
|
|
15
|
+
layer — slotted content as a `Snippet` under the `children` prop key (a builder-
|
|
16
|
+
returning callable for the client, a `$snip`-string-returning callable for SSR) — or
|
|
16
17
|
undefined when the component has no slotted children.
|
|
17
18
|
*/
|
|
18
19
|
export function composeProps(
|
|
@@ -430,7 +430,7 @@ function propsStatements(
|
|
|
430
430
|
),
|
|
431
431
|
)
|
|
432
432
|
}
|
|
433
|
-
/* The rest bag gathers every prop not named above (and not
|
|
433
|
+
/* The rest bag gathers every prop not named above (and not `children`). */
|
|
434
434
|
if (rest !== undefined) {
|
|
435
435
|
const consumed = propBindings.map((binding) => factory.createStringLiteral(binding.key))
|
|
436
436
|
statements.push(
|
|
@@ -200,7 +200,7 @@ export function generateBuild(
|
|
|
200
200
|
(`type="number"`/`"range"`) reports its edit as a string on `el.value`, which
|
|
201
201
|
would corrupt number-typed state; source the write-back from `valueAsNumber`
|
|
202
202
|
instead (empty field → `undefined`), gated on a statically-known type. */
|
|
203
|
-
const event = bindListenEvent(attr.property
|
|
203
|
+
const event = bindListenEvent(attr.property)
|
|
204
204
|
const staticType = staticAttrValue(node, 'type')
|
|
205
205
|
const isNumericInput =
|
|
206
206
|
attr.property === 'value' &&
|
|
@@ -547,21 +547,29 @@ export function generateBuild(
|
|
|
547
547
|
return `$$switchBlock(${parentVar}, () => (${lowerExpression(node.subject)}), [${cases}], ${before});\n`
|
|
548
548
|
}
|
|
549
549
|
|
|
550
|
-
/* A `{children()}`
|
|
551
|
-
|
|
552
|
-
|
|
550
|
+
/* A component `{children()}` fill point: mount the `children` prop (a `Snippet`) by
|
|
551
|
+
calling it and mounting the resulting value through the snippet interpolation path,
|
|
552
|
+
exactly as any `{snippet(args)}`. `lowerExpression('children()')` reads the destructured
|
|
553
|
+
`children` computed and calls it → a `SnippetValue`, which `appendText` routes to
|
|
554
|
+
`appendSnippet`. A fallback is now an authored `{#if children}…{:else}…{/if}`, so the slot
|
|
555
|
+
node carries no children. Layouts never reach here — their slots are rewritten to `outlet`
|
|
556
|
+
elements by `asOutlet`. */
|
|
553
557
|
function generateSlot(
|
|
554
558
|
_node: Extract<TemplateNode, { kind: 'element' }>,
|
|
555
559
|
parentVar: string,
|
|
556
560
|
): string {
|
|
557
|
-
return
|
|
561
|
+
return `$$appendText(${parentVar}, () => (${lowerExpression('children()')}));\n`
|
|
558
562
|
}
|
|
559
563
|
|
|
560
|
-
/*
|
|
561
|
-
|
|
564
|
+
/* Slot content as a zero-arg `Snippet` under the `children` key — a callable returning a
|
|
565
|
+
`$$snippet`-branded builder, so it unifies with a passed `children={snippet}` and mounts
|
|
566
|
+
through the standard snippet interpolation path. `composeProps` wraps it in the prop-bag
|
|
567
|
+
thunk, so the final bag value is `() => (Snippet callable)`. */
|
|
562
568
|
function slotPart(node: Extract<TemplateNode, { kind: 'component' }>): string | undefined {
|
|
563
569
|
const slotCode = generateChildren(node.children, '$slot')
|
|
564
|
-
return slotCode.trim() === ''
|
|
570
|
+
return slotCode.trim() === ''
|
|
571
|
+
? undefined
|
|
572
|
+
: `"children": () => (() => $$snippet(($slot) => {\n${slotCode}}))`
|
|
565
573
|
}
|
|
566
574
|
|
|
567
575
|
/* The props bag a child mount receives — composed by the shared `composeProps` so the
|
|
@@ -581,7 +589,13 @@ export function generateBuild(
|
|
|
581
589
|
parentVar: string,
|
|
582
590
|
before: string,
|
|
583
591
|
): string {
|
|
584
|
-
|
|
592
|
+
/* The tag lowers through `lowerExpression` like any other reference: a static
|
|
593
|
+
module import (`Button`) is left bare, but a reactive binding — a `{#for}`
|
|
594
|
+
item, an `await` `then` value, a component signal — derefs to its `.value`
|
|
595
|
+
cell, so `<Icon>` from `{#for {icon: Icon} of …}` mounts the component the cell
|
|
596
|
+
holds, not the cell object (whose `.build` is undefined → `build is not a
|
|
597
|
+
function`). SSR emits the same lowering for congruence. */
|
|
598
|
+
return `$$mountChild(${parentVar}, ${lowerExpression(node.name)}, ${propsArg(node)}, ${before}, ${JSON.stringify(node.name)});\n`
|
|
585
599
|
}
|
|
586
600
|
|
|
587
601
|
/* An await block: pending → resolved(value) / error branches. Each branch is a
|