@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
|
@@ -3,7 +3,6 @@ import type { BunPlugin } from 'bun'
|
|
|
3
3
|
import { fileName } from '../../shared/fileName.ts'
|
|
4
4
|
import { messageFromError } from '../../shared/messageFromError.ts'
|
|
5
5
|
import { AbideCompileError } from './AbideCompileError.ts'
|
|
6
|
-
import { analyzeComponent } from './analyzeComponent.ts'
|
|
7
6
|
import { compileModule } from './compileModule.ts'
|
|
8
7
|
import { nearestProjectRoot } from './nearestProjectRoot.ts'
|
|
9
8
|
import { offsetToLineColumn } from './offsetToLineColumn.ts'
|
|
@@ -15,7 +14,7 @@ only UI loader in the dev/build/preload pipeline; the emitted module's
|
|
|
15
14
|
`abide/ui/*` imports resolve through the package exports.
|
|
16
15
|
|
|
17
16
|
A `layout.abide` compiles as a layout: its `<slot/>` lowers to the router's page
|
|
18
|
-
outlet (
|
|
17
|
+
outlet (a `<!--abide:outlet-->…<!--/abide:outlet-->` comment boundary the router fills) rather than a passed-children slot — the file's role is
|
|
19
18
|
its name, so the loader flags it from the path stem.
|
|
20
19
|
|
|
21
20
|
The emitted module embeds the component's `<script>` and `{expr}` bodies verbatim,
|
|
@@ -63,14 +62,17 @@ export const abideUiPlugin: BunPlugin = {
|
|
|
63
62
|
throw new Error(`${message.replace(/^\[abide\]\s*/, `[abide] ${at} — `)}`)
|
|
64
63
|
}
|
|
65
64
|
}
|
|
66
|
-
|
|
65
|
+
/* One compile pass yields both the module code and its scoped `<style>` blocks —
|
|
66
|
+
the styles come from the analysis `compileModule` already ran, so the loader no
|
|
67
|
+
longer re-analyzes the source just to recover them. */
|
|
68
|
+
const { code, styles } = compileAbide(() =>
|
|
69
|
+
compileModule(source, { isLayout, moduleId }),
|
|
70
|
+
)
|
|
67
71
|
/* Browser build with `<style>`(s): concatenate every scoped block's CSS and
|
|
68
72
|
pull it into the bundle via one virtual import, keyed by `moduleId` so the
|
|
69
|
-
registry id and the CSS id agree.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
)
|
|
73
|
-
if (styles.length === 0) {
|
|
73
|
+
registry id and the CSS id agree. Server builds skip the import (SSR styling
|
|
74
|
+
comes from the already-linked sheet). */
|
|
75
|
+
if (!toBrowser || styles.length === 0) {
|
|
74
76
|
return { contents: code, loader: 'ts' }
|
|
75
77
|
}
|
|
76
78
|
const virtual = `abide-style:${moduleId}`
|
|
@@ -15,18 +15,32 @@ and require that name to be imported or locally bound. A helper name inside a st
|
|
|
15
15
|
comment is not a `CallExpression` callee, so a docs component quoting framework code
|
|
16
16
|
(`mount(...)` in a snippet) never false-positives. Compile-time only; never on the hot path.
|
|
17
17
|
*/
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
18
|
+
/* `module` is either the source string (standalone use — parsed here) or the already-parsed
|
|
19
|
+
tree of the bodies WITHOUT the prepended import block (compile pipeline — shares the one
|
|
20
|
+
parse the dead-import filter made). In the latter case the import block's bindings aren't in
|
|
21
|
+
the tree, so `importedHelpers` supplies the helper names that block will bind. */
|
|
22
|
+
export function assertRuntimeHelpersBound(
|
|
23
|
+
module: string | ts.SourceFile,
|
|
24
|
+
importedHelpers: Set<string>,
|
|
25
|
+
context: string,
|
|
26
|
+
): void {
|
|
27
|
+
/* The EMITTED local names (the `$$` alias when set) — codegen calls those, and the
|
|
28
|
+
aliased import binds them, so the bound/called check must use the same form. */
|
|
29
|
+
const helperNames = new Set(UI_RUNTIME_IMPORTS.map((entry) => entry.alias ?? entry.name))
|
|
30
|
+
const source =
|
|
31
|
+
typeof module === 'string'
|
|
32
|
+
? ts.createSourceFile(
|
|
33
|
+
'module.ts',
|
|
34
|
+
module,
|
|
35
|
+
ts.ScriptTarget.Latest,
|
|
36
|
+
/* setParentNodes */ true,
|
|
37
|
+
)
|
|
38
|
+
: module
|
|
39
|
+
/* Names a bare call can resolve to: the import block's helper bindings (supplied when the
|
|
40
|
+
tree omits them) plus every import binding and declared name in the tree. Collected
|
|
41
|
+
generously (any identifier in a binding position) — over-approximating the bound set only
|
|
42
|
+
risks missing a defect, never raising a false alarm on valid output. */
|
|
43
|
+
const bound = new Set<string>(importedHelpers)
|
|
30
44
|
const calledHelpers: { name: string; position: number }[] = []
|
|
31
45
|
const visit = (node: ts.Node): void => {
|
|
32
46
|
if (
|
|
@@ -60,7 +74,7 @@ export function assertRuntimeHelpersBound(module: string, context: string): void
|
|
|
60
74
|
if (unbound !== undefined) {
|
|
61
75
|
const { line, character } = source.getLineAndCharacterOfPosition(unbound.position)
|
|
62
76
|
throw new Error(
|
|
63
|
-
`[abide] ${context} calls runtime helper \`${unbound.name}\` at line ${line + 1}:${character + 1} but never imports it — the dead-import filter dropped it. Please report this with the component source.\nOutput:\n${
|
|
77
|
+
`[abide] ${context} calls runtime helper \`${unbound.name}\` at line ${line + 1}:${character + 1} but never imports it — the dead-import filter dropped it. Please report this with the component source.\nOutput:\n${source.text}`,
|
|
64
78
|
)
|
|
65
79
|
}
|
|
66
80
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { catchBinding } from './catchBinding.ts'
|
|
1
2
|
import { resolveBranches } from './resolveBranches.ts'
|
|
3
|
+
import type { Binding } from './types/Binding.ts'
|
|
2
4
|
import type { TemplateNode } from './types/TemplateNode.ts'
|
|
3
5
|
|
|
4
6
|
/* The structural shape of an `await` block, resolved ONCE from the node so the build and
|
|
@@ -23,6 +25,12 @@ export type AwaitPlan = {
|
|
|
23
25
|
/* Neither catch nor finally → a rejection must surface (re-throw / `undefined` catch thunk)
|
|
24
26
|
instead of rendering an empty branch. */
|
|
25
27
|
surfaceRejection: boolean
|
|
28
|
+
/* The resolved branch's binding (`resolvedAs`, `reactive` — a `.value` cell on the
|
|
29
|
+
client, a re-settle updates it in place). One element. */
|
|
30
|
+
resolvedBindings: Binding[]
|
|
31
|
+
/* The catch branch's binding (`catchAs`, `plain`); empty when no catch branch. The
|
|
32
|
+
`finally` branch binds nothing, so it registers no binding. */
|
|
33
|
+
catchBindings: Binding[]
|
|
26
34
|
}
|
|
27
35
|
|
|
28
36
|
/* Resolves an `await` node's branches into the shared structural plan. */
|
|
@@ -35,14 +43,18 @@ export function awaitPlan(node: Extract<TemplateNode, { kind: 'await' }>): Await
|
|
|
35
43
|
)
|
|
36
44
|
const finallyChildren = finallyBranch?.children ?? []
|
|
37
45
|
const nonBranch = node.children.filter((child) => child.kind !== 'branch')
|
|
46
|
+
const resolvedAs = (node.blocking ? node.as : thenBranch?.as) ?? '_value'
|
|
47
|
+
const catchAs = catchBranch?.as ?? '_error'
|
|
38
48
|
return {
|
|
39
49
|
blocking: node.blocking,
|
|
40
50
|
pending: node.blocking ? [] : nonBranch,
|
|
41
51
|
resolvedChildren: node.blocking ? nonBranch : (thenBranch?.children ?? []),
|
|
42
|
-
resolvedAs
|
|
52
|
+
resolvedAs,
|
|
43
53
|
catchChildren: catchBranch?.children ?? [],
|
|
44
|
-
catchAs
|
|
54
|
+
catchAs,
|
|
45
55
|
finallyChildren,
|
|
46
56
|
surfaceRejection: catchBranch === undefined && finallyChildren.length === 0,
|
|
57
|
+
resolvedBindings: [{ name: resolvedAs, classification: 'reactive' }],
|
|
58
|
+
catchBindings: catchBinding(catchAs, catchBranch !== undefined),
|
|
47
59
|
}
|
|
48
60
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Binding } from './types/Binding.ts'
|
|
2
|
+
|
|
3
|
+
/* A block's `catch` branch binding — the error name bound `plain`, or none when the block
|
|
4
|
+
has no catch branch. Shared by the await/each/try plans so the one catch-binding shape
|
|
5
|
+
stays single-source. */
|
|
6
|
+
export function catchBinding(catchAs: string, hasCatch: boolean): Binding[] {
|
|
7
|
+
return hasCatch ? [{ name: catchAs, classification: 'plain' }] : []
|
|
8
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { interpolatedTemplateLiteral } from './interpolatedTemplateLiteral.ts'
|
|
2
|
+
import type { TemplateAttr } from './types/TemplateAttr.ts'
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
The one decision site for how an element's `class`/`style` base attribute composes
|
|
6
|
+
with its `class:`/`style:` directives — lifted out of the two code-gen back-ends
|
|
7
|
+
(`generateBuild`, `generateSSR`) so the load-bearing congruence (which attrs fold
|
|
8
|
+
into a merged value, and HOW that value's parts concatenate) lives once. Each
|
|
9
|
+
back-end RENDERS the returned plan differently — the build into a reactive `effect`,
|
|
10
|
+
SSR into a pushed attribute string — but consults the same decision, so the markup
|
|
11
|
+
the client clones and the server emits can never drift on the merge logic.
|
|
12
|
+
|
|
13
|
+
The two back-ends differ ONLY in their merge TRIGGER, because their non-merge
|
|
14
|
+
fallbacks differ: the build leaves a STATIC base in the cloned skeleton and toggles
|
|
15
|
+
directives surgically, so it merges only when the base is INTERPOLATED (re-set on
|
|
16
|
+
every update, which would wipe additive toggles) — `mergeClassBuild`/`mergeStyleBuild`.
|
|
17
|
+
SSR must always emit ONE merged attribute string (a duplicate `class`/`style` is
|
|
18
|
+
invalid), so it merges whenever a directive exists — `mergeClass`/`mergeStyle`. The
|
|
19
|
+
PARTS that compose the value (`classParts`/`styleParts`) are identical either way, so
|
|
20
|
+
they are built here once.
|
|
21
|
+
|
|
22
|
+
`attrs` classified up front; `lower` rewrites the signal surface to `model` (the same
|
|
23
|
+
transformer each back-end already threads through its expressions).
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/* The base class/style attribute (static or interpolated) found on the element. */
|
|
27
|
+
type MergeBase =
|
|
28
|
+
| Extract<TemplateAttr, { kind: 'static' }>
|
|
29
|
+
| Extract<TemplateAttr, { kind: 'interpolated' }>
|
|
30
|
+
| undefined
|
|
31
|
+
|
|
32
|
+
export type ClassStyleMergePlan = {
|
|
33
|
+
classBase: MergeBase
|
|
34
|
+
styleBase: MergeBase
|
|
35
|
+
classDirectives: Extract<TemplateAttr, { kind: 'class' }>[]
|
|
36
|
+
styleDirectives: Extract<TemplateAttr, { kind: 'style' }>[]
|
|
37
|
+
/* SSR trigger: merge whenever a directive exists (must emit one attribute). */
|
|
38
|
+
mergeClass: boolean
|
|
39
|
+
mergeStyle: boolean
|
|
40
|
+
/* Build trigger: merge only when the base is interpolated (a static base stays in
|
|
41
|
+
the skeleton + surgical toggles). */
|
|
42
|
+
mergeClassBuild: boolean
|
|
43
|
+
mergeStyleBuild: boolean
|
|
44
|
+
/* The JS expression list a merged value joins — base (if any) + directive parts.
|
|
45
|
+
`class` joins on a space (directive name when truthy); `style` joins on `;`
|
|
46
|
+
(`property:value`, String-coerced). Identical across back-ends. */
|
|
47
|
+
classParts: string[]
|
|
48
|
+
styleParts: string[]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/* The base class/style attribute (static or interpolated) for one property name. */
|
|
52
|
+
function findBase(attrs: TemplateAttr[], name: 'class' | 'style'): MergeBase {
|
|
53
|
+
return attrs.find(
|
|
54
|
+
(attr): attr is NonNullable<MergeBase> =>
|
|
55
|
+
(attr.kind === 'static' || attr.kind === 'interpolated') && attr.name === name,
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* The base value as a lowered JS string expression, or undefined when there is no
|
|
60
|
+
base. A static base is a JSON string literal; an interpolated base is its
|
|
61
|
+
template-literal source run through `lower` (embedded signals → `model`). */
|
|
62
|
+
function baseExpr(base: MergeBase, lower: (code: string) => string): string | undefined {
|
|
63
|
+
if (base === undefined) {
|
|
64
|
+
return undefined
|
|
65
|
+
}
|
|
66
|
+
return base.kind === 'static'
|
|
67
|
+
? JSON.stringify(base.value)
|
|
68
|
+
: lower(interpolatedTemplateLiteral(base.parts))
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* Classifies an element's attrs into the class/style merge plan. */
|
|
72
|
+
export function classStyleMergePlan(
|
|
73
|
+
attrs: TemplateAttr[],
|
|
74
|
+
lower: (code: string) => string,
|
|
75
|
+
): ClassStyleMergePlan {
|
|
76
|
+
const classBase = findBase(attrs, 'class')
|
|
77
|
+
const styleBase = findBase(attrs, 'style')
|
|
78
|
+
const classDirectives = attrs.filter(
|
|
79
|
+
(attr): attr is Extract<TemplateAttr, { kind: 'class' }> => attr.kind === 'class',
|
|
80
|
+
)
|
|
81
|
+
const styleDirectives = attrs.filter(
|
|
82
|
+
(attr): attr is Extract<TemplateAttr, { kind: 'style' }> => attr.kind === 'style',
|
|
83
|
+
)
|
|
84
|
+
const classBaseExpr = baseExpr(classBase, lower)
|
|
85
|
+
const styleBaseExpr = baseExpr(styleBase, lower)
|
|
86
|
+
return {
|
|
87
|
+
classBase,
|
|
88
|
+
styleBase,
|
|
89
|
+
classDirectives,
|
|
90
|
+
styleDirectives,
|
|
91
|
+
mergeClass: classDirectives.length > 0,
|
|
92
|
+
mergeStyle: styleDirectives.length > 0,
|
|
93
|
+
mergeClassBuild: classBase?.kind === 'interpolated' && classDirectives.length > 0,
|
|
94
|
+
mergeStyleBuild: styleBase?.kind === 'interpolated' && styleDirectives.length > 0,
|
|
95
|
+
classParts: [
|
|
96
|
+
...(classBaseExpr === undefined ? [] : [classBaseExpr]),
|
|
97
|
+
...classDirectives.map(
|
|
98
|
+
(dir) => `((${lower(dir.code)}) ? ${JSON.stringify(dir.name)} : "")`,
|
|
99
|
+
),
|
|
100
|
+
],
|
|
101
|
+
styleParts: [
|
|
102
|
+
...(styleBaseExpr === undefined ? [] : [styleBaseExpr]),
|
|
103
|
+
...styleDirectives.map(
|
|
104
|
+
(dir) => `(${JSON.stringify(`${dir.property}:`)} + String(${lower(dir.code)}))`,
|
|
105
|
+
),
|
|
106
|
+
],
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -25,6 +25,17 @@ export function collectAbideDiagnostics(shadow: ShadowProgram): AbideDiagnostic[
|
|
|
25
25
|
})
|
|
26
26
|
continue
|
|
27
27
|
}
|
|
28
|
+
/* Shadow-raised author rules (e.g. importing a compiler-internal helper) are
|
|
29
|
+
already in source coordinates — emit them directly, no segment remap. */
|
|
30
|
+
for (const diagnostic of shadows.get(abidePath)?.diagnostics ?? []) {
|
|
31
|
+
diagnostics.push({
|
|
32
|
+
file: abidePath,
|
|
33
|
+
start: diagnostic.start,
|
|
34
|
+
length: diagnostic.length,
|
|
35
|
+
message: diagnostic.message,
|
|
36
|
+
category: ts.DiagnosticCategory.Error,
|
|
37
|
+
})
|
|
38
|
+
}
|
|
28
39
|
const sourceFile = program.getSourceFile(`${abidePath}.ts`)
|
|
29
40
|
const mappings = shadows.get(abidePath)?.mappings
|
|
30
41
|
if (sourceFile === undefined || mappings === undefined) {
|
|
@@ -6,9 +6,9 @@ import type { AnalyzedComponent } from './types/AnalyzedComponent.ts'
|
|
|
6
6
|
/*
|
|
7
7
|
Compiles a single-file abide component into the body of a client build function.
|
|
8
8
|
Runs the shared front-end (`analyzeComponent`), generates the DOM build from the
|
|
9
|
-
template, and hoists static paths to cells. The returned body
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
template, and hoists static paths to cells. The returned body operates on `host`
|
|
10
|
+
with the `$$`-prefixed dom bindings and `$$model` in scope (the latter emitted by
|
|
11
|
+
`desugarSignals`). `compileModule` wraps it (and the SSR body) into
|
|
12
12
|
a real module; tests wrap it with `new Function`.
|
|
13
13
|
|
|
14
14
|
`analyzed` is a lazy default: a direct caller (tests) omits it and the front-end
|
|
@@ -26,5 +26,5 @@ export function compileComponent(
|
|
|
26
26
|
/* The scoped CSS is bundled into the entry stylesheet (see `abideUiPlugin`), not
|
|
27
27
|
injected at runtime; the build only needs the `data-a-…` scope attributes on
|
|
28
28
|
elements, which `generateBuild` reads from each node's annotated `scopes`. */
|
|
29
|
-
return `${script}\n${hoistCells(build, 'model')}`
|
|
29
|
+
return `${script}\n${hoistCells(build, '$$model')}`
|
|
30
30
|
}
|
|
@@ -5,6 +5,7 @@ import { assertRuntimeHelpersBound } from './assertRuntimeHelpersBound.ts'
|
|
|
5
5
|
import { assertTranspiles } from './assertTranspiles.ts'
|
|
6
6
|
import { compileComponent } from './compileComponent.ts'
|
|
7
7
|
import { compileSSR } from './compileSSR.ts'
|
|
8
|
+
import type { AnalyzedComponent } from './types/AnalyzedComponent.ts'
|
|
8
9
|
import { UI_RUNTIME_IMPORTS } from './UI_RUNTIME_IMPORTS.ts'
|
|
9
10
|
|
|
10
11
|
/*
|
|
@@ -26,7 +27,7 @@ what the `.abide` bundler loader emits.
|
|
|
26
27
|
export function compileModule(
|
|
27
28
|
source: string,
|
|
28
29
|
options: { isLayout?: boolean; moduleId?: string; hot?: boolean } = {},
|
|
29
|
-
): string {
|
|
30
|
+
): { code: string; styles: AnalyzedComponent['styles'] } {
|
|
30
31
|
const isLayout = options.isLayout ?? false
|
|
31
32
|
/* Run the shared front-end once and feed it to both back-ends — the analysis is
|
|
32
33
|
pure over (source, moduleId), so the client and SSR builds reuse one parse
|
|
@@ -44,20 +45,31 @@ export function compileModule(
|
|
|
44
45
|
leaf components today; one importing children falls back to a reload (the dev
|
|
45
46
|
layer decides what to hot-swap). */
|
|
46
47
|
if (options.hot) {
|
|
47
|
-
|
|
48
|
+
/* Bridge keys are the bare source names; bind each to its emitted local (`$$`
|
|
49
|
+
alias when set), so the hot body's `$$mountChild(...)` resolves. */
|
|
50
|
+
const names = UI_RUNTIME_IMPORTS.map((entry) =>
|
|
51
|
+
entry.alias === undefined || entry.alias === entry.name
|
|
52
|
+
? entry.name
|
|
53
|
+
: `${entry.name}: ${entry.alias}`,
|
|
54
|
+
).join(', ')
|
|
48
55
|
const id = JSON.stringify(options.moduleId)
|
|
49
|
-
|
|
56
|
+
/* Hot mode never imports the scoped CSS (it reuses the live bundle's sheet), so
|
|
57
|
+
styles are empty here regardless of the analyzed blocks. */
|
|
58
|
+
return {
|
|
59
|
+
code: `const { ${names}, hotReplace } = window.__abide
|
|
50
60
|
${userImports}
|
|
51
61
|
function build(host, $props) {
|
|
52
62
|
${body}
|
|
53
63
|
}
|
|
54
64
|
function component(host, $props) {
|
|
55
|
-
return mount(host, build, $props)
|
|
65
|
+
return $$mount(host, build, $props)
|
|
56
66
|
}
|
|
57
67
|
component.build = build
|
|
58
68
|
component.__abideId = ${id}
|
|
59
69
|
if (!hotReplace(${id}, component)) location.reload()
|
|
60
|
-
|
|
70
|
+
`,
|
|
71
|
+
styles: [],
|
|
72
|
+
}
|
|
61
73
|
}
|
|
62
74
|
|
|
63
75
|
const ssrBody = indent(compileSSR(source, isLayout, options.moduleId, analyzed))
|
|
@@ -66,12 +78,12 @@ ${body}
|
|
|
66
78
|
}
|
|
67
79
|
|
|
68
80
|
export default function component(host, $props) {
|
|
69
|
-
return mount(host, build, $props)
|
|
81
|
+
return $$mount(host, build, $props)
|
|
70
82
|
}
|
|
71
83
|
|
|
72
84
|
/* Adopt the server-rendered DOM in place instead of rebuilding it. */
|
|
73
85
|
export function hydrateInto(host, $props) {
|
|
74
|
-
return hydrate(host, build, $props)
|
|
86
|
+
return $$hydrate(host, build, $props)
|
|
75
87
|
}
|
|
76
88
|
|
|
77
89
|
export function render($props, $ctx) {
|
|
@@ -95,9 +107,31 @@ ${options.moduleId === undefined ? '' : `component.__abideId = ${JSON.stringify(
|
|
|
95
107
|
string/comment/HTML literal (e.g. an `on`-attribute in static markup) no longer
|
|
96
108
|
forces a spurious import, so no per-surface scoping is needed: a client-only helper
|
|
97
109
|
simply doesn't appear as an identifier in the SSR body. */
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
110
|
+
/* Parse the generated bodies ONCE and feed the single tree to both AST passes: the
|
|
111
|
+
dead-import filter (`collectIdentifiers`) and the binding backstop
|
|
112
|
+
(`assertRuntimeHelpersBound`). The import block isn't in this tree yet — it is derived
|
|
113
|
+
from the filter's result — so the backstop is told the names that block will bind
|
|
114
|
+
(`importedHelpers`), which is exactly what it would have read from the prepended imports
|
|
115
|
+
had it re-parsed the whole module. `setParentNodes` is required by the backstop's
|
|
116
|
+
`getStart`/line lookups. */
|
|
117
|
+
const bodySource = ts.createSourceFile(
|
|
118
|
+
'module.ts',
|
|
119
|
+
`${userImports}\n${moduleBody}`,
|
|
120
|
+
ts.ScriptTarget.Latest,
|
|
121
|
+
/* setParentNodes */ true,
|
|
122
|
+
)
|
|
123
|
+
const referenced = collectIdentifiers(bodySource)
|
|
124
|
+
const keptImports = UI_RUNTIME_IMPORTS.filter((entry) =>
|
|
125
|
+
referenced.has(entry.alias ?? entry.name),
|
|
126
|
+
)
|
|
127
|
+
const importBlock = keptImports
|
|
128
|
+
.map((entry) => {
|
|
129
|
+
const local =
|
|
130
|
+
entry.alias === undefined || entry.alias === entry.name
|
|
131
|
+
? entry.name
|
|
132
|
+
: `${entry.name} as ${entry.alias}`
|
|
133
|
+
return `import { ${local} } from '${ABIDE_PACKAGE_NAME}/${entry.specifier}'`
|
|
134
|
+
})
|
|
101
135
|
.join('\n')
|
|
102
136
|
const module = `${importBlock}
|
|
103
137
|
${userImports}
|
|
@@ -112,9 +146,15 @@ ${moduleBody}`
|
|
|
112
146
|
/* `assertTranspiles` only proves the output PARSES — a call to an un-imported helper is
|
|
113
147
|
valid syntax, so it slips through. This second guard proves the output is BOUND: every
|
|
114
148
|
runtime helper it calls is actually imported (an independent check of the dead-import
|
|
115
|
-
filter above), turning a runtime `ReferenceError` into a located compile error.
|
|
116
|
-
|
|
117
|
-
|
|
149
|
+
filter above), turning a runtime `ReferenceError` into a located compile error. It walks
|
|
150
|
+
the SAME tree the filter just parsed, with the kept helper imports supplied as the bound
|
|
151
|
+
set the prepended import block provides. */
|
|
152
|
+
assertRuntimeHelpersBound(
|
|
153
|
+
bodySource,
|
|
154
|
+
new Set(keptImports.map((entry) => entry.alias ?? entry.name)),
|
|
155
|
+
'component module generation',
|
|
156
|
+
)
|
|
157
|
+
return { code: module, styles: analyzed.styles }
|
|
118
158
|
}
|
|
119
159
|
|
|
120
160
|
/* Indents a body block for embedding inside a wrapper function. Lines whose start
|
|
@@ -156,14 +196,9 @@ function unescapedBacktickCount(line: string): number {
|
|
|
156
196
|
in a handler — leaves the scanner unable to find the substitution's closing `}` without
|
|
157
197
|
the parser's re-scan, so a token-only pass mis-reads the rest of the module as template
|
|
158
198
|
text and drops every helper referenced after it (an `import`-less `effect`/`mountChild`
|
|
159
|
-
→ a `ReferenceError` at mount → the router's reload fallback → a refresh loop).
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
'module.ts',
|
|
163
|
-
code,
|
|
164
|
-
ts.ScriptTarget.Latest,
|
|
165
|
-
/* setParentNodes */ false,
|
|
166
|
-
)
|
|
199
|
+
→ a `ReferenceError` at mount → the router's reload fallback → a refresh loop). Takes the
|
|
200
|
+
already-parsed source so the one tree feeds this pass and the binding backstop both. */
|
|
201
|
+
function collectIdentifiers(source: ts.SourceFile): Set<string> {
|
|
167
202
|
const names = new Set<string>()
|
|
168
203
|
const visit = (node: ts.Node): void => {
|
|
169
204
|
if (ts.isIdentifier(node)) {
|
|
@@ -5,17 +5,20 @@ import type { AnalyzedComponent } from './types/AnalyzedComponent.ts'
|
|
|
5
5
|
|
|
6
6
|
/*
|
|
7
7
|
Compiles a component into the body of a server render function. Runs the shared
|
|
8
|
-
front-end, then the SSR back-end, and returns `{ html, state, awaits }`:
|
|
8
|
+
front-end, then the SSR back-end, and returns `{ html, state, awaits, resume }`:
|
|
9
9
|
|
|
10
10
|
- `html` — server-rendered markup (await blocks render their pending shell);
|
|
11
11
|
- `state` — the document snapshot the client adopts on resume;
|
|
12
12
|
- `awaits` — pending await blocks (id + promise + resolved/error renderers) that
|
|
13
|
-
`renderToStream` flushes out of order; empty for a fully synchronous component
|
|
13
|
+
`renderToStream` flushes out of order; empty for a fully synchronous component;
|
|
14
|
+
- `resume` — blocking await values rendered inline, keyed by boundary id, seeded
|
|
15
|
+
into the hydration manifest.
|
|
14
16
|
|
|
15
17
|
Effects are stripped — they are client lifecycle and emit no HTML, so the server
|
|
16
18
|
render is a snapshot of the markup before any effect runs.
|
|
17
19
|
|
|
18
|
-
Defines
|
|
20
|
+
Defines `$$model` via the lowered script; the reactive scope is entered via `$$enterScope`
|
|
21
|
+
and exited in a `finally` block. The block-id counter is
|
|
19
22
|
the request-local `$ctx` (threaded into child renders), not a module global — a blocking
|
|
20
23
|
`await` block awaits its promise at its structural position and renders inline, so ids
|
|
21
24
|
allocate depth-first like the client; render yields at that `await`, and a shared global
|
|
@@ -29,10 +32,9 @@ purely synchronous / streaming-await / try-only component returns `SsrRender` di
|
|
|
29
32
|
framework always `await`s `render()`, so production is uniform either way; the sync return
|
|
30
33
|
keeps static pages off the microtask queue and leaves the bulk of the SSR tests synchronous.
|
|
31
34
|
|
|
32
|
-
The reactive scope brackets the body
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
the awaits).
|
|
35
|
+
The reactive scope brackets the body: `$$enterScope()` runs synchronously before any inline
|
|
36
|
+
`await`, and `$$model` is captured for the rest of the render (its reads are object-method
|
|
37
|
+
calls, not scope lookups, so they stay correct across the awaits).
|
|
36
38
|
|
|
37
39
|
`analyzed` is a lazy default: a direct caller (tests) omits it and the front-end
|
|
38
40
|
runs here, but `compileModule` shares one analysis across both back-ends.
|
|
@@ -51,9 +53,9 @@ export function compileSSR(
|
|
|
51
53
|
/* `typeof model` guards a component with no reactive state (a pure-async or
|
|
52
54
|
static component declares no `model`); its snapshot is then empty. */
|
|
53
55
|
const body =
|
|
54
|
-
`const $scope = enterScope();\ntry {\n${lowered}\n${SSR_ESCAPE}\nconst $out = [];\nconst $awaits = [];\nconst $resume = {};\n${ssr}` +
|
|
55
|
-
`return { html: $out.join(''), state: (typeof model !== 'undefined' ? model.snapshot() : {}), awaits: $awaits, resume: $resume };\n` +
|
|
56
|
-
`} finally { exitScope($scope); }`
|
|
56
|
+
`const $scope = $$enterScope();\ntry {\n${lowered}\n${SSR_ESCAPE}\nconst $out = [];\nconst $awaits = [];\nconst $resume = {};\n${ssr}` +
|
|
57
|
+
`return { html: $out.join(''), state: (typeof $$model !== 'undefined' ? $$model.snapshot() : {}), awaits: $awaits, resume: $resume };\n` +
|
|
58
|
+
`} finally { $$exitScope($scope); }`
|
|
57
59
|
/* An inline `await` — a blocking await block, a child render, a slot read, or a
|
|
58
60
|
top-level `await` in the author script — forces an async wrapper. Match `await` as a
|
|
59
61
|
standalone token (operator), so a no-space form (`await(x)`, `await[i]`, `` await`t` ``)
|