@abide/abide 0.41.1 → 0.43.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +234 -294
- package/CHANGELOG.md +82 -0
- package/README.md +51 -95
- package/package.json +3 -2
- package/src/abideLsp.ts +46 -8
- package/src/abideResolverPlugin.ts +107 -19
- package/src/buildCli.ts +1 -1
- package/src/discoveryEntry.ts +3 -3
- package/src/lib/bundle/BundleMenuItem.ts +1 -1
- package/src/lib/cli/createClient.ts +12 -12
- package/src/lib/cli/dispatchCommand.ts +1 -1
- package/src/lib/cli/printSessionStatus.ts +1 -1
- package/src/lib/cli/resolveCliTarget.ts +1 -1
- package/src/lib/cli/runCli.ts +1 -1
- package/src/lib/cli/types/CliManifest.ts +1 -1
- package/src/lib/cli/types/CliManifestEntry.ts +2 -2
- package/src/lib/mcp/annotationsForMethod.ts +5 -5
- package/src/lib/mcp/createMcpServer.ts +3 -3
- package/src/lib/mcp/mcpSurface.ts +14 -14
- package/src/lib/mcp/types/McpServerOptions.ts +1 -1
- package/src/lib/server/DELETE.ts +4 -4
- package/src/lib/server/GET.ts +4 -4
- package/src/lib/server/HEAD.ts +4 -4
- package/src/lib/server/PATCH.ts +4 -4
- package/src/lib/server/POST.ts +4 -4
- package/src/lib/server/PUT.ts +4 -4
- package/src/lib/server/agent.ts +4 -4
- package/src/lib/server/env.ts +1 -1
- package/src/lib/server/error.ts +49 -7
- package/src/lib/server/json.ts +1 -1
- package/src/lib/server/rpc/buildErrorConstructors.ts +19 -0
- package/src/lib/server/rpc/{defineVerb.ts → defineRpc.ts} +52 -34
- package/src/lib/server/rpc/{dispatchVerbInProcess.ts → dispatchRpcInProcess.ts} +3 -3
- package/src/lib/server/rpc/fieldErrorsFromIssues.ts +23 -0
- package/src/lib/server/rpc/{findVerbByCommandName.ts → findRpcByCommandName.ts} +5 -5
- package/src/lib/server/rpc/parseArgs.ts +6 -6
- package/src/lib/server/rpc/readBodyWithinLimit.ts +2 -2
- package/src/lib/server/rpc/registerRpc.ts +6 -0
- package/src/lib/server/rpc/{verbRegistry.ts → rpcRegistry.ts} +4 -4
- package/src/lib/server/rpc/{runWithVerbTimeout.ts → runWithRpcTimeout.ts} +4 -4
- package/src/lib/server/rpc/types/RemoteHandler.ts +9 -3
- package/src/lib/server/rpc/types/{VerbHelper.ts → RpcHelper.ts} +57 -23
- package/src/lib/server/rpc/types/{VerbRegistryEntry.ts → RpcRegistryEntry.ts} +3 -3
- package/src/lib/server/rpc/types/TypedResponse.ts +2 -2
- package/src/lib/server/rpc/unprocessed.ts +6 -7
- package/src/lib/server/rpc/validationError.ts +17 -0
- package/src/lib/server/runtime/buildInspectorSurface.ts +7 -7
- package/src/lib/server/runtime/buildOpenApiSpec.ts +6 -6
- package/src/lib/server/runtime/createRouteDispatcher.ts +7 -7
- package/src/lib/server/runtime/createServer.ts +4 -4
- package/src/lib/server/runtime/crossOriginForbidden.ts +1 -1
- package/src/lib/server/runtime/crossOriginGate.ts +4 -4
- package/src/lib/server/runtime/devClientFingerprint.ts +2 -1
- package/src/lib/server/runtime/logExposedSurfaces.ts +4 -4
- package/src/lib/server/runtime/registryManifests.ts +2 -2
- package/src/lib/server/runtime/types/InspectorCacheEntry.ts +1 -1
- package/src/lib/server/runtime/types/InspectorRpc.ts +27 -0
- package/src/lib/server/runtime/types/InspectorSurface.ts +4 -4
- package/src/lib/server/runtime/types/RequestStore.ts +1 -1
- package/src/lib/server/runtime/warnUnguardedMcp.ts +2 -2
- package/src/lib/server/sockets/createSocketDispatcher.ts +15 -14
- package/src/lib/server/sockets/types/SocketOperation.ts +2 -2
- package/src/lib/shared/HttpError.ts +15 -1
- package/src/lib/shared/UNREACHABLE_STATUSES.ts +13 -0
- package/src/lib/shared/buildRpcRequest.ts +5 -5
- package/src/lib/shared/cache.ts +50 -168
- package/src/lib/shared/carriesBodyArgs.ts +4 -4
- package/src/lib/shared/createCacheStore.ts +12 -0
- package/src/lib/shared/createRemoteFunction.ts +11 -6
- package/src/lib/shared/decodeResponse.ts +2 -2
- package/src/lib/shared/detectRpcMethod.ts +17 -0
- package/src/lib/shared/emitLogRecord.ts +3 -3
- package/src/lib/shared/escapeRegex.ts +9 -0
- package/src/lib/shared/extraForwardHeaders.ts +1 -1
- package/src/lib/shared/fileName.ts +9 -0
- package/src/lib/shared/fileStem.ts +3 -1
- package/src/lib/shared/findExportCallSite.ts +30 -273
- package/src/lib/shared/forwardHeaders.ts +2 -2
- package/src/lib/shared/httpErrorFor.ts +26 -0
- package/src/lib/shared/isAsciiWhitespace.ts +5 -0
- package/src/lib/shared/isIdentPart.ts +9 -0
- package/src/lib/shared/isIdentStart.ts +8 -0
- package/src/lib/shared/isReadOnlyMethod.ts +4 -4
- package/src/lib/shared/keyForRemoteCall.ts +4 -4
- package/src/lib/shared/log.ts +1 -1
- package/src/lib/shared/outboxProbeSlot.ts +20 -0
- package/src/lib/shared/pending.ts +4 -1
- package/src/lib/shared/prepareRpcModule.ts +96 -14
- package/src/lib/shared/probeRegistries.ts +17 -4
- package/src/lib/shared/programNameForPackage.ts +3 -1
- package/src/lib/shared/queryStringFromArgs.ts +1 -1
- package/src/lib/shared/remoteMetaStore.ts +2 -2
- package/src/lib/shared/resolveClientFlags.ts +1 -1
- package/src/lib/shared/skipNonCode.ts +230 -0
- package/src/lib/shared/streamResponse.ts +9 -6
- package/src/lib/shared/stripImport.ts +6 -4
- package/src/lib/shared/types/CacheEntry.ts +2 -4
- package/src/lib/shared/types/CacheOnContext.ts +1 -11
- package/src/lib/shared/types/CacheStore.ts +13 -7
- package/src/lib/shared/types/ClientFlags.ts +1 -1
- package/src/lib/shared/types/ErrorConstructors.ts +17 -0
- package/src/lib/shared/types/ErrorDescriptor.ts +10 -0
- package/src/lib/shared/types/ErrorSpec.ts +11 -0
- package/src/lib/shared/types/HttpMethod.ts +1 -0
- package/src/lib/shared/types/Outbox.ts +9 -0
- package/src/lib/shared/types/OutboxEntry.ts +27 -0
- package/src/lib/shared/types/RawRemoteFunction.ts +2 -2
- package/src/lib/shared/types/RemoteCallable.ts +2 -2
- package/src/lib/shared/types/RemoteFunction.ts +21 -6
- package/src/lib/shared/types/RequestScopeInfo.ts +1 -1
- package/src/lib/shared/types/RpcErrorGuard.ts +40 -0
- package/src/lib/shared/types/RpcInvoker.ts +1 -1
- package/src/lib/shared/types/StandardSchemaV1.ts +1 -1
- package/src/lib/shared/types/ValidationErrorData.ts +20 -0
- package/src/lib/shared/url.ts +3 -3
- package/src/lib/shared/writeRpcDts.ts +7 -7
- package/src/lib/shared/writeTestRpcDts.ts +3 -3
- package/src/lib/test/createTestApp.ts +10 -10
- package/src/lib/ui/compile/ABIDE_SEMANTIC_TOKENS_LEGEND.ts +45 -0
- package/src/lib/ui/compile/abideUiPlugin.ts +2 -1
- package/src/lib/ui/compile/awaitPlan.ts +48 -0
- package/src/lib/ui/compile/compileShadow.ts +117 -31
- package/src/lib/ui/compile/createShadowLanguageService.ts +48 -0
- package/src/lib/ui/compile/desugarSignals.ts +42 -6
- package/src/lib/ui/compile/encodeSemanticTokens.ts +37 -0
- package/src/lib/ui/compile/generateBuild.ts +45 -62
- package/src/lib/ui/compile/generateSSR.ts +90 -60
- package/src/lib/ui/compile/ifPlan.ts +30 -0
- package/src/lib/ui/compile/makeVarNamer.ts +10 -0
- package/src/lib/ui/compile/offsetToPosition.ts +9 -0
- package/src/lib/ui/compile/parseTemplate.ts +565 -115
- package/src/lib/ui/compile/stripEffects.ts +17 -9
- package/src/lib/ui/compile/structuralBlockTokens.ts +101 -0
- package/src/lib/ui/compile/switchPlan.ts +22 -0
- package/src/lib/ui/compile/tryPlan.ts +29 -0
- package/src/lib/ui/compile/types/SemanticToken.ts +11 -0
- package/src/lib/ui/compile/types/TemplateAttr.ts +8 -2
- package/src/lib/ui/compile/types/TemplateNode.ts +15 -2
- package/src/lib/ui/createScope.ts +20 -2
- package/src/lib/ui/dom/appendText.ts +1 -5
- package/src/lib/ui/dom/applyResolved.ts +1 -5
- package/src/lib/ui/dom/disposeRange.ts +6 -10
- package/src/lib/ui/dom/hydrate.ts +2 -5
- package/src/lib/ui/dom/isComment.ts +6 -0
- package/src/lib/ui/dom/mount.ts +1 -2
- package/src/lib/ui/dom/withScope.ts +6 -9
- package/src/lib/ui/effect.ts +4 -2
- package/src/lib/ui/navigate.ts +40 -13
- package/src/lib/ui/outbox.ts +30 -110
- package/src/lib/ui/remoteProxy.ts +160 -10
- package/src/lib/ui/router.ts +3 -3
- package/src/lib/ui/rpcOutbox/createOutboxQueue.ts +281 -0
- package/src/lib/ui/rpcOutbox/outboxRegistry.ts +69 -0
- package/src/lib/ui/runtime/createDoc.ts +3 -3
- package/src/lib/ui/runtime/pathSegments.ts +10 -0
- package/src/lib/ui/socketChannel.ts +7 -1
- package/src/lib/ui/types/Scope.ts +20 -5
- package/template/.zed/settings.json +4 -0
- package/template/CLAUDE.md +1 -1
- package/template/src/server/rpc/getHello.ts +3 -3
- package/template/src/ui/pages/page.abide +4 -4
- package/template/test/app.test.ts +2 -2
- package/src/lib/server/rpc/registerVerb.ts +0 -6
- package/src/lib/server/runtime/types/InspectorVerb.ts +0 -27
- package/src/lib/shared/detectVerbMethod.ts +0 -17
- package/src/lib/shared/types/HttpVerb.ts +0 -1
- package/src/lib/ui/types/Outbox.ts +0 -14
|
@@ -2,12 +2,14 @@ import ts from 'typescript'
|
|
|
2
2
|
import { TS_PRINTER } from './TS_PRINTER.ts'
|
|
3
3
|
|
|
4
4
|
/*
|
|
5
|
-
Removes
|
|
5
|
+
Removes effect calls from a script for the SSR back-end. Effects are client
|
|
6
6
|
lifecycle — they touch the DOM / run side effects and emit no HTML, so the server
|
|
7
7
|
render (a snapshot of the pre-effect markup, like every framework) must not run
|
|
8
|
-
them.
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
them. Both surfaces are stripped: the generated/runtime bare `effect(<args>)` and
|
|
9
|
+
the authored scope form `scope().effect(<args>)` (and a captured/destructured handle
|
|
10
|
+
`c.effect(<args>)`), each replaced by `undefined` — an `effect(() => …)` statement
|
|
11
|
+
becomes a no-op, and a `const stop = effect(…)` binding keeps a defined (unused) name.
|
|
12
|
+
Client compilation keeps effects untouched.
|
|
11
13
|
*/
|
|
12
14
|
export function stripEffects(code: string): string {
|
|
13
15
|
const source = ts.createSourceFile('script.ts', code, ts.ScriptTarget.Latest, true)
|
|
@@ -23,11 +25,7 @@ export function stripEffects(code: string): string {
|
|
|
23
25
|
export function stripEffectsTransformer(): ts.TransformerFactory<ts.SourceFile> {
|
|
24
26
|
return (context) => (root) => {
|
|
25
27
|
const visit = (node: ts.Node): ts.Node => {
|
|
26
|
-
if (
|
|
27
|
-
ts.isCallExpression(node) &&
|
|
28
|
-
ts.isIdentifier(node.expression) &&
|
|
29
|
-
node.expression.text === 'effect'
|
|
30
|
-
) {
|
|
28
|
+
if (ts.isCallExpression(node) && isEffectCallee(node.expression)) {
|
|
31
29
|
return ts.factory.createIdentifier('undefined')
|
|
32
30
|
}
|
|
33
31
|
return ts.visitEachChild(node, visit, context)
|
|
@@ -35,3 +33,13 @@ export function stripEffectsTransformer(): ts.TransformerFactory<ts.SourceFile>
|
|
|
35
33
|
return ts.visitNode(root, visit) as ts.SourceFile
|
|
36
34
|
}
|
|
37
35
|
}
|
|
36
|
+
|
|
37
|
+
/* An effect callee: the bare runtime helper (`effect`) or the scope-method form
|
|
38
|
+
(`scope().effect`, `c.effect`) the author writes. Receiver-agnostic on the `.effect`
|
|
39
|
+
member name, matching `effect` being reserved as a scope primitive. */
|
|
40
|
+
function isEffectCallee(expression: ts.Expression): boolean {
|
|
41
|
+
if (ts.isIdentifier(expression)) {
|
|
42
|
+
return expression.text === 'effect'
|
|
43
|
+
}
|
|
44
|
+
return ts.isPropertyAccessExpression(expression) && expression.name.text === 'effect'
|
|
45
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { SemanticToken } from './types/SemanticToken.ts'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
Lexical highlighting for `{#…}`/`{:…}`/`{/…}` control-flow framing — the part the
|
|
5
|
+
HTML grammar sees only as text and the shadow program lowers away. A pure scan of
|
|
6
|
+
raw source (independent of a successful parse, so it survives mid-edit), it emits
|
|
7
|
+
an `operator` token for the `{`+sigil opener and a `keyword` token for the block
|
|
8
|
+
word, plus the `of`/`by` connectors inside a `{#for …}` head. Expression interiors
|
|
9
|
+
(and the `{@const}`/`{@html}` tags and bare `{expr}` interpolations) are NOT touched
|
|
10
|
+
here — those are the shadow's job. A keyword allowlist after the sigil prevents
|
|
11
|
+
matching arbitrary `{:foo}` text. Longest phrases first so `for await` beats `for`
|
|
12
|
+
and `else if` beats `else`.
|
|
13
|
+
*/
|
|
14
|
+
const BLOCK_KEYWORDS = [
|
|
15
|
+
'for await',
|
|
16
|
+
'else if',
|
|
17
|
+
'if',
|
|
18
|
+
'for',
|
|
19
|
+
'await',
|
|
20
|
+
'switch',
|
|
21
|
+
'case',
|
|
22
|
+
'default',
|
|
23
|
+
'try',
|
|
24
|
+
'catch',
|
|
25
|
+
'finally',
|
|
26
|
+
'then',
|
|
27
|
+
'else',
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
const BLOCK_HEAD = new RegExp(`\\{([#:/])\\s*(${BLOCK_KEYWORDS.join('|')})\\b`, 'g')
|
|
31
|
+
|
|
32
|
+
export function structuralBlockTokens(source: string): SemanticToken[] {
|
|
33
|
+
const tokens: SemanticToken[] = []
|
|
34
|
+
for (const match of source.matchAll(BLOCK_HEAD)) {
|
|
35
|
+
const braceStart = match.index
|
|
36
|
+
const sigil = match[1]
|
|
37
|
+
const keyword = match[2]
|
|
38
|
+
if (keyword === undefined) {
|
|
39
|
+
continue
|
|
40
|
+
}
|
|
41
|
+
const keywordStart = braceStart + match[0].length - keyword.length
|
|
42
|
+
tokens.push({ start: braceStart, length: 2, type: 'operator', modifiers: [] })
|
|
43
|
+
tokens.push({ start: keywordStart, length: keyword.length, type: 'keyword', modifiers: [] })
|
|
44
|
+
/* A `{#for …}` head carries abide-only connectors `of`/`by` that the shadow
|
|
45
|
+
lowers away (`of`) or never emits (`by`), so color them here. */
|
|
46
|
+
if (sigil === '#' && (keyword === 'for' || keyword === 'for await')) {
|
|
47
|
+
tokens.push(...forHeadConnectors(source, keywordStart + keyword.length))
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return tokens
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/*
|
|
54
|
+
Scans a `{#for …}` head from `from` to its closing `}`, emitting `keyword` tokens
|
|
55
|
+
for the `of`/`by` connectors at brace depth 0 — skipping any `of`/`by` nested in a
|
|
56
|
+
destructure, call, or string so an identifier or object key is never miscolored.
|
|
57
|
+
*/
|
|
58
|
+
function forHeadConnectors(source: string, from: number): SemanticToken[] {
|
|
59
|
+
const tokens: SemanticToken[] = []
|
|
60
|
+
let depth = 0
|
|
61
|
+
let cursor = from
|
|
62
|
+
while (cursor < source.length) {
|
|
63
|
+
const char = source.charAt(cursor)
|
|
64
|
+
if (char === '"' || char === "'" || char === '`') {
|
|
65
|
+
cursor += 1
|
|
66
|
+
while (cursor < source.length && source.charAt(cursor) !== char) {
|
|
67
|
+
if (source.charAt(cursor) === '\\') {
|
|
68
|
+
cursor += 1
|
|
69
|
+
}
|
|
70
|
+
cursor += 1
|
|
71
|
+
}
|
|
72
|
+
} else if (char === '{' || char === '(' || char === '[') {
|
|
73
|
+
depth += 1
|
|
74
|
+
} else if (char === ')' || char === ']') {
|
|
75
|
+
depth -= 1
|
|
76
|
+
} else if (char === '}') {
|
|
77
|
+
if (depth === 0) {
|
|
78
|
+
break
|
|
79
|
+
}
|
|
80
|
+
depth -= 1
|
|
81
|
+
} else if (depth === 0 && (char === 'o' || char === 'b')) {
|
|
82
|
+
const word = source.startsWith('of', cursor)
|
|
83
|
+
? 'of'
|
|
84
|
+
: source.startsWith('by', cursor)
|
|
85
|
+
? 'by'
|
|
86
|
+
: undefined
|
|
87
|
+
const isWordBoundary = (offset: number): boolean => /\s/.test(source.charAt(offset))
|
|
88
|
+
if (
|
|
89
|
+
word !== undefined &&
|
|
90
|
+
isWordBoundary(cursor - 1) &&
|
|
91
|
+
isWordBoundary(cursor + word.length)
|
|
92
|
+
) {
|
|
93
|
+
tokens.push({ start: cursor, length: word.length, type: 'keyword', modifiers: [] })
|
|
94
|
+
cursor += word.length
|
|
95
|
+
continue
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
cursor += 1
|
|
99
|
+
}
|
|
100
|
+
return tokens
|
|
101
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { TemplateNode } from './types/TemplateNode.ts'
|
|
2
|
+
|
|
3
|
+
type Case = Extract<TemplateNode, { kind: 'case' }>
|
|
4
|
+
|
|
5
|
+
/* The structural shape of a `switch`, resolved once so the build and SSR back-ends share one
|
|
6
|
+
reading of its cases and only own emission. Both previously recomputed the `case` filter;
|
|
7
|
+
SSR additionally located the match-less default. */
|
|
8
|
+
export type SwitchPlan = {
|
|
9
|
+
/* The `case` branches in source order (`match` set; the default leaves it unset). */
|
|
10
|
+
cases: Case[]
|
|
11
|
+
/* The match-less default branch, if present. */
|
|
12
|
+
fallback: Case | undefined
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/* Resolves a `switch` node's cases into the shared structural plan. */
|
|
16
|
+
export function switchPlan(node: Extract<TemplateNode, { kind: 'switch' }>): SwitchPlan {
|
|
17
|
+
const cases = node.children.filter((child): child is Case => child.kind === 'case')
|
|
18
|
+
return {
|
|
19
|
+
cases,
|
|
20
|
+
fallback: cases.find((branch) => branch.match === undefined),
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { resolveBranches } from './resolveBranches.ts'
|
|
2
|
+
import type { TemplateNode } from './types/TemplateNode.ts'
|
|
3
|
+
|
|
4
|
+
/* The structural shape of a `try` error boundary, resolved once so the build and SSR
|
|
5
|
+
back-ends share one reading and only own emission. Both previously recomputed branch
|
|
6
|
+
resolution, the `finallyChildren` default, the guarded (children-minus-branch) split, and
|
|
7
|
+
the catch binding / presence. */
|
|
8
|
+
export type TryPlan = {
|
|
9
|
+
/* The guarded content — every child except the `catch`/`finally` branch nodes. */
|
|
10
|
+
guarded: TemplateNode[]
|
|
11
|
+
/* The catch content + its bound name (`_error` default); empty when no catch branch. */
|
|
12
|
+
catchChildren: TemplateNode[]
|
|
13
|
+
catchAs: string
|
|
14
|
+
finallyChildren: TemplateNode[]
|
|
15
|
+
/* No catch → a throw propagates to the enclosing boundary (re-throw / `undefined` thunk). */
|
|
16
|
+
hasCatch: boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/* Resolves a `try` node's branches into the shared structural plan. */
|
|
20
|
+
export function tryPlan(node: Extract<TemplateNode, { kind: 'try' }>): TryPlan {
|
|
21
|
+
const [catchBranch, finallyBranch] = resolveBranches(node, 'catch', 'finally')
|
|
22
|
+
return {
|
|
23
|
+
guarded: node.children.filter((child) => child.kind !== 'branch'),
|
|
24
|
+
catchChildren: catchBranch?.children ?? [],
|
|
25
|
+
catchAs: catchBranch?.as ?? '_error',
|
|
26
|
+
finallyChildren: finallyBranch?.children ?? [],
|
|
27
|
+
hasCatch: catchBranch !== undefined,
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/*
|
|
2
|
+
A semantic-highlighting token in original `.abide` source coordinates. `type` and
|
|
3
|
+
`modifiers` are legend names (see ABIDE_SEMANTIC_TOKENS_LEGEND), resolved to wire
|
|
4
|
+
indices only at encode time.
|
|
5
|
+
*/
|
|
6
|
+
export type SemanticToken = {
|
|
7
|
+
start: number
|
|
8
|
+
length: number
|
|
9
|
+
type: string
|
|
10
|
+
modifiers: string[]
|
|
11
|
+
}
|
|
@@ -13,9 +13,15 @@ absolute offset of `code` in the original `.abide` source (see TextPart) —
|
|
|
13
13
|
optional, set only when the parser tracks positions for the type-checking shadow.
|
|
14
14
|
*/
|
|
15
15
|
export type TemplateAttr =
|
|
16
|
-
| { kind: 'static'; name: string; value: string; bare?: true }
|
|
17
|
-
| { kind: 'expression'; name: string; code: string; loc?: number }
|
|
16
|
+
| { kind: 'static'; name: string; value: string; bare?: true; nameLoc?: number }
|
|
17
|
+
| { kind: 'expression'; name: string; code: string; loc?: number; nameLoc?: number }
|
|
18
18
|
| { kind: 'event'; event: string; code: string; loc?: number }
|
|
19
19
|
| { kind: 'bind'; property: string; code: string; loc?: number }
|
|
20
|
+
/* `class:<name>={code}` — toggles the class on/off by `code`'s truthiness, written
|
|
21
|
+
surgically (classList.toggle) without re-rendering the element. */
|
|
22
|
+
| { kind: 'class'; name: string; code: string; loc?: number }
|
|
23
|
+
/* `style:<property>={code}` — writes one inline style/custom property (style.setProperty)
|
|
24
|
+
reactively, again without a re-render. `property` may be a CSS custom prop (`--pct`). */
|
|
25
|
+
| { kind: 'style'; property: string; code: string; loc?: number }
|
|
20
26
|
| { kind: 'attach'; code: string; loc?: number }
|
|
21
27
|
| { kind: 'spread'; code: string; loc?: number }
|
|
@@ -40,6 +40,11 @@ export type TemplateNode =
|
|
|
40
40
|
async: boolean
|
|
41
41
|
children: TemplateNode[]
|
|
42
42
|
loc?: number
|
|
43
|
+
/* Source offsets of the binding name, `by` key, and index — so the shadow
|
|
44
|
+
maps hover/highlighting onto them (absent for synthesised/missing parts). */
|
|
45
|
+
asLoc?: number
|
|
46
|
+
keyLoc?: number
|
|
47
|
+
indexLoc?: number
|
|
43
48
|
}
|
|
44
49
|
| { kind: 'if'; condition: string; children: TemplateNode[]; loc?: number }
|
|
45
50
|
| {
|
|
@@ -52,6 +57,8 @@ export type TemplateNode =
|
|
|
52
57
|
as: string | undefined
|
|
53
58
|
children: TemplateNode[]
|
|
54
59
|
loc?: number
|
|
60
|
+
/* Source offset of an inline blocking `then` binding (`{#await p then v}`). */
|
|
61
|
+
asLoc?: number
|
|
55
62
|
}
|
|
56
63
|
| { kind: 'try'; children: TemplateNode[] }
|
|
57
64
|
| {
|
|
@@ -59,13 +66,19 @@ export type TemplateNode =
|
|
|
59
66
|
branch: 'then' | 'catch' | 'finally'
|
|
60
67
|
as: string | undefined
|
|
61
68
|
children: TemplateNode[]
|
|
69
|
+
/* Source offset of the `then`/`catch` binding, so the shadow maps it. */
|
|
70
|
+
asLoc?: number
|
|
62
71
|
}
|
|
63
72
|
| {
|
|
64
73
|
kind: 'component'
|
|
65
74
|
name: string
|
|
75
|
+
/* Source offset of the tag name — the anchor for a whole-mount diagnostic
|
|
76
|
+
(a missing required prop, which has no supplied expression to point at). */
|
|
77
|
+
loc?: number
|
|
66
78
|
/* Each authored attribute as a prop. A `spread` entry (`{...code}`) carries no
|
|
67
|
-
`name`; its keys merge in at runtime (`mergeProps`/`spreadProps`).
|
|
68
|
-
|
|
79
|
+
`name`; its keys merge in at runtime (`mergeProps`/`spreadProps`). `nameLoc` is
|
|
80
|
+
the prop name's source offset — the anchor for an excess-prop diagnostic. */
|
|
81
|
+
props: { name: string; code: string; loc?: number; nameLoc?: number; spread?: boolean }[]
|
|
69
82
|
children: TemplateNode[]
|
|
70
83
|
}
|
|
71
84
|
| { kind: 'switch'; subject: string; children: TemplateNode[]; loc?: number }
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { computed } from './computed.ts'
|
|
2
|
+
import { effect } from './effect.ts'
|
|
2
3
|
import { history } from './history.ts'
|
|
3
4
|
import { linked } from './linked.ts'
|
|
4
5
|
import { persist as persistDoc } from './persist.ts'
|
|
@@ -37,6 +38,10 @@ export function createScope(
|
|
|
37
38
|
const data = (): Doc => (document ??= createDoc({}))
|
|
38
39
|
const id = parent === undefined ? `scope-${nextId++}` : `${parent.id}.${nextId++}`
|
|
39
40
|
const children: Scope[] = []
|
|
41
|
+
/* Adopted build teardowns (the reactivity stopper from the mount core). Disposed
|
|
42
|
+
first and in reverse on teardown, before children and capabilities — so the one
|
|
43
|
+
`dispose` runs the order the call sites hand-composed as `stop(); lexical.dispose()`. */
|
|
44
|
+
const owned: Array<() => void> = []
|
|
40
45
|
/* Context values shared down the tree, held apart from the reactive doc (which
|
|
41
46
|
a child does not inherit): keyed by name, read by the closest ancestor walk. */
|
|
42
47
|
const shared = new Map<string, unknown>()
|
|
@@ -56,11 +61,17 @@ export function createScope(
|
|
|
56
61
|
cell: (path) => data().cell(path),
|
|
57
62
|
derive: (path, compute) => data().derive(path, compute),
|
|
58
63
|
snapshot: () => data().snapshot(),
|
|
59
|
-
/* The
|
|
60
|
-
|
|
64
|
+
/* The reactive primitives — namespaced under the scope but AMBIENT-bound, not
|
|
65
|
+
receiver-bound: each binds whatever scope is rendering and the finest ambient
|
|
66
|
+
build window (branch/row), so the handle is namespacing, not a binding target.
|
|
67
|
+
Binding to the receiver would leak branch-local cells (see ADR-0012). */
|
|
61
68
|
state,
|
|
62
69
|
linked,
|
|
63
70
|
computed,
|
|
71
|
+
effect,
|
|
72
|
+
own: (dispose) => {
|
|
73
|
+
owned.push(dispose)
|
|
74
|
+
},
|
|
64
75
|
child: (childInitial = {}) => {
|
|
65
76
|
const created = createScope(childInitial, self)
|
|
66
77
|
children.push(created)
|
|
@@ -91,6 +102,13 @@ export function createScope(
|
|
|
91
102
|
canUndo: () => past?.canUndo() ?? false,
|
|
92
103
|
canRedo: () => past?.canRedo() ?? false,
|
|
93
104
|
dispose: () => {
|
|
105
|
+
/* Stop the build's reactivity first (reverse order), before tearing down nested
|
|
106
|
+
children and the boundary-crossing capabilities — the order the call sites
|
|
107
|
+
hand-composed as `stop(); lexical.dispose()`. */
|
|
108
|
+
for (let index = owned.length - 1; index >= 0; index -= 1) {
|
|
109
|
+
owned[index]?.()
|
|
110
|
+
}
|
|
111
|
+
owned.length = 0
|
|
94
112
|
for (const created of children) {
|
|
95
113
|
created.dispose()
|
|
96
114
|
}
|
|
@@ -4,6 +4,7 @@ import { effect } from '../effect.ts'
|
|
|
4
4
|
import { claimChild } from '../runtime/claimChild.ts'
|
|
5
5
|
import { RENDER } from '../runtime/RENDER.ts'
|
|
6
6
|
import { appendSnippet } from './appendSnippet.ts'
|
|
7
|
+
import { isComment } from './isComment.ts'
|
|
7
8
|
import { parseRawNodes } from './parseRawNodes.ts'
|
|
8
9
|
|
|
9
10
|
const CLOSE = '/abide:html'
|
|
@@ -106,8 +107,3 @@ function appendRawHtml(parent: Node, read: () => unknown): void {
|
|
|
106
107
|
set(markup())
|
|
107
108
|
})
|
|
108
109
|
}
|
|
109
|
-
|
|
110
|
-
/* A comment node carrying exactly `data`. */
|
|
111
|
-
function isComment(node: Node, data: string): boolean {
|
|
112
|
-
return (node as { data?: string }).data === data && node.childNodes.length === 0
|
|
113
|
-
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { seedResolved } from '../seedResolved.ts'
|
|
2
|
+
import { isComment } from './isComment.ts'
|
|
2
3
|
|
|
3
4
|
/*
|
|
4
5
|
Bundle-side consumer of an SSR stream chunk, the counterpart of the doc stream's inline
|
|
@@ -71,11 +72,6 @@ export function applyResolved(root: Element, frame: string): void {
|
|
|
71
72
|
}
|
|
72
73
|
}
|
|
73
74
|
|
|
74
|
-
/* A comment node carrying exactly `data`. */
|
|
75
|
-
function isComment(node: Node, data: string): boolean {
|
|
76
|
-
return (node as { data?: string }).data === data && node.childNodes.length === 0
|
|
77
|
-
}
|
|
78
|
-
|
|
79
75
|
/* Depth-first search for the parent + open-marker comment of a boundary. */
|
|
80
76
|
function findBoundary(node: Node, open: string): { parent: Node; start: Node } | undefined {
|
|
81
77
|
for (const child of [...node.childNodes]) {
|
|
@@ -3,18 +3,14 @@ import { clearBetween } from './clearBetween.ts'
|
|
|
3
3
|
|
|
4
4
|
/*
|
|
5
5
|
The standard teardown for a marker-bounded range (component, layout/page boundary,
|
|
6
|
-
slot):
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
disposer contract lives in a
|
|
6
|
+
slot): dispose its lexical scope (which stops the content's reactivity first, then its
|
|
7
|
+
nested children and capabilities), and clear the nodes between the markers — leaving the
|
|
8
|
+
markers in place so a hot swap rebuilds the range. Shared by every range mount
|
|
9
|
+
(`fillRange`, `mountRange`, `fillBoundary`) so the one disposer contract lives in a
|
|
10
|
+
single place.
|
|
10
11
|
*/
|
|
11
|
-
export function disposeRange(
|
|
12
|
-
scoped: { stop: () => void; lexical: Scope },
|
|
13
|
-
start: Comment,
|
|
14
|
-
end: Comment,
|
|
15
|
-
): () => void {
|
|
12
|
+
export function disposeRange(scoped: { lexical: Scope }, start: Comment, end: Comment): () => void {
|
|
16
13
|
return () => {
|
|
17
|
-
scoped.stop()
|
|
18
14
|
scoped.lexical.dispose()
|
|
19
15
|
clearBetween(start, end)
|
|
20
16
|
}
|
|
@@ -29,11 +29,8 @@ export function hydrate(
|
|
|
29
29
|
try {
|
|
30
30
|
/* Same shared mount core as `mount` (see `withScope`) — a hydrated component owns a
|
|
31
31
|
scope too, adopting the model its build adopts — run with the claim cursor active. */
|
|
32
|
-
const {
|
|
33
|
-
return () =>
|
|
34
|
-
stop()
|
|
35
|
-
lexical.dispose()
|
|
36
|
-
}
|
|
32
|
+
const { lexical } = withScope(scopeLabel(host), () => scope(() => build(host, props)))
|
|
33
|
+
return () => lexical.dispose()
|
|
37
34
|
} finally {
|
|
38
35
|
RENDER.hydration = previous
|
|
39
36
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/* A comment node carrying exactly `data`. Empty-child check distinguishes the
|
|
2
|
+
`<!--data-->` marker from an element that happens to expose a `data` property.
|
|
3
|
+
Shared by the marker-range scanners in appendText/applyResolved. */
|
|
4
|
+
export function isComment(node: Node, data: string): boolean {
|
|
5
|
+
return (node as { data?: string }).data === data && node.childNodes.length === 0
|
|
6
|
+
}
|
package/src/lib/ui/dom/mount.ts
CHANGED
|
@@ -23,9 +23,8 @@ export function mount(
|
|
|
23
23
|
/* Establish this component's lexical scope (nested, `awaiting` so it adopts the model
|
|
24
24
|
doc the build's first `doc()` creates) and render pass, run the build under it, and
|
|
25
25
|
restore the previous scope — the shared mount core (see `withScope`). */
|
|
26
|
-
const {
|
|
26
|
+
const { lexical } = withScope(scopeLabel(host), () => scope(() => build(host, props)))
|
|
27
27
|
return () => {
|
|
28
|
-
stop()
|
|
29
28
|
lexical.dispose()
|
|
30
29
|
host.textContent = ''
|
|
31
30
|
}
|
|
@@ -11,23 +11,20 @@ under the current one in `awaiting` mode — so it adopts the model doc its firs
|
|
|
11
11
|
creates — brackets a render pass — so its `await`/`try` block ids draw from the shared
|
|
12
12
|
counter in SSR-stream order — runs `build`, then restores the previous scope (synchronous
|
|
13
13
|
build, so the restore is exact). `build` returns its reactivity stopper (from `scope` or
|
|
14
|
-
`fillBefore`)
|
|
15
|
-
the
|
|
14
|
+
`fillBefore`), which the lexical scope ADOPTS (`own`) so the scope has a single teardown:
|
|
15
|
+
the caller wraps its own DOM teardown (clear host vs clear range) around one
|
|
16
|
+
`lexical.dispose()`, no longer composing a separate `stop()` at every site.
|
|
16
17
|
*/
|
|
17
|
-
export function withScope(
|
|
18
|
-
label: string | undefined,
|
|
19
|
-
build: () => () => void,
|
|
20
|
-
): { stop: () => void; lexical: Scope } {
|
|
18
|
+
export function withScope(label: string | undefined, build: () => () => void): { lexical: Scope } {
|
|
21
19
|
const parentScope = CURRENT_SCOPE.current
|
|
22
20
|
const lexical = createScope({}, parentScope, true, label)
|
|
23
21
|
enterRenderPass()
|
|
24
22
|
CURRENT_SCOPE.current = lexical
|
|
25
|
-
let stop: () => void = () => undefined
|
|
26
23
|
try {
|
|
27
|
-
|
|
24
|
+
lexical.own(build())
|
|
28
25
|
} finally {
|
|
29
26
|
exitRenderPass()
|
|
30
27
|
CURRENT_SCOPE.current = parentScope
|
|
31
28
|
}
|
|
32
|
-
return {
|
|
29
|
+
return { lexical }
|
|
33
30
|
}
|
package/src/lib/ui/effect.ts
CHANGED
|
@@ -10,9 +10,11 @@ dispose — and may be async (its teardown then runs once the promise settles, b
|
|
|
10
10
|
only the reads before its first `await` are tracked). Returns a dispose that runs
|
|
11
11
|
the final teardown and detaches it from the graph. This is abide's from-scratch
|
|
12
12
|
effect primitive: the open-on-first-read / close-on-last-reader lifecycle,
|
|
13
|
-
grounded in abide's own reactive core.
|
|
13
|
+
grounded in abide's own reactive core. Authored as `scope().effect(...)` (the bare
|
|
14
|
+
name is reserved); this module export stays for the generated binding code that
|
|
15
|
+
calls it directly (and for the SSR strip), so it carries no standalone demo.
|
|
14
16
|
*/
|
|
15
|
-
// @documentation
|
|
17
|
+
// @documentation plumbing
|
|
16
18
|
export function effect(fn: () => EffectResult): () => void {
|
|
17
19
|
/* Re-runs fire after the build, so pin the scope this effect was created under
|
|
18
20
|
(the dep-tracking observer is managed separately, so this can't disturb it). */
|
package/src/lib/ui/navigate.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { parseRouteSegments } from '../shared/parseRouteSegments.ts'
|
|
2
|
+
import type { PageRoutes, PathParams } from '../shared/url.ts'
|
|
3
|
+
import { url } from '../shared/url.ts'
|
|
1
4
|
import { historyEntries } from './runtime/historyEntries.ts'
|
|
2
5
|
import { runtimePath } from './runtime/runtimePath.ts'
|
|
3
6
|
|
|
@@ -8,21 +11,19 @@ export type NavigateOptions = {
|
|
|
8
11
|
keepScroll?: boolean
|
|
9
12
|
}
|
|
10
13
|
|
|
11
|
-
/*
|
|
12
|
-
|
|
13
|
-
the
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
entry and lands fresh content (a redirect), so its saved scroll no longer applies:
|
|
14
|
+
/* Writes an ALREADY-RESOLVED path into history + the reactive route. The router uses this
|
|
15
|
+
for paths it built from a real URL (a clicked link, a popstate, a server redirect) — they
|
|
16
|
+
already carry the mount base, so they must NOT pass through `url()` again. Each entry
|
|
17
|
+
carries a monotonic `abideEntry` id so the router can bucket/restore its scroll offset
|
|
18
|
+
across the page teardown the rebuild does. A push leaves the current entry behind — its
|
|
19
|
+
scroll is bucketed so back restores it — and mints a fresh id. A replace destroys the
|
|
20
|
+
current entry and lands fresh content (a redirect), so its saved scroll no longer applies:
|
|
19
21
|
the bucket is discarded and the id kept, so the new page restores to top/anchor.
|
|
20
22
|
`keepScroll` opts the destination out of that reset — for an in-page URL swap (e.g.
|
|
21
|
-
selecting another episode on the same detail page) where a top jump is jarring: the
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
export function navigate(
|
|
23
|
+
selecting another episode on the same detail page) where a top jump is jarring: the live
|
|
24
|
+
offset is bucketed under the destination entry id, so the post-rebuild `restore` reapplies
|
|
25
|
+
it instead of scrolling to top. */
|
|
26
|
+
export function navigatePath(
|
|
26
27
|
path: string,
|
|
27
28
|
{ replace = false, keepScroll = false }: NavigateOptions = {},
|
|
28
29
|
): void {
|
|
@@ -48,3 +49,29 @@ export function navigate(
|
|
|
48
49
|
}
|
|
49
50
|
runtimePath.value = path
|
|
50
51
|
}
|
|
52
|
+
|
|
53
|
+
/* Navigates to a typed in-app path: a route literal with `[name]` segments takes its params
|
|
54
|
+
first (`navigate('/p/[id]', { id })`), then options; a paramless or already-interpolated
|
|
55
|
+
path takes options directly (`navigate('/p', { replace })`). The path is built through
|
|
56
|
+
`url()` — base-correct, params interpolated — exactly as a link href would be, so a
|
|
57
|
+
dynamic `/p/${x}` falls through url()'s paramless branch and is base-prefixed too. */
|
|
58
|
+
// @documentation navigate
|
|
59
|
+
export function navigate<P extends keyof PageRoutes | (string & {})>(
|
|
60
|
+
path: P,
|
|
61
|
+
...rest: keyof PathParams<P> extends never
|
|
62
|
+
? [options?: NavigateOptions]
|
|
63
|
+
: [params: PathParams<P>, options?: NavigateOptions]
|
|
64
|
+
): void {
|
|
65
|
+
/* A path declaring `[name]` segments takes params first, options second — mirroring
|
|
66
|
+
url()'s own arg discrimination; otherwise the first arg is options. */
|
|
67
|
+
const hasParams = parseRouteSegments(path).some((segment) => segment.kind === 'param')
|
|
68
|
+
const [first, second] = rest as [unknown, NavigateOptions?]
|
|
69
|
+
/* Params ride url()'s first vararg slot — a path with `[name]` segments interpolates
|
|
70
|
+
them, a paramless one ignores undefined and just base-prefixes. Widen `path` to
|
|
71
|
+
string so url() takes its plain-path overload; its runtime keys off the segments. */
|
|
72
|
+
const params = hasParams
|
|
73
|
+
? (first as Record<string, string | number | boolean | undefined>)
|
|
74
|
+
: undefined
|
|
75
|
+
const options = (hasParams ? second : (first as NavigateOptions | undefined)) ?? {}
|
|
76
|
+
navigatePath(url(path as string, params), options)
|
|
77
|
+
}
|