@abide/abide 0.48.0 → 0.50.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 +85 -59
- package/CHANGELOG.md +216 -0
- package/README.md +21 -10
- package/package.json +15 -4
- package/src/abideLsp.ts +5 -6
- package/src/abideResolverPlugin.ts +197 -169
- package/src/build.ts +79 -56
- package/src/buildCli.ts +3 -5
- package/src/buildDisconnected.ts +2 -5
- package/src/bundleApp.ts +2 -3
- package/src/checkAbide.ts +12 -2
- package/src/compile.ts +2 -4
- package/src/devEntry.ts +113 -28
- package/src/discoveryEntry.ts +3 -2
- package/src/lib/bundle/disconnected.abide +69 -73
- package/src/lib/bundle/infoPlist.ts +13 -7
- package/src/lib/bundle/installDownloads.ts +13 -3
- package/src/lib/bundle/installMacMenu.ts +13 -3
- package/src/lib/bundle/spawnEmbeddedServer.ts +13 -4
- package/src/lib/bundle/waitForServer.ts +6 -1
- package/src/lib/cli/parseArgvForRpc.ts +36 -9
- package/src/lib/cli/tokenizeArgvFlags.ts +4 -2
- package/src/lib/mcp/createMcpResourceServer.ts +13 -2
- package/src/lib/mcp/mcpTools.ts +15 -6
- package/src/lib/mcp/toolResultFromResponse.ts +40 -12
- package/src/lib/server/error.ts +6 -6
- package/src/lib/server/json.ts +17 -3
- package/src/lib/server/rpc/defineRpc.ts +58 -24
- package/src/lib/server/rpc/parseArgs.ts +113 -14
- package/src/lib/server/rpc/resolveRpcJsonSchema.ts +28 -0
- package/src/lib/server/rpc/runWithRpcTimeout.ts +12 -1
- package/src/lib/server/rpc/types/RpcHelper.ts +75 -105
- package/src/lib/server/rpc/types/RpcRegistryEntry.ts +24 -9
- package/src/lib/server/rpc/validationError.ts +1 -1
- package/src/lib/server/runtime/DEV_RELOAD_CLIENT_SCRIPT.ts +0 -15
- package/src/lib/server/runtime/buildCacheSnapshot.ts +10 -9
- package/src/lib/server/runtime/buildInspectorSurface.ts +6 -4
- package/src/lib/server/runtime/buildOpenApiSpec.ts +55 -11
- package/src/lib/server/runtime/buildPreloadManifest.ts +12 -10
- package/src/lib/server/runtime/createAppAssetServer.ts +18 -15
- package/src/lib/server/runtime/createAppRouteResolver.ts +145 -0
- package/src/lib/server/runtime/createPlumbingRouter.ts +212 -0
- package/src/lib/server/runtime/createRouteDispatcher.ts +4 -10
- package/src/lib/server/runtime/createServer.ts +120 -324
- package/src/lib/server/runtime/createUiPageRenderer.ts +137 -26
- package/src/lib/server/runtime/devClientFingerprint.ts +19 -34
- package/src/lib/server/runtime/finalizeResponse.ts +11 -1
- package/src/lib/server/runtime/installAmbientScopeStore.ts +30 -9
- package/src/lib/server/runtime/logExposedSurfaces.ts +8 -7
- package/src/lib/server/runtime/pathStore.ts +15 -0
- package/src/lib/server/runtime/registryManifests.ts +1 -1
- package/src/lib/server/runtime/renderCellBarrierStore.ts +15 -0
- package/src/lib/server/runtime/runWithRequestScope.ts +3 -0
- package/src/lib/server/runtime/serializeCacheSnapshot.ts +6 -4
- package/src/lib/server/runtime/snapshotEntryFromCache.ts +24 -16
- package/src/lib/server/runtime/streamCacheResolutions.ts +14 -7
- package/src/lib/server/runtime/streamFromIterator.ts +30 -2
- package/src/lib/server/runtime/textResponse.ts +23 -0
- package/src/lib/server/runtime/types/DevReloadStamp.ts +6 -10
- package/src/lib/server/runtime/types/InspectorCacheEntry.ts +1 -1
- package/src/lib/server/runtime/types/RequestStore.ts +27 -0
- package/src/lib/server/runtime/warnUnguardedMcp.ts +15 -5
- package/src/lib/server/sockets/createSocketDispatcher.ts +9 -11
- package/src/lib/server/sockets/defineSocket.ts +5 -4
- package/src/lib/server/sse.ts +5 -1
- package/src/lib/shared/ASYNC_CELL.ts +5 -0
- package/src/lib/shared/DEV_REBUILD_PATH.ts +6 -0
- package/src/lib/shared/HTTP_METHODS.ts +6 -0
- package/src/lib/shared/HttpError.ts +1 -5
- package/src/lib/shared/MCP_PATH.ts +6 -0
- package/src/lib/shared/PROXIED_SERVER_SUBDIRS.ts +15 -0
- package/src/lib/shared/REF_JSON_TAGS.ts +13 -1
- package/src/lib/shared/RPC_SHIM_GLOBALS.ts +9 -0
- package/src/lib/shared/activeCacheStore.ts +1 -0
- package/src/lib/shared/activePage.ts +1 -0
- package/src/lib/shared/buildArtifact.ts +20 -0
- package/src/lib/shared/buildRpcRequest.ts +6 -2
- package/src/lib/shared/buildSocketOverChannel.ts +4 -3
- package/src/lib/shared/bundleGraphFromMetafile.ts +68 -0
- package/src/lib/shared/cache.ts +156 -131
- package/src/lib/shared/canonicalJson.ts +24 -1
- package/src/lib/shared/changeAffectsClient.ts +12 -7
- package/src/lib/shared/createCacheStore.ts +31 -8
- package/src/lib/shared/createChannelLog.ts +24 -0
- package/src/lib/shared/createLifecycleChannel.ts +7 -1
- package/src/lib/shared/createReachable.ts +47 -78
- package/src/lib/shared/createRemoteFunction.ts +52 -30
- package/src/lib/shared/createRpcServerProgram.ts +820 -0
- package/src/lib/shared/decodeRefJson.ts +4 -4
- package/src/lib/shared/decodeResponse.ts +2 -2
- package/src/lib/shared/decodeWireBody.ts +35 -0
- package/src/lib/shared/detectRpcMethod.ts +8 -1
- package/src/lib/shared/done.ts +8 -2
- package/src/lib/shared/encodeRefJson.ts +4 -4
- package/src/lib/shared/encodeWireBody.ts +18 -0
- package/src/lib/shared/exitOnBuildFailure.ts +4 -3
- package/src/lib/{server/rpc → shared}/fieldErrorsFromIssues.ts +5 -1
- package/src/lib/shared/generateDeclarations.ts +72 -0
- package/src/lib/shared/hasSeedableRequest.ts +25 -0
- package/src/lib/shared/hydrationWindow.ts +53 -0
- package/src/lib/shared/isAsyncCell.ts +11 -0
- package/src/lib/shared/isAsyncIterable.ts +8 -0
- package/src/lib/shared/isSubscribable.ts +3 -3
- package/src/lib/shared/isThenable.ts +9 -0
- package/src/lib/shared/jsonSchemaForType.ts +258 -0
- package/src/lib/shared/lenientDecode.ts +15 -0
- package/src/lib/shared/loadProjectTsConfig.ts +28 -0
- package/src/lib/shared/markFrameworkSourcesIgnored.ts +1 -1
- package/src/lib/shared/matchRoute.ts +4 -14
- package/src/lib/shared/parseEnv.ts +17 -6
- package/src/lib/shared/peek.ts +14 -0
- package/src/lib/shared/pending.ts +9 -6
- package/src/lib/shared/pendingAsyncCellsSlot.ts +13 -0
- package/src/lib/shared/prepareRpcModule.ts +91 -93
- package/src/lib/shared/prepareSocketModule.ts +3 -2
- package/src/lib/shared/probeRegistries.ts +5 -18
- package/src/lib/shared/reachable.ts +7 -10
- package/src/lib/shared/refresh.ts +12 -2
- package/src/lib/shared/refreshing.ts +8 -2
- package/src/lib/shared/resolvedCellsSlot.ts +13 -0
- package/src/lib/shared/reviveWireField.ts +49 -0
- package/src/lib/shared/reviveWireOutput.ts +36 -0
- package/src/lib/shared/rpcServerForRoot.ts +25 -0
- package/src/lib/shared/scanPages.ts +25 -0
- package/src/lib/shared/serializeEnv.ts +18 -6
- package/src/lib/shared/snapshotShippable.ts +8 -7
- package/src/lib/shared/snippet.ts +11 -6
- package/src/lib/shared/streamedCellsSlot.ts +14 -0
- package/src/lib/shared/subscribableFromResponse.ts +4 -4
- package/src/lib/shared/subscribableProbes.ts +1 -1
- package/src/lib/shared/tailProbeSlot.ts +1 -1
- package/src/lib/shared/types/AsyncComputed.ts +20 -0
- package/src/lib/shared/types/AsyncState.ts +13 -0
- package/src/lib/shared/types/CacheEntry.ts +7 -7
- package/src/lib/shared/types/CacheOptions.ts +23 -37
- package/src/lib/shared/types/CachePolicy.ts +25 -0
- package/src/lib/shared/types/CacheSnapshotEntry.ts +6 -5
- package/src/lib/shared/types/ErrorJsonSchemas.ts +8 -0
- package/src/lib/shared/types/HttpMethod.ts +3 -1
- package/src/lib/shared/types/InputCoercion.ts +17 -0
- package/src/lib/shared/types/{Subscribable.ts → NamedAsyncIterable.ts} +1 -1
- package/src/lib/shared/types/OutputWirePlan.ts +17 -0
- package/src/lib/shared/types/PagesScan.ts +7 -0
- package/src/lib/shared/types/PendingAsyncCells.ts +10 -0
- package/src/lib/shared/types/RawRemoteFunction.ts +6 -0
- package/src/lib/shared/types/RemoteCallable.ts +8 -7
- package/src/lib/shared/types/RemoteFunction.ts +16 -15
- package/src/lib/shared/types/ResolvedCells.ts +11 -0
- package/src/lib/shared/types/ReturnBody.ts +15 -0
- package/src/lib/shared/types/RpcBuildStamps.ts +23 -0
- package/src/lib/shared/types/RpcErrorGuard.ts +3 -8
- package/src/lib/shared/types/Socket.ts +4 -4
- package/src/lib/shared/types/SsrPayload.ts +5 -1
- package/src/lib/shared/types/StreamPolicy.ts +11 -0
- package/src/lib/shared/types/StreamedCells.ts +19 -0
- package/src/lib/shared/types/StreamedResolution.ts +24 -6
- package/src/lib/shared/types/TailHooks.ts +1 -1
- package/src/lib/shared/types/WireKind.ts +11 -0
- package/src/lib/shared/url.ts +5 -0
- package/src/lib/shared/validationHttpError.ts +33 -0
- package/src/lib/shared/warmSeedKey.ts +16 -0
- package/src/lib/shared/wireJsonReplacer.ts +30 -0
- package/src/lib/shared/writeRpcDts.ts +11 -1
- package/src/lib/shared/writeTestSocketsDts.ts +1 -1
- package/src/lib/test/createTestApp.ts +19 -1
- package/src/lib/ui/README.md +1 -1
- package/src/lib/ui/activePendingCells.ts +14 -0
- package/src/lib/ui/compile/BLOCK_KEYWORDS.ts +21 -0
- package/src/lib/ui/compile/SSR_ESCAPE.ts +4 -1
- package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +35 -7
- package/src/lib/ui/compile/abideUiPlugin.ts +19 -2
- package/src/lib/ui/compile/analyzeComponent.ts +89 -6
- package/src/lib/ui/compile/assignmentTargetNames.ts +54 -0
- package/src/lib/ui/compile/asyncInterpolationFields.ts +152 -0
- package/src/lib/ui/compile/asyncValuePositionError.ts +21 -0
- package/src/lib/ui/compile/asyncValuePositionInterpolations.ts +64 -0
- package/src/lib/ui/compile/attrLiftPosition.ts +18 -0
- package/src/lib/ui/compile/bindListenEvent.ts +5 -6
- package/src/lib/ui/compile/cachedSourceFile.ts +40 -0
- package/src/lib/ui/compile/catchBinding.ts +7 -5
- package/src/lib/ui/compile/classifyInterpolationType.ts +46 -0
- package/src/lib/ui/compile/collectAbideDiagnostics.ts +96 -0
- package/src/lib/ui/compile/compileComponent.ts +19 -3
- package/src/lib/ui/compile/compileModule.ts +27 -58
- package/src/lib/ui/compile/compileSSR.ts +52 -13
- package/src/lib/ui/compile/compileShadow.ts +265 -42
- package/src/lib/ui/compile/composeProps.ts +15 -4
- package/src/lib/ui/compile/createShadowLanguageService.ts +116 -68
- package/src/lib/ui/compile/createShadowProgram.ts +35 -6
- package/src/lib/ui/compile/declaredNames.ts +36 -0
- package/src/lib/ui/compile/desugarSignals.ts +419 -37
- package/src/lib/ui/compile/expressionIsPrefixEvaluable.ts +19 -0
- package/src/lib/ui/compile/generateBuild.ts +74 -17
- package/src/lib/ui/compile/generateSSR.ts +286 -75
- package/src/lib/ui/compile/hoistableAwaits.ts +193 -0
- package/src/lib/ui/compile/hoistableChildRenders.ts +112 -0
- package/src/lib/ui/compile/interpolatedTemplateLiteral.ts +3 -1
- package/src/lib/ui/compile/interpolationClassifierForRoot.ts +37 -0
- package/src/lib/ui/compile/isSpuriousAsyncReadDiagnostic.ts +174 -0
- package/src/lib/ui/compile/isWhitespaceText.ts +10 -2
- package/src/lib/ui/compile/liftAsyncSubExpressions.ts +166 -0
- package/src/lib/ui/compile/lowerAsyncInterpolations.ts +92 -0
- package/src/lib/ui/compile/lowerContext.ts +10 -0
- package/src/lib/ui/compile/lowerDocAccess.ts +51 -10
- package/src/lib/ui/compile/lowerScript.ts +40 -4
- package/src/lib/ui/compile/nodeAtShadowOffset.ts +28 -0
- package/src/lib/ui/compile/parseTemplate.ts +18 -1090
- package/src/lib/ui/compile/parseTemplateRecovering.ts +1385 -0
- package/src/lib/ui/compile/referencedIdentifiers.ts +35 -0
- package/src/lib/ui/compile/renameSignalRefs.ts +26 -40
- package/src/lib/ui/compile/resolveReactiveExport.ts +4 -7
- package/src/lib/ui/compile/scopeCss.ts +27 -4
- package/src/lib/ui/compile/seedTypeClassifierForRoot.ts +65 -0
- package/src/lib/ui/compile/shadowInterpolationClassifier.ts +47 -0
- package/src/lib/ui/compile/sourceFileOptionsSignature.ts +20 -0
- package/src/lib/ui/compile/structuralHeadTokens.ts +107 -0
- package/src/lib/ui/compile/templateSemanticTokens.ts +21 -0
- package/src/lib/ui/compile/templateStartOffset.ts +15 -0
- package/src/lib/ui/compile/tryPlan.ts +4 -3
- package/src/lib/ui/compile/types/AnalyzedComponent.ts +4 -0
- package/src/lib/ui/compile/types/AsyncInterpolationField.ts +19 -0
- package/src/lib/ui/compile/types/InterpolationClassifier.ts +12 -0
- package/src/lib/ui/compile/types/InterpolationKind.ts +7 -0
- package/src/lib/ui/compile/types/ParseDiagnostic.ts +8 -0
- package/src/lib/ui/compile/types/SeedTypeClassifier.ts +15 -0
- package/src/lib/ui/compile/types/TemplateNode.ts +36 -4
- package/src/lib/ui/compile/types/ValuePositionInterpolation.ts +13 -0
- package/src/lib/ui/compile/writtenTemplateNames.ts +84 -0
- package/src/lib/ui/computed.ts +28 -1
- package/src/lib/ui/createScope.ts +35 -68
- package/src/lib/ui/dom/anchoredBranch.ts +142 -0
- package/src/lib/ui/dom/appendText.ts +8 -3
- package/src/lib/ui/dom/awaitBlock.ts +62 -75
- package/src/lib/ui/dom/bindProp.ts +22 -0
- package/src/lib/ui/dom/bindableProp.ts +47 -0
- package/src/lib/ui/dom/cellPending.ts +24 -0
- package/src/lib/ui/dom/discardBoundary.ts +24 -6
- package/src/lib/ui/dom/disposeRange.ts +2 -1
- package/src/lib/ui/dom/each.ts +41 -12
- package/src/lib/ui/dom/eachAsync.ts +39 -3
- package/src/lib/ui/dom/fillBefore.ts +6 -7
- package/src/lib/ui/dom/fillBoundary.ts +2 -3
- package/src/lib/ui/dom/fillRange.ts +4 -4
- package/src/lib/ui/dom/hydrate.ts +6 -12
- package/src/lib/ui/dom/isComment.ts +1 -1
- package/src/lib/ui/dom/matchingRangeClose.ts +29 -0
- package/src/lib/ui/dom/mergeProps.ts +1 -1
- package/src/lib/ui/dom/mount.ts +1 -2
- package/src/lib/ui/dom/mountChild.ts +9 -40
- package/src/lib/ui/dom/mountRange.ts +2 -3
- package/src/lib/ui/dom/mountSlot.ts +1 -1
- package/src/lib/ui/dom/mountStreamedChild.ts +84 -0
- package/src/lib/ui/dom/mountSwappableRange.ts +46 -4
- package/src/lib/ui/dom/mutateDocContainer.ts +65 -0
- package/src/lib/ui/dom/on.ts +2 -2
- package/src/lib/ui/dom/readCell.ts +40 -0
- package/src/lib/ui/dom/restProps.ts +2 -2
- package/src/lib/ui/dom/spreadProps.ts +3 -4
- package/src/lib/ui/dom/switchBlock.ts +30 -7
- package/src/lib/ui/dom/tryBlock.ts +233 -70
- package/src/lib/ui/dom/types/SwitchCase.ts +5 -1
- package/src/lib/ui/dom/when.ts +12 -2
- package/src/lib/ui/dom/withScope.ts +9 -2
- package/src/lib/ui/finalizeStreamedChildren.ts +89 -0
- package/src/lib/ui/flight.ts +56 -0
- package/src/lib/ui/html.ts +12 -1
- package/src/lib/ui/isolateCellBarrier.ts +17 -0
- package/src/lib/ui/linked.ts +48 -2
- package/src/lib/ui/props.ts +17 -0
- package/src/lib/ui/remoteProxy.ts +84 -159
- package/src/lib/ui/renderChain.ts +53 -13
- package/src/lib/ui/renderToStream.ts +22 -18
- package/src/lib/ui/resumeSeedScript.ts +1 -1
- package/src/lib/ui/router.ts +86 -53
- package/src/lib/ui/runtime/AsyncCellError.ts +20 -0
- package/src/lib/ui/runtime/CELL_SEED.ts +14 -0
- package/src/lib/ui/runtime/CHILD_PRESENT.ts +2 -2
- package/src/lib/ui/runtime/CURRENT_BOUNDARY.ts +11 -0
- package/src/lib/ui/runtime/CURRENT_PATH.ts +29 -0
- package/src/lib/ui/runtime/RENDER.ts +10 -7
- package/src/lib/ui/runtime/RESUME.ts +4 -4
- package/src/lib/ui/runtime/STREAMED_CELLS.ts +57 -0
- package/src/lib/ui/runtime/ambientPathBacking.ts +32 -0
- package/src/lib/ui/runtime/applyPatchToTree.ts +17 -4
- package/src/lib/ui/runtime/blockId.ts +31 -0
- package/src/lib/ui/runtime/boundaryFor.ts +11 -0
- package/src/lib/ui/runtime/cellBarrierBacking.ts +30 -0
- package/src/lib/ui/runtime/createAsyncCell.ts +300 -0
- package/src/lib/ui/runtime/createDoc.ts +31 -54
- package/src/lib/ui/runtime/createEffectNode.ts +9 -0
- package/src/lib/ui/runtime/enterRenderPass.ts +5 -4
- package/src/lib/ui/runtime/flushEffects.ts +33 -1
- package/src/lib/ui/runtime/isAsyncFunction.ts +14 -0
- package/src/lib/ui/runtime/nextBlockId.ts +8 -7
- package/src/lib/ui/runtime/renderPath.ts +16 -0
- package/src/lib/ui/runtime/scope.ts +11 -0
- package/src/lib/ui/runtime/toTeardown.ts +10 -3
- package/src/lib/ui/runtime/types/Boundary.ts +9 -0
- package/src/lib/ui/runtime/types/RenderContext.ts +9 -7
- package/src/lib/ui/runtime/types/SsrRender.ts +9 -2
- package/src/lib/ui/runtime/types/UiComponent.ts +1 -5
- package/src/lib/ui/runtime/types/UiProps.ts +6 -5
- package/src/lib/ui/runtime/withOptionalPath.ts +8 -0
- package/src/lib/ui/runtime/withPath.ts +16 -0
- package/src/lib/ui/runtime/withPathFrom.ts +20 -0
- package/src/lib/ui/runtime/withoutHydration.ts +22 -0
- package/src/lib/ui/seedStreamedResolution.ts +31 -6
- package/src/lib/ui/settleAsyncCells.ts +24 -0
- package/src/lib/ui/socketProxy.ts +1 -1
- package/src/lib/ui/startClient.ts +16 -31
- package/src/lib/ui/state.ts +9 -3
- package/src/lib/ui/trackedComputed.ts +68 -0
- package/src/lib/ui/types/Scope.ts +8 -24
- package/src/lib/ui/watch.ts +3 -3
- package/src/serverEntry.ts +14 -0
- package/template/src/server/rpc/getHello.ts +15 -13
- package/template/test/app.test.ts +1 -1
- package/src/lib/server/runtime/devHotModuleResponse.ts +0 -41
- package/src/lib/shared/DEV_HOT_PREFIX.ts +0 -7
- package/src/lib/shared/UNREACHABLE_STATUSES.ts +0 -13
- package/src/lib/shared/hasReplayableRequest.ts +0 -17
- package/src/lib/shared/hydratingSlot.ts +0 -12
- package/src/lib/shared/outboxProbeSlot.ts +0 -20
- package/src/lib/shared/types/Outbox.ts +0 -9
- package/src/lib/shared/types/OutboxEntry.ts +0 -27
- package/src/lib/shared/types/SmartReadOptions.ts +0 -36
- package/src/lib/shared/wakeHydrationPeeks.ts +0 -20
- package/src/lib/ui/COMPONENT_WRAPPER_PREFIX.ts +0 -5
- package/src/lib/ui/compile/REACTIVE_CALLEES.ts +0 -11
- package/src/lib/ui/compile/assertRuntimeHelpersBound.ts +0 -80
- package/src/lib/ui/compile/markupTokens.ts +0 -313
- package/src/lib/ui/compile/structuralBlockTokens.ts +0 -100
- package/src/lib/ui/dom/applyResolved.ts +0 -87
- package/src/lib/ui/dom/scopeLabel.ts +0 -21
- package/src/lib/ui/dom/text.ts +0 -20
- package/src/lib/ui/history.ts +0 -101
- package/src/lib/ui/installHotBridge.ts +0 -93
- package/src/lib/ui/installInspectorBridge.ts +0 -140
- package/src/lib/ui/outbox.ts +0 -35
- package/src/lib/ui/persist.ts +0 -115
- package/src/lib/ui/rpcOutbox/createOutboxQueue.ts +0 -281
- package/src/lib/ui/rpcOutbox/outboxRegistry.ts +0 -69
- package/src/lib/ui/runtime/PATCH_BUS.ts +0 -28
- package/src/lib/ui/runtime/captureModelDoc.ts +0 -28
- package/src/lib/ui/runtime/hotInstances.ts +0 -10
- package/src/lib/ui/runtime/hotReloadEnabled.ts +0 -8
- package/src/lib/ui/runtime/hotReplace.ts +0 -38
- package/src/lib/ui/runtime/liveScopes.ts +0 -15
- package/src/lib/ui/runtime/localStoragePersistence.ts +0 -47
- package/src/lib/ui/runtime/registerHotInstance.ts +0 -23
- package/src/lib/ui/runtime/seedModelDoc.ts +0 -26
- package/src/lib/ui/runtime/types/HotInstance.ts +0 -22
- package/src/lib/ui/runtime/types/PatchEvent.ts +0 -16
- package/src/lib/ui/seedResolved.ts +0 -28
- package/src/lib/ui/sync.ts +0 -48
- package/src/lib/ui/types/History.ts +0 -14
- package/src/lib/ui/types/PersistHandle.ts +0 -11
- package/src/lib/ui/types/PersistenceStore.ts +0 -12
- package/src/lib/ui/types/ResolvedFrame.ts +0 -15
- package/src/lib/ui/types/SyncTransport.ts +0 -13
|
@@ -1,1097 +1,25 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { isWhitespaceText } from './isWhitespaceText.ts'
|
|
4
|
-
import type { TemplateAttr } from './types/TemplateAttr.ts'
|
|
1
|
+
import { AbideCompileError } from './AbideCompileError.ts'
|
|
2
|
+
import { parseTemplateRecovering } from './parseTemplateRecovering.ts'
|
|
5
3
|
import type { TemplateNode } from './types/TemplateNode.ts'
|
|
6
|
-
import type { TextPart } from './types/TextPart.ts'
|
|
7
|
-
import { VOID_TAGS } from './VOID_TAGS.ts'
|
|
8
|
-
|
|
9
|
-
/* Compiled once — the parser tests these per source character, so a per-call literal
|
|
10
|
-
would recompile them across the whole template scan. */
|
|
11
|
-
const WHITESPACE = /\s/
|
|
12
|
-
const WHITESPACE_OR_GT = /[\s>]/
|
|
13
|
-
const ATTR_NAME_END = /[\s=>/]/
|
|
14
|
-
const TAG_NAME_END = /[\s>/]/
|
|
15
|
-
const UPPERCASE_START = /^[A-Z]/
|
|
16
|
-
const AWAIT_PREFIX = /^await\b/
|
|
17
|
-
const LEADING_KEYWORD = /^\s*(\S+)/
|
|
18
4
|
|
|
19
5
|
/*
|
|
20
|
-
|
|
21
|
-
`
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
or attribute — e.g. one quoted in a code sample — is read as that expression's
|
|
31
|
-
text, never mistaken for a real style. Keeping it in the tree lets the front-end
|
|
32
|
-
scope it to its sibling subtree (`analyzeComponent`); the node emits no DOM/markup.
|
|
6
|
+
The throwing facade over the error-recovering core (`parseTemplateRecovering`). The
|
|
7
|
+
compile path (SSR / client / type-check via `analyzeComponent` + `compileShadow`) is
|
|
8
|
+
FAIL-FAST: the first malformed construct must abort the build with a located error.
|
|
9
|
+
This delegates to the non-throwing core, then re-throws its FIRST diagnostic — the
|
|
10
|
+
first source-order failure — as an `AbideCompileError` carrying the same message and
|
|
11
|
+
offset the parser recorded, so the loader resolves it to `file:line:col`.
|
|
12
|
+
|
|
13
|
+
The core keeps every diagnostic (the LSP consumes them via `parseTemplateRecovering`
|
|
14
|
+
directly). Well-formed input yields zero diagnostics, so this returns `{ nodes }`
|
|
15
|
+
unchanged — no caller/type ripple, and the golden corpus stays byte-identical.
|
|
33
16
|
*/
|
|
34
|
-
|
|
35
|
-
/* A line-leading static `import` in a nested script body. The `(?=\s)` requires
|
|
36
|
-
whitespace after the keyword (sparing `import.meta` and no-space `import(...)`),
|
|
37
|
-
and `(?!\s*\()` spares a dynamic `import (...)` written with whitespace before the
|
|
38
|
-
paren — both legitimate lazy paths — so only a true static import statement matches. */
|
|
39
|
-
const NESTED_STATIC_IMPORT = /^[ \t]*import(?=\s)(?!\s*\()/m
|
|
40
|
-
|
|
41
|
-
/* A braced template expression with the absolute source offset of its first
|
|
42
|
-
(post-trim) character, so the type-checking shadow can map a diagnostic back. */
|
|
43
|
-
type Braced = { code: string; loc: number }
|
|
44
|
-
|
|
45
17
|
export function parseTemplate(source: string, baseOffset = 0): { nodes: TemplateNode[] } {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
function readBracedExpression(): Braced {
|
|
52
|
-
cursor += 1 // past '{'
|
|
53
|
-
const start = cursor
|
|
54
|
-
let depth = 1
|
|
55
|
-
while (cursor < source.length && depth > 0) {
|
|
56
|
-
const char = source.charAt(cursor)
|
|
57
|
-
if (char === '"' || char === "'" || char === '`') {
|
|
58
|
-
cursor += 1
|
|
59
|
-
while (cursor < source.length && source.charAt(cursor) !== char) {
|
|
60
|
-
if (source.charAt(cursor) === '\\') {
|
|
61
|
-
cursor += 1
|
|
62
|
-
}
|
|
63
|
-
cursor += 1
|
|
64
|
-
}
|
|
65
|
-
} else if (char === '{') {
|
|
66
|
-
depth += 1
|
|
67
|
-
} else if (char === '}') {
|
|
68
|
-
depth -= 1
|
|
69
|
-
}
|
|
70
|
-
cursor += 1
|
|
71
|
-
}
|
|
72
|
-
const raw = source.slice(start, cursor - 1)
|
|
73
|
-
const leading = raw.length - raw.trimStart().length
|
|
74
|
-
return { code: raw.trim(), loc: baseOffset + start + leading }
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/* Skips an HTML comment starting at `cursor` (on `<!--`), advancing past its
|
|
78
|
-
`-->`; an unterminated comment runs to the end of source. Emits no node. */
|
|
79
|
-
function skipComment(): void {
|
|
80
|
-
const close = source.indexOf('-->', cursor)
|
|
81
|
-
cursor = close === -1 ? source.length : close + '-->'.length
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/* True when `cursor` is on a `<style>` open tag — read raw (`readStyle`) so its
|
|
85
|
-
CSS braces never misparse as `{expr}` interpolations. */
|
|
86
|
-
function atStyleTag(): boolean {
|
|
87
|
-
return (
|
|
88
|
-
source.startsWith('<style', cursor) && WHITESPACE_OR_GT.test(source.charAt(cursor + 6))
|
|
89
|
-
)
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/* Reads a `<style>…</style>` block into a `style` node carrying its CSS body; an
|
|
93
|
-
unterminated block runs to end. The body is read raw (not parsed as markup) so
|
|
94
|
-
CSS braces never misparse as `{expr}`. */
|
|
95
|
-
function readStyle(): TemplateNode {
|
|
96
|
-
const openEnd = source.indexOf('>', cursor)
|
|
97
|
-
const close = source.indexOf('</style>', cursor)
|
|
98
|
-
const css =
|
|
99
|
-
openEnd !== -1 && (close === -1 || openEnd < close)
|
|
100
|
-
? source.slice(openEnd + 1, close === -1 ? source.length : close).trim()
|
|
101
|
-
: ''
|
|
102
|
-
cursor = close === -1 ? source.length : close + '</style>'.length
|
|
103
|
-
return { kind: 'style', css }
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/* True when the cursor sits on a block-directive open `{#`. A `{:`/`{/` is only
|
|
107
|
-
valid INSIDE a block (consumed by readBlockChildren), so at top level it would be
|
|
108
|
-
a stray-branch error — handled where blocks are read, not here. */
|
|
109
|
-
function atBlock(): boolean {
|
|
110
|
-
return source.charAt(cursor) === '{' && source.charAt(cursor + 1) === '#'
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/* `{children()}` is the slot fill point — the content a parent passed (a component)
|
|
114
|
-
or the route chain below (a layout). It parses to the SAME node the retired `<slot>`
|
|
115
|
-
element produced, so every downstream helper that branches on `tag === 'slot'` is
|
|
116
|
-
unchanged. */
|
|
117
|
-
const CHILDREN_CALL = /^\{\s*children\s*\(\s*\)\s*\}/
|
|
118
|
-
function atChildrenCall(): boolean {
|
|
119
|
-
return source.charAt(cursor) === '{' && CHILDREN_CALL.test(source.slice(cursor))
|
|
120
|
-
}
|
|
121
|
-
function readChildrenCall(): TemplateNode {
|
|
122
|
-
cursor = source.indexOf('}', cursor) + 1 // consume `{children()}`
|
|
123
|
-
return { kind: 'element', tag: 'slot', attrs: [], children: [] }
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/* Reads a `{#…}` / `{:…}` / `{/…}` token starting on `{`. Tracks string literals
|
|
127
|
-
and nested braces (same scan as readBracedExpression) so a brace/quote inside an
|
|
128
|
-
expression (`{#if obj.has('}')}`) doesn't end the token early. Returns the sigil,
|
|
129
|
-
the trimmed directive body WITHOUT the sigil, and the absolute loc of the body's
|
|
130
|
-
first post-trim char. */
|
|
131
|
-
function readBlockToken(): { sigil: '#' | ':' | '/'; body: string; loc: number } {
|
|
132
|
-
const sigil = source.charAt(cursor + 1) as '#' | ':' | '/'
|
|
133
|
-
cursor += 2 // past `{` and the sigil
|
|
134
|
-
const start = cursor
|
|
135
|
-
let depth = 1
|
|
136
|
-
while (cursor < source.length && depth > 0) {
|
|
137
|
-
const char = source.charAt(cursor)
|
|
138
|
-
if (char === '"' || char === "'" || char === '`') {
|
|
139
|
-
cursor += 1
|
|
140
|
-
while (cursor < source.length && source.charAt(cursor) !== char) {
|
|
141
|
-
if (source.charAt(cursor) === '\\') {
|
|
142
|
-
cursor += 1
|
|
143
|
-
}
|
|
144
|
-
cursor += 1
|
|
145
|
-
}
|
|
146
|
-
} else if (char === '{') {
|
|
147
|
-
depth += 1
|
|
148
|
-
} else if (char === '}') {
|
|
149
|
-
depth -= 1
|
|
150
|
-
}
|
|
151
|
-
cursor += 1
|
|
152
|
-
}
|
|
153
|
-
const raw = source.slice(start, cursor - 1)
|
|
154
|
-
const leading = raw.length - raw.trimStart().length
|
|
155
|
-
return { sigil, body: raw.trim(), loc: baseOffset + start + leading }
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/* The leading keyword of a directive body (`if`, `for`, `await`, `switch`, `try`,
|
|
159
|
-
`else`, `then`, `catch`, `finally`, `case`, `default`). */
|
|
160
|
-
function headKeyword(body: string): string {
|
|
161
|
-
const match = body.match(LEADING_KEYWORD)
|
|
162
|
-
return match?.[1] ?? ''
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/* Reads a `{#…}` control block: the open token, its children up to a continuation
|
|
166
|
-
`{:…}` (a branch) or close `{/…}`, recursing. Emits `if`/`each`/`await`/`switch`/`try`
|
|
167
|
-
nodes with their `case`/`branch` children — the same TemplateNode kinds the back-ends
|
|
168
|
-
and `skeletonContext` consume. */
|
|
169
|
-
function readBlock(): TemplateNode {
|
|
170
|
-
const open = readBlockToken() // sigil is '#'
|
|
171
|
-
const keyword = headKeyword(open.body)
|
|
172
|
-
if (keyword === 'if') {
|
|
173
|
-
const start = open.body.indexOf('if') + 2
|
|
174
|
-
const condition = open.body.slice(start).trim()
|
|
175
|
-
if (condition === '') {
|
|
176
|
-
throw new Error('[abide] {#if} requires a condition expression')
|
|
177
|
-
}
|
|
178
|
-
const children = readBlockChildren('if')
|
|
179
|
-
return { kind: 'if', condition, children, loc: exprLoc(open.loc, open.body, start) }
|
|
180
|
-
}
|
|
181
|
-
if (keyword === 'for') {
|
|
182
|
-
const head = parseForHead(open.body, open.loc)
|
|
183
|
-
const children = readBlockChildren('for')
|
|
184
|
-
return {
|
|
185
|
-
kind: 'each',
|
|
186
|
-
items: head.items,
|
|
187
|
-
as: head.as,
|
|
188
|
-
index: head.index,
|
|
189
|
-
key: head.key,
|
|
190
|
-
async: head.async,
|
|
191
|
-
children,
|
|
192
|
-
loc: head.loc,
|
|
193
|
-
asLoc: head.asLoc,
|
|
194
|
-
keyLoc: head.keyLoc,
|
|
195
|
-
indexLoc: head.indexLoc,
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
if (keyword === 'await') {
|
|
199
|
-
const start = open.body.indexOf('await') + 5
|
|
200
|
-
const afterAwait = open.body.slice(start)
|
|
201
|
-
const thenAt = keywordAtDepthZero(afterAwait, 'then')
|
|
202
|
-
const promise = (thenAt === -1 ? afterAwait : afterAwait.slice(0, thenAt)).trim()
|
|
203
|
-
if (promise === '') {
|
|
204
|
-
throw new Error('[abide] {#await} requires a promise expression')
|
|
205
|
-
}
|
|
206
|
-
const as =
|
|
207
|
-
thenAt === -1
|
|
208
|
-
? undefined
|
|
209
|
-
: afterAwait.slice(thenAt + 'then'.length).trim() || undefined
|
|
210
|
-
const children = readBlockChildren('await')
|
|
211
|
-
return {
|
|
212
|
-
kind: 'await',
|
|
213
|
-
promise,
|
|
214
|
-
blocking: thenAt !== -1,
|
|
215
|
-
as,
|
|
216
|
-
children,
|
|
217
|
-
loc: exprLoc(open.loc, open.body, start),
|
|
218
|
-
asLoc:
|
|
219
|
-
as === undefined
|
|
220
|
-
? undefined
|
|
221
|
-
: exprLoc(open.loc, open.body, start + thenAt + 'then'.length),
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
if (keyword === 'switch') {
|
|
225
|
-
const start = open.body.indexOf('switch') + 6
|
|
226
|
-
const subject = open.body.slice(start).trim()
|
|
227
|
-
if (subject === '') {
|
|
228
|
-
throw new Error('[abide] {#switch} requires a subject expression')
|
|
229
|
-
}
|
|
230
|
-
const children = readBlockChildren('switch')
|
|
231
|
-
return { kind: 'switch', subject, children, loc: exprLoc(open.loc, open.body, start) }
|
|
232
|
-
}
|
|
233
|
-
if (keyword === 'try') {
|
|
234
|
-
const children = readBlockChildren('try')
|
|
235
|
-
return { kind: 'try', children }
|
|
236
|
-
}
|
|
237
|
-
if (keyword === 'snippet') {
|
|
238
|
-
const head = open.body.slice(open.body.indexOf('snippet') + 'snippet'.length).trim()
|
|
239
|
-
const parenAt = head.indexOf('(')
|
|
240
|
-
const name = (parenAt === -1 ? head : head.slice(0, parenAt)).trim()
|
|
241
|
-
if (name === '') {
|
|
242
|
-
throw new Error('[abide] {#snippet} requires a name, e.g. {#snippet row(item)}')
|
|
243
|
-
}
|
|
244
|
-
/* Params ride the parens: `{#snippet row({ item })}` → `{ item }`. */
|
|
245
|
-
const params =
|
|
246
|
-
parenAt === -1
|
|
247
|
-
? undefined
|
|
248
|
-
: head.slice(parenAt + 1, head.lastIndexOf(')')).trim() || undefined
|
|
249
|
-
const children = readBlockChildren('snippet')
|
|
250
|
-
return {
|
|
251
|
-
kind: 'snippet',
|
|
252
|
-
name,
|
|
253
|
-
params,
|
|
254
|
-
children,
|
|
255
|
-
loc:
|
|
256
|
-
parenAt === -1
|
|
257
|
-
? undefined
|
|
258
|
-
: exprLoc(open.loc, open.body, open.body.indexOf('(') + 1),
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
throw new Error(
|
|
262
|
-
`[abide] unknown control block {#${keyword}} — expected if/for/await/switch/try/snippet`,
|
|
263
|
-
)
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/* Shared scan body for block/branch child loops. Reads one node at the current
|
|
267
|
-
cursor position and pushes it onto `nodes`. Returns false when the caller's
|
|
268
|
-
terminator fires (caller decides what to do next); returns true while scanning
|
|
269
|
-
continues. `onBranch` fires when `{:` is seen — block-children consume it as a
|
|
270
|
-
new branch node; branch-children let the caller exit instead. */
|
|
271
|
-
function scanNode(nodes: TemplateNode[], onBranch: (() => boolean) | undefined): boolean {
|
|
272
|
-
/* Branch/close tokens — delegate to the caller's terminator logic. */
|
|
273
|
-
if (source.startsWith('{/', cursor)) {
|
|
274
|
-
return false
|
|
275
|
-
}
|
|
276
|
-
if (source.startsWith('{:', cursor)) {
|
|
277
|
-
return onBranch === undefined ? false : onBranch()
|
|
278
|
-
}
|
|
279
|
-
if (atBlock()) {
|
|
280
|
-
nodes.push(readBlock())
|
|
281
|
-
} else if (atChildrenCall()) {
|
|
282
|
-
nodes.push(readChildrenCall())
|
|
283
|
-
} else if (source.startsWith('<!--', cursor)) {
|
|
284
|
-
skipComment()
|
|
285
|
-
} else if (atStyleTag()) {
|
|
286
|
-
nodes.push(readStyle())
|
|
287
|
-
} else if (source.charAt(cursor) === '<') {
|
|
288
|
-
nodes.push(readElement())
|
|
289
|
-
} else {
|
|
290
|
-
nodes.push(readText())
|
|
291
|
-
}
|
|
292
|
-
return true
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/* Reads children of a block until its close `{/<keyword>}`. A continuation token
|
|
296
|
-
`{:…}` ends the current branch's children and starts a new `case`/`branch` node
|
|
297
|
-
(per construct). The leading children (before the first `{:…}`) are the block's
|
|
298
|
-
own children (the `if`/`await`/`try` then-content). Returns the full children list
|
|
299
|
-
INCLUDING the case/branch nodes, producing the same TemplateNode tree the back-ends consume. */
|
|
300
|
-
function readBlockChildren(keyword: string): TemplateNode[] {
|
|
301
|
-
const nodes: TemplateNode[] = []
|
|
302
|
-
while (cursor < source.length) {
|
|
303
|
-
const keepGoing = scanNode(nodes, () => {
|
|
304
|
-
nodes.push(readBranch(keyword))
|
|
305
|
-
return true
|
|
306
|
-
})
|
|
307
|
-
if (!keepGoing) {
|
|
308
|
-
const close = readBlockToken() // consume the close `{/keyword}`
|
|
309
|
-
const closeKeyword = headKeyword(close.body)
|
|
310
|
-
/* The close must name its open block — a mismatch (`{#if}…{/for}`) or
|
|
311
|
-
crossed nesting (`{#if}{#for}…{/if}{/for}`) would otherwise silently
|
|
312
|
-
mis-parse into a structurally wrong tree. */
|
|
313
|
-
if (closeKeyword !== keyword) {
|
|
314
|
-
throw new Error(
|
|
315
|
-
`[abide] {/${closeKeyword}} does not close the open {#${keyword}} — expected {/${keyword}}`,
|
|
316
|
-
)
|
|
317
|
-
}
|
|
318
|
-
return nodes
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
throw new Error(`[abide] unterminated {#${keyword}} block — missing {/${keyword}}`)
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
/* Reads a continuation token `{:…}` and the children up to the NEXT continuation or
|
|
325
|
-
close, returning the branch node for the parent construct. Handles `if`-chain
|
|
326
|
-
branches (else / else if) and `for await` catch branches. */
|
|
327
|
-
function readBranch(parentKeyword: string): TemplateNode {
|
|
328
|
-
const token = readBlockToken() // sigil ':'
|
|
329
|
-
const keyword = headKeyword(token.body)
|
|
330
|
-
const branchChildren = readBranchChildren()
|
|
331
|
-
if (parentKeyword === 'if') {
|
|
332
|
-
if (keyword === 'else' && headKeyword(token.body.slice(4).trim()) === 'if') {
|
|
333
|
-
const start = token.body.indexOf('if') + 2
|
|
334
|
-
const condition = token.body.slice(start).trim()
|
|
335
|
-
if (condition === '') {
|
|
336
|
-
throw new Error('[abide] {:else if} requires a condition expression')
|
|
337
|
-
}
|
|
338
|
-
return {
|
|
339
|
-
kind: 'case',
|
|
340
|
-
match: undefined,
|
|
341
|
-
condition,
|
|
342
|
-
children: branchChildren,
|
|
343
|
-
loc: exprLoc(token.loc, token.body, start),
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
if (keyword === 'else') {
|
|
347
|
-
return { kind: 'case', match: undefined, children: branchChildren }
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
if (parentKeyword === 'for' && keyword === 'catch') {
|
|
351
|
-
const as = token.body.slice(token.body.indexOf('catch') + 5).trim() || undefined
|
|
352
|
-
return {
|
|
353
|
-
kind: 'branch',
|
|
354
|
-
branch: 'catch',
|
|
355
|
-
as,
|
|
356
|
-
children: branchChildren,
|
|
357
|
-
asLoc:
|
|
358
|
-
as === undefined
|
|
359
|
-
? undefined
|
|
360
|
-
: exprLoc(token.loc, token.body, token.body.indexOf('catch') + 5),
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
if (parentKeyword === 'switch') {
|
|
364
|
-
if (keyword === 'case') {
|
|
365
|
-
const start = token.body.indexOf('case') + 4
|
|
366
|
-
const match = token.body.slice(start).trim()
|
|
367
|
-
if (match === '') {
|
|
368
|
-
throw new Error('[abide] {:case} requires a value expression')
|
|
369
|
-
}
|
|
370
|
-
return {
|
|
371
|
-
kind: 'case',
|
|
372
|
-
match,
|
|
373
|
-
children: branchChildren,
|
|
374
|
-
loc: exprLoc(token.loc, token.body, start),
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
if (keyword === 'default') {
|
|
378
|
-
return { kind: 'case', match: undefined, children: branchChildren }
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
if (parentKeyword === 'await') {
|
|
382
|
-
if (keyword === 'then') {
|
|
383
|
-
const as = token.body.slice(token.body.indexOf('then') + 4).trim() || undefined
|
|
384
|
-
return {
|
|
385
|
-
kind: 'branch',
|
|
386
|
-
branch: 'then',
|
|
387
|
-
as,
|
|
388
|
-
children: branchChildren,
|
|
389
|
-
asLoc:
|
|
390
|
-
as === undefined
|
|
391
|
-
? undefined
|
|
392
|
-
: exprLoc(token.loc, token.body, token.body.indexOf('then') + 4),
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
if (keyword === 'catch') {
|
|
396
|
-
const as = token.body.slice(token.body.indexOf('catch') + 5).trim() || undefined
|
|
397
|
-
return {
|
|
398
|
-
kind: 'branch',
|
|
399
|
-
branch: 'catch',
|
|
400
|
-
as,
|
|
401
|
-
children: branchChildren,
|
|
402
|
-
asLoc:
|
|
403
|
-
as === undefined
|
|
404
|
-
? undefined
|
|
405
|
-
: exprLoc(token.loc, token.body, token.body.indexOf('catch') + 5),
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
if (keyword === 'finally') {
|
|
409
|
-
return {
|
|
410
|
-
kind: 'branch',
|
|
411
|
-
branch: 'finally',
|
|
412
|
-
as: undefined,
|
|
413
|
-
children: branchChildren,
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
if (parentKeyword === 'try') {
|
|
418
|
-
if (keyword === 'catch') {
|
|
419
|
-
const as = token.body.slice(token.body.indexOf('catch') + 5).trim() || undefined
|
|
420
|
-
return {
|
|
421
|
-
kind: 'branch',
|
|
422
|
-
branch: 'catch',
|
|
423
|
-
as,
|
|
424
|
-
children: branchChildren,
|
|
425
|
-
asLoc:
|
|
426
|
-
as === undefined
|
|
427
|
-
? undefined
|
|
428
|
-
: exprLoc(token.loc, token.body, token.body.indexOf('catch') + 5),
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
if (keyword === 'finally') {
|
|
432
|
-
return {
|
|
433
|
-
kind: 'branch',
|
|
434
|
-
branch: 'finally',
|
|
435
|
-
as: undefined,
|
|
436
|
-
children: branchChildren,
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
throw new Error(`[abide] {:${keyword}} is not valid inside {#${parentKeyword}}`)
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
/* Children of a branch: read until the next `{:…}` or `{/…}` WITHOUT consuming it
|
|
444
|
-
(the caller's readBlockChildren loop handles those). */
|
|
445
|
-
function readBranchChildren(): TemplateNode[] {
|
|
446
|
-
const nodes: TemplateNode[] = []
|
|
447
|
-
/* Pass undefined for onBranch so scanNode returns false on `{:`, leaving it unconsumed. */
|
|
448
|
-
while (cursor < source.length && scanNode(nodes, undefined)) {
|
|
449
|
-
// continue
|
|
450
|
-
}
|
|
451
|
-
return nodes
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
function readText(): TemplateNode {
|
|
455
|
-
const parts: TextPart[] = []
|
|
456
|
-
let literal = ''
|
|
457
|
-
while (cursor < source.length && source.charAt(cursor) !== '<') {
|
|
458
|
-
if (source.charAt(cursor) === '{') {
|
|
459
|
-
if (atChildrenCall()) {
|
|
460
|
-
break // a slot fill point — handled by the enclosing scan loop
|
|
461
|
-
}
|
|
462
|
-
const next = source.charAt(cursor + 1)
|
|
463
|
-
if (next === '#' || next === ':' || next === '/') {
|
|
464
|
-
break // a block/continuation/close token — not interpolation
|
|
465
|
-
}
|
|
466
|
-
if (literal !== '') {
|
|
467
|
-
parts.push({ kind: 'static', value: decodeHtmlEntities(literal) })
|
|
468
|
-
literal = ''
|
|
469
|
-
}
|
|
470
|
-
const { code, loc } = readBracedExpression()
|
|
471
|
-
parts.push({ kind: 'expression', code, loc })
|
|
472
|
-
} else {
|
|
473
|
-
literal += source.charAt(cursor)
|
|
474
|
-
cursor += 1
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
if (literal !== '') {
|
|
478
|
-
parts.push({ kind: 'static', value: decodeHtmlEntities(literal) })
|
|
479
|
-
}
|
|
480
|
-
return { kind: 'text', parts }
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
function readAttributes(): TemplateAttr[] {
|
|
484
|
-
const attrs: TemplateAttr[] = []
|
|
485
|
-
while (cursor < source.length) {
|
|
486
|
-
while (WHITESPACE.test(source.charAt(cursor))) {
|
|
487
|
-
cursor += 1
|
|
488
|
-
}
|
|
489
|
-
const char = source.charAt(cursor)
|
|
490
|
-
if (char === '>' || char === '/' || char === undefined) {
|
|
491
|
-
break
|
|
492
|
-
}
|
|
493
|
-
/* `{...expr}` standing where an attribute name would — a spread of an object's
|
|
494
|
-
keys onto the tag: props on a component, attributes on a native element. Only
|
|
495
|
-
a `<template>` directive rejects it (see `readElement`). */
|
|
496
|
-
if (char === '{') {
|
|
497
|
-
const { code, loc } = readBracedExpression()
|
|
498
|
-
if (!code.startsWith('...')) {
|
|
499
|
-
throw new Error(
|
|
500
|
-
`[abide] a bare {expr} is not a valid attribute — write {...expr} to spread an object's keys as props`,
|
|
501
|
-
)
|
|
502
|
-
}
|
|
503
|
-
/* Advance `loc` past `...` and any whitespace so it points at the spread
|
|
504
|
-
EXPRESSION, not the dots — the shadow source-map invariant (source text at
|
|
505
|
-
`loc` equals the emitted code) requires `loc` and `code` to align. */
|
|
506
|
-
const inner = code.slice(3)
|
|
507
|
-
const leading = inner.length - inner.trimStart().length
|
|
508
|
-
attrs.push({ kind: 'spread', code: inner.trim(), loc: loc + 3 + leading })
|
|
509
|
-
continue
|
|
510
|
-
}
|
|
511
|
-
let name = ''
|
|
512
|
-
const nameLoc = baseOffset + cursor // absolute offset of the attribute name
|
|
513
|
-
while (cursor < source.length && !ATTR_NAME_END.test(source.charAt(cursor))) {
|
|
514
|
-
name += source.charAt(cursor)
|
|
515
|
-
cursor += 1
|
|
516
|
-
}
|
|
517
|
-
while (WHITESPACE.test(source.charAt(cursor))) {
|
|
518
|
-
cursor += 1
|
|
519
|
-
}
|
|
520
|
-
if (source.charAt(cursor) !== '=') {
|
|
521
|
-
attrs.push({ kind: 'static', name, value: '', bare: true, nameLoc }) // boolean attribute
|
|
522
|
-
continue
|
|
523
|
-
}
|
|
524
|
-
cursor += 1 // past '='
|
|
525
|
-
while (WHITESPACE.test(source.charAt(cursor))) {
|
|
526
|
-
cursor += 1
|
|
527
|
-
}
|
|
528
|
-
if (source.charAt(cursor) === '{') {
|
|
529
|
-
const { code, loc } = readBracedExpression()
|
|
530
|
-
if (name.startsWith('on')) {
|
|
531
|
-
attrs.push({ kind: 'event', event: name.slice(2), code, loc })
|
|
532
|
-
} else if (name.startsWith('bind:')) {
|
|
533
|
-
attrs.push({ kind: 'bind', property: name.slice(5), code, loc })
|
|
534
|
-
} else if (name.startsWith('class:')) {
|
|
535
|
-
attrs.push({ kind: 'class', name: name.slice(6), code, loc })
|
|
536
|
-
} else if (name.startsWith('style:')) {
|
|
537
|
-
attrs.push({ kind: 'style', property: name.slice(6), code, loc })
|
|
538
|
-
} else if (name === 'attach') {
|
|
539
|
-
attrs.push({ kind: 'attach', code, loc })
|
|
540
|
-
} else {
|
|
541
|
-
attrs.push({ kind: 'expression', name, code, loc, nameLoc })
|
|
542
|
-
}
|
|
543
|
-
} else if (source.charAt(cursor) === '"' || source.charAt(cursor) === "'") {
|
|
544
|
-
const quote = source.charAt(cursor)
|
|
545
|
-
cursor += 1
|
|
546
|
-
/* Scan the quoted span into static/`{expr}` parts (the text-node model),
|
|
547
|
-
reusing the brace/quote-aware expression reader so a `}` inside an
|
|
548
|
-
expression doesn't close the value early. */
|
|
549
|
-
const parts: TextPart[] = []
|
|
550
|
-
let literal = ''
|
|
551
|
-
while (cursor < source.length && source.charAt(cursor) !== quote) {
|
|
552
|
-
if (source.charAt(cursor) === '{') {
|
|
553
|
-
if (literal !== '') {
|
|
554
|
-
parts.push({ kind: 'static', value: decodeHtmlEntities(literal) })
|
|
555
|
-
literal = ''
|
|
556
|
-
}
|
|
557
|
-
const { code, loc } = readBracedExpression()
|
|
558
|
-
parts.push({ kind: 'expression', code, loc })
|
|
559
|
-
} else {
|
|
560
|
-
literal += source.charAt(cursor)
|
|
561
|
-
cursor += 1
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
cursor += 1 // past closing quote
|
|
565
|
-
if (literal !== '') {
|
|
566
|
-
parts.push({ kind: 'static', value: decodeHtmlEntities(literal) })
|
|
567
|
-
}
|
|
568
|
-
/* No `{expr}` → a plain static attribute; its literal is entity-decoded
|
|
569
|
-
like a text node (SSR/clone re-escape it), so a value such as `&`
|
|
570
|
-
round-trips instead of double-escaping. Otherwise an interpolated value. */
|
|
571
|
-
if (parts.every((part) => part.kind === 'static')) {
|
|
572
|
-
const value = parts
|
|
573
|
-
.map((part) => (part.kind === 'static' ? part.value : ''))
|
|
574
|
-
.join('')
|
|
575
|
-
attrs.push({ kind: 'static', name, value, nameLoc })
|
|
576
|
-
} else {
|
|
577
|
-
attrs.push({ kind: 'interpolated', name, parts, nameLoc })
|
|
578
|
-
}
|
|
579
|
-
} else {
|
|
580
|
-
/* Unquoted value (`<input type=text>`): runs to the next whitespace or
|
|
581
|
-
`>`, per the HTML unquoted-attribute rule. No delimiter to consume. */
|
|
582
|
-
let value = ''
|
|
583
|
-
while (cursor < source.length && !WHITESPACE_OR_GT.test(source.charAt(cursor))) {
|
|
584
|
-
/* Stop before a `/` that closes the tag (`<Comp x=y/>`) so the value
|
|
585
|
-
doesn't swallow the self-closing slash and defeat detection; a `/`
|
|
586
|
-
elsewhere (e.g. a URL `href=/a/b`) stays part of the value. */
|
|
587
|
-
if (source.charAt(cursor) === '/' && source.charAt(cursor + 1) === '>') {
|
|
588
|
-
break
|
|
589
|
-
}
|
|
590
|
-
value += source.charAt(cursor)
|
|
591
|
-
cursor += 1
|
|
592
|
-
}
|
|
593
|
-
attrs.push({ kind: 'static', name, value })
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
return attrs
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
function readElement(): TemplateNode {
|
|
600
|
-
cursor += 1 // past '<'
|
|
601
|
-
let tag = ''
|
|
602
|
-
const tagStart = cursor // absolute (baseOffset-relative) offset of the tag name
|
|
603
|
-
while (cursor < source.length && !TAG_NAME_END.test(source.charAt(cursor))) {
|
|
604
|
-
tag += source.charAt(cursor)
|
|
605
|
-
cursor += 1
|
|
606
|
-
}
|
|
607
|
-
const attrs = readAttributes()
|
|
608
|
-
let selfClosing = false
|
|
609
|
-
if (source.charAt(cursor) === '/') {
|
|
610
|
-
selfClosing = true
|
|
611
|
-
cursor += 1
|
|
612
|
-
}
|
|
613
|
-
cursor += 1 // past '>'
|
|
614
|
-
/* A nested `<script>` is a scoped reactive block: its body is raw JS read
|
|
615
|
-
verbatim to its `</script>` (not parsed as markup), scoped by the
|
|
616
|
-
containing branch when compiled. */
|
|
617
|
-
if (tag === 'script' && !selfClosing) {
|
|
618
|
-
/* Body starts at the current cursor; its absolute source offset lets the shadow
|
|
619
|
-
map a diagnostic raised inside it (e.g. a top-level await) back to source. */
|
|
620
|
-
const bodyLoc = baseOffset + cursor
|
|
621
|
-
const close = source.indexOf('</script>', cursor)
|
|
622
|
-
const end = close === -1 ? source.length : close
|
|
623
|
-
const code = source.slice(cursor, end)
|
|
624
|
-
cursor = close === -1 ? source.length : end + '</script>'.length
|
|
625
|
-
/* A static `import` can't live here: a nested script compiles INTO the
|
|
626
|
-
branch's render-function body, where an import is illegal — and an
|
|
627
|
-
import nested in a branch falsely implies conditional/lazy loading ES
|
|
628
|
-
imports can't do (they hoist module-wide and load unconditionally). The
|
|
629
|
-
leading `<script>` hoists imports to module scope for the whole template,
|
|
630
|
-
so they belong there. The pattern spares dynamic `import(...)` (with or
|
|
631
|
-
without whitespace) and `import.meta` — the real lazy paths. */
|
|
632
|
-
if (NESTED_STATIC_IMPORT.test(code)) {
|
|
633
|
-
throw new Error(
|
|
634
|
-
"import statements must live in the component's leading <script>, not a nested <template> script — they hoist to module scope for the whole template. For lazy loading, use a dynamic import() inside an effect.",
|
|
635
|
-
)
|
|
636
|
-
}
|
|
637
|
-
return { kind: 'script', code, loc: bodyLoc }
|
|
638
|
-
}
|
|
639
|
-
/* A capitalised tag is a child component; its attributes become props and
|
|
640
|
-
its children become slot content (rendered where the child puts <slot>). */
|
|
641
|
-
if (UPPERCASE_START.test(tag)) {
|
|
642
|
-
const slotted = selfClosing ? [] : readChildren(tag)
|
|
643
|
-
return {
|
|
644
|
-
kind: 'component',
|
|
645
|
-
name: tag,
|
|
646
|
-
loc: baseOffset + tagStart,
|
|
647
|
-
props: toProps(attrs),
|
|
648
|
-
children: slotted,
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
/* `{...expr}` spreads onto a component (its props) or a native element (its
|
|
652
|
-
attributes), but a `<template>` directive has no such bag — reject it there. */
|
|
653
|
-
if (tag === 'template' && attrs.some((attr) => attr.kind === 'spread')) {
|
|
654
|
-
throw new Error('[abide] {...expr} spread is not supported on a <template> directive')
|
|
655
|
-
}
|
|
656
|
-
const children = selfClosing || VOID_TAGS.has(tag) ? [] : readChildren(tag)
|
|
657
|
-
if (tag === 'slot') {
|
|
658
|
-
throw new Error(
|
|
659
|
-
'[abide] the <slot> element was removed — render passed content with {children()} (with {#if children}{children()}{:else}…{/if} for a fallback)',
|
|
660
|
-
)
|
|
661
|
-
}
|
|
662
|
-
if (tag === 'template') {
|
|
663
|
-
return toSnippetOrTemplate(attrs, children)
|
|
664
|
-
}
|
|
665
|
-
return { kind: 'element', tag, attrs, children }
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
/* A `{:…}`/`{/…}` reached OUTSIDE a block (the root scan or an element's children —
|
|
669
|
-
not readBlockChildren/readBranchChildren, which consume their own) is a continuation
|
|
670
|
-
or close with no open `{#…}`. Surface it as an error: readText breaks on `{:`/`{/`
|
|
671
|
-
without advancing, so falling through would loop forever. */
|
|
672
|
-
function throwIfStrayBranch(): void {
|
|
673
|
-
if (source.startsWith('{:', cursor) || source.startsWith('{/', cursor)) {
|
|
674
|
-
const end = source.indexOf('}', cursor)
|
|
675
|
-
const token = source.slice(cursor, end === -1 ? source.length : end + 1)
|
|
676
|
-
throw new Error(`[abide] ${token} has no open {#…} block`)
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
function readChildren(closeTag: string): TemplateNode[] {
|
|
681
|
-
const nodes: TemplateNode[] = []
|
|
682
|
-
while (cursor < source.length) {
|
|
683
|
-
if (source.startsWith(`</${closeTag}`, cursor)) {
|
|
684
|
-
cursor = source.indexOf('>', cursor) + 1
|
|
685
|
-
break
|
|
686
|
-
}
|
|
687
|
-
throwIfStrayBranch()
|
|
688
|
-
if (source.startsWith('<!--', cursor)) {
|
|
689
|
-
skipComment()
|
|
690
|
-
} else if (atStyleTag()) {
|
|
691
|
-
nodes.push(readStyle())
|
|
692
|
-
} else if (atBlock()) {
|
|
693
|
-
nodes.push(readBlock())
|
|
694
|
-
} else if (atChildrenCall()) {
|
|
695
|
-
nodes.push(readChildrenCall())
|
|
696
|
-
} else if (source.charAt(cursor) === '<') {
|
|
697
|
-
nodes.push(readElement())
|
|
698
|
-
} else {
|
|
699
|
-
nodes.push(readText())
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
return nodes
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
const roots: TemplateNode[] = []
|
|
706
|
-
while (cursor < source.length) {
|
|
707
|
-
throwIfStrayBranch()
|
|
708
|
-
if (source.startsWith('<!--', cursor)) {
|
|
709
|
-
skipComment()
|
|
710
|
-
} else if (atStyleTag()) {
|
|
711
|
-
roots.push(readStyle())
|
|
712
|
-
} else if (atBlock()) {
|
|
713
|
-
roots.push(readBlock())
|
|
714
|
-
} else if (atChildrenCall()) {
|
|
715
|
-
roots.push(readChildrenCall())
|
|
716
|
-
} else if (source.charAt(cursor) === '<') {
|
|
717
|
-
roots.push(readElement())
|
|
718
|
-
} else {
|
|
719
|
-
roots.push(readText())
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
rejectStrayBranches(roots, undefined)
|
|
723
|
-
return { nodes: roots }
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
/* Finds the index of a ` <token> ` keyword (` of `, ` by `) at brace/paren/bracket
|
|
727
|
-
depth 0, scanning left to right, skipping string literals. Returns -1 if absent. */
|
|
728
|
-
function indexOfKeywordAtDepthZero(text: string, keyword: string): number {
|
|
729
|
-
let depth = 0
|
|
730
|
-
let i = 0
|
|
731
|
-
while (i < text.length) {
|
|
732
|
-
const char = text.charAt(i)
|
|
733
|
-
if (char === '"' || char === "'" || char === '`') {
|
|
734
|
-
i += 1
|
|
735
|
-
while (i < text.length && text.charAt(i) !== char) {
|
|
736
|
-
if (text.charAt(i) === '\\') {
|
|
737
|
-
i += 1
|
|
738
|
-
}
|
|
739
|
-
i += 1
|
|
740
|
-
}
|
|
741
|
-
} else if (char === '{' || char === '(' || char === '[') {
|
|
742
|
-
depth += 1
|
|
743
|
-
} else if (char === '}' || char === ')' || char === ']') {
|
|
744
|
-
depth -= 1
|
|
745
|
-
} else if (depth === 0 && text.startsWith(keyword, i)) {
|
|
746
|
-
return i
|
|
747
|
-
}
|
|
748
|
-
i += 1
|
|
749
|
-
}
|
|
750
|
-
return -1
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
/* Index of a bare keyword (`then`) at brace/paren/bracket depth 0, requiring whitespace
|
|
754
|
-
on both sides — so it isn't matched inside an identifier (`q.then(x)`) and MAY sit
|
|
755
|
-
terminally. Unlike ` of `/` by `, an await `then` can end the head: `{#await p then}`
|
|
756
|
-
is a blocking await with no value binding, which a trailing-space match would miss
|
|
757
|
-
(folding `then` into the promise). Skips string literals. -1 if absent. */
|
|
758
|
-
function keywordAtDepthZero(text: string, word: string): number {
|
|
759
|
-
let depth = 0
|
|
760
|
-
let i = 0
|
|
761
|
-
while (i < text.length) {
|
|
762
|
-
const char = text.charAt(i)
|
|
763
|
-
if (char === '"' || char === "'" || char === '`') {
|
|
764
|
-
i += 1
|
|
765
|
-
while (i < text.length && text.charAt(i) !== char) {
|
|
766
|
-
if (text.charAt(i) === '\\') {
|
|
767
|
-
i += 1
|
|
768
|
-
}
|
|
769
|
-
i += 1
|
|
770
|
-
}
|
|
771
|
-
} else if (char === '{' || char === '(' || char === '[') {
|
|
772
|
-
depth += 1
|
|
773
|
-
} else if (char === '}' || char === ')' || char === ']') {
|
|
774
|
-
depth -= 1
|
|
775
|
-
} else if (
|
|
776
|
-
depth === 0 &&
|
|
777
|
-
text.startsWith(word, i) &&
|
|
778
|
-
i > 0 &&
|
|
779
|
-
WHITESPACE.test(text.charAt(i - 1)) &&
|
|
780
|
-
(i + word.length === text.length || WHITESPACE.test(text.charAt(i + word.length)))
|
|
781
|
-
) {
|
|
782
|
-
return i
|
|
783
|
-
}
|
|
784
|
-
i += 1
|
|
785
|
-
}
|
|
786
|
-
return -1
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
/* The depth-0 comma index in a binding (`{id, title}, i` → the comma after `}`),
|
|
790
|
-
so a destructuring pattern's inner commas don't split the binding from its index. */
|
|
791
|
-
function bindingCommaAtDepthZero(text: string): number {
|
|
792
|
-
let depth = 0
|
|
793
|
-
let i = 0
|
|
794
|
-
while (i < text.length) {
|
|
795
|
-
const char = text.charAt(i)
|
|
796
|
-
if (char === '{' || char === '(' || char === '[') {
|
|
797
|
-
depth += 1
|
|
798
|
-
} else if (char === '}' || char === ')' || char === ']') {
|
|
799
|
-
depth -= 1
|
|
800
|
-
} else if (char === ',' && depth === 0) {
|
|
801
|
-
return i
|
|
802
|
-
}
|
|
803
|
-
i += 1
|
|
804
|
-
}
|
|
805
|
-
return -1
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
/* Absolute source offset of the trimmed expression beginning at `start` within a
|
|
809
|
-
directive `body` whose first char is at absolute `bodyLoc` — skips the leading
|
|
810
|
-
whitespace `.trim()` drops so the offset points at the expression's first real char
|
|
811
|
-
(the shadow source-map invariant: source text at `loc` equals the emitted code). */
|
|
812
|
-
function exprLoc(bodyLoc: number, body: string, start: number): number {
|
|
813
|
-
const raw = body.slice(start)
|
|
814
|
-
return bodyLoc + start + (raw.length - raw.trimStart().length)
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
/* Parses `for [await] <binding>[, <index>] of <iterable> [by <key>]`. Offsets are
|
|
818
|
-
tracked against the original `body` (not trimmed slices) so `loc` points at the
|
|
819
|
-
iterable expression's first char — `bodyLoc` is the absolute offset of `body[0]`. */
|
|
820
|
-
function parseForHead(
|
|
821
|
-
body: string,
|
|
822
|
-
bodyLoc: number,
|
|
823
|
-
): {
|
|
824
|
-
items: string
|
|
825
|
-
as: string
|
|
826
|
-
index: string | undefined
|
|
827
|
-
key: string | undefined
|
|
828
|
-
async: boolean
|
|
829
|
-
loc: number
|
|
830
|
-
asLoc: number | undefined
|
|
831
|
-
keyLoc: number | undefined
|
|
832
|
-
indexLoc: number | undefined
|
|
833
|
-
} {
|
|
834
|
-
const leadingSpace = (text: string): number => text.length - text.trimStart().length
|
|
835
|
-
const skipSpace = (i: number): number => {
|
|
836
|
-
let at = i
|
|
837
|
-
while (at < body.length && WHITESPACE.test(body.charAt(at))) {
|
|
838
|
-
at += 1
|
|
839
|
-
}
|
|
840
|
-
return at
|
|
841
|
-
}
|
|
842
|
-
let bindingStart = skipSpace(body.indexOf('for') + 3)
|
|
843
|
-
const isAsync = AWAIT_PREFIX.test(body.slice(bindingStart))
|
|
844
|
-
if (isAsync) {
|
|
845
|
-
bindingStart = skipSpace(bindingStart + 'await'.length)
|
|
846
|
-
}
|
|
847
|
-
const region = body.slice(bindingStart)
|
|
848
|
-
const ofAt = indexOfKeywordAtDepthZero(region, ' of ')
|
|
849
|
-
if (ofAt === -1) {
|
|
850
|
-
throw new Error('[abide] {#for} requires `<binding> of <iterable>`')
|
|
851
|
-
}
|
|
852
|
-
const left = region.slice(0, ofAt).trim()
|
|
853
|
-
const itemsStart = skipSpace(bindingStart + ofAt + ' of '.length)
|
|
854
|
-
let itemsRegion = body.slice(itemsStart)
|
|
855
|
-
const byAt = indexOfKeywordAtDepthZero(itemsRegion, ' by ')
|
|
856
|
-
const keyRaw = byAt === -1 ? '' : itemsRegion.slice(byAt + ' by '.length)
|
|
857
|
-
const key = byAt === -1 ? undefined : keyRaw.trim()
|
|
858
|
-
const keyLoc =
|
|
859
|
-
byAt === -1 ? undefined : bodyLoc + itemsStart + byAt + ' by '.length + leadingSpace(keyRaw)
|
|
860
|
-
if (byAt !== -1) {
|
|
861
|
-
itemsRegion = itemsRegion.slice(0, byAt)
|
|
862
|
-
}
|
|
863
|
-
const commaAt = bindingCommaAtDepthZero(left)
|
|
864
|
-
const as = (commaAt === -1 ? left : left.slice(0, commaAt)).trim()
|
|
865
|
-
const indexRaw = commaAt === -1 ? '' : left.slice(commaAt + 1)
|
|
866
|
-
const index = commaAt === -1 ? undefined : indexRaw.trim()
|
|
867
|
-
/* `left` is trimmed and starts at `bindingStart` (skipSpace'd), so the binding
|
|
868
|
-
name begins exactly there; the index sits past the comma. */
|
|
869
|
-
return {
|
|
870
|
-
items: itemsRegion.trim(),
|
|
871
|
-
as: as === '' ? '_item' : as,
|
|
872
|
-
index,
|
|
873
|
-
key,
|
|
874
|
-
async: isAsync,
|
|
875
|
-
loc: bodyLoc + itemsStart,
|
|
876
|
-
asLoc: as === '' ? undefined : bodyLoc + bindingStart,
|
|
877
|
-
keyLoc,
|
|
878
|
-
indexLoc:
|
|
879
|
-
commaAt === -1
|
|
880
|
-
? undefined
|
|
881
|
-
: bodyLoc + bindingStart + commaAt + 1 + leadingSpace(indexRaw),
|
|
882
|
-
}
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
/* A `case` node (`<template else>`/`<template case>`/`<template default>`) is valid
|
|
886
|
-
only as a direct child of its `<template if>`/`<template switch>`; a `branch`
|
|
887
|
-
(`then`/`catch`/`finally`) only inside its `<template await>`/`<template try>`.
|
|
888
|
-
A sibling `<template else>` — closed off from its `if` — parses to a stray `case`
|
|
889
|
-
sitting beside the `if`, which would otherwise be silently dropped. Reject it so
|
|
890
|
-
the mistake surfaces at compile time. Recurses so a stray branch nested anywhere
|
|
891
|
-
is caught, not just at the root. */
|
|
892
|
-
function rejectStrayBranches(
|
|
893
|
-
nodes: TemplateNode[],
|
|
894
|
-
parentKind: TemplateNode['kind'] | undefined,
|
|
895
|
-
): void {
|
|
896
|
-
for (const node of nodes) {
|
|
897
|
-
if (node.kind === 'case' && parentKind !== 'if' && parentKind !== 'switch') {
|
|
898
|
-
throw new Error(
|
|
899
|
-
'[abide] <template else>/<template case> must be nested inside its <template if>/<template switch> — a sibling branch is not supported',
|
|
900
|
-
)
|
|
901
|
-
}
|
|
902
|
-
/* `elseif` is an `if`-chain branch; inside a `switch` it would silently read as the
|
|
903
|
-
default (match-less), so reject the cross-construct mix. */
|
|
904
|
-
if (node.kind === 'case' && node.condition !== undefined && parentKind === 'switch') {
|
|
905
|
-
throw new Error(
|
|
906
|
-
'[abide] <template elseif> belongs to a <template if> chain, not a <template switch> — use <template case>',
|
|
907
|
-
)
|
|
908
|
-
}
|
|
909
|
-
if (
|
|
910
|
-
node.kind === 'branch' &&
|
|
911
|
-
parentKind !== 'await' &&
|
|
912
|
-
parentKind !== 'try' &&
|
|
913
|
-
parentKind !== 'each'
|
|
914
|
-
) {
|
|
915
|
-
throw new Error(
|
|
916
|
-
'[abide] <template then>/<template catch>/<template finally> must be nested inside its <template await>/<template try>/<template each await>',
|
|
917
|
-
)
|
|
918
|
-
}
|
|
919
|
-
if (node.kind === 'if' || node.kind === 'switch') {
|
|
920
|
-
rejectMisplacedBranchContent(node)
|
|
921
|
-
}
|
|
922
|
-
if ('children' in node) {
|
|
923
|
-
rejectStrayBranches(node.children, node.kind)
|
|
924
|
-
}
|
|
925
|
-
}
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
/* A `<template if>` chain's `then` content precedes its first branch tag
|
|
929
|
-
(`elseif`/`else`); a `<template switch>` renders only its branch tags. So rendered
|
|
930
|
-
content sitting AFTER the first branch in an `if` — or ANYWHERE in a `switch` —
|
|
931
|
-
belongs to no branch: today it silently folds into `then` / is dropped. Reject it so
|
|
932
|
-
the misplacement surfaces. Whitespace stays transparent, and `<script>`/`<style>` are
|
|
933
|
-
directives (scoping, not rendered output), so both remain legal anywhere. */
|
|
934
|
-
function rejectMisplacedBranchContent(
|
|
935
|
-
node: Extract<TemplateNode, { kind: 'if' | 'switch' }>,
|
|
936
|
-
): void {
|
|
937
|
-
const firstBranch = node.children.findIndex((child) => child.kind === 'case')
|
|
938
|
-
node.children.forEach((child, index) => {
|
|
939
|
-
const isRenderedContent =
|
|
940
|
-
child.kind !== 'case' &&
|
|
941
|
-
child.kind !== 'script' &&
|
|
942
|
-
child.kind !== 'style' &&
|
|
943
|
-
!isWhitespaceText(child)
|
|
944
|
-
/* In a switch nothing but branches renders, so every position is illegal; in an if
|
|
945
|
-
only content past the first branch is (the leading then-content is legal). */
|
|
946
|
-
const illegalPosition =
|
|
947
|
-
node.kind === 'switch' || (firstBranch !== -1 && index > firstBranch)
|
|
948
|
-
if (isRenderedContent && illegalPosition) {
|
|
949
|
-
throw new Error(
|
|
950
|
-
node.kind === 'switch'
|
|
951
|
-
? '[abide] a <template switch> renders only its <template case>/<template default> branches — move stray content into a branch'
|
|
952
|
-
: '[abide] content after a <template elseif>/<template else> belongs to no branch — the then-content must precede the first branch tag',
|
|
953
|
-
)
|
|
954
|
-
}
|
|
955
|
-
})
|
|
956
|
-
/* In an `if` chain the match-less `<template else>` is the trailing block, so any
|
|
957
|
-
branch after it (a second `else`, or an `elseif`) compiles to invalid `} else {…}
|
|
958
|
-
else if {…}` (SSR/type-shadow) or a silently-shadowed branch (the `switchBlock`
|
|
959
|
-
default wins). Reject so the misordering surfaces here, not as opaque codegen. */
|
|
960
|
-
if (node.kind === 'if') {
|
|
961
|
-
const branches = node.children.filter(
|
|
962
|
-
(child): child is Extract<TemplateNode, { kind: 'case' }> => child.kind === 'case',
|
|
963
|
-
)
|
|
964
|
-
const elseIndex = branches.findIndex(
|
|
965
|
-
(branch) => branch.match === undefined && branch.condition === undefined,
|
|
966
|
-
)
|
|
967
|
-
if (elseIndex !== -1 && elseIndex < branches.length - 1) {
|
|
968
|
-
throw new Error(
|
|
969
|
-
'[abide] <template else> must be the last branch of its <template if> chain — no <template elseif>/<template else> may follow it',
|
|
970
|
-
)
|
|
971
|
-
}
|
|
972
|
-
}
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
/* Turns a component's attributes into props. A component has no directives —
|
|
976
|
-
every attribute is a prop under its written name, so `on*`/`bind:`/`attach`
|
|
977
|
-
round-trip to their original names (the kinds the tag-blind attribute parser
|
|
978
|
-
assigned) instead of being dropped. A static value becomes a string literal —
|
|
979
|
-
a bare attribute coerces to `true` instead; every other kind keeps its `code`,
|
|
980
|
-
letting a prop hold any value, functions included (e.g. an `onclick` callback). */
|
|
981
|
-
function toProps(
|
|
982
|
-
attrs: TemplateAttr[],
|
|
983
|
-
): { name: string; code: string; loc?: number; nameLoc?: number; spread?: boolean }[] {
|
|
984
|
-
return attrs.map((attr) => {
|
|
985
|
-
/* A `{...expr}` spread carries no name — its keys merge in at runtime
|
|
986
|
-
(`mergeProps`/`spreadProps`); `spread: true` marks it for the back-ends. */
|
|
987
|
-
if (attr.kind === 'spread') {
|
|
988
|
-
return { name: '', code: attr.code, loc: attr.loc, spread: true }
|
|
989
|
-
}
|
|
990
|
-
if (attr.kind === 'static') {
|
|
991
|
-
/* A bare attribute (`<Toggle on />`) is a boolean flag: coerce it to
|
|
992
|
-
`true` so the prop reads as a boolean, not the empty string a native
|
|
993
|
-
element would serialise. An explicit `on=""` stays the empty string. */
|
|
994
|
-
return {
|
|
995
|
-
name: attr.name,
|
|
996
|
-
code: attr.bare ? 'true' : JSON.stringify(attr.value),
|
|
997
|
-
nameLoc: attr.nameLoc,
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
|
-
/* `label="hi {name}"` — a string-valued prop, the template-literal concatenation
|
|
1001
|
-
of its parts (lowered downstream like any other prop value). */
|
|
1002
|
-
if (attr.kind === 'interpolated') {
|
|
1003
|
-
return {
|
|
1004
|
-
name: attr.name,
|
|
1005
|
-
code: interpolatedTemplateLiteral(attr.parts),
|
|
1006
|
-
nameLoc: attr.nameLoc,
|
|
1007
|
-
}
|
|
1008
|
-
}
|
|
1009
|
-
/* Every non-static kind keeps its `code`/`loc`; only the prop name differs —
|
|
1010
|
-
a directive (`event`/`bind`/`class`/`style`/`attach`) round-trips to its written
|
|
1011
|
-
name as a passthrough prop. */
|
|
1012
|
-
const name =
|
|
1013
|
-
attr.kind === 'event'
|
|
1014
|
-
? `on${attr.event}`
|
|
1015
|
-
: attr.kind === 'bind'
|
|
1016
|
-
? `bind:${attr.property}`
|
|
1017
|
-
: attr.kind === 'class'
|
|
1018
|
-
? `class:${attr.name}`
|
|
1019
|
-
: attr.kind === 'style'
|
|
1020
|
-
? `style:${attr.property}`
|
|
1021
|
-
: attr.kind === 'attach'
|
|
1022
|
-
? 'attach'
|
|
1023
|
-
: attr.name
|
|
1024
|
-
/* Only `expression` carries a name offset; event/bind/attach are framework-handled
|
|
1025
|
-
passthrough excluded from the strict prop check, so theirs is unused. */
|
|
1026
|
-
const nameLoc = attr.kind === 'expression' ? attr.nameLoc : undefined
|
|
1027
|
-
return { name, code: attr.code, loc: attr.loc, nameLoc }
|
|
1028
|
-
})
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
/* The attribute's source name (`on<event>`/`bind:<property>` reconstructed). */
|
|
1032
|
-
function attrName(attr: TemplateAttr): string {
|
|
1033
|
-
if (attr.kind === 'event') {
|
|
1034
|
-
return `on${attr.event}`
|
|
1035
|
-
}
|
|
1036
|
-
if (attr.kind === 'bind') {
|
|
1037
|
-
return `bind:${attr.property}`
|
|
1038
|
-
}
|
|
1039
|
-
if (attr.kind === 'attach') {
|
|
1040
|
-
return 'attach'
|
|
1041
|
-
}
|
|
1042
|
-
if (attr.kind === 'class') {
|
|
1043
|
-
return `class:${attr.name}`
|
|
1044
|
-
}
|
|
1045
|
-
if (attr.kind === 'style') {
|
|
1046
|
-
return `style:${attr.property}`
|
|
1047
|
-
}
|
|
1048
|
-
/* A spread has no name. `attrName` only feeds `<template>` directive lookups, and a
|
|
1049
|
-
spread on a `<template>` is rejected at parse, so this branch is unreachable in
|
|
1050
|
-
practice. (Spread itself IS supported — on components as props and on native
|
|
1051
|
-
elements as attributes; only `<template>` directives reject it.) */
|
|
1052
|
-
if (attr.kind === 'spread') {
|
|
1053
|
-
return ''
|
|
1054
|
-
}
|
|
1055
|
-
return attr.name
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
/* The control-flow attribute names that used to drive `<template>` directives — now
|
|
1059
|
-
moved to `{#…}` blocks. A `<template>` carrying one is a migration error. */
|
|
1060
|
-
const CONTROL_DIRECTIVES = new Set([
|
|
1061
|
-
'if',
|
|
1062
|
-
'elseif',
|
|
1063
|
-
'else',
|
|
1064
|
-
'each',
|
|
1065
|
-
'await',
|
|
1066
|
-
'then',
|
|
1067
|
-
'catch',
|
|
1068
|
-
'finally',
|
|
1069
|
-
'switch',
|
|
1070
|
-
'case',
|
|
1071
|
-
'default',
|
|
1072
|
-
'try',
|
|
1073
|
-
])
|
|
1074
|
-
|
|
1075
|
-
/* A `<template>` is now ONLY a plain inert element (e.g. client-side cloning).
|
|
1076
|
-
Snippet declarations moved to `{#snippet name(args)}…{/snippet}` blocks; control
|
|
1077
|
-
flow moved to `{#…}` blocks — both throw migration errors here. */
|
|
1078
|
-
function toSnippetOrTemplate(attrs: TemplateAttr[], children: TemplateNode[]): TemplateNode {
|
|
1079
|
-
const find = (name: string) => attrs.find((attr) => attrName(attr) === name)
|
|
1080
|
-
const directive = attrs.find((attr) => CONTROL_DIRECTIVES.has(attrName(attr)))
|
|
1081
|
-
if (directive !== undefined) {
|
|
1082
|
-
const name = attrName(directive)
|
|
1083
|
-
const block = name === 'elseif' || name === 'else' ? 'if' : name
|
|
1084
|
-
throw new Error(
|
|
1085
|
-
`[abide] <template ${name}> control flow was removed — use the {#${block}…} block instead`,
|
|
1086
|
-
)
|
|
1087
|
-
}
|
|
1088
|
-
/* `<template name>` snippet declarations were retired for the `{#snippet name(args)}`
|
|
1089
|
-
block — reject with a migration error. */
|
|
1090
|
-
if (find('name') !== undefined) {
|
|
1091
|
-
throw new Error(
|
|
1092
|
-
'[abide] <template name> snippet declarations were removed — use a {#snippet name(args)}…{/snippet} block',
|
|
1093
|
-
)
|
|
18
|
+
const { nodes, diagnostics } = parseTemplateRecovering(source, baseOffset)
|
|
19
|
+
if (diagnostics.length > 0) {
|
|
20
|
+
// biome-ignore lint/style/noNonNullAssertion: length > 0 guarantees diagnostics[0] is defined
|
|
21
|
+
const first = diagnostics[0]!
|
|
22
|
+
throw new AbideCompileError(first.message, first.start)
|
|
1094
23
|
}
|
|
1095
|
-
|
|
1096
|
-
return { kind: 'element', tag: 'template', attrs, children }
|
|
24
|
+
return { nodes }
|
|
1097
25
|
}
|