@abide/abide 0.49.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 +70 -52
- package/CHANGELOG.md +177 -0
- package/README.md +12 -7
- package/package.json +14 -5
- package/src/abideLsp.ts +5 -6
- package/src/abideResolverPlugin.ts +197 -169
- package/src/build.ts +79 -56
- package/src/buildDisconnected.ts +0 -2
- package/src/checkAbide.ts +12 -2
- 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/spawnEmbeddedServer.ts +13 -4
- package/src/lib/bundle/waitForServer.ts +6 -1
- package/src/lib/mcp/mcpTools.ts +15 -6
- 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 +98 -13
- package/src/lib/server/rpc/resolveRpcJsonSchema.ts +28 -0
- 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/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 +14 -14
- 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 +4 -1
- 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/changeAffectsClient.ts +12 -7
- package/src/lib/shared/createCacheStore.ts +31 -8
- 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/{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/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/snapshotShippable.ts +8 -7
- 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/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 -8
- 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/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 +51 -12
- package/src/lib/ui/compile/compileShadow.ts +145 -18
- package/src/lib/ui/compile/composeProps.ts +12 -2
- 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 +418 -36
- package/src/lib/ui/compile/expressionIsPrefixEvaluable.ts +19 -0
- package/src/lib/ui/compile/generateBuild.ts +52 -9
- package/src/lib/ui/compile/generateSSR.ts +242 -43
- 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/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 +26 -23
- 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 -1092
- 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 +21 -22
- 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 +43 -94
- 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/disposeRange.ts +2 -1
- package/src/lib/ui/dom/each.ts +26 -4
- package/src/lib/ui/dom/eachAsync.ts +39 -3
- 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/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/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/switchBlock.ts +30 -7
- package/src/lib/ui/dom/tryBlock.ts +230 -66
- 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 +2 -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/remoteProxy.ts +84 -159
- package/src/lib/ui/renderChain.ts +49 -13
- package/src/lib/ui/renderToStream.ts +22 -18
- package/src/lib/ui/resumeSeedScript.ts +1 -1
- package/src/lib/ui/router.ts +63 -36
- package/src/lib/ui/runtime/AsyncCellError.ts +20 -0
- package/src/lib/ui/runtime/CELL_SEED.ts +14 -0
- 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 +1 -1
- 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 +3 -42
- 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 +16 -6
- 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/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/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/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/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/mutateDocArray.ts +0 -38
- 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 -108
- package/src/lib/ui/installHotBridge.ts +0 -95
- 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 -43
- 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,7 +1,10 @@
|
|
|
1
1
|
import { carriesBodyArgs } from '../../shared/carriesBodyArgs.ts'
|
|
2
2
|
import { commandNameForUrl } from '../../shared/commandNameForUrl.ts'
|
|
3
3
|
import { jsonSchemaForSchema } from '../../shared/jsonSchemaForSchema.ts'
|
|
4
|
+
import type { ErrorJsonSchemas } from '../../shared/types/ErrorJsonSchemas.ts'
|
|
5
|
+
import { resolveInputJsonSchema, resolveOutputJsonSchema } from '../rpc/resolveRpcJsonSchema.ts'
|
|
4
6
|
import { rpcRegistry } from '../rpc/rpcRegistry.ts'
|
|
7
|
+
import { STATUS_TEXT } from './STATUS_TEXT.ts'
|
|
5
8
|
|
|
6
9
|
/*
|
|
7
10
|
Turns a rpc's resolved JSON Schema into OpenAPI query parameters — one
|
|
@@ -43,6 +46,34 @@ function multipartBodySchema(textSchema: Record<string, unknown>): Record<string
|
|
|
43
46
|
return schema
|
|
44
47
|
}
|
|
45
48
|
|
|
49
|
+
/*
|
|
50
|
+
Adds one `responses[status]` entry per typed-error branch (ADR-0030) — its description is the
|
|
51
|
+
status's standard reason phrase, and its `application/json` content is the error's projected `data`
|
|
52
|
+
schema (omitted for a bare `{}`, a nullary error with no payload). An already-present status (the
|
|
53
|
+
200, or any explicit response) is left untouched, so the success body and validation responses win a
|
|
54
|
+
collision. A no-op when the handler declared no typed errors (`errorJsonSchemas` undefined).
|
|
55
|
+
*/
|
|
56
|
+
function mergeErrorResponses(
|
|
57
|
+
responses: Record<string, unknown>,
|
|
58
|
+
errorJsonSchemas: ErrorJsonSchemas | undefined,
|
|
59
|
+
): void {
|
|
60
|
+
if (errorJsonSchemas === undefined) {
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
for (const [status, schema] of Object.entries(errorJsonSchemas)) {
|
|
64
|
+
if (responses[status] !== undefined) {
|
|
65
|
+
continue
|
|
66
|
+
}
|
|
67
|
+
const response: Record<string, unknown> = {
|
|
68
|
+
description: STATUS_TEXT[Number(status)] ?? 'Error',
|
|
69
|
+
}
|
|
70
|
+
if (Object.keys(schema).length > 0) {
|
|
71
|
+
response.content = { 'application/json': { schema } }
|
|
72
|
+
}
|
|
73
|
+
responses[status] = response
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
46
77
|
/*
|
|
47
78
|
Builds an OpenAPI 3.1 document from the rpc registry — the HTTP surface
|
|
48
79
|
every rpc exposes regardless of which non-browser clients it advertises.
|
|
@@ -58,25 +89,38 @@ export function buildOpenApiSpec(info: {
|
|
|
58
89
|
for (const entry of rpcRegistry.values()) {
|
|
59
90
|
const url = entry.remote.url
|
|
60
91
|
const method = entry.remote.method
|
|
61
|
-
|
|
92
|
+
/*
|
|
93
|
+
Describe the parameters/request body from the `inputSchema` VALIDATOR when declared, else from
|
|
94
|
+
the handler's input parameter type projected to JSON Schema at build time (ADR-0030 input side —
|
|
95
|
+
`entry.inputJsonSchema`); otherwise the opaque fallback. The validator overrides the projection
|
|
96
|
+
(a runtime narrowing the type can't express), mirroring the 200-body output path below.
|
|
97
|
+
*/
|
|
98
|
+
const jsonSchema = resolveInputJsonSchema(entry) ?? jsonSchemaForSchema(undefined)
|
|
62
99
|
const description = jsonSchema.description as string | undefined
|
|
63
100
|
/*
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
fall back to a bare OK.
|
|
101
|
+
Describe the 200 body from the `outputSchema` VALIDATOR when declared, else from the handler
|
|
102
|
+
return type projected to JSON Schema at build time (ADR-0030 D2 — `entry.outputJsonSchema`);
|
|
103
|
+
otherwise fall back to a bare OK. The validator overrides the projection (a runtime narrowing
|
|
104
|
+
the type can't express).
|
|
67
105
|
*/
|
|
68
106
|
const okResponse: Record<string, unknown> = { description: 'OK' }
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
schema: jsonSchemaForSchema(entry.outputSchema),
|
|
73
|
-
},
|
|
74
|
-
}
|
|
107
|
+
const outputSchema = resolveOutputJsonSchema(entry)
|
|
108
|
+
if (outputSchema) {
|
|
109
|
+
okResponse.content = { 'application/json': { schema: outputSchema } }
|
|
75
110
|
}
|
|
111
|
+
/*
|
|
112
|
+
Merge the handler's typed-error branches (ADR-0030 — `entry.errorJsonSchemas`, a status-keyed
|
|
113
|
+
data-schema map baked from the `error.typed(...)` return branches) into the responses. Each
|
|
114
|
+
status gets a `responses[status]` entry describing the error's data payload; an existing
|
|
115
|
+
response (the 200, or a future explicit entry) is never clobbered. So the 200 success body and
|
|
116
|
+
every typed error's status + payload are documented from the one handler return type.
|
|
117
|
+
*/
|
|
118
|
+
const responses: Record<string, unknown> = { '200': okResponse }
|
|
119
|
+
mergeErrorResponses(responses, entry.errorJsonSchemas)
|
|
76
120
|
const operation: Record<string, unknown> = {
|
|
77
121
|
operationId: commandNameForUrl(url),
|
|
78
122
|
...(description ? { description } : {}),
|
|
79
|
-
responses
|
|
123
|
+
responses,
|
|
80
124
|
}
|
|
81
125
|
if (carriesBodyArgs(method)) {
|
|
82
126
|
operation.requestBody = entry.filesSchema
|
|
@@ -18,7 +18,7 @@ const ROUTE_CHUNK = /"(\/[^"]*)"\s*:\s*\(\)\s*=>\s*import\("\.\/((?:page|layout)
|
|
|
18
18
|
/* Reads a `_app` chunk's source by filename — gunzipped from the embedded asset map
|
|
19
19
|
(standalone compile) or off disk (dev + `abide start`). Undefined on a miss. */
|
|
20
20
|
function chunkReader(
|
|
21
|
-
|
|
21
|
+
appDir: string,
|
|
22
22
|
assets?: Assets,
|
|
23
23
|
): (name: string) => Promise<string | undefined> {
|
|
24
24
|
if (assets) {
|
|
@@ -28,23 +28,25 @@ function chunkReader(
|
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
return async (name) => {
|
|
31
|
-
const file = Bun.file(`${
|
|
31
|
+
const file = Bun.file(`${appDir}/${name}`)
|
|
32
32
|
return (await file.exists()) ? file.text() : undefined
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
/* The hashed client entry filename (`client-<hash>.js`), from the asset map or disk.
|
|
37
|
-
|
|
36
|
+
/* The hashed client entry filename (`client-<hash>.js`), from the asset map or disk.
|
|
37
|
+
Each build's `appDir` holds exactly one entry (production `_app`, or a dev
|
|
38
|
+
generation dir), so `.find` is unambiguous. */
|
|
39
|
+
async function findEntry(appDir: string, assets?: Assets): Promise<string | undefined> {
|
|
38
40
|
const isEntry = (name: string): boolean => /^client-[a-z0-9]+\.js$/i.test(name)
|
|
39
41
|
if (assets) {
|
|
40
42
|
const key = Object.keys(assets).find((path) => isEntry(path.replace('/_app/', '')))
|
|
41
43
|
return key?.replace('/_app/', '')
|
|
42
44
|
}
|
|
43
|
-
if (!existsSync(
|
|
45
|
+
if (!existsSync(appDir)) {
|
|
44
46
|
return undefined
|
|
45
47
|
}
|
|
46
48
|
const names = await Array.fromAsync(
|
|
47
|
-
new Glob('client-*.js').scan({ cwd:
|
|
49
|
+
new Glob('client-*.js').scan({ cwd: appDir, onlyFiles: true }),
|
|
48
50
|
)
|
|
49
51
|
return names.find(isEntry)
|
|
50
52
|
}
|
|
@@ -64,14 +66,14 @@ stream-close instead of waterfalling after it. Returns an empty map when the bun
|
|
|
64
66
|
absent or unparseable, so rendering degrades to the entry-only preload.
|
|
65
67
|
*/
|
|
66
68
|
export async function buildPreloadManifest({
|
|
67
|
-
|
|
69
|
+
appDir,
|
|
68
70
|
assets,
|
|
69
71
|
}: {
|
|
70
|
-
|
|
72
|
+
appDir: string
|
|
71
73
|
assets?: Assets
|
|
72
74
|
}): Promise<Record<string, string[]>> {
|
|
73
|
-
const read = chunkReader(
|
|
74
|
-
const entryName = await findEntry(
|
|
75
|
+
const read = chunkReader(appDir, assets)
|
|
76
|
+
const entryName = await findEntry(appDir, assets)
|
|
75
77
|
const entrySource = entryName ? await read(entryName) : undefined
|
|
76
78
|
if (entryName === undefined || entrySource === undefined) {
|
|
77
79
|
return {}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { NO_STORE } from '../../shared/CACHE_CONTROL_VALUES.ts'
|
|
2
|
-
import { TEXT_PLAIN } from '../../shared/TEXT_PLAIN.ts'
|
|
3
1
|
import { acceptsGzip } from './acceptsGzip.ts'
|
|
4
2
|
import { cacheControlForAsset } from './cacheControlForAsset.ts'
|
|
5
3
|
import { containsTraversal } from './containsTraversal.ts'
|
|
6
4
|
import { createAssetHeaderCache } from './createAssetHeaderCache.ts'
|
|
7
5
|
import { globToPathSet } from './globToPathSet.ts'
|
|
8
6
|
import { respondWithEmbeddedAsset } from './respondWithEmbeddedAsset.ts'
|
|
7
|
+
import { textResponse } from './textResponse.ts'
|
|
9
8
|
import type { Assets } from './types/Assets.ts'
|
|
10
9
|
|
|
11
10
|
/*
|
|
@@ -24,23 +23,29 @@ Unlike the public server this answers every `/_app/` request itself (404 on
|
|
|
24
23
|
a miss — nothing falls through past the build tree). The path-traversal
|
|
25
24
|
guard inspects the raw request URL because the WHATWG parser normalizes
|
|
26
25
|
encoded `..` segments away before `url.pathname` is visible.
|
|
26
|
+
|
|
27
|
+
`appDir` is the physical directory the `/_app/*` URLs map onto: `dist/_app` for
|
|
28
|
+
a production build, or — under `abide dev` — the generation dir this worker was
|
|
29
|
+
spawned on (`ABIDE_APP_DIR`). The URL space stays `/_app/*` either way; only the
|
|
30
|
+
disk location this worker reads from moves, so a rebuild's new worker reads a new
|
|
31
|
+
dir while this one keeps serving its own, immutable for its lifetime.
|
|
27
32
|
*/
|
|
28
33
|
export async function createAppAssetServer({
|
|
29
|
-
|
|
34
|
+
appDir,
|
|
30
35
|
assets,
|
|
31
36
|
}: {
|
|
32
|
-
|
|
37
|
+
appDir: string
|
|
33
38
|
assets?: Assets
|
|
34
39
|
}): Promise<(req: Request, url: URL) => Promise<Response>> {
|
|
35
40
|
// Per-pathname asset header bundles, hashed-chunk-aware Cache-Control.
|
|
36
41
|
const headersForAsset = createAssetHeaderCache(cacheControlForAsset)
|
|
37
|
-
/* Boot snapshot of every disk
|
|
38
|
-
header cache keys on (request-controlled) pathnames,
|
|
39
|
-
`/_app/*` probes would grow it without bound. A
|
|
40
|
-
|
|
42
|
+
/* Boot snapshot of every disk chunk path (keyed by its `/_app/*` URL), mirroring
|
|
43
|
+
createPublicAssetServer: the header cache keys on (request-controlled) pathnames,
|
|
44
|
+
so building bundles for junk `/_app/*` probes would grow it without bound. A
|
|
45
|
+
rebuild spawns a fresh worker (against a fresh generation dir), re-snapshotting. */
|
|
41
46
|
const diskPaths = assets
|
|
42
47
|
? new Set<string>()
|
|
43
|
-
: await globToPathSet(
|
|
48
|
+
: await globToPathSet(appDir, '**/*', (file) => `/_app/${file}`)
|
|
44
49
|
/* Derive the precompressed `.gz` sibling set from the single tree scan (it already
|
|
45
50
|
includes the `.gz` files) — a gzip-capable client gets those bytes without
|
|
46
51
|
on-the-fly compression. Keyed by the base path (the `.gz` suffix stripped). */
|
|
@@ -49,11 +54,7 @@ export async function createAppAssetServer({
|
|
|
49
54
|
)
|
|
50
55
|
|
|
51
56
|
// Fresh 404 per call — a Response body is single-use, so it can't be hoisted to a const.
|
|
52
|
-
const notFound = (): Response =>
|
|
53
|
-
new Response('Not Found', {
|
|
54
|
-
status: 404,
|
|
55
|
-
headers: { 'Content-Type': TEXT_PLAIN, 'Cache-Control': NO_STORE },
|
|
56
|
-
})
|
|
57
|
+
const notFound = (): Response => textResponse(404)
|
|
57
58
|
|
|
58
59
|
return async function serveAppAsset(req, url) {
|
|
59
60
|
if (containsTraversal(req.url)) {
|
|
@@ -88,7 +89,9 @@ export async function createAppAssetServer({
|
|
|
88
89
|
return notFound()
|
|
89
90
|
}
|
|
90
91
|
const { base: baseHeaders, gzip: gzipHeaders } = headersForAsset(assetPath)
|
|
91
|
-
|
|
92
|
+
// Map the `/_app/<file>` URL onto the physical dir: strip the `/_app` prefix
|
|
93
|
+
// (5 chars) and join under appDir, which already IS the `_app` (or generation) dir.
|
|
94
|
+
const diskPath = appDir + assetPath.slice('/_app'.length)
|
|
92
95
|
if (acceptsGzip(req) && diskGzipPaths.has(assetPath)) {
|
|
93
96
|
return new Response(Bun.file(`${diskPath}.gz`), { headers: gzipHeaders })
|
|
94
97
|
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { NO_STORE } from '../../shared/CACHE_CONTROL_VALUES.ts'
|
|
2
|
+
import { matchRoute } from '../../shared/matchRoute.ts'
|
|
3
|
+
import { normalizePathname } from '../../shared/normalizePathname.ts'
|
|
4
|
+
import { parseRouteSegments } from '../../shared/parseRouteSegments.ts'
|
|
5
|
+
import type { Pages } from '../../ui/types/Pages.ts'
|
|
6
|
+
import type { RemoteRoutes } from '../rpc/types/RemoteRoutes.ts'
|
|
7
|
+
import type { createRouteDispatcher } from './createRouteDispatcher.ts'
|
|
8
|
+
|
|
9
|
+
/* A route handler pre-bound to one registered URL — the dispatcher's output. */
|
|
10
|
+
type RouteHandler = ReturnType<ReturnType<typeof createRouteDispatcher>>
|
|
11
|
+
|
|
12
|
+
/*
|
|
13
|
+
The URL-shape decision for one request, once the framework plumbing has passed
|
|
14
|
+
on it. A `handler` case (rpc, page, or the openapi doc) is dispatched through
|
|
15
|
+
the request scope; a `redirect` is returned verbatim; the asset cases name
|
|
16
|
+
which asset server serves the path, with `publicAsset` as the catch-all bucket
|
|
17
|
+
whose miss falls through to the 404 path. Keeping the decision data-only lets a
|
|
18
|
+
test assert it without booting Bun.serve — the effects (ALS dispatch, asset
|
|
19
|
+
serving, 404 render) stay in the fetch closure.
|
|
20
|
+
*/
|
|
21
|
+
export type RouteResolution =
|
|
22
|
+
| { kind: 'redirect'; response: Response }
|
|
23
|
+
| { kind: 'handler'; handler: RouteHandler; params: Record<string, string> }
|
|
24
|
+
| { kind: 'appAsset' }
|
|
25
|
+
| { kind: 'publicAsset' }
|
|
26
|
+
|
|
27
|
+
/*
|
|
28
|
+
Owns the URL-shape decisions that used to sit inline in the Bun.serve fetch
|
|
29
|
+
closure: canonical-slash redirects (308), the openapi document's build +
|
|
30
|
+
memo, and the asset-precedence ordering. Route dispatch (rpc-vs-page-vs-404,
|
|
31
|
+
method matching) stays behind createRouteDispatcher; this resolver composes its
|
|
32
|
+
per-URL handlers and adds the request-URL → decision layer on top, so both the
|
|
33
|
+
redirect logic and the memo semantics become unit-testable without a listener.
|
|
34
|
+
|
|
35
|
+
App routes are matched AFTER the `/__abide/*` plumbing (a reserved namespace no
|
|
36
|
+
app route occupies) and BEFORE the root-level framework surfaces (the openapi
|
|
37
|
+
doc, `/_app/`, public/ files), so a page route shadows a same-path public file
|
|
38
|
+
— the precedence the Bun routes table used to impose implicitly, now pinned
|
|
39
|
+
here. rpc URLs are flat literals (always `/rpc/...`) resolved by direct lookup;
|
|
40
|
+
page URLs carry `[name]` / `[[name]]` / `[...rest]` segments resolved through
|
|
41
|
+
the shared matchRoute — the same matcher the client router runs — so params
|
|
42
|
+
decode and precedence agree across the sides by construction.
|
|
43
|
+
*/
|
|
44
|
+
export function createAppRouteResolver({
|
|
45
|
+
pages,
|
|
46
|
+
rpc,
|
|
47
|
+
buildRouteHandler,
|
|
48
|
+
openApiPath,
|
|
49
|
+
buildOpenApiDocument,
|
|
50
|
+
}: {
|
|
51
|
+
pages: Pages
|
|
52
|
+
rpc: RemoteRoutes
|
|
53
|
+
buildRouteHandler: ReturnType<typeof createRouteDispatcher>
|
|
54
|
+
openApiPath: string
|
|
55
|
+
buildOpenApiDocument: () => Promise<Record<string, unknown>>
|
|
56
|
+
}): (req: Request, url: URL) => RouteResolution {
|
|
57
|
+
/* Handlers pre-bound per registered URL. rpc URLs dispatch by direct lookup;
|
|
58
|
+
page URLs resolve through matchRoute against pageRouteUrls below. */
|
|
59
|
+
const rpcHandlers = new Map<string, RouteHandler>()
|
|
60
|
+
for (const routeUrl of Object.keys(rpc)) {
|
|
61
|
+
rpcHandlers.set(routeUrl, buildRouteHandler(routeUrl))
|
|
62
|
+
}
|
|
63
|
+
const pageHandlers = new Map<string, RouteHandler>()
|
|
64
|
+
for (const routeUrl of Object.keys(pages)) {
|
|
65
|
+
/* A `[...rest]` consumes every remaining segment, so segments after it
|
|
66
|
+
can never constrain matching — the route would silently serve paths
|
|
67
|
+
it shouldn't. Fail at boot instead. */
|
|
68
|
+
const segments = parseRouteSegments(routeUrl)
|
|
69
|
+
const catchAllIndex = segments.findIndex(
|
|
70
|
+
(segment) => segment.kind === 'param' && segment.catchAll,
|
|
71
|
+
)
|
|
72
|
+
if (catchAllIndex !== -1 && catchAllIndex !== segments.length - 1) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
`[abide] invalid page route ${routeUrl}: a [...name] catch-all must be the last segment`,
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
pageHandlers.set(routeUrl, buildRouteHandler(routeUrl))
|
|
78
|
+
}
|
|
79
|
+
const pageRouteUrls = Object.keys(pages)
|
|
80
|
+
|
|
81
|
+
/* Built on first request, then reused — the rpc registry is frozen after load.
|
|
82
|
+
Memoised as a promise so two concurrent cold requests share one build instead
|
|
83
|
+
of both building (the second otherwise clobbering the first). A rejected build
|
|
84
|
+
clears the memo so the next request retries rather than caching the failure. */
|
|
85
|
+
let openApiSpec: Promise<Record<string, unknown>> | undefined
|
|
86
|
+
const openApiHandler: RouteHandler = async () => {
|
|
87
|
+
openApiSpec ??= buildOpenApiDocument().catch((error) => {
|
|
88
|
+
// Don't cache a failed build — clear the memo so a later request
|
|
89
|
+
// retries instead of 500-ing forever.
|
|
90
|
+
openApiSpec = undefined
|
|
91
|
+
throw error
|
|
92
|
+
})
|
|
93
|
+
return Response.json(await openApiSpec, { headers: { 'Cache-Control': NO_STORE } })
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return function resolveAppRoute(_req, url): RouteResolution {
|
|
97
|
+
const rpcHandler = rpcHandlers.get(url.pathname)
|
|
98
|
+
if (rpcHandler) {
|
|
99
|
+
return { kind: 'handler', handler: rpcHandler, params: {} }
|
|
100
|
+
}
|
|
101
|
+
/*
|
|
102
|
+
Pages match only in canonical slash form; a non-canonical request
|
|
103
|
+
(`/admin/`, `//admin`) that would match is 308'd to the canonical URL
|
|
104
|
+
instead of served. Serving it directly would hand app.handle — the auth
|
|
105
|
+
seam — a pathname the matcher silently normalized away, so an exact-match
|
|
106
|
+
guard on `/admin` never sees the request it's guarding (the old Bun routes
|
|
107
|
+
table 404'd these forms; the redirect keeps the guard sound AND the URL
|
|
108
|
+
friendly). rpc URLs stay exact-match strict, as they always were.
|
|
109
|
+
*/
|
|
110
|
+
const canonicalPathname = normalizePathname(url.pathname)
|
|
111
|
+
if (canonicalPathname !== url.pathname) {
|
|
112
|
+
if (matchRoute(pageRouteUrls, canonicalPathname)) {
|
|
113
|
+
return {
|
|
114
|
+
kind: 'redirect',
|
|
115
|
+
response: new Response(null, {
|
|
116
|
+
status: 308,
|
|
117
|
+
headers: {
|
|
118
|
+
Location: `${canonicalPathname}${url.search}`,
|
|
119
|
+
'Cache-Control': NO_STORE,
|
|
120
|
+
},
|
|
121
|
+
}),
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
const matchedPage = matchRoute(pageRouteUrls, url.pathname)
|
|
126
|
+
if (matchedPage) {
|
|
127
|
+
const pageHandler = pageHandlers.get(matchedPage.route)
|
|
128
|
+
if (pageHandler) {
|
|
129
|
+
return { kind: 'handler', handler: pageHandler, params: matchedPage.params }
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (url.pathname === openApiPath) {
|
|
134
|
+
return { kind: 'handler', handler: openApiHandler, params: {} }
|
|
135
|
+
}
|
|
136
|
+
/* Static assets sidestep ALS + the per-request CacheStore + app.handle
|
|
137
|
+
(see the fetch closure); `/_app/` build output takes precedence over
|
|
138
|
+
public/ files, and anything else falls to the public bucket whose miss
|
|
139
|
+
becomes the 404. */
|
|
140
|
+
if (url.pathname.startsWith('/_app/')) {
|
|
141
|
+
return { kind: 'appAsset' }
|
|
142
|
+
}
|
|
143
|
+
return { kind: 'publicAsset' }
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import type { Server } from 'bun'
|
|
2
|
+
import type { McpServer } from '../../mcp/types/McpServer.ts'
|
|
3
|
+
import { NO_STORE } from '../../shared/CACHE_CONTROL_VALUES.ts'
|
|
4
|
+
import { CLI_PATH } from '../../shared/CLI_PATH.ts'
|
|
5
|
+
import { DEV_REBUILD_PATH } from '../../shared/DEV_REBUILD_PATH.ts'
|
|
6
|
+
import { DEV_RELOAD_PATH } from '../../shared/DEV_RELOAD_PATH.ts'
|
|
7
|
+
import { HEALTH_PATH } from '../../shared/HEALTH_PATH.ts'
|
|
8
|
+
import { IDENTITY_PATH } from '../../shared/IDENTITY_PATH.ts'
|
|
9
|
+
import { INSPECTOR_PATH } from '../../shared/INSPECTOR_PATH.ts'
|
|
10
|
+
import { lenientDecode } from '../../shared/lenientDecode.ts'
|
|
11
|
+
import { MCP_PATH } from '../../shared/MCP_PATH.ts'
|
|
12
|
+
import { SOCKETS_PATH } from '../../shared/SOCKETS_PATH.ts'
|
|
13
|
+
import { TEXT_PLAIN } from '../../shared/TEXT_PLAIN.ts'
|
|
14
|
+
import { handleCliDownload } from '../cli/handleCliDownload.ts'
|
|
15
|
+
import { handleCliInstall } from '../cli/handleCliInstall.ts'
|
|
16
|
+
import type { createSocketDispatcher } from '../sockets/createSocketDispatcher.ts'
|
|
17
|
+
import type { buildHealthPayload } from './buildHealthPayload.ts'
|
|
18
|
+
import { crossOriginGate } from './crossOriginGate.ts'
|
|
19
|
+
import { DEV_REBUILD_MESSAGE } from './DEV_REBUILD_MESSAGE.ts'
|
|
20
|
+
import { devReloadResponse } from './devReloadResponse.ts'
|
|
21
|
+
import { disableIdleTimeoutForStream } from './disableIdleTimeoutForStream.ts'
|
|
22
|
+
import { gzipResponse } from './gzipResponse.ts'
|
|
23
|
+
import { textResponse } from './textResponse.ts'
|
|
24
|
+
import type { DevReloadStamp } from './types/DevReloadStamp.ts'
|
|
25
|
+
import type { RequestStore } from './types/RequestStore.ts'
|
|
26
|
+
|
|
27
|
+
const SOCKETS_REST_PREFIX = `${SOCKETS_PATH}/`
|
|
28
|
+
const CLI_DOWNLOAD_PREFIX = `${CLI_PATH}/`
|
|
29
|
+
|
|
30
|
+
/*
|
|
31
|
+
Returned by the router when a request matches none of its framework routes, so
|
|
32
|
+
the fetch handler falls through to the app's rpc/page/asset routes. A dedicated
|
|
33
|
+
sentinel — not `undefined` — because a successful socket upgrade legitimately
|
|
34
|
+
returns `undefined` (Bun's signal that it took the connection over), which must
|
|
35
|
+
stay distinct from "not a plumbing route".
|
|
36
|
+
*/
|
|
37
|
+
export const PLUMBING_PASS: unique symbol = Symbol('abide.plumbingPass')
|
|
38
|
+
|
|
39
|
+
/* Either the handled response (buffered, streamed, or `undefined` for a socket
|
|
40
|
+
upgrade handoff) or the pass sentinel. Returned synchronously so the common
|
|
41
|
+
app-route path pays no extra await/promise allocation — only a matched route
|
|
42
|
+
yields a promise. */
|
|
43
|
+
type PlumbingResult = Response | undefined | Promise<Response | undefined> | typeof PLUMBING_PASS
|
|
44
|
+
|
|
45
|
+
/* dispatchRequest, injected: runs a handler inside the per-request scope + app.handle middleware. */
|
|
46
|
+
type DispatchRequest = (
|
|
47
|
+
req: Request,
|
|
48
|
+
pathParams: Record<string, string>,
|
|
49
|
+
handler: (
|
|
50
|
+
req: Request,
|
|
51
|
+
pathParams: Record<string, string>,
|
|
52
|
+
store: RequestStore,
|
|
53
|
+
) => Promise<Response>,
|
|
54
|
+
url: URL,
|
|
55
|
+
) => Promise<Response>
|
|
56
|
+
|
|
57
|
+
/*
|
|
58
|
+
The framework's own HTTP surface — health/identity probe, inspector, the dev
|
|
59
|
+
live-reload + rebuild channels, the sockets upgrade and its
|
|
60
|
+
SSE/JSON HTTP face, the MCP endpoint, and CLI install/download — resolved ahead
|
|
61
|
+
of the app's rpc/page routes. Extracted from createServer's fetch handler as a
|
|
62
|
+
sibling of createRouteDispatcher: the app-route half already sat behind a seam,
|
|
63
|
+
this is the framework-plumbing half, testable without a live socket. The probe
|
|
64
|
+
and dev/inspector channels answer directly (ahead of app.handle) so they land
|
|
65
|
+
even when the app guards everything behind auth; the sockets-REST/MCP/CLI faces
|
|
66
|
+
run through dispatchRequest so app.handle auth applies like the rpc paths.
|
|
67
|
+
Returns PLUMBING_PASS for any path it doesn't own, so the caller continues to
|
|
68
|
+
the app routes.
|
|
69
|
+
*/
|
|
70
|
+
export function createPlumbingRouter({
|
|
71
|
+
dev,
|
|
72
|
+
clientFingerprint,
|
|
73
|
+
inspectorHandler,
|
|
74
|
+
socketDispatcher,
|
|
75
|
+
mcp,
|
|
76
|
+
cliName,
|
|
77
|
+
cliCwd,
|
|
78
|
+
healthPayload,
|
|
79
|
+
dispatchRequest,
|
|
80
|
+
}: {
|
|
81
|
+
dev: boolean
|
|
82
|
+
clientFingerprint: DevReloadStamp | undefined
|
|
83
|
+
inspectorHandler: ((request: Request, url: URL) => Promise<Response>) | undefined
|
|
84
|
+
socketDispatcher: ReturnType<typeof createSocketDispatcher>
|
|
85
|
+
mcp: McpServer | undefined
|
|
86
|
+
cliName: string
|
|
87
|
+
cliCwd: string
|
|
88
|
+
healthPayload: (req: Request) => ReturnType<typeof buildHealthPayload>
|
|
89
|
+
dispatchRequest: DispatchRequest
|
|
90
|
+
}): (req: Request, url: URL, bunServer: Server<unknown>) => PlumbingResult {
|
|
91
|
+
/*
|
|
92
|
+
Health/identity probe — answered directly, ahead of any app.handle
|
|
93
|
+
middleware, so the bundle's connect screen, the CLI, and the client
|
|
94
|
+
health() can confirm a URL really is a live abide server even when the app
|
|
95
|
+
guards everything behind auth (reporting `authenticated: false` requires
|
|
96
|
+
exactly that). The app's optional health hook contributes fields; the
|
|
97
|
+
framework's identity keys win on collision, and a thrown hook is logged and
|
|
98
|
+
skipped so an app bug can't masquerade as an unreachable server.
|
|
99
|
+
IDENTITY_PATH is the compatibility alias for the same payload.
|
|
100
|
+
*/
|
|
101
|
+
async function healthProbe(req: Request, url: URL): Promise<Response> {
|
|
102
|
+
const payload = await healthPayload(req)
|
|
103
|
+
return gzipResponse(
|
|
104
|
+
req,
|
|
105
|
+
Response.json(
|
|
106
|
+
/*
|
|
107
|
+
The IDENTITY_PATH alias keeps the legacy `abide: true` shape:
|
|
108
|
+
already-shipped probers check it with strict equality, and a
|
|
109
|
+
version string would make them treat an upgraded healthy server
|
|
110
|
+
as not-abide.
|
|
111
|
+
*/
|
|
112
|
+
url.pathname === IDENTITY_PATH ? { ...payload, abide: true } : payload,
|
|
113
|
+
{ headers: { 'Cache-Control': NO_STORE } },
|
|
114
|
+
),
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return function routePlumbing(req, url, bunServer) {
|
|
119
|
+
if (url.pathname === HEALTH_PATH || url.pathname === IDENTITY_PATH) {
|
|
120
|
+
return healthProbe(req, url)
|
|
121
|
+
}
|
|
122
|
+
/*
|
|
123
|
+
Inspector surface — answered directly, ahead of app.handle, since it's
|
|
124
|
+
privileged operator tooling gated by ABIDE_ENABLE_INSPECTOR (not the
|
|
125
|
+
app's user auth). Undefined handler = flag off, so the whole block
|
|
126
|
+
compiles out of the hot path when the inspector's off.
|
|
127
|
+
*/
|
|
128
|
+
if (
|
|
129
|
+
inspectorHandler &&
|
|
130
|
+
(url.pathname === INSPECTOR_PATH || url.pathname.startsWith(`${INSPECTOR_PATH}/`))
|
|
131
|
+
) {
|
|
132
|
+
// The events feed is long-lived SSE: opt it out of the idle timeout,
|
|
133
|
+
// else Bun reaps it and the reconnect replays the whole buffer
|
|
134
|
+
// (duplicate boot logs every ~10s).
|
|
135
|
+
return inspectorHandler(req, url).then((response) =>
|
|
136
|
+
disableIdleTimeoutForStream(bunServer, req, response),
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
/*
|
|
140
|
+
Dev live-reload channel — answered directly, ahead of app.handle, so a
|
|
141
|
+
restart-driven reconnect always lands even when the app guards
|
|
142
|
+
everything behind auth. Only mounted under `abide dev`.
|
|
143
|
+
*/
|
|
144
|
+
if (clientFingerprint !== undefined && url.pathname === DEV_RELOAD_PATH) {
|
|
145
|
+
// Long-lived SSE: opt out of the idle timeout, else Bun reaps it and
|
|
146
|
+
// the reconnect triggers a spurious reload loop.
|
|
147
|
+
return disableIdleTimeoutForStream(bunServer, req, devReloadResponse(clientFingerprint))
|
|
148
|
+
}
|
|
149
|
+
/*
|
|
150
|
+
Manual rebuild trigger: signal the orchestrator parent over IPC to
|
|
151
|
+
rebuild + restart. Same-origin sibling of the live-reload channel, so a
|
|
152
|
+
script refreshes on the app's own port. process.send exists only when
|
|
153
|
+
the dev orchestrator spawned us with ipc; the optional chain no-ops on a
|
|
154
|
+
bare server.
|
|
155
|
+
*/
|
|
156
|
+
if (dev && req.method === 'POST' && url.pathname === DEV_REBUILD_PATH) {
|
|
157
|
+
process.send?.(DEV_REBUILD_MESSAGE)
|
|
158
|
+
return new Response('rebuilding\n', { headers: { 'Content-Type': TEXT_PLAIN } })
|
|
159
|
+
}
|
|
160
|
+
if (url.pathname === SOCKETS_PATH) {
|
|
161
|
+
// Reject cross-origin upgrades (CSWSH) before handing off to Bun.
|
|
162
|
+
const upgradeForbidden = crossOriginGate(req, url)
|
|
163
|
+
if (upgradeForbidden) {
|
|
164
|
+
return upgradeForbidden
|
|
165
|
+
}
|
|
166
|
+
if (bunServer.upgrade(req, { data: {} })) {
|
|
167
|
+
return undefined
|
|
168
|
+
}
|
|
169
|
+
return textResponse(400, 'Upgrade failed')
|
|
170
|
+
}
|
|
171
|
+
/*
|
|
172
|
+
HTTP face of a socket (`/__abide/sockets/<name>`) — tail over SSE / JSON
|
|
173
|
+
and publish — for the CLI and MCP. Runs through dispatchRequest so
|
|
174
|
+
app.handle auth applies, like the rpc paths. The socket name may contain
|
|
175
|
+
`/` (nested files), so it's the whole remaining pathname, percent-decoded.
|
|
176
|
+
*/
|
|
177
|
+
if (url.pathname.startsWith(SOCKETS_REST_PREFIX)) {
|
|
178
|
+
/*
|
|
179
|
+
Gate cross-origin browser publishes (CSRF, see crossOriginGate). GET
|
|
180
|
+
tail reads stay open cross-origin like rpc reads; only the mutating
|
|
181
|
+
POST is gated.
|
|
182
|
+
*/
|
|
183
|
+
const publishForbidden = crossOriginGate(req, url, { allowReadOnly: true })
|
|
184
|
+
if (publishForbidden) {
|
|
185
|
+
return publishForbidden
|
|
186
|
+
}
|
|
187
|
+
const name = lenientDecode(url.pathname.slice(SOCKETS_REST_PREFIX.length))
|
|
188
|
+
return dispatchRequest(req, {}, async () => socketDispatcher.rest(req, name), url)
|
|
189
|
+
}
|
|
190
|
+
if (url.pathname === MCP_PATH && mcp) {
|
|
191
|
+
// Gate cross-site browser posts (CSRF, see crossOriginGate).
|
|
192
|
+
const mcpForbidden = crossOriginGate(req, url)
|
|
193
|
+
if (mcpForbidden) {
|
|
194
|
+
return mcpForbidden
|
|
195
|
+
}
|
|
196
|
+
return dispatchRequest(req, {}, async () => mcp.handle(req), url)
|
|
197
|
+
}
|
|
198
|
+
if (url.pathname === CLI_PATH) {
|
|
199
|
+
return dispatchRequest(req, {}, async () => handleCliInstall(req, cliName), url)
|
|
200
|
+
}
|
|
201
|
+
if (url.pathname.startsWith(CLI_DOWNLOAD_PREFIX)) {
|
|
202
|
+
const platform = url.pathname.slice(CLI_DOWNLOAD_PREFIX.length)
|
|
203
|
+
return dispatchRequest(
|
|
204
|
+
req,
|
|
205
|
+
{},
|
|
206
|
+
async () => handleCliDownload(req, platform, cliName, cliCwd),
|
|
207
|
+
url,
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
return PLUMBING_PASS
|
|
211
|
+
}
|
|
212
|
+
}
|
|
@@ -2,12 +2,12 @@ import { NO_STORE } from '../../shared/CACHE_CONTROL_VALUES.ts'
|
|
|
2
2
|
import { memoizeByKey } from '../../shared/memoizeByKey.ts'
|
|
3
3
|
import { NAV_HEADER } from '../../shared/NAV_HEADER.ts'
|
|
4
4
|
import { REMOTE_FUNCTION } from '../../shared/REMOTE_FUNCTION.ts'
|
|
5
|
-
import { TEXT_PLAIN } from '../../shared/TEXT_PLAIN.ts'
|
|
6
5
|
import type { HttpMethod } from '../../shared/types/HttpMethod.ts'
|
|
7
6
|
import type { RemoteFunction } from '../../shared/types/RemoteFunction.ts'
|
|
8
7
|
import type { Pages } from '../../ui/types/Pages.ts'
|
|
9
8
|
import type { RemoteRoutes } from '../rpc/types/RemoteRoutes.ts'
|
|
10
9
|
import { crossOriginGate } from './crossOriginGate.ts'
|
|
10
|
+
import { textResponse } from './textResponse.ts'
|
|
11
11
|
import type { RequestStore } from './types/RequestStore.ts'
|
|
12
12
|
|
|
13
13
|
type AnyRemoteFunction = RemoteFunction<unknown, unknown>
|
|
@@ -26,12 +26,9 @@ type RenderPage = (
|
|
|
26
26
|
store: RequestStore,
|
|
27
27
|
) => Promise<Response>
|
|
28
28
|
|
|
29
|
-
/* The framework's 405 — `Allow` names the permitted rpc(s)
|
|
29
|
+
/* The framework's 405 — `Allow` names the permitted rpc(s); body/headers come from the shared textResponse so the rpc and page branches can't drift. */
|
|
30
30
|
function methodNotAllowed(allow: string): Response {
|
|
31
|
-
return
|
|
32
|
-
status: 405,
|
|
33
|
-
headers: { Allow: allow, 'Content-Type': TEXT_PLAIN, 'Cache-Control': NO_STORE },
|
|
34
|
-
})
|
|
31
|
+
return textResponse(405, undefined, { Allow: allow })
|
|
35
32
|
}
|
|
36
33
|
|
|
37
34
|
/*
|
|
@@ -118,10 +115,7 @@ export function createRouteDispatcher({
|
|
|
118
115
|
}
|
|
119
116
|
return renderPage(routeUrl, pathParams, store)
|
|
120
117
|
}
|
|
121
|
-
return
|
|
122
|
-
status: 404,
|
|
123
|
-
headers: { 'Content-Type': TEXT_PLAIN, 'Cache-Control': NO_STORE },
|
|
124
|
-
})
|
|
118
|
+
return textResponse(404)
|
|
125
119
|
}
|
|
126
120
|
}
|
|
127
121
|
}
|