@abide/abide 0.43.1 → 0.44.1
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 +63 -0
- package/README.md +191 -67
- package/package.json +2 -2
- package/src/abideLsp.ts +8 -4
- package/src/abideResolverPlugin.ts +17 -2
- package/src/build.ts +9 -0
- package/src/buildDisconnected.ts +2 -0
- package/src/devEntry.ts +44 -9
- package/src/lib/bundle/probeAbideServer.ts +1 -1
- package/src/lib/cli/parseArgvForRpc.ts +25 -26
- package/src/lib/cli/runCli.ts +9 -24
- package/src/lib/cli/tokenizeArgvFlags.ts +84 -0
- 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/defineRpc.ts +9 -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 +50 -12
- 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 +3 -3
- package/src/lib/server/runtime/disableIdleTimeoutForStream.ts +1 -1
- package/src/lib/server/runtime/gzipResponse.ts +14 -16
- package/src/lib/server/runtime/logExposedSurfaces.ts +3 -4
- package/src/lib/server/runtime/runWithRequestScope.ts +4 -1
- 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/runtime/types/RequestStore.ts +16 -0
- package/src/lib/server/runtime/withResponseDefaults.ts +8 -3
- package/src/lib/server/socket.ts +1 -1
- package/src/lib/server/sockets/createSocketDispatcher.ts +2 -2
- package/src/lib/server/sockets/defineSocket.ts +5 -1
- 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/RPC_ARGS_TYPE.ts +7 -0
- package/src/lib/shared/STREAMING_CONTENT_TYPES.ts +3 -2
- package/src/lib/shared/activeCacheStore.ts +5 -14
- package/src/lib/shared/activePage.ts +5 -19
- package/src/lib/shared/augmentModule.ts +18 -0
- package/src/lib/shared/basePath.ts +6 -5
- package/src/lib/shared/baseResolver.ts +10 -0
- package/src/lib/shared/baseSlot.ts +6 -12
- package/src/lib/shared/bodyValueForKind.ts +35 -0
- package/src/lib/shared/cache.ts +22 -2
- package/src/lib/shared/cacheEntryFromSnapshot.ts +23 -18
- package/src/lib/shared/cacheStoreResolver.ts +12 -0
- package/src/lib/shared/cacheStoreSlot.ts +5 -13
- package/src/lib/shared/changeAffectsClient.ts +35 -0
- package/src/lib/shared/createPushIterator.ts +1 -1
- package/src/lib/shared/createResolverSlot.ts +37 -0
- package/src/lib/shared/debugGate.ts +90 -0
- package/src/lib/shared/decodeResponse.ts +11 -8
- package/src/lib/shared/emitLogRecord.ts +18 -4
- package/src/lib/shared/globalCacheStoreResolver.ts +12 -0
- package/src/lib/shared/globalCacheStoreSlot.ts +6 -11
- package/src/lib/shared/hasReplayableRequest.ts +17 -0
- package/src/lib/shared/html.ts +7 -26
- package/src/lib/shared/isDebugEnabled.ts +3 -10
- package/src/lib/shared/isDebugNegated.ts +4 -6
- 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/pageResolver.ts +16 -0
- package/src/lib/shared/pageSlot.ts +5 -14
- package/src/lib/shared/pageUrlForFile.ts +1 -1
- package/src/lib/shared/requestScopeResolver.ts +12 -0
- package/src/lib/shared/requestScopeSlot.ts +7 -12
- package/src/lib/shared/responseBodyKind.ts +35 -0
- package/src/lib/shared/routeParamsShape.ts +20 -0
- package/src/lib/shared/setBaseResolver.ts +2 -4
- package/src/lib/shared/setCacheStoreResolver.ts +3 -5
- package/src/lib/shared/setGlobalCacheStoreResolver.ts +3 -5
- package/src/lib/shared/setPageResolver.ts +2 -5
- package/src/lib/shared/setRequestScopeResolver.ts +3 -5
- 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/DebugGate.ts +10 -0
- package/src/lib/shared/types/LastConnection.ts +1 -1
- package/src/lib/shared/types/ResolverSlot.ts +10 -0
- package/src/lib/shared/url.ts +4 -1
- package/src/lib/shared/withBase.ts +1 -2
- package/src/lib/shared/writeDts.ts +10 -3
- package/src/lib/shared/writePublicAssetsDts.ts +5 -9
- package/src/lib/shared/writeRoutesDts.ts +14 -46
- package/src/lib/shared/writeRpcDts.ts +13 -12
- package/src/lib/shared/writeTestRpcDts.ts +11 -11
- package/src/lib/shared/writeTestSocketsDts.ts +9 -9
- 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 +10 -8
- package/src/lib/ui/compile/assertRuntimeHelpersBound.ts +27 -13
- 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 +56 -21
- package/src/lib/ui/compile/compileSSR.ts +12 -10
- package/src/lib/ui/compile/compileShadow.ts +163 -12
- package/src/lib/ui/compile/composeProps.ts +3 -3
- package/src/lib/ui/compile/createShadowLanguageService.ts +58 -4
- 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 +156 -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 +135 -62
- 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 +9 -9
- 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/mountSwappableRange.ts +79 -0
- package/src/lib/ui/dom/readCall.ts +6 -4
- package/src/lib/ui/dom/scopeLabel.ts +3 -3
- package/src/lib/ui/dom/skeleton.ts +10 -1
- package/src/lib/ui/dom/switchBlock.ts +17 -63
- package/src/lib/ui/dom/types/EachRow.ts +4 -0
- package/src/lib/ui/dom/types/SkeletonHoles.ts +4 -2
- package/src/lib/ui/dom/when.ts +11 -64
- package/src/lib/ui/history.ts +1 -2
- package/src/lib/ui/html.ts +28 -0
- package/src/lib/ui/installHotBridge.ts +0 -2
- package/src/lib/ui/renderChain.ts +16 -3
- package/src/lib/ui/router.ts +24 -3
- package/src/lib/ui/rpcOutbox/createOutboxQueue.ts +1 -1
- package/src/lib/ui/runtime/CHILD_PRESENT.ts +8 -0
- package/src/lib/ui/runtime/OUTLET_MARKER.ts +2 -2
- package/src/lib/ui/runtime/RANGE_MARKER.ts +1 -1
- package/src/lib/ui/runtime/REACTIVE_CONTEXT.ts +9 -3
- package/src/lib/ui/runtime/captureModelDoc.ts +1 -1
- package/src/lib/ui/runtime/createDoc.ts +107 -13
- 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 +51 -0
- package/template/.zed/settings.json +7 -1
- package/template/package.json +3 -0
- package/template/src/ui/pages/layout.abide +3 -5
- package/src/lib/shared/matchesDebugPattern.ts +0 -16
- package/src/lib/shared/parseDebugPatterns.ts +0 -21
|
@@ -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
|
}
|
|
@@ -80,9 +97,13 @@ export function compileShadow(source: string): CompiledShadow {
|
|
|
80
97
|
for (const line of scope) {
|
|
81
98
|
builder.flush(line)
|
|
82
99
|
}
|
|
83
|
-
|
|
100
|
+
const templateNodes = parseTemplate(source.slice(templateStart), templateStart).nodes
|
|
101
|
+
/* Nested `<script>` blocks inline into the synchronous `build()` too, so a top-level
|
|
102
|
+
await in one is the same build-breaker — flag it, mapped via the node's body offset. */
|
|
103
|
+
collectNestedScriptAwaitDiagnostics(templateNodes, diagnostics)
|
|
104
|
+
emitNodes(templateNodes, builder)
|
|
84
105
|
builder.raw('}\n')
|
|
85
|
-
return builder.result()
|
|
106
|
+
return { ...builder.result(), diagnostics }
|
|
86
107
|
}
|
|
87
108
|
|
|
88
109
|
/* The shadow text builder: `raw` appends synthesised scaffolding (no mapping),
|
|
@@ -162,6 +183,106 @@ type ScriptAnalysis = {
|
|
|
162
183
|
types: ScopeLine[]
|
|
163
184
|
scope: ScopeLine[]
|
|
164
185
|
propsShapes: string[]
|
|
186
|
+
diagnostics: ShadowDiagnostic[]
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/* Pushes a diagnostic for every author binding whose name starts with the reserved `$$`
|
|
190
|
+
prefix — variable/function/class declarations, parameters, and destructuring leaves.
|
|
191
|
+
Read structurally off the parsed script; the message points at the offending name. */
|
|
192
|
+
function collectReservedNameDiagnostics(
|
|
193
|
+
file: ts.SourceFile,
|
|
194
|
+
scriptStart: number,
|
|
195
|
+
diagnostics: ShadowDiagnostic[],
|
|
196
|
+
): void {
|
|
197
|
+
const flag = (name: ts.Node | undefined): void => {
|
|
198
|
+
if (name !== undefined && ts.isIdentifier(name) && name.text.startsWith('$$')) {
|
|
199
|
+
diagnostics.push({
|
|
200
|
+
start: scriptStart + name.getStart(file),
|
|
201
|
+
length: name.getEnd() - name.getStart(file),
|
|
202
|
+
message: `\`${name.text}\` is reserved — the \`$$\` prefix is the compiler's injected runtime namespace; rename this binding.`,
|
|
203
|
+
})
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const visit = (node: ts.Node): void => {
|
|
207
|
+
if (
|
|
208
|
+
ts.isVariableDeclaration(node) ||
|
|
209
|
+
ts.isFunctionDeclaration(node) ||
|
|
210
|
+
ts.isClassDeclaration(node) ||
|
|
211
|
+
ts.isParameter(node) ||
|
|
212
|
+
ts.isBindingElement(node)
|
|
213
|
+
) {
|
|
214
|
+
flag(node.name)
|
|
215
|
+
}
|
|
216
|
+
ts.forEachChild(node, visit)
|
|
217
|
+
}
|
|
218
|
+
visit(file)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/* Pushes a diagnostic for every `await` sitting at the script's top level — outside any
|
|
222
|
+
nested function. The generated `build()` runs the leading script synchronously, so a
|
|
223
|
+
top-level await transpiles to `await` in a non-async function and breaks the bundle; the
|
|
224
|
+
shadow's render fn is async, so `tsc` alone never flags it (a check/runtime parity gap).
|
|
225
|
+
Stops descending at function boundaries — their own async-ness is the author's concern —
|
|
226
|
+
and catches both `await expr` and `for await (… of …)`, flagging the `await` keyword. */
|
|
227
|
+
function collectTopLevelAwaitDiagnostics(
|
|
228
|
+
file: ts.SourceFile,
|
|
229
|
+
scriptStart: number,
|
|
230
|
+
diagnostics: ShadowDiagnostic[],
|
|
231
|
+
): void {
|
|
232
|
+
const message =
|
|
233
|
+
'top-level `await` is not allowed in a `<script>` — the component build runs synchronously. Use `{#await expr}…{:then value}…{/await}` markup for blocking data, or `await` inside an async event handler.'
|
|
234
|
+
const visit = (node: ts.Node): void => {
|
|
235
|
+
/* A nested function introduces its own (possibly async) scope; its awaits are legal. */
|
|
236
|
+
if (
|
|
237
|
+
ts.isFunctionDeclaration(node) ||
|
|
238
|
+
ts.isFunctionExpression(node) ||
|
|
239
|
+
ts.isArrowFunction(node) ||
|
|
240
|
+
ts.isMethodDeclaration(node) ||
|
|
241
|
+
ts.isGetAccessorDeclaration(node) ||
|
|
242
|
+
ts.isSetAccessorDeclaration(node) ||
|
|
243
|
+
ts.isConstructorDeclaration(node)
|
|
244
|
+
) {
|
|
245
|
+
return
|
|
246
|
+
}
|
|
247
|
+
/* The `await` keyword token: the AwaitExpression's first child, or a `for await`'s
|
|
248
|
+
await modifier. */
|
|
249
|
+
const keyword = ts.isAwaitExpression(node)
|
|
250
|
+
? node.getChildAt(0, file)
|
|
251
|
+
: ts.isForOfStatement(node)
|
|
252
|
+
? node.awaitModifier
|
|
253
|
+
: undefined
|
|
254
|
+
if (keyword !== undefined) {
|
|
255
|
+
diagnostics.push({
|
|
256
|
+
start: scriptStart + keyword.getStart(file),
|
|
257
|
+
length: keyword.getEnd() - keyword.getStart(file),
|
|
258
|
+
message,
|
|
259
|
+
})
|
|
260
|
+
}
|
|
261
|
+
ts.forEachChild(node, visit)
|
|
262
|
+
}
|
|
263
|
+
visit(file)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/* Recursively visits every nested `<script>` in the template and flags a top-level await in
|
|
267
|
+
its body — the same trap as the leading script (it inlines into the sync `build()`), mapped
|
|
268
|
+
through the node's body offset (`loc`). Walks `children` on every node kind that carries
|
|
269
|
+
them, so a script buried in an `{#if}`/`{#for}`/`{#await}` branch is still reached. */
|
|
270
|
+
function collectNestedScriptAwaitDiagnostics(
|
|
271
|
+
nodes: TemplateNode[],
|
|
272
|
+
diagnostics: ShadowDiagnostic[],
|
|
273
|
+
): void {
|
|
274
|
+
for (const node of nodes) {
|
|
275
|
+
if (node === undefined) {
|
|
276
|
+
continue
|
|
277
|
+
}
|
|
278
|
+
if (node.kind === 'script' && node.loc !== undefined) {
|
|
279
|
+
const file = ts.createSourceFile('nested.ts', node.code, ts.ScriptTarget.Latest, true)
|
|
280
|
+
collectTopLevelAwaitDiagnostics(file, node.loc, diagnostics)
|
|
281
|
+
}
|
|
282
|
+
if ('children' in node) {
|
|
283
|
+
collectNestedScriptAwaitDiagnostics(node.children, diagnostics)
|
|
284
|
+
}
|
|
285
|
+
}
|
|
165
286
|
}
|
|
166
287
|
|
|
167
288
|
/* Walks the leading `<script>` and produces the shadow's module imports, the
|
|
@@ -173,10 +294,18 @@ function analyzeScript(scriptBody: string, scriptStart: number): ScriptAnalysis
|
|
|
173
294
|
const types: ScopeLine[] = []
|
|
174
295
|
const scope: ScopeLine[] = []
|
|
175
296
|
const propsShapes: string[] = []
|
|
297
|
+
const diagnostics: ShadowDiagnostic[] = []
|
|
176
298
|
if (scriptBody.trim() === '') {
|
|
177
|
-
return { imports, types, scope, propsShapes }
|
|
299
|
+
return { imports, types, scope, propsShapes, diagnostics }
|
|
178
300
|
}
|
|
179
301
|
const file = ts.createSourceFile('script.ts', scriptBody, ts.ScriptTarget.Latest, true)
|
|
302
|
+
/* The `$$` prefix is reserved for the compiler's injected runtime (`$$each`, `$$model`,
|
|
303
|
+
`$$scope`, …), so an author binding may not start with it — that's the contract that
|
|
304
|
+
lets a user freely name a variable after any helper. Flag every such declaration. */
|
|
305
|
+
collectReservedNameDiagnostics(file, scriptStart, diagnostics)
|
|
306
|
+
/* The leading script runs in the synchronous `build()`, so a top-level await breaks the
|
|
307
|
+
bundle — catch it here with a legible message instead of an opaque transpile error. */
|
|
308
|
+
collectTopLevelAwaitDiagnostics(file, scriptStart, diagnostics)
|
|
180
309
|
/* A verbatim span: original text + the segment mapping it back, relative to the
|
|
181
310
|
line start (the caller rebases shadowStart onto the running shadow length). */
|
|
182
311
|
const span = (node: ts.Node, prefixLength: number): ScopeLine['segments'][number] => ({
|
|
@@ -188,6 +317,20 @@ function analyzeScript(scriptBody: string, scriptStart: number): ScriptAnalysis
|
|
|
188
317
|
|
|
189
318
|
for (const statement of file.statements) {
|
|
190
319
|
if (ts.isImportDeclaration(statement)) {
|
|
320
|
+
const specifier = ts.isStringLiteral(statement.moduleSpecifier)
|
|
321
|
+
? statement.moduleSpecifier.text
|
|
322
|
+
: undefined
|
|
323
|
+
const banned = specifier !== undefined ? INTERNAL_ONLY_IMPORTS[specifier] : undefined
|
|
324
|
+
if (banned !== undefined) {
|
|
325
|
+
/* Flag and drop it — leaving it out keeps `effect` resolving via the
|
|
326
|
+
preamble (no duplicate-identifier noise on top of our message). */
|
|
327
|
+
diagnostics.push({
|
|
328
|
+
start: scriptStart + statement.getStart(file),
|
|
329
|
+
length: statement.getEnd() - statement.getStart(file),
|
|
330
|
+
message: banned,
|
|
331
|
+
})
|
|
332
|
+
continue
|
|
333
|
+
}
|
|
191
334
|
/* Emit verbatim with a span so hover/go-to resolve on the imported names. */
|
|
192
335
|
imports.push({ text: verbatim(statement), segments: [span(statement, 0)] })
|
|
193
336
|
continue
|
|
@@ -207,7 +350,7 @@ function analyzeScript(scriptBody: string, scriptStart: number): ScriptAnalysis
|
|
|
207
350
|
scope.push(scopeLineFor(declaration, propsShapes, verbatim, span))
|
|
208
351
|
}
|
|
209
352
|
}
|
|
210
|
-
return { imports, types, scope, propsShapes }
|
|
353
|
+
return { imports, types, scope, propsShapes, diagnostics }
|
|
211
354
|
}
|
|
212
355
|
|
|
213
356
|
/* Value-projects a nested control-flow `<script>` body the way `analyzeScript`
|
|
@@ -363,7 +506,15 @@ function emitNode(node: TemplateNode, builder: Builder): void {
|
|
|
363
506
|
return
|
|
364
507
|
case 'element':
|
|
365
508
|
for (const attr of node.attrs) {
|
|
366
|
-
|
|
509
|
+
/* An interpolated value checks each `{expr}` part on its own offset; every
|
|
510
|
+
other dynamic attribute checks its single `code`. */
|
|
511
|
+
if (attr.kind === 'interpolated') {
|
|
512
|
+
for (const part of attr.parts) {
|
|
513
|
+
if (part.kind === 'expression') {
|
|
514
|
+
builder.stmt(part.code, part.loc)
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
} else if (attr.kind !== 'static') {
|
|
367
518
|
builder.stmt(attr.code, attr.loc)
|
|
368
519
|
}
|
|
369
520
|
}
|
|
@@ -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'
|
|
@@ -46,6 +47,21 @@ export function createShadowLanguageService(cwd: string): ShadowLanguageService
|
|
|
46
47
|
const versions = new Map<string, number>()
|
|
47
48
|
const shadows = new Map<string, CompiledShadow>()
|
|
48
49
|
const parseErrors = new Map<string, string>()
|
|
50
|
+
/* Memo of `shadowText` output keyed by source path, tagged with the shadow
|
|
51
|
+
version it was compiled at; a stale tag forces recompilation. `update`/`close`
|
|
52
|
+
bump that version, so the next read recompiles. */
|
|
53
|
+
const compiledAt = new Map<string, { version: number; code: string }>()
|
|
54
|
+
/* Memo of `pagePropsType` (route re-parse) per source path; the path → props
|
|
55
|
+
type mapping is immutable, so no version tag is needed. */
|
|
56
|
+
const propsTypes = new Map<string, string | undefined>()
|
|
57
|
+
|
|
58
|
+
/* The route props type for a component, parsed once and reused. */
|
|
59
|
+
const propsTypeOf = (abidePath: string): string | undefined => {
|
|
60
|
+
if (!propsTypes.has(abidePath)) {
|
|
61
|
+
propsTypes.set(abidePath, pagePropsType(abidePath))
|
|
62
|
+
}
|
|
63
|
+
return propsTypes.get(abidePath)
|
|
64
|
+
}
|
|
49
65
|
|
|
50
66
|
/* Ambient declarations for bundler-handled asset imports (`*.css`, …). */
|
|
51
67
|
const assets = assetModulesFile(cwd)
|
|
@@ -54,18 +70,29 @@ export function createShadowLanguageService(cwd: string): ShadowLanguageService
|
|
|
54
70
|
overlays.has(abidePath) || ts.sys.fileExists(abidePath)
|
|
55
71
|
|
|
56
72
|
/* Compiles (and caches) a component's shadow from its overlay or disk text; a
|
|
57
|
-
template parse error yields a minimal valid module + a recorded message.
|
|
73
|
+
template parse error yields a minimal valid module + a recorded message.
|
|
74
|
+
Memoised against the shadow's version (bumped by `update`/`close`), so
|
|
75
|
+
repeated reads at the same version skip the ~700-line compile and the
|
|
76
|
+
`shadows`/`parseErrors` caches stay current. */
|
|
58
77
|
const shadowText = (abidePath: string): string => {
|
|
78
|
+
const version = versions.get(suffixed(abidePath)) ?? 0
|
|
79
|
+
const memo = compiledAt.get(abidePath)
|
|
80
|
+
if (memo !== undefined && memo.version === version) {
|
|
81
|
+
return memo.code
|
|
82
|
+
}
|
|
59
83
|
const source = overlays.get(abidePath) ?? ts.sys.readFile(abidePath) ?? ''
|
|
60
84
|
try {
|
|
61
|
-
const compiled = compileShadow(source)
|
|
85
|
+
const compiled = compileShadow(source, propsTypeOf(abidePath))
|
|
62
86
|
shadows.set(abidePath, compiled)
|
|
63
87
|
parseErrors.delete(abidePath)
|
|
88
|
+
compiledAt.set(abidePath, { version, code: compiled.code })
|
|
64
89
|
return compiled.code
|
|
65
90
|
} catch (error) {
|
|
66
91
|
shadows.set(abidePath, { code: '', mappings: [] })
|
|
67
92
|
parseErrors.set(abidePath, messageFromError(error))
|
|
68
|
-
|
|
93
|
+
const code = 'export default function (): void {}\n'
|
|
94
|
+
compiledAt.set(abidePath, { version, code })
|
|
95
|
+
return code
|
|
69
96
|
}
|
|
70
97
|
}
|
|
71
98
|
|
|
@@ -240,6 +267,33 @@ export function createShadowLanguageService(cwd: string): ShadowLanguageService
|
|
|
240
267
|
modifiers: mapped.modifiers,
|
|
241
268
|
})
|
|
242
269
|
}
|
|
270
|
+
/* Literal syntactic tokens (string/number/regex) the semantic classifier
|
|
271
|
+
never emits — so a template-literal string inside `{…}` gets colored. */
|
|
272
|
+
const syntactic = service.getEncodedSyntacticClassifications(fileName, {
|
|
273
|
+
start: 0,
|
|
274
|
+
length: shadow.code.length,
|
|
275
|
+
})
|
|
276
|
+
for (let index = 0; index + 2 < syntactic.spans.length; index += 3) {
|
|
277
|
+
const spanStart = syntactic.spans[index]
|
|
278
|
+
const spanLength = syntactic.spans[index + 1]
|
|
279
|
+
const classification = syntactic.spans[index + 2]
|
|
280
|
+
if (
|
|
281
|
+
spanStart === undefined ||
|
|
282
|
+
spanLength === undefined ||
|
|
283
|
+
classification === undefined
|
|
284
|
+
) {
|
|
285
|
+
continue
|
|
286
|
+
}
|
|
287
|
+
const type = mapSyntacticClassification(classification)
|
|
288
|
+
if (type === undefined) {
|
|
289
|
+
continue
|
|
290
|
+
}
|
|
291
|
+
const located = remapShadowDiagnostic(shadow.mappings, spanStart, spanLength)
|
|
292
|
+
if (located === undefined) {
|
|
293
|
+
continue
|
|
294
|
+
}
|
|
295
|
+
tokens.push({ start: located.start, length: located.length, type, modifiers: [] })
|
|
296
|
+
}
|
|
243
297
|
return tokens
|
|
244
298
|
},
|
|
245
299
|
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { classStyleMergePlan } from './classStyleMergePlan.ts'
|
|
2
|
+
import type { ElementPlan, ElementPlanAttr } from './types/ElementPlan.ts'
|
|
3
|
+
import type { TemplateAttr } from './types/TemplateAttr.ts'
|
|
4
|
+
import type { TemplateNode } from './types/TemplateNode.ts'
|
|
5
|
+
import { VOID_TAGS } from './VOID_TAGS.ts'
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
The single decision site for one element's attribute emission — lifted out of the two
|
|
9
|
+
hand-mirrored code-gen back-ends (`generateBuild`, `generateSSR`) so the load-bearing
|
|
10
|
+
agreement on which attribute is which kind, which class/style parts merge, and which tag
|
|
11
|
+
is void lives ONCE (ADR-0013, the locality milestone). Each back-end RENDERS the plan
|
|
12
|
+
differently — build wires live effects/thunks, SSR pushes escaped strings — but consults
|
|
13
|
+
the same classification, so the markup the client clones and the server emits cannot drift.
|
|
14
|
+
|
|
15
|
+
`classStyleMergePlan` folds in as the class/style branch. Each attr is tagged `mergedBuild`
|
|
16
|
+
/`mergedSSR` from the two merge triggers (build merges only on an interpolated base, SSR on
|
|
17
|
+
any directive), so a back-end skips exactly the attrs it folded into its merged class/style
|
|
18
|
+
attribute. The positional holes/anchors stay in `skeletonContext`; this plan layers over it.
|
|
19
|
+
*/
|
|
20
|
+
export function elementPlan(
|
|
21
|
+
node: Extract<TemplateNode, { kind: 'element' }>,
|
|
22
|
+
lower: (code: string) => string,
|
|
23
|
+
): ElementPlan {
|
|
24
|
+
const merge = classStyleMergePlan(node.attrs, lower)
|
|
25
|
+
/* True when this attr is folded into the build back-end's merged class/style effect — the
|
|
26
|
+
interpolated base or any directive of a merged property (a static base stays surgical). */
|
|
27
|
+
const isMergedBuild = (attr: TemplateAttr): boolean =>
|
|
28
|
+
(merge.mergeClassBuild && (attr === merge.classBase || attr.kind === 'class')) ||
|
|
29
|
+
(merge.mergeStyleBuild && (attr === merge.styleBase || attr.kind === 'style'))
|
|
30
|
+
/* True when this attr is folded into the SSR back-end's merged class/style attribute,
|
|
31
|
+
whenever a directive exists. A `class:`/`style:` directive always folds; a static or
|
|
32
|
+
interpolated base folds by NAME (so a duplicate `class`/`style` base is folded too,
|
|
33
|
+
never double-emitted next to the merged attribute — the old SSR by-name skip). */
|
|
34
|
+
const isMergedSSR = (attr: TemplateAttr): boolean => {
|
|
35
|
+
if (attr.kind === 'class') {
|
|
36
|
+
return merge.mergeClass
|
|
37
|
+
}
|
|
38
|
+
if (attr.kind === 'style') {
|
|
39
|
+
return merge.mergeStyle
|
|
40
|
+
}
|
|
41
|
+
if (attr.kind === 'static' || attr.kind === 'interpolated') {
|
|
42
|
+
return (
|
|
43
|
+
(attr.name === 'class' && merge.mergeClass) ||
|
|
44
|
+
(attr.name === 'style' && merge.mergeStyle)
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
return false
|
|
48
|
+
}
|
|
49
|
+
const attrs: ElementPlanAttr[] = node.attrs.map((attr) => ({
|
|
50
|
+
attr,
|
|
51
|
+
mergedBuild: isMergedBuild(attr),
|
|
52
|
+
mergedSSR: isMergedSSR(attr),
|
|
53
|
+
}))
|
|
54
|
+
return { attrs, merge, isVoid: VOID_TAGS.has(node.tag) }
|
|
55
|
+
}
|
|
@@ -10,8 +10,31 @@ by start; tokens with an unknown legend type, and any token overlapping the one
|
|
|
10
10
|
before it, are dropped — the protocol requires a strictly non-overlapping,
|
|
11
11
|
positionally-ordered stream.
|
|
12
12
|
*/
|
|
13
|
+
/*
|
|
14
|
+
A token may span newlines (a multiline template-literal string), but the protocol
|
|
15
|
+
forbids a token crossing a line — so split it into one segment per covered line,
|
|
16
|
+
dropping empty lines. A single-line token returns unchanged.
|
|
17
|
+
*/
|
|
18
|
+
function splitByLine(text: string, token: SemanticToken): SemanticToken[] {
|
|
19
|
+
const raw = text.slice(token.start, token.start + token.length)
|
|
20
|
+
if (!raw.includes('\n')) {
|
|
21
|
+
return [token]
|
|
22
|
+
}
|
|
23
|
+
const segments: SemanticToken[] = []
|
|
24
|
+
let offset = token.start
|
|
25
|
+
for (const line of raw.split('\n')) {
|
|
26
|
+
if (line.length > 0) {
|
|
27
|
+
segments.push({ ...token, start: offset, length: line.length })
|
|
28
|
+
}
|
|
29
|
+
offset += line.length + 1
|
|
30
|
+
}
|
|
31
|
+
return segments
|
|
32
|
+
}
|
|
33
|
+
|
|
13
34
|
export function encodeSemanticTokens(text: string, tokens: SemanticToken[]): number[] {
|
|
14
|
-
const sorted =
|
|
35
|
+
const sorted = tokens
|
|
36
|
+
.flatMap((token) => splitByLine(text, token))
|
|
37
|
+
.sort((a, b) => a.start - b.start || a.length - b.length)
|
|
15
38
|
const data: number[] = []
|
|
16
39
|
let previousLine = 0
|
|
17
40
|
let previousCharacter = 0
|