@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
|
@@ -0,0 +1,820 @@
|
|
|
1
|
+
import { resolve } from 'node:path'
|
|
2
|
+
import ts from 'typescript'
|
|
3
|
+
import { HTTP_METHODS } from './HTTP_METHODS.ts'
|
|
4
|
+
import { jsonSchemaForType } from './jsonSchemaForType.ts'
|
|
5
|
+
import { loadProjectTsConfig } from './loadProjectTsConfig.ts'
|
|
6
|
+
import type { ErrorJsonSchemas } from './types/ErrorJsonSchemas.ts'
|
|
7
|
+
import type { HttpMethod } from './types/HttpMethod.ts'
|
|
8
|
+
import type { InputCoercion } from './types/InputCoercion.ts'
|
|
9
|
+
import type { OutputWirePlan } from './types/OutputWirePlan.ts'
|
|
10
|
+
import type { ReturnBody } from './types/ReturnBody.ts'
|
|
11
|
+
import type { WireKind } from './types/WireKind.ts'
|
|
12
|
+
|
|
13
|
+
const RPC_HELPERS = new Set<string>(HTTP_METHODS)
|
|
14
|
+
const NUMBER_FLAGS = ts.TypeFlags.Number | ts.TypeFlags.NumberLiteral
|
|
15
|
+
const BOOLEAN_FLAGS = ts.TypeFlags.Boolean | ts.TypeFlags.BooleanLiteral
|
|
16
|
+
const BIGINT_FLAGS = ts.TypeFlags.BigInt | ts.TypeFlags.BigIntLiteral
|
|
17
|
+
const NULLISH_FLAGS = ts.TypeFlags.Undefined | ts.TypeFlags.Null | ts.TypeFlags.Void
|
|
18
|
+
// Only `undefined`/`void` make a property OPTIONAL (a `T | null` is a required-but-nullable field).
|
|
19
|
+
const OPTIONALITY_FLAGS = ts.TypeFlags.Undefined | ts.TypeFlags.Void
|
|
20
|
+
|
|
21
|
+
export type RpcServerProgram = {
|
|
22
|
+
/*
|
|
23
|
+
Whether the rpc module at `modulePath` has a STREAMING handler, decided through the
|
|
24
|
+
type graph: the handler's return type resolves to a `TypedResponse<AsyncIterable<…>>`
|
|
25
|
+
(the branded shape only `jsonl()`/`sse()` produce). Returns true/false when the export
|
|
26
|
+
call site and a call signature are found — INCLUDING when the stream is produced through
|
|
27
|
+
a wrapper function, the case the char-scan `detectStreaming` misses. undefined when the
|
|
28
|
+
module or its export call can't be resolved, so the caller fails open to the scan.
|
|
29
|
+
*/
|
|
30
|
+
streamingForModule(modulePath: string): boolean | undefined
|
|
31
|
+
/*
|
|
32
|
+
The rpc's HTTP method, read from the export binding's helper SYMBOL (aliases and
|
|
33
|
+
re-exports followed through the checker) rather than the callee's source text. So an
|
|
34
|
+
`import { GET as read }` / a re-exported helper resolves to `GET`, where the `RPC_EXPORT`
|
|
35
|
+
regex — keyed on the literal helper name — misses it. undefined when the export call or
|
|
36
|
+
its helper symbol can't be resolved, so the caller fails open to `detectRpcMethod`.
|
|
37
|
+
*/
|
|
38
|
+
methodForModule(modulePath: string): HttpMethod | undefined
|
|
39
|
+
/*
|
|
40
|
+
The input-args wire codec plan (ADR-0028 scalars, ADR-0029 structured): the endpoint's wire
|
|
41
|
+
`Args` type read off the exported RemoteFunction's call signature, projected to the
|
|
42
|
+
number/boolean/date/bigint/set/map fields parseArgs revives a plain-JSON wire value into. Reads
|
|
43
|
+
`InferInput` (the wire shape the schema validates), so a self-coercing schema whose input is
|
|
44
|
+
loose is correctly left uncoerced. undefined when the export call or its Args type can't be
|
|
45
|
+
resolved, or no field is a recognized WireKind — the caller then stamps no plan, so a GET field
|
|
46
|
+
stays a string exactly as today.
|
|
47
|
+
*/
|
|
48
|
+
inputCoercionForModule(modulePath: string): InputCoercion | undefined
|
|
49
|
+
/*
|
|
50
|
+
The output-args wire codec plan (ADR-0029 output path): the endpoint's success RESPONSE body type
|
|
51
|
+
(resolved the same way `returnBodyForModule`/`walkSuccessBodies` read it — unwrap `Promise`, drop
|
|
52
|
+
the `TypedError` branches) projected to the structured `date`/`bigint`/`set`/`map` fields the
|
|
53
|
+
client proxy revives from a decoded response. The response-side sibling of
|
|
54
|
+
`inputCoercionForModule`: it is baked onto the CLIENT `remoteProxy` stub (not the server
|
|
55
|
+
`defineRpc`), so an abide client revives a `Set`/`Map`/`bigint`/`Date` the server encoded to
|
|
56
|
+
honest JSON. Only the structured kinds are listed — `number`/`boolean` already ride as their JSON
|
|
57
|
+
type. undefined when the export call or its success body can't be resolved, or no field is a
|
|
58
|
+
structured WireKind, so the client bakes no plan and a response array stays an array.
|
|
59
|
+
*/
|
|
60
|
+
outputWirePlanForModule(modulePath: string): OutputWirePlan | undefined
|
|
61
|
+
/*
|
|
62
|
+
The endpoint's success-BODY type (ADR-0030), read from the handler's return type the same way
|
|
63
|
+
streaming detection reads it: unwrap `Promise`, drop the `TypedError` branches, take each
|
|
64
|
+
success `TypedResponse<Body>` body. `type` is that body rendered as a TS type string; a
|
|
65
|
+
streaming endpoint reports `streaming: true` and the per-FRAME type (the `AsyncIterable`
|
|
66
|
+
element) so a surface describes one streamed item. This is the output-side sibling of
|
|
67
|
+
`inputCoercionForModule` — the handler's return is the single source for the generated surfaces
|
|
68
|
+
(`.d.ts`/OpenAPI 200/MCP outputSchema) instead of a hand-written `schemas.output`. undefined
|
|
69
|
+
when the export call or its handler body type can't be resolved, so a consumer falls open to an
|
|
70
|
+
author-declared `schemas.output`.
|
|
71
|
+
*/
|
|
72
|
+
returnBodyForModule(modulePath: string): ReturnBody | undefined
|
|
73
|
+
/*
|
|
74
|
+
The endpoint's success-body projected to JSON Schema (ADR-0030 D2), for the OpenAPI 200 / MCP
|
|
75
|
+
outputSchema / inspector surface when the author declared no `schemas.output` validator. Resolves
|
|
76
|
+
the same success-body `ts.Type`(s) `returnBodyForModule` reads — unwrap `Promise`, drop the
|
|
77
|
+
`TypedError` branches, frame-unwrap a streaming body — then projects each through
|
|
78
|
+
`jsonSchemaForType`, combining multiple success branches under `anyOf`. undefined when no body
|
|
79
|
+
resolves or the projection is bare permissive `{}`, so a surface omits the schema exactly as today
|
|
80
|
+
when no `schemas.output` is declared. The build stamps the resolved schema into the server
|
|
81
|
+
`defineRpc` call (like the ADR-0028 `coerce` plan) so the runtime registry can carry it.
|
|
82
|
+
*/
|
|
83
|
+
returnBodySchemaForModule(modulePath: string): Record<string, unknown> | undefined
|
|
84
|
+
/*
|
|
85
|
+
The endpoint's typed-error branches surfaced as a status-keyed JSON-Schema map (ADR-0030) — the
|
|
86
|
+
error-branch sibling of `returnBodySchemaForModule`. Walks the SAME return union but KEEPS the
|
|
87
|
+
`TypedError` branches success projection drops: for each, reads the `error.typed(name, status,
|
|
88
|
+
schema?)` brand's numeric `status` and projects the error's `data` type (its schema's
|
|
89
|
+
`~standard.types.input`) through `jsonSchemaForType`. Errors sharing a status combine under
|
|
90
|
+
`anyOf`; a nullary error (no data) contributes a bare `{}` so the status still surfaces. undefined
|
|
91
|
+
when no handler signature resolves or the handler declares no typed errors, so a consumer omits
|
|
92
|
+
the error responses exactly as today. The build stamps the resolved map into the server
|
|
93
|
+
`defineRpc` call (like `outputJsonSchema`) so the runtime registry can carry it to OpenAPI.
|
|
94
|
+
*/
|
|
95
|
+
errorSchemasForModule(modulePath: string): ErrorJsonSchemas | undefined
|
|
96
|
+
/*
|
|
97
|
+
The endpoint's INPUT args projected to JSON Schema (ADR-0030 input side — the input-surface
|
|
98
|
+
sibling of `returnBodySchemaForModule`), for the OpenAPI parameters/request body / MCP
|
|
99
|
+
inputSchema / inspector input surface when the author declared no `schemas.input` VALIDATOR.
|
|
100
|
+
Resolves the SAME wire `Args` bag `inputCoercionForModule` reads — the exported RemoteFunction's
|
|
101
|
+
first call-signature parameter, with the multipart `FormData` member dropped — then projects each
|
|
102
|
+
property through `jsonSchemaForType` into an object schema. File-typed properties are EXCLUDED
|
|
103
|
+
exactly as `filesSchema` keeps File parts out of the `inputSchema` projection (a File has no
|
|
104
|
+
honest JSON-Schema form). This is a SHAPE description only: it is never wired into runtime
|
|
105
|
+
validation, so it can never produce a 422 — an author who wants that still declares
|
|
106
|
+
`schemas.input`, which also OVERRIDES this projection on every surface. undefined when no call
|
|
107
|
+
signature / Args bag resolves or every property is excluded, so the surface behaves as today. The
|
|
108
|
+
build stamps the resolved schema into the server `defineRpc` call so the runtime registry carries
|
|
109
|
+
it.
|
|
110
|
+
*/
|
|
111
|
+
inputSchemaForModule(modulePath: string): Record<string, unknown> | undefined
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/*
|
|
115
|
+
Warms one `ts.Program` over every `.ts` under `rpcDir`, reused across every rpc transform
|
|
116
|
+
in the root (ADR-0025 D1 — the server-side mirror of the UI shadow program). The handler's
|
|
117
|
+
transitive imports resolve on demand through the default compiler host, so the type queries
|
|
118
|
+
see the real `jsonl()`/`sse()` return shape, the aliased helper's origin symbol, and an
|
|
119
|
+
imported const's literal type. Type queries only — never emits, never reports diagnostics; a
|
|
120
|
+
module that fails to type-check still yields its answers. Building is the expensive step (a
|
|
121
|
+
few hundred ms once per root), so the resolver plugin caches it per-root and builds it lazily
|
|
122
|
+
on the first rpc transform.
|
|
123
|
+
*/
|
|
124
|
+
export function createRpcServerProgram(cwd: string, rpcDir: string): RpcServerProgram {
|
|
125
|
+
const rpcFiles = [...new Bun.Glob('**/*.ts').scanSync({ cwd: rpcDir, onlyFiles: true })].map(
|
|
126
|
+
(relative) => resolve(rpcDir, relative),
|
|
127
|
+
)
|
|
128
|
+
const { options } = loadProjectTsConfig(cwd)
|
|
129
|
+
const program = ts.createProgram({ rootNames: rpcFiles, options })
|
|
130
|
+
const checker = program.getTypeChecker()
|
|
131
|
+
/* Every query fails open to its char-scan/regex counterpart: an unresolved module or any
|
|
132
|
+
checker throw yields undefined so a type-resolution hiccup never breaks a build (ADR-0025
|
|
133
|
+
D3). */
|
|
134
|
+
const query = <T>(
|
|
135
|
+
modulePath: string,
|
|
136
|
+
read: (call: ResolvedRpcCall) => T | undefined,
|
|
137
|
+
): T | undefined => {
|
|
138
|
+
try {
|
|
139
|
+
const sourceFile = program.getSourceFile(modulePath)
|
|
140
|
+
if (sourceFile === undefined) {
|
|
141
|
+
return undefined
|
|
142
|
+
}
|
|
143
|
+
const call = resolveRpcCall(checker, sourceFile)
|
|
144
|
+
if (call === undefined) {
|
|
145
|
+
return undefined
|
|
146
|
+
}
|
|
147
|
+
return read(call)
|
|
148
|
+
} catch {
|
|
149
|
+
return undefined
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
streamingForModule(modulePath) {
|
|
154
|
+
return query(modulePath, (call) => handlerReturnsStream(checker, call.node))
|
|
155
|
+
},
|
|
156
|
+
methodForModule(modulePath) {
|
|
157
|
+
return query(modulePath, (call) => call.method)
|
|
158
|
+
},
|
|
159
|
+
inputCoercionForModule(modulePath) {
|
|
160
|
+
return query(modulePath, (call) => inputCoercionPlan(checker, call.node))
|
|
161
|
+
},
|
|
162
|
+
outputWirePlanForModule(modulePath) {
|
|
163
|
+
return query(modulePath, (call) => outputWirePlan(checker, call.node))
|
|
164
|
+
},
|
|
165
|
+
returnBodyForModule(modulePath) {
|
|
166
|
+
return query(modulePath, (call) => handlerReturnBody(checker, call.node))
|
|
167
|
+
},
|
|
168
|
+
returnBodySchemaForModule(modulePath) {
|
|
169
|
+
return query(modulePath, (call) => returnBodyJsonSchema(checker, call.node))
|
|
170
|
+
},
|
|
171
|
+
errorSchemasForModule(modulePath) {
|
|
172
|
+
return query(modulePath, (call) => errorBranchSchemas(checker, call.node))
|
|
173
|
+
},
|
|
174
|
+
inputSchemaForModule(modulePath) {
|
|
175
|
+
return query(modulePath, (call) => inputArgsJsonSchema(checker, call.node))
|
|
176
|
+
},
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
type ResolvedRpcCall = {
|
|
181
|
+
/* The exported `<METHOD>(handler, opts?)` CallExpression. */
|
|
182
|
+
node: ts.CallExpression
|
|
183
|
+
/* The method the callee's helper symbol resolves to (alias/re-export followed). */
|
|
184
|
+
method: HttpMethod
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/*
|
|
188
|
+
The module's exported rpc call — the initializer of `export const <name> = <helper>(…)` whose
|
|
189
|
+
callee resolves (through the checker, following import aliases and re-exports) to one of the
|
|
190
|
+
rpc helper symbols. Scoped to top-level exported const initializers, so a nested `jsonl(` in a
|
|
191
|
+
handler body can't match and an aliased helper (`import { GET as read }`) still resolves. undefined
|
|
192
|
+
when no such export exists — the caller falls back to the char-scan.
|
|
193
|
+
*/
|
|
194
|
+
function resolveRpcCall(
|
|
195
|
+
checker: ts.TypeChecker,
|
|
196
|
+
sourceFile: ts.SourceFile,
|
|
197
|
+
): ResolvedRpcCall | undefined {
|
|
198
|
+
for (const statement of sourceFile.statements) {
|
|
199
|
+
if (!ts.isVariableStatement(statement)) {
|
|
200
|
+
continue
|
|
201
|
+
}
|
|
202
|
+
const isExported = statement.modifiers?.some(
|
|
203
|
+
(modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword,
|
|
204
|
+
)
|
|
205
|
+
if (!isExported) {
|
|
206
|
+
continue
|
|
207
|
+
}
|
|
208
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
209
|
+
const initializer = declaration.initializer
|
|
210
|
+
if (initializer === undefined || !ts.isCallExpression(initializer)) {
|
|
211
|
+
continue
|
|
212
|
+
}
|
|
213
|
+
const method = methodOfCallee(checker, initializer.expression)
|
|
214
|
+
if (method !== undefined) {
|
|
215
|
+
return { node: initializer, method }
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return undefined
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/*
|
|
223
|
+
The HTTP method a call's callee names — the origin helper symbol's name, following import
|
|
224
|
+
aliases and re-exports through the checker (so `read` from `import { GET as read }` yields
|
|
225
|
+
`GET`). Falls back to the callee's own identifier text when the symbol doesn't resolve, so a
|
|
226
|
+
bare `GET(` still reads as `GET` even if type resolution hiccups. undefined when neither is an
|
|
227
|
+
rpc helper.
|
|
228
|
+
*/
|
|
229
|
+
function methodOfCallee(checker: ts.TypeChecker, callee: ts.Expression): HttpMethod | undefined {
|
|
230
|
+
if (!ts.isIdentifier(callee)) {
|
|
231
|
+
return undefined
|
|
232
|
+
}
|
|
233
|
+
let symbol = checker.getSymbolAtLocation(callee)
|
|
234
|
+
if (symbol !== undefined && symbol.flags & ts.SymbolFlags.Alias) {
|
|
235
|
+
symbol = checker.getAliasedSymbol(symbol)
|
|
236
|
+
}
|
|
237
|
+
const resolvedName = symbol?.getName()
|
|
238
|
+
if (resolvedName !== undefined && RPC_HELPERS.has(resolvedName)) {
|
|
239
|
+
return resolvedName as HttpMethod
|
|
240
|
+
}
|
|
241
|
+
if (RPC_HELPERS.has(callee.text)) {
|
|
242
|
+
return callee.text as HttpMethod
|
|
243
|
+
}
|
|
244
|
+
return undefined
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/*
|
|
248
|
+
The rpc export's handler return type, tested for the streaming brand. undefined when no handler
|
|
249
|
+
call signature is found — the caller falls back to the char-scan. A resolvable handler that
|
|
250
|
+
returns a plain value yields `false` (definitively not streaming), so a warm program does not
|
|
251
|
+
needlessly defer to the scan for ordinary handlers.
|
|
252
|
+
*/
|
|
253
|
+
function handlerReturnsStream(
|
|
254
|
+
checker: ts.TypeChecker,
|
|
255
|
+
call: ts.CallExpression,
|
|
256
|
+
): boolean | undefined {
|
|
257
|
+
const handler = call.arguments[0]
|
|
258
|
+
if (handler === undefined) {
|
|
259
|
+
return undefined
|
|
260
|
+
}
|
|
261
|
+
const signature = checker.getTypeAtLocation(handler).getCallSignatures()[0]
|
|
262
|
+
if (signature === undefined) {
|
|
263
|
+
return undefined
|
|
264
|
+
}
|
|
265
|
+
const returnType = unwrapPromise(checker, checker.getReturnTypeOfSignature(signature))
|
|
266
|
+
return returnTypeIsStreaming(checker, returnType)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/* An async handler returns `Promise<TypedResponse<…>>`; unwrap one Promise layer so the
|
|
270
|
+
brand check sees the response type. */
|
|
271
|
+
function unwrapPromise(checker: ts.TypeChecker, type: ts.Type): ts.Type {
|
|
272
|
+
if (type.getSymbol()?.name === 'Promise') {
|
|
273
|
+
const inner = checker.getTypeArguments(type as ts.TypeReference)[0]
|
|
274
|
+
if (inner !== undefined) {
|
|
275
|
+
return inner
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return type
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/* True when the type is (or, across a union of return branches, contains) a
|
|
282
|
+
`TypedResponse<AsyncIterable<…>>` — a `__body` phantom property whose type is async-iterable.
|
|
283
|
+
`jsonl()`/`sse()` are the only constructors of that shape, so this is congruent by
|
|
284
|
+
construction with the RemoteCallable streaming conditional. */
|
|
285
|
+
function returnTypeIsStreaming(checker: ts.TypeChecker, type: ts.Type): boolean {
|
|
286
|
+
if (type.isUnion()) {
|
|
287
|
+
return type.types.some((member) => returnTypeIsStreaming(checker, member))
|
|
288
|
+
}
|
|
289
|
+
const bodyProperty = type.getProperty('__body')
|
|
290
|
+
if (bodyProperty === undefined) {
|
|
291
|
+
return false
|
|
292
|
+
}
|
|
293
|
+
const declaration = bodyProperty.valueDeclaration ?? bodyProperty.declarations?.[0]
|
|
294
|
+
if (declaration === undefined) {
|
|
295
|
+
return false
|
|
296
|
+
}
|
|
297
|
+
return typeIsAsyncIterable(
|
|
298
|
+
checker,
|
|
299
|
+
checker.getTypeOfSymbolAtLocation(bodyProperty, declaration),
|
|
300
|
+
)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/* Async-iterable iff the type exposes a `[Symbol.asyncIterator]` member — the computed
|
|
304
|
+
well-known symbol surfaces as an `__@asyncIterator@<n>` property name. Recurses through a
|
|
305
|
+
union so `AsyncIterable<A> | AsyncIterable<B>` still counts. */
|
|
306
|
+
function typeIsAsyncIterable(checker: ts.TypeChecker, type: ts.Type): boolean {
|
|
307
|
+
if (type.isUnion()) {
|
|
308
|
+
return type.types.some((member) => typeIsAsyncIterable(checker, member))
|
|
309
|
+
}
|
|
310
|
+
for (const property of checker.getPropertiesOfType(type)) {
|
|
311
|
+
if (property.getName().includes('asyncIterator')) {
|
|
312
|
+
return true
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return false
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/*
|
|
319
|
+
The rpc export's success-body descriptor (ADR-0030), projected from the handler's return type the
|
|
320
|
+
same way `handlerReturnsStream` reads it — unwrap one `Promise` layer, then walk the return union.
|
|
321
|
+
This mirrors `RpcHelper`'s `SuccessBody<R>`: a `TypedError` branch (its `__abideError` brand) is an
|
|
322
|
+
error response and is dropped; every success `TypedResponse<Body>` (its `__body` phantom) yields its
|
|
323
|
+
body. A branch is a streaming body when the body is async-iterable, in which case its per-FRAME type
|
|
324
|
+
(the `AsyncIterable` element) is taken and the descriptor marked streaming. undefined when no handler
|
|
325
|
+
call signature or no success body resolves — the caller falls open to an author-declared schema.
|
|
326
|
+
*/
|
|
327
|
+
function handlerReturnBody(
|
|
328
|
+
checker: ts.TypeChecker,
|
|
329
|
+
call: ts.CallExpression,
|
|
330
|
+
): ReturnBody | undefined {
|
|
331
|
+
const rendered: string[] = []
|
|
332
|
+
let streaming = false
|
|
333
|
+
const resolved = walkSuccessBodies(checker, call, (part, isStreaming) => {
|
|
334
|
+
if (isStreaming) {
|
|
335
|
+
streaming = true
|
|
336
|
+
}
|
|
337
|
+
// An untagged Response (no `__body` phantom) has an `unknown` success body — SuccessBody's fallback.
|
|
338
|
+
pushUnique(rendered, part === undefined ? 'unknown' : checker.typeToString(part))
|
|
339
|
+
})
|
|
340
|
+
if (!resolved || rendered.length === 0) {
|
|
341
|
+
return undefined
|
|
342
|
+
}
|
|
343
|
+
return { type: rendered.join(' | '), streaming }
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/*
|
|
347
|
+
The rpc export's success body projected to JSON Schema (ADR-0030 D2) — the output-surface sibling of
|
|
348
|
+
`handlerReturnBody`, walking the identical success-body constituents but projecting each through
|
|
349
|
+
`jsonSchemaForType` instead of rendering a TS string. Multiple success branches combine under `anyOf`;
|
|
350
|
+
an untagged/unprojectable body contributes the permissive `{}`. undefined when no handler signature
|
|
351
|
+
resolves or the combined schema is bare permissive `{}` — the caller falls open to an author-declared
|
|
352
|
+
`schemas.output`.
|
|
353
|
+
*/
|
|
354
|
+
function returnBodyJsonSchema(
|
|
355
|
+
checker: ts.TypeChecker,
|
|
356
|
+
call: ts.CallExpression,
|
|
357
|
+
): Record<string, unknown> | undefined {
|
|
358
|
+
const schemas: Record<string, unknown>[] = []
|
|
359
|
+
const resolved = walkSuccessBodies(checker, call, (part) => {
|
|
360
|
+
const projected = part === undefined ? undefined : jsonSchemaForType(checker, part)
|
|
361
|
+
pushUniqueSchema(schemas, projected ?? {})
|
|
362
|
+
})
|
|
363
|
+
if (!resolved || schemas.length === 0) {
|
|
364
|
+
return undefined
|
|
365
|
+
}
|
|
366
|
+
const [onlySchema] = schemas
|
|
367
|
+
const combined =
|
|
368
|
+
schemas.length === 1 && onlySchema !== undefined ? onlySchema : { anyOf: schemas }
|
|
369
|
+
return Object.keys(combined).length === 0 ? undefined : combined
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/*
|
|
373
|
+
Walks a handler's success-body constituents, shared by `handlerReturnBody` (renders each to a TS
|
|
374
|
+
string) and `returnBodyJsonSchema` (projects each to JSON Schema) so the two surfaces stay congruent.
|
|
375
|
+
Unwraps one `Promise`, then over the return union drops the `TypedError` branches (their `__abideError`
|
|
376
|
+
brand) and, for each success `TypedResponse<Body>` (its `__body` phantom), visits every meaningful body
|
|
377
|
+
part — frame-unwrapped and flagged streaming when async-iterable. `visit` receives undefined for an
|
|
378
|
+
untagged Response (its body is `unknown`). Returns false when no handler call signature resolves, so
|
|
379
|
+
callers fail open.
|
|
380
|
+
*/
|
|
381
|
+
function walkSuccessBodies(
|
|
382
|
+
checker: ts.TypeChecker,
|
|
383
|
+
call: ts.CallExpression,
|
|
384
|
+
visit: (part: ts.Type | undefined, streaming: boolean) => void,
|
|
385
|
+
): boolean {
|
|
386
|
+
const members = returnUnionMembers(checker, call)
|
|
387
|
+
if (members === undefined) {
|
|
388
|
+
return false
|
|
389
|
+
}
|
|
390
|
+
for (const member of members) {
|
|
391
|
+
if (member.getProperty('__abideError') !== undefined) {
|
|
392
|
+
continue
|
|
393
|
+
}
|
|
394
|
+
const bodyProperty = member.getProperty('__body')
|
|
395
|
+
if (bodyProperty === undefined) {
|
|
396
|
+
visit(undefined, false)
|
|
397
|
+
continue
|
|
398
|
+
}
|
|
399
|
+
const declaration = bodyProperty.valueDeclaration ?? bodyProperty.declarations?.[0]
|
|
400
|
+
if (declaration === undefined) {
|
|
401
|
+
visit(undefined, false)
|
|
402
|
+
continue
|
|
403
|
+
}
|
|
404
|
+
// The `__body?` phantom is optional, so reading its type adds a spurious `| undefined`;
|
|
405
|
+
// drop the nullish members (indistinguishable from an authored optional body — the surface
|
|
406
|
+
// describes the present value's shape either way).
|
|
407
|
+
const bodyType = checker.getTypeOfSymbolAtLocation(bodyProperty, declaration)
|
|
408
|
+
for (const part of meaningfulMembers(bodyType)) {
|
|
409
|
+
if (typeIsAsyncIterable(checker, part)) {
|
|
410
|
+
visit(asyncIterableElement(checker, part) ?? part, true)
|
|
411
|
+
} else {
|
|
412
|
+
visit(part, false)
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return true
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/* The handler's return-type union members — the shared front of both the success walk
|
|
420
|
+
(`walkSuccessBodies`) and the error walk (`errorBranchSchemas`): resolve the handler call
|
|
421
|
+
signature, unwrap one `Promise` layer, then split the union (a lone return is a one-member list).
|
|
422
|
+
undefined when no handler call signature resolves, so both callers fail open. */
|
|
423
|
+
function returnUnionMembers(
|
|
424
|
+
checker: ts.TypeChecker,
|
|
425
|
+
call: ts.CallExpression,
|
|
426
|
+
): ts.Type[] | undefined {
|
|
427
|
+
const handler = call.arguments[0]
|
|
428
|
+
if (handler === undefined) {
|
|
429
|
+
return undefined
|
|
430
|
+
}
|
|
431
|
+
const signature = checker.getTypeAtLocation(handler).getCallSignatures()[0]
|
|
432
|
+
if (signature === undefined) {
|
|
433
|
+
return undefined
|
|
434
|
+
}
|
|
435
|
+
const returnType = unwrapPromise(checker, checker.getReturnTypeOfSignature(signature))
|
|
436
|
+
return returnType.isUnion() ? returnType.types : [returnType]
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/*
|
|
440
|
+
The handler's typed-error branches projected to a status-keyed JSON-Schema map (ADR-0030) — the
|
|
441
|
+
error-branch sibling of `returnBodyJsonSchema`, walking the SAME return union but keeping the
|
|
442
|
+
`TypedError` members the success walk drops. For each `__abideError`-branded branch it reads the
|
|
443
|
+
brand's numeric `entry.status` (a literal, captured by `error.typed`'s `Status` type param) and
|
|
444
|
+
projects the error's `data` type (its schema's `~standard.types.input`) through `jsonSchemaForType`.
|
|
445
|
+
Branches sharing a status combine their distinct data schemas under `anyOf`; a nullary error (no
|
|
446
|
+
`data`) contributes a bare `{}` so the status still surfaces. undefined when no handler signature
|
|
447
|
+
resolves or no typed-error branch is present, so the caller stamps nothing and the surface omits the
|
|
448
|
+
error responses exactly as today.
|
|
449
|
+
*/
|
|
450
|
+
function errorBranchSchemas(
|
|
451
|
+
checker: ts.TypeChecker,
|
|
452
|
+
call: ts.CallExpression,
|
|
453
|
+
): ErrorJsonSchemas | undefined {
|
|
454
|
+
const members = returnUnionMembers(checker, call)
|
|
455
|
+
if (members === undefined) {
|
|
456
|
+
return undefined
|
|
457
|
+
}
|
|
458
|
+
// Status → its collected distinct data schemas (empty when only nullary errors share the status).
|
|
459
|
+
const byStatus = new Map<number, Record<string, unknown>[]>()
|
|
460
|
+
for (const member of members) {
|
|
461
|
+
const brand = member.getProperty('__abideError')
|
|
462
|
+
if (brand === undefined) {
|
|
463
|
+
continue
|
|
464
|
+
}
|
|
465
|
+
const entry = memberType(checker, checker.getTypeOfSymbol(brand), 'entry')
|
|
466
|
+
if (entry === undefined) {
|
|
467
|
+
continue
|
|
468
|
+
}
|
|
469
|
+
const status = numberLiteral(memberType(checker, entry, 'status'))
|
|
470
|
+
if (status === undefined) {
|
|
471
|
+
continue
|
|
472
|
+
}
|
|
473
|
+
let schemas = byStatus.get(status)
|
|
474
|
+
if (schemas === undefined) {
|
|
475
|
+
schemas = []
|
|
476
|
+
byStatus.set(status, schemas)
|
|
477
|
+
}
|
|
478
|
+
const dataSchema = errorDataSchema(checker, memberType(checker, entry, 'data'))
|
|
479
|
+
if (dataSchema !== undefined) {
|
|
480
|
+
pushUniqueSchema(schemas, dataSchema)
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
if (byStatus.size === 0) {
|
|
484
|
+
return undefined
|
|
485
|
+
}
|
|
486
|
+
const result: ErrorJsonSchemas = {}
|
|
487
|
+
for (const [status, schemas] of byStatus) {
|
|
488
|
+
const [onlySchema] = schemas
|
|
489
|
+
result[status] =
|
|
490
|
+
schemas.length === 0
|
|
491
|
+
? {}
|
|
492
|
+
: schemas.length === 1 && onlySchema !== undefined
|
|
493
|
+
? onlySchema
|
|
494
|
+
: { anyOf: schemas }
|
|
495
|
+
}
|
|
496
|
+
return result
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/* An error branch's `data` type projected to JSON Schema — navigates the schema brand's phantom
|
|
500
|
+
`~standard.types.input` (the data payload the `error.typed` constructor receives) and projects it
|
|
501
|
+
via `jsonSchemaForType`. undefined for a nullary error (its `entry.data` is `undefined`, so the
|
|
502
|
+
`~standard` chain doesn't resolve) or an unprojectable/permissive data type, so the status
|
|
503
|
+
surfaces without a content schema. */
|
|
504
|
+
function errorDataSchema(
|
|
505
|
+
checker: ts.TypeChecker,
|
|
506
|
+
dataType: ts.Type | undefined,
|
|
507
|
+
): Record<string, unknown> | undefined {
|
|
508
|
+
if (dataType === undefined) {
|
|
509
|
+
return undefined
|
|
510
|
+
}
|
|
511
|
+
const standard = memberType(checker, dataType, '~standard')
|
|
512
|
+
if (standard === undefined) {
|
|
513
|
+
return undefined
|
|
514
|
+
}
|
|
515
|
+
// `types` is `{ input; output } | undefined` (the spec's optional phantom) — take the non-nullish member.
|
|
516
|
+
const types = memberType(checker, standard, 'types')
|
|
517
|
+
if (types === undefined) {
|
|
518
|
+
return undefined
|
|
519
|
+
}
|
|
520
|
+
const input = memberType(checker, nonNullishType(types), 'input')
|
|
521
|
+
if (input === undefined) {
|
|
522
|
+
return undefined
|
|
523
|
+
}
|
|
524
|
+
return jsonSchemaForType(checker, input)
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/* A named property's type, read location-free off a resolved type. undefined when the type has no
|
|
528
|
+
such property — the phantom-navigation guard for the error brand's nested `entry`/`status`/`data`
|
|
529
|
+
/ `~standard`/`types`/`input` chain. */
|
|
530
|
+
function memberType(checker: ts.TypeChecker, type: ts.Type, name: string): ts.Type | undefined {
|
|
531
|
+
const symbol = type.getProperty(name)
|
|
532
|
+
return symbol === undefined ? undefined : checker.getTypeOfSymbol(symbol)
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/* A type's numeric-literal value (e.g. `404`), or undefined when it isn't a single number literal
|
|
536
|
+
(a widened `number`, a union, or undefined) — so a computed error status the type can't pin down
|
|
537
|
+
is skipped rather than guessed. */
|
|
538
|
+
function numberLiteral(type: ts.Type | undefined): number | undefined {
|
|
539
|
+
if (type === undefined || (type.flags & ts.TypeFlags.NumberLiteral) === 0) {
|
|
540
|
+
return undefined
|
|
541
|
+
}
|
|
542
|
+
return (type as ts.NumberLiteralType).value
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/* Drops the nullish members of a `T | undefined` union, returning the lone survivor; else the type
|
|
546
|
+
unchanged (a non-union or a genuine multi-member union stays as-is). Used to strip the spec's
|
|
547
|
+
optional `types?` phantom down to its concrete `Types` object. */
|
|
548
|
+
function nonNullishType(type: ts.Type): ts.Type {
|
|
549
|
+
if (!type.isUnion()) {
|
|
550
|
+
return type
|
|
551
|
+
}
|
|
552
|
+
const kept = type.types.filter((member) => (member.flags & NULLISH_FLAGS) === 0)
|
|
553
|
+
const [onlyKept] = kept
|
|
554
|
+
return kept.length === 1 && onlyKept !== undefined ? onlyKept : type
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/* The non-nullish constituents of a body type — a single-element list for a non-union, else the
|
|
558
|
+
union's members with `undefined`/`null`/`void` dropped (the `__body?` phantom's optionality). All
|
|
559
|
+
members nullish (an unusual `undefined`-only body) keeps them so something still renders. */
|
|
560
|
+
function meaningfulMembers(type: ts.Type): ts.Type[] {
|
|
561
|
+
if (!type.isUnion()) {
|
|
562
|
+
return [type]
|
|
563
|
+
}
|
|
564
|
+
const kept = type.types.filter((member) => (member.flags & NULLISH_FLAGS) === 0)
|
|
565
|
+
return kept.length > 0 ? kept : type.types
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/* The element type of an `AsyncIterable<Frame>` (or the AsyncIterableIterator/AsyncGenerator a
|
|
569
|
+
generator produces) — the per-frame type `jsonl()`/`sse()` streams. undefined when the type isn't
|
|
570
|
+
one of those references, so the caller renders the whole body instead. */
|
|
571
|
+
function asyncIterableElement(checker: ts.TypeChecker, type: ts.Type): ts.Type | undefined {
|
|
572
|
+
const name = type.getSymbol()?.name
|
|
573
|
+
if (name === 'AsyncIterable' || name === 'AsyncIterableIterator' || name === 'AsyncGenerator') {
|
|
574
|
+
return checker.getTypeArguments(type as ts.TypeReference)[0]
|
|
575
|
+
}
|
|
576
|
+
return undefined
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/* Appends `value` only when not already present, so a union of identical success branches renders
|
|
580
|
+
once (the rendered list stays a small monomorphic string array). */
|
|
581
|
+
function pushUnique(list: string[], value: string): void {
|
|
582
|
+
if (!list.includes(value)) {
|
|
583
|
+
list.push(value)
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/* Appends a projected schema only when structurally new, so identical success branches contribute one
|
|
588
|
+
`anyOf` member. Dedup by serialization — the schema objects are small plain data, and this build
|
|
589
|
+
path runs once per rpc. */
|
|
590
|
+
function pushUniqueSchema(list: Record<string, unknown>[], schema: Record<string, unknown>): void {
|
|
591
|
+
const serialized = JSON.stringify(schema)
|
|
592
|
+
if (!list.some((existing) => JSON.stringify(existing) === serialized)) {
|
|
593
|
+
list.push(schema)
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/*
|
|
598
|
+
The wire codec plan for an rpc export's input args (ADR-0028 scalars, ADR-0029 structured). The
|
|
599
|
+
export's type is its RemoteFunction; its call signature's first parameter is the wire `Args` type
|
|
600
|
+
(`InferInput` of the input schema, or the handler's annotated param when schemaless). Each
|
|
601
|
+
number/boolean/date/bigint/set/map field of that Args object becomes a plan entry; every other
|
|
602
|
+
field is omitted. undefined when the RemoteFunction has no call signature, the Args aren't a
|
|
603
|
+
single object type, or no field is a recognized WireKind — the caller then ships no plan (today's
|
|
604
|
+
string-through behavior).
|
|
605
|
+
*/
|
|
606
|
+
function inputCoercionPlan(
|
|
607
|
+
checker: ts.TypeChecker,
|
|
608
|
+
call: ts.CallExpression,
|
|
609
|
+
): InputCoercion | undefined {
|
|
610
|
+
const signature = checker.getTypeAtLocation(call).getCallSignatures()[0]
|
|
611
|
+
if (signature === undefined) {
|
|
612
|
+
return undefined
|
|
613
|
+
}
|
|
614
|
+
const parameter = signature.getParameters()[0]
|
|
615
|
+
if (parameter === undefined) {
|
|
616
|
+
return undefined
|
|
617
|
+
}
|
|
618
|
+
const argsType = argsBagType(checker.getTypeOfSymbolAtLocation(parameter, call))
|
|
619
|
+
if (argsType === undefined) {
|
|
620
|
+
return undefined
|
|
621
|
+
}
|
|
622
|
+
const plan: InputCoercion = {}
|
|
623
|
+
for (const property of argsType.getProperties()) {
|
|
624
|
+
const kind = wireKind(checker, checker.getTypeOfSymbolAtLocation(property, call))
|
|
625
|
+
if (kind !== undefined) {
|
|
626
|
+
plan[property.getName()] = kind
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
/* An empty plan (no codec-eligible field) ships nothing, so no opts are injected. */
|
|
630
|
+
return Object.keys(plan).length === 0 ? undefined : plan
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/*
|
|
634
|
+
The wire codec plan for an rpc export's success RESPONSE body (ADR-0029 output path) — the
|
|
635
|
+
response-side sibling of `inputCoercionPlan`. Walks the handler's success-body constituents the same
|
|
636
|
+
way the ADR-0030 surfaces do (`walkSuccessBodies` — unwrap `Promise`, drop the `TypedError` branches)
|
|
637
|
+
and, for each OBJECT body, reads each property's `WireKind` and keeps only the STRUCTURED kinds
|
|
638
|
+
(`date`/`bigint`/`set`/`map`) the client revives — `number`/`boolean` already ride as their JSON type.
|
|
639
|
+
A streaming body is skipped (its frame encoding is deferred). undefined when no handler signature
|
|
640
|
+
resolves or no structured field is present, so the client bakes no plan.
|
|
641
|
+
*/
|
|
642
|
+
function outputWirePlan(
|
|
643
|
+
checker: ts.TypeChecker,
|
|
644
|
+
call: ts.CallExpression,
|
|
645
|
+
): OutputWirePlan | undefined {
|
|
646
|
+
const plan: OutputWirePlan = {}
|
|
647
|
+
const resolved = walkSuccessBodies(checker, call, (part, streaming) => {
|
|
648
|
+
/* Frame-level encoding for jsonl()/sse() is deferred (ADR-0029), and an untagged Response
|
|
649
|
+
body is `unknown` — neither yields a top-level field plan. */
|
|
650
|
+
if (streaming || part === undefined) {
|
|
651
|
+
return
|
|
652
|
+
}
|
|
653
|
+
for (const property of part.getProperties()) {
|
|
654
|
+
const kind = wireKind(checker, checker.getTypeOfSymbol(property))
|
|
655
|
+
if (kind === 'date' || kind === 'bigint' || kind === 'set' || kind === 'map') {
|
|
656
|
+
plan[property.getName()] = kind
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
})
|
|
660
|
+
if (!resolved || Object.keys(plan).length === 0) {
|
|
661
|
+
return undefined
|
|
662
|
+
}
|
|
663
|
+
return plan
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/*
|
|
667
|
+
The rpc export's INPUT args projected to JSON Schema (ADR-0030 input side) — the input-surface
|
|
668
|
+
sibling of `returnBodyJsonSchema`. Resolves the SAME wire `Args` bag `inputCoercionPlan` reads (the
|
|
669
|
+
exported RemoteFunction's first call-signature parameter, `FormData` dropped by `argsBagType`), then
|
|
670
|
+
builds an object schema, projecting each property through `jsonSchemaForType`. A File-typed property
|
|
671
|
+
is EXCLUDED exactly as `filesSchema` keeps File parts out of the `inputSchema` projection — a File has
|
|
672
|
+
no honest JSON-Schema form, so the multipart body advertises binaries generically instead. A property
|
|
673
|
+
is required unless it is `?` optional or its type bears `undefined`. This is purely a SHAPE
|
|
674
|
+
description: the surfaces consume it for docs, never for runtime validation, so it can't cause a 422.
|
|
675
|
+
undefined when no call signature / single-object Args bag resolves or every property is excluded, so
|
|
676
|
+
the caller stamps nothing and the surface falls open to an author-declared `schemas.input`.
|
|
677
|
+
*/
|
|
678
|
+
function inputArgsJsonSchema(
|
|
679
|
+
checker: ts.TypeChecker,
|
|
680
|
+
call: ts.CallExpression,
|
|
681
|
+
): Record<string, unknown> | undefined {
|
|
682
|
+
const signature = checker.getTypeAtLocation(call).getCallSignatures()[0]
|
|
683
|
+
if (signature === undefined) {
|
|
684
|
+
return undefined
|
|
685
|
+
}
|
|
686
|
+
const parameter = signature.getParameters()[0]
|
|
687
|
+
if (parameter === undefined) {
|
|
688
|
+
return undefined
|
|
689
|
+
}
|
|
690
|
+
const argsType = argsBagType(checker.getTypeOfSymbolAtLocation(parameter, call))
|
|
691
|
+
if (argsType === undefined) {
|
|
692
|
+
return undefined
|
|
693
|
+
}
|
|
694
|
+
const properties: Record<string, unknown> = {}
|
|
695
|
+
const required: string[] = []
|
|
696
|
+
for (const property of argsType.getProperties()) {
|
|
697
|
+
const propertyType = checker.getTypeOfSymbolAtLocation(property, call)
|
|
698
|
+
if (isFileType(propertyType)) {
|
|
699
|
+
continue
|
|
700
|
+
}
|
|
701
|
+
// jsonSchemaForType collapses a bare permissive projection to undefined; restore `{}` so an
|
|
702
|
+
// unprojectable property still appears as an "any JSON" field of the object.
|
|
703
|
+
properties[property.getName()] = jsonSchemaForType(checker, propertyType) ?? {}
|
|
704
|
+
const optional =
|
|
705
|
+
(property.flags & ts.SymbolFlags.Optional) !== 0 || bearsOptionality(propertyType)
|
|
706
|
+
if (!optional) {
|
|
707
|
+
required.push(property.getName())
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
if (Object.keys(properties).length === 0) {
|
|
711
|
+
return undefined
|
|
712
|
+
}
|
|
713
|
+
const schema: Record<string, unknown> = { type: 'object', properties }
|
|
714
|
+
if (required.length > 0) {
|
|
715
|
+
schema.required = required
|
|
716
|
+
}
|
|
717
|
+
return schema
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/* True when a type is (or, across a union, contains) `File`/`Blob` — the multipart binary members
|
|
721
|
+
excluded from the input-schema projection, mirroring how `filesSchema` stays out of `inputSchema`. */
|
|
722
|
+
function isFileType(type: ts.Type): boolean {
|
|
723
|
+
if (type.isUnion()) {
|
|
724
|
+
return type.types.some((member) => isFileType(member))
|
|
725
|
+
}
|
|
726
|
+
const name = type.getSymbol()?.name
|
|
727
|
+
return name === 'File' || name === 'Blob'
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/* True when a property's type bears `undefined`/`void` — an implicit optional even without the `?`
|
|
731
|
+
modifier (mirrors jsonSchemaForType's own `bearsUndefined` so the two projections agree). */
|
|
732
|
+
function bearsOptionality(type: ts.Type): boolean {
|
|
733
|
+
if (type.isUnion()) {
|
|
734
|
+
return type.types.some((member) => (member.flags & OPTIONALITY_FLAGS) !== 0)
|
|
735
|
+
}
|
|
736
|
+
return (type.flags & OPTIONALITY_FLAGS) !== 0
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/*
|
|
740
|
+
The object `Args` constituent of a RemoteCallable parameter. Every rpc call signature types its
|
|
741
|
+
first parameter `Args | FormData` (the multipart upload escape hatch), so the FormData and any
|
|
742
|
+
nullish members are dropped and the lone remaining type returned. undefined when the arg is not
|
|
743
|
+
a single object type — a no-input rpc (`Args = undefined`) or a FormData-only body — so no plan
|
|
744
|
+
is produced.
|
|
745
|
+
*/
|
|
746
|
+
function argsBagType(parameterType: ts.Type): ts.Type | undefined {
|
|
747
|
+
if (!parameterType.isUnion()) {
|
|
748
|
+
return parameterType
|
|
749
|
+
}
|
|
750
|
+
const kept = parameterType.types.filter(
|
|
751
|
+
(member) => member.getSymbol()?.name !== 'FormData' && (member.flags & NULLISH_FLAGS) === 0,
|
|
752
|
+
)
|
|
753
|
+
return kept.length === 1 ? kept[0] : undefined
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/*
|
|
757
|
+
A field's wire codec kind, or undefined when it isn't codec-eligible. Unwraps `T | undefined` (an
|
|
758
|
+
optional field) and a `T[]` (a repeated query key / JSON array whose ELEMENTS revive per item) to
|
|
759
|
+
the target type, then classifies: a pure `number`/`boolean`/`bigint` by its type flag, a `Date`/
|
|
760
|
+
`Set`/`Map` by its symbol identity through the checker. Anything else — a string, a `number |
|
|
761
|
+
string` union, a plain object — → undefined, so it stays its JSON form and the schema decides. The
|
|
762
|
+
symbol checks are top-level only: a `Set` NESTED in another value is not descended into (ADR-0029
|
|
763
|
+
defers recursive descent).
|
|
764
|
+
*/
|
|
765
|
+
function wireKind(checker: ts.TypeChecker, type: ts.Type): WireKind | undefined {
|
|
766
|
+
const bare = unwrapOptional(type)
|
|
767
|
+
const target = arrayElement(checker, bare) ?? bare
|
|
768
|
+
if (isKind(target, NUMBER_FLAGS)) {
|
|
769
|
+
return 'number'
|
|
770
|
+
}
|
|
771
|
+
if (isKind(target, BOOLEAN_FLAGS)) {
|
|
772
|
+
return 'boolean'
|
|
773
|
+
}
|
|
774
|
+
if (isKind(target, BIGINT_FLAGS)) {
|
|
775
|
+
return 'bigint'
|
|
776
|
+
}
|
|
777
|
+
const symbolName = target.getSymbol()?.name
|
|
778
|
+
if (symbolName === 'Date') {
|
|
779
|
+
return 'date'
|
|
780
|
+
}
|
|
781
|
+
if (symbolName === 'Set' || symbolName === 'ReadonlySet') {
|
|
782
|
+
return 'set'
|
|
783
|
+
}
|
|
784
|
+
if (symbolName === 'Map' || symbolName === 'ReadonlyMap') {
|
|
785
|
+
return 'map'
|
|
786
|
+
}
|
|
787
|
+
return undefined
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/* Drops the nullish members of a `T | undefined` optional; returns the lone survivor, else the
|
|
791
|
+
type unchanged (a genuine multi-member union stays as-is for isKind to reject). */
|
|
792
|
+
function unwrapOptional(type: ts.Type): ts.Type {
|
|
793
|
+
if (!type.isUnion()) {
|
|
794
|
+
return type
|
|
795
|
+
}
|
|
796
|
+
const kept = type.types.filter((member) => (member.flags & NULLISH_FLAGS) === 0)
|
|
797
|
+
const [onlyKept] = kept
|
|
798
|
+
return kept.length === 1 && onlyKept !== undefined ? onlyKept : type
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/* The element type of a `T[]`/`readonly T[]`, so a repeated query key (`?tag=1&tag=2`) coerces
|
|
802
|
+
per element; undefined for a non-array (tuples included — their element type is heterogeneous). */
|
|
803
|
+
function arrayElement(checker: ts.TypeChecker, type: ts.Type): ts.Type | undefined {
|
|
804
|
+
const name = type.getSymbol()?.name
|
|
805
|
+
if (name === 'Array' || name === 'ReadonlyArray') {
|
|
806
|
+
return checker.getTypeArguments(type as ts.TypeReference)[0]
|
|
807
|
+
}
|
|
808
|
+
return undefined
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/* True when every meaningful (non-nullish) member of the type matches `mask` — so `number`,
|
|
812
|
+
`number | undefined`, and a `1 | 2` literal union all read as number; a `number | string`
|
|
813
|
+
union does not. A bare `boolean` is a `true | false` union, handled by the recursion. */
|
|
814
|
+
function isKind(type: ts.Type, mask: ts.TypeFlags): boolean {
|
|
815
|
+
if (type.isUnion()) {
|
|
816
|
+
const meaningful = type.types.filter((member) => (member.flags & NULLISH_FLAGS) === 0)
|
|
817
|
+
return meaningful.length > 0 && meaningful.every((member) => isKind(member, mask))
|
|
818
|
+
}
|
|
819
|
+
return (type.flags & mask) !== 0
|
|
820
|
+
}
|