@abide/abide 0.42.0 → 0.43.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 +234 -300
- package/CHANGELOG.md +34 -0
- package/README.md +50 -93
- package/package.json +3 -2
- package/src/abideResolverPlugin.ts +104 -14
- package/src/buildCli.ts +1 -1
- package/src/discoveryEntry.ts +3 -3
- package/src/lib/bundle/BundleMenuItem.ts +1 -1
- package/src/lib/cli/createClient.ts +12 -12
- package/src/lib/cli/dispatchCommand.ts +1 -1
- package/src/lib/cli/printSessionStatus.ts +1 -1
- package/src/lib/cli/resolveCliTarget.ts +1 -1
- package/src/lib/cli/runCli.ts +1 -1
- package/src/lib/cli/types/CliManifest.ts +1 -1
- package/src/lib/cli/types/CliManifestEntry.ts +2 -2
- package/src/lib/mcp/annotationsForMethod.ts +5 -5
- package/src/lib/mcp/createMcpServer.ts +3 -3
- package/src/lib/mcp/mcpSurface.ts +14 -14
- package/src/lib/mcp/types/McpServerOptions.ts +1 -1
- package/src/lib/server/DELETE.ts +4 -4
- package/src/lib/server/GET.ts +4 -4
- package/src/lib/server/HEAD.ts +4 -4
- package/src/lib/server/PATCH.ts +4 -4
- package/src/lib/server/POST.ts +4 -4
- package/src/lib/server/PUT.ts +4 -4
- package/src/lib/server/agent.ts +4 -4
- package/src/lib/server/env.ts +1 -1
- package/src/lib/server/error.ts +49 -7
- package/src/lib/server/json.ts +1 -1
- package/src/lib/server/rpc/buildErrorConstructors.ts +19 -0
- package/src/lib/server/rpc/{defineVerb.ts → defineRpc.ts} +52 -34
- package/src/lib/server/rpc/{dispatchVerbInProcess.ts → dispatchRpcInProcess.ts} +3 -3
- package/src/lib/server/rpc/fieldErrorsFromIssues.ts +23 -0
- package/src/lib/server/rpc/{findVerbByCommandName.ts → findRpcByCommandName.ts} +5 -5
- package/src/lib/server/rpc/parseArgs.ts +6 -6
- package/src/lib/server/rpc/readBodyWithinLimit.ts +2 -2
- package/src/lib/server/rpc/registerRpc.ts +6 -0
- package/src/lib/server/rpc/{verbRegistry.ts → rpcRegistry.ts} +4 -4
- package/src/lib/server/rpc/{runWithVerbTimeout.ts → runWithRpcTimeout.ts} +4 -4
- package/src/lib/server/rpc/types/RemoteHandler.ts +9 -3
- package/src/lib/server/rpc/types/{VerbHelper.ts → RpcHelper.ts} +57 -23
- package/src/lib/server/rpc/types/{VerbRegistryEntry.ts → RpcRegistryEntry.ts} +3 -3
- package/src/lib/server/rpc/types/TypedResponse.ts +2 -2
- package/src/lib/server/rpc/unprocessed.ts +6 -7
- package/src/lib/server/rpc/validationError.ts +17 -0
- package/src/lib/server/runtime/buildInspectorSurface.ts +7 -7
- package/src/lib/server/runtime/buildOpenApiSpec.ts +6 -6
- package/src/lib/server/runtime/createRouteDispatcher.ts +7 -7
- package/src/lib/server/runtime/createServer.ts +4 -4
- package/src/lib/server/runtime/crossOriginForbidden.ts +1 -1
- package/src/lib/server/runtime/crossOriginGate.ts +4 -4
- package/src/lib/server/runtime/logExposedSurfaces.ts +4 -4
- package/src/lib/server/runtime/registryManifests.ts +2 -2
- package/src/lib/server/runtime/types/InspectorCacheEntry.ts +1 -1
- package/src/lib/server/runtime/types/InspectorRpc.ts +27 -0
- package/src/lib/server/runtime/types/InspectorSurface.ts +4 -4
- package/src/lib/server/runtime/types/RequestStore.ts +1 -1
- package/src/lib/server/runtime/warnUnguardedMcp.ts +2 -2
- package/src/lib/server/sockets/createSocketDispatcher.ts +15 -14
- package/src/lib/server/sockets/types/SocketOperation.ts +2 -2
- package/src/lib/shared/HttpError.ts +15 -1
- package/src/lib/shared/UNREACHABLE_STATUSES.ts +13 -0
- package/src/lib/shared/buildRpcRequest.ts +5 -5
- package/src/lib/shared/cache.ts +50 -168
- package/src/lib/shared/carriesBodyArgs.ts +4 -4
- package/src/lib/shared/createCacheStore.ts +12 -0
- package/src/lib/shared/createRemoteFunction.ts +11 -6
- package/src/lib/shared/decodeResponse.ts +2 -2
- package/src/lib/shared/detectRpcMethod.ts +17 -0
- package/src/lib/shared/emitLogRecord.ts +3 -3
- package/src/lib/shared/extraForwardHeaders.ts +1 -1
- package/src/lib/shared/findExportCallSite.ts +30 -273
- package/src/lib/shared/forwardHeaders.ts +2 -2
- package/src/lib/shared/httpErrorFor.ts +26 -0
- package/src/lib/shared/isAsciiWhitespace.ts +5 -0
- package/src/lib/shared/isIdentPart.ts +9 -0
- package/src/lib/shared/isIdentStart.ts +8 -0
- package/src/lib/shared/isReadOnlyMethod.ts +4 -4
- package/src/lib/shared/keyForRemoteCall.ts +4 -4
- package/src/lib/shared/log.ts +1 -1
- package/src/lib/shared/outboxProbeSlot.ts +20 -0
- package/src/lib/shared/pending.ts +4 -1
- package/src/lib/shared/prepareRpcModule.ts +96 -14
- package/src/lib/shared/probeRegistries.ts +17 -4
- package/src/lib/shared/queryStringFromArgs.ts +1 -1
- package/src/lib/shared/remoteMetaStore.ts +2 -2
- package/src/lib/shared/resolveClientFlags.ts +1 -1
- package/src/lib/shared/skipNonCode.ts +230 -0
- package/src/lib/shared/streamResponse.ts +9 -6
- package/src/lib/shared/stripImport.ts +3 -3
- package/src/lib/shared/types/CacheEntry.ts +2 -4
- package/src/lib/shared/types/CacheOnContext.ts +1 -11
- package/src/lib/shared/types/CacheStore.ts +13 -7
- package/src/lib/shared/types/ClientFlags.ts +1 -1
- package/src/lib/shared/types/ErrorConstructors.ts +17 -0
- package/src/lib/shared/types/ErrorDescriptor.ts +10 -0
- package/src/lib/shared/types/ErrorSpec.ts +11 -0
- package/src/lib/shared/types/HttpMethod.ts +1 -0
- package/src/lib/shared/types/Outbox.ts +9 -0
- package/src/lib/shared/types/OutboxEntry.ts +27 -0
- package/src/lib/shared/types/RawRemoteFunction.ts +2 -2
- package/src/lib/shared/types/RemoteCallable.ts +2 -2
- package/src/lib/shared/types/RemoteFunction.ts +21 -6
- package/src/lib/shared/types/RequestScopeInfo.ts +1 -1
- package/src/lib/shared/types/RpcErrorGuard.ts +40 -0
- package/src/lib/shared/types/RpcInvoker.ts +1 -1
- package/src/lib/shared/types/StandardSchemaV1.ts +1 -1
- package/src/lib/shared/types/ValidationErrorData.ts +20 -0
- package/src/lib/shared/url.ts +3 -3
- package/src/lib/shared/writeRpcDts.ts +7 -7
- package/src/lib/shared/writeTestRpcDts.ts +3 -3
- package/src/lib/test/createTestApp.ts +10 -10
- package/src/lib/ui/compile/awaitPlan.ts +48 -0
- package/src/lib/ui/compile/compileShadow.ts +75 -21
- package/src/lib/ui/compile/desugarSignals.ts +42 -6
- package/src/lib/ui/compile/generateBuild.ts +43 -60
- package/src/lib/ui/compile/generateSSR.ts +84 -54
- package/src/lib/ui/compile/ifPlan.ts +30 -0
- package/src/lib/ui/compile/parseTemplate.ts +41 -11
- package/src/lib/ui/compile/stripEffects.ts +17 -9
- package/src/lib/ui/compile/switchPlan.ts +22 -0
- package/src/lib/ui/compile/tryPlan.ts +29 -0
- package/src/lib/ui/compile/types/TemplateAttr.ts +8 -2
- package/src/lib/ui/compile/types/TemplateNode.ts +6 -2
- package/src/lib/ui/createScope.ts +20 -2
- package/src/lib/ui/dom/disposeRange.ts +6 -10
- package/src/lib/ui/dom/hydrate.ts +2 -5
- package/src/lib/ui/dom/mount.ts +1 -2
- package/src/lib/ui/dom/withScope.ts +6 -9
- package/src/lib/ui/effect.ts +4 -2
- package/src/lib/ui/navigate.ts +40 -13
- package/src/lib/ui/outbox.ts +30 -110
- package/src/lib/ui/remoteProxy.ts +160 -10
- package/src/lib/ui/router.ts +3 -3
- package/src/lib/ui/rpcOutbox/createOutboxQueue.ts +281 -0
- package/src/lib/ui/rpcOutbox/outboxRegistry.ts +69 -0
- package/src/lib/ui/socketChannel.ts +7 -1
- package/src/lib/ui/types/Scope.ts +20 -5
- package/template/CLAUDE.md +1 -1
- package/template/src/server/rpc/getHello.ts +3 -3
- package/template/test/app.test.ts +2 -2
- package/src/lib/server/rpc/registerVerb.ts +0 -6
- package/src/lib/server/runtime/types/InspectorVerb.ts +0 -27
- package/src/lib/shared/detectVerbMethod.ts +0 -17
- package/src/lib/shared/types/HttpVerb.ts +0 -1
- package/src/lib/ui/types/Outbox.ts +0 -14
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/* The lifecycle status of a parked mutation: waiting to (re)send, or currently sending.
|
|
2
|
+
A write lives in the outbox ONLY while it can't reach the server; the instant the server
|
|
3
|
+
responds at all — a 2xx OR a real 4xx/500 — the entry leaves the queue, so there is no
|
|
4
|
+
terminal `error` state. Only the unreachable family (transport failure / 502/503/504/52x)
|
|
5
|
+
keeps it `queued`. */
|
|
6
|
+
export type OutboxStatus = 'queued' | 'sending'
|
|
7
|
+
|
|
8
|
+
/* One durable, replayable mutation in an RPC's outbox — parked because the original
|
|
9
|
+
call couldn't reach the server (transport failure, or a 502/503/504/52x). `controller`
|
|
10
|
+
is the entry's own abort handle (cancel = `controller.abort()`); `request` is the
|
|
11
|
+
synthesized, persisted Request the drain re-sends; `args` is the typed input (for
|
|
12
|
+
rendering); `error` is why it's parked (the `kind: 'queued'` HttpError from the
|
|
13
|
+
unreachable attempt); `retry()` kicks a FIFO drain. `settled` is the eventual outcome
|
|
14
|
+
of THIS write as if the original call had reached the server — it resolves with the
|
|
15
|
+
decoded result on delivery and rejects with the real HttpError on a server refusal (or
|
|
16
|
+
an AbortError on cancel). It may stay pending indefinitely if the write is never
|
|
17
|
+
replayed; reading it is what arms it (a never-read `settled` never rejects). */
|
|
18
|
+
export type OutboxEntry<Args> = {
|
|
19
|
+
id: string
|
|
20
|
+
controller: AbortController
|
|
21
|
+
request: Request
|
|
22
|
+
args: Args
|
|
23
|
+
status: OutboxStatus
|
|
24
|
+
error?: unknown
|
|
25
|
+
retry: () => Promise<void>
|
|
26
|
+
settled: Promise<unknown>
|
|
27
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { HttpMethod } from './HttpMethod.ts'
|
|
2
2
|
import type { RemoteCallable } from './RemoteCallable.ts'
|
|
3
3
|
|
|
4
4
|
/*
|
|
@@ -9,6 +9,6 @@ so callers that need status / headers / body streaming or want to
|
|
|
9
9
|
implement custom error handling can opt out of the decode.
|
|
10
10
|
*/
|
|
11
11
|
export type RawRemoteFunction<Args> = RemoteCallable<Args, Response> & {
|
|
12
|
-
readonly method:
|
|
12
|
+
readonly method: HttpMethod
|
|
13
13
|
readonly url: string
|
|
14
14
|
}
|
|
@@ -2,8 +2,8 @@ import type { RpcOptions } from './RpcOptions.ts'
|
|
|
2
2
|
|
|
3
3
|
/*
|
|
4
4
|
Call signature shared by RemoteFunction and RawRemoteFunction. The base
|
|
5
|
-
signature keeps `args` required so a schema'd
|
|
6
|
-
input; when `Args` admits undefined (no-input
|
|
5
|
+
signature keeps `args` required so a schema'd rpc can't silently drop its
|
|
6
|
+
input; when `Args` admits undefined (no-input rpcs) an intersected
|
|
7
7
|
optional-arg signature lets call sites write `fn()` instead of
|
|
8
8
|
`fn(undefined)`. Intersection rather than a bare conditional so the type
|
|
9
9
|
stays callable while `Args` is still generic (cache() invokes producers
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import type { ClientFlags } from './ClientFlags.ts'
|
|
2
|
-
import type {
|
|
2
|
+
import type { ErrorSpec } from './ErrorSpec.ts'
|
|
3
|
+
import type { HttpMethod } from './HttpMethod.ts'
|
|
4
|
+
import type { Outbox } from './Outbox.ts'
|
|
3
5
|
import type { RawRemoteFunction } from './RawRemoteFunction.ts'
|
|
4
6
|
import type { RemoteCallable } from './RemoteCallable.ts'
|
|
7
|
+
import type { RpcErrorGuard } from './RpcErrorGuard.ts'
|
|
5
8
|
import type { Subscribable } from './Subscribable.ts'
|
|
6
9
|
|
|
7
10
|
/*
|
|
@@ -25,23 +28,35 @@ HTTP rpc isn't the place for long-lived multi-publisher subscriptions.
|
|
|
25
28
|
`.fetch(req)` is the framework's request-dispatch entry point — used by
|
|
26
29
|
the router to invoke the handler from an incoming HTTP request, not
|
|
27
30
|
for user code.
|
|
28
|
-
`crossOrigin` (server-side only, set via the
|
|
29
|
-
mutating
|
|
31
|
+
`crossOrigin` (server-side only, set via the rpc's opts) exempts a
|
|
32
|
+
mutating rpc from the router's same-origin CSRF gate, accepting browser
|
|
30
33
|
requests whose Origin doesn't match the app's own host.
|
|
31
34
|
*/
|
|
32
35
|
/*
|
|
33
|
-
A body
|
|
36
|
+
A body rpc (POST/PUT/PATCH) also accepts a FormData in place of typed args:
|
|
34
37
|
buildRpcRequest ships it as a multipart body and the server splits text fields
|
|
35
38
|
into args (still schema-validated) and File parts into files(). FormData is
|
|
36
39
|
stringly-typed, so this is the upload escape hatch — typed object args remain
|
|
37
40
|
the default for everything else.
|
|
38
41
|
*/
|
|
39
|
-
export type RemoteFunction<
|
|
40
|
-
|
|
42
|
+
export type RemoteFunction<
|
|
43
|
+
Args,
|
|
44
|
+
Return,
|
|
45
|
+
Errors extends ErrorSpec = Record<never, never>,
|
|
46
|
+
> = RemoteCallable<Args, Return> & {
|
|
47
|
+
readonly method: HttpMethod
|
|
41
48
|
readonly url: string
|
|
42
49
|
readonly clients: ClientFlags
|
|
43
50
|
readonly crossOrigin?: boolean
|
|
44
51
|
readonly raw: RawRemoteFunction<Args>
|
|
45
52
|
stream(args?: Args | FormData): Subscribable<Return>
|
|
46
53
|
fetch(request: Request): Promise<Response>
|
|
54
|
+
/* Type-guard a caught error against this rpc's declared `errors` (plus the framework
|
|
55
|
+
`'validation'` / `'queued'`): narrows `.kind` and, for a known kind, `.data` — the
|
|
56
|
+
per-rpc replacement for a global guard, since the error name → data type mapping
|
|
57
|
+
lives in the rpc's own spec. */
|
|
58
|
+
readonly isError: RpcErrorGuard<Errors>
|
|
59
|
+
/* Present only on a durable (`outbox`) RPC's client proxy: callable for the reactive
|
|
60
|
+
list of undelivered entries, with `retry()` to drain the queue. Undefined otherwise. */
|
|
61
|
+
readonly outbox?: Outbox<Args>
|
|
47
62
|
}
|
|
@@ -3,7 +3,7 @@ import type { TraceContext } from './TraceContext.ts'
|
|
|
3
3
|
/*
|
|
4
4
|
The request-scope facts the shared layer reads through requestScopeSlot:
|
|
5
5
|
trace position, elapsed ms since the scope opened (request start on the
|
|
6
|
-
server, navigation start in the browser), and the
|
|
6
|
+
server, navigation start in the browser), and the method+path that anchors log
|
|
7
7
|
lines. Resolved fresh per read so `elapsedMs` is current at the call.
|
|
8
8
|
*/
|
|
9
9
|
export type RequestScopeInfo = {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { HttpError } from '../HttpError.ts'
|
|
2
|
+
import type { ErrorSpec } from './ErrorSpec.ts'
|
|
3
|
+
import type { OutboxEntry } from './OutboxEntry.ts'
|
|
4
|
+
import type { StandardSchemaV1 } from './StandardSchemaV1.ts'
|
|
5
|
+
import type { ValidationErrorData } from './ValidationErrorData.ts'
|
|
6
|
+
|
|
7
|
+
/* The payload a declared error name carries: its data schema's inferred input, or
|
|
8
|
+
`unknown` for a nullary error (no data schema). */
|
|
9
|
+
type DeclaredErrorData<
|
|
10
|
+
Errors extends ErrorSpec,
|
|
11
|
+
Name extends keyof Errors,
|
|
12
|
+
> = Errors[Name]['data'] extends StandardSchemaV1
|
|
13
|
+
? StandardSchemaV1.InferInput<Errors[Name]['data']>
|
|
14
|
+
: unknown
|
|
15
|
+
|
|
16
|
+
/*
|
|
17
|
+
A rpc's `isError` guard — branch a caught error on a kind THIS rpc knows, with the
|
|
18
|
+
data type narrowed from the rpc's own `errors` spec. `rpc.isError(err, 'outOfStock')`
|
|
19
|
+
narrows `.data` to that error's payload; the framework-reserved `'validation'` /
|
|
20
|
+
`'queued'` to their fixed shapes; any other string narrows `.kind` only (`.data`
|
|
21
|
+
stays `unknown` — it belongs to a different rpc, whose type isn't in scope here).
|
|
22
|
+
|
|
23
|
+
The declared-name overload comes first so an app error shadows the reserved ones on a
|
|
24
|
+
name collision, and so the kind argument autocompletes to the rpc's declared names.
|
|
25
|
+
*/
|
|
26
|
+
export interface RpcErrorGuard<Errors extends ErrorSpec> {
|
|
27
|
+
<Name extends keyof Errors & string>(
|
|
28
|
+
error: unknown,
|
|
29
|
+
kind: Name,
|
|
30
|
+
): error is HttpError & { kind: Name; data: DeclaredErrorData<Errors, Name> }
|
|
31
|
+
(
|
|
32
|
+
error: unknown,
|
|
33
|
+
kind: 'validation',
|
|
34
|
+
): error is HttpError & { kind: 'validation'; data: ValidationErrorData }
|
|
35
|
+
(
|
|
36
|
+
error: unknown,
|
|
37
|
+
kind: 'queued',
|
|
38
|
+
): error is HttpError & { kind: 'queued'; data: OutboxEntry<unknown> }
|
|
39
|
+
<Kind extends string>(error: unknown, kind: Kind): error is HttpError & { kind: Kind }
|
|
40
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* A
|
|
1
|
+
/* A rpc invoker mirrors the rpc's own call: a plain call decodes the body
|
|
2
2
|
(throws HttpError on non-2xx), `.raw` returns the Response untouched. Shared
|
|
3
3
|
by every name→callable RPC proxy (the CLI client, the test harness). */
|
|
4
4
|
export type RpcInvoker = ((args?: unknown) => Promise<unknown>) & {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Mirror of the Standard Schema v1 spec interface (standardschema.dev). Any
|
|
3
3
|
library that implements the spec — zod, valibot, arktype, etc. — produces
|
|
4
4
|
values whose `~standard` property structurally matches this shape, so users
|
|
5
|
-
can pass their existing schemas to
|
|
5
|
+
can pass their existing schemas to rpc helpers without an adapter.
|
|
6
6
|
|
|
7
7
|
Kept inline (no `@standard-schema/spec` dep) because the spec is type-only
|
|
8
8
|
and tiny; adding a package for ~30 lines of interface would be churn. The
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from './StandardSchemaV1.ts'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
The `data` payload an input/file validation failure carries on its HttpError —
|
|
5
|
+
`kind: 'validation'`, status 422. `issues` is the raw Standard Schema issue list
|
|
6
|
+
(every message + full path); `fields` is the form-friendly top-level-field →
|
|
7
|
+
first-message map (fieldErrorsFromIssues). Because a throw can't carry the rpc's
|
|
8
|
+
per-kind type to the catch, `HttpError.data` is typed `unknown` — narrow it with
|
|
9
|
+
this when `err.kind === 'validation'`:
|
|
10
|
+
|
|
11
|
+
if (err instanceof HttpError && err.kind === 'validation') {
|
|
12
|
+
const { fields } = err.data as ValidationErrorData
|
|
13
|
+
// fields.email === 'Required', …
|
|
14
|
+
}
|
|
15
|
+
*/
|
|
16
|
+
// @documentation response
|
|
17
|
+
export type ValidationErrorData = {
|
|
18
|
+
readonly issues: readonly StandardSchemaV1.Issue[]
|
|
19
|
+
readonly fields: Record<string, string>
|
|
20
|
+
}
|
package/src/lib/shared/url.ts
CHANGED
|
@@ -11,7 +11,7 @@ type Query = Record<string, QueryValue>
|
|
|
11
11
|
/*
|
|
12
12
|
Augmentable rpc map. Codegen emits a `declare module '<importName>/shared/url'`
|
|
13
13
|
block filling this with `'/rpc/<file>': args` for each query-carrying (GET)
|
|
14
|
-
|
|
14
|
+
rpc, so url('/rpc/search', { q }) types its args against the rpc's own
|
|
15
15
|
signature. Empty by default — an absent entry falls through to the page/asset
|
|
16
16
|
branch, so the helper works before the generated d.ts lands.
|
|
17
17
|
*/
|
|
@@ -47,7 +47,7 @@ no bracket segments yields {}, which collapses the params slot away. The
|
|
|
47
47
|
catch-all branch recurses on its head too, so `[name]` segments before a
|
|
48
48
|
`[...rest]` are kept.
|
|
49
49
|
*/
|
|
50
|
-
type PathParams<P extends string> = P extends `${infer Head}[...${infer Rest}]${infer Tail}`
|
|
50
|
+
export type PathParams<P extends string> = P extends `${infer Head}[...${infer Rest}]${infer Tail}`
|
|
51
51
|
? PathParams<Head> & { [K in Rest]: string | number } & PathParams<Tail>
|
|
52
52
|
: P extends `${string}[${infer Name}]${infer Tail}`
|
|
53
53
|
? { [K in Name]: string | number } & PathParams<Tail>
|
|
@@ -59,7 +59,7 @@ Resolves any in-app URL to its base-correct, typed form — the single chokepoin
|
|
|
59
59
|
so a project mounted under APP_URL's subpath (e.g. /v2) generates links, asset
|
|
60
60
|
refs, and rpc hrefs that stay within the mount. Three disjoint URL kinds, keyed
|
|
61
61
|
off the path:
|
|
62
|
-
- rpc (flat /rpc/*, present in RpcRoutes): the
|
|
62
|
+
- rpc (flat /rpc/*, present in RpcRoutes): the rpc's args, serialised to query.
|
|
63
63
|
- page route (has [name] segments): path params, then optional query.
|
|
64
64
|
- asset / paramless / raw: bare path, then optional query.
|
|
65
65
|
External URLs (scheme-qualified or protocol-relative) skip the base untouched,
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { carriesBodyArgs } from './carriesBodyArgs.ts'
|
|
2
|
-
import {
|
|
2
|
+
import { detectRpcMethod } from './detectRpcMethod.ts'
|
|
3
3
|
import { fileStem } from './fileStem.ts'
|
|
4
4
|
import { rpcUrlForFile } from './rpcUrlForFile.ts'
|
|
5
5
|
import { writeDts } from './writeDts.ts'
|
|
6
6
|
|
|
7
7
|
/*
|
|
8
8
|
Emits a `.d.ts` that augments abide's `RpcRoutes` interface with one entry per
|
|
9
|
-
query-carrying $rpc
|
|
10
|
-
the
|
|
11
|
-
a url() can carry a query string but not a request body, so a POST
|
|
12
|
-
URL form. `RpcArgs` lifts the args type out of the
|
|
9
|
+
query-carrying $rpc rpc, so `url('/rpc/search', { q })` types its args against
|
|
10
|
+
the rpc's own signature. Only GET/DELETE/HEAD (non-body) rpcs are included —
|
|
11
|
+
a url() can carry a query string but not a request body, so a POST rpc has no
|
|
12
|
+
URL form. `RpcArgs` lifts the args type out of the rpc's RemoteFunction
|
|
13
13
|
(dropping the FormData upload variant); the file path resolves the export by
|
|
14
14
|
its filename, the abide one-export-per-file convention. Written to
|
|
15
15
|
`src/.abide/rpc.d.ts` so the consumer's src tsconfig include picks it up, keyed
|
|
@@ -28,8 +28,8 @@ export async function writeRpcDts({
|
|
|
28
28
|
}): Promise<void> {
|
|
29
29
|
const lines = await Promise.all(
|
|
30
30
|
rpcFiles.map(async (file) => {
|
|
31
|
-
const method =
|
|
32
|
-
// A body
|
|
31
|
+
const method = detectRpcMethod(await Bun.file(`${rpcDir}/${file}`).text())
|
|
32
|
+
// A body rpc's args can't ride a URL — leave it out of the url() rpc map.
|
|
33
33
|
if (!method || carriesBodyArgs(method)) {
|
|
34
34
|
return undefined
|
|
35
35
|
}
|
|
@@ -5,9 +5,9 @@ import { writeDts } from './writeDts.ts'
|
|
|
5
5
|
|
|
6
6
|
/*
|
|
7
7
|
Emits a `.d.ts` that augments createTestApp's `RpcClient` interface with one
|
|
8
|
-
entry per $rpc
|
|
9
|
-
resolves at runtime). Each entry lifts the
|
|
10
|
-
its RemoteFunction so `app.rpc.getProduct({ id })` types against the
|
|
8
|
+
entry per $rpc rpc, keyed by command name (the same key `app.rpc.<name>`
|
|
9
|
+
resolves at runtime). Each entry lifts the rpc's args + resolved return out of
|
|
10
|
+
its RemoteFunction so `app.rpc.getProduct({ id })` types against the rpc's own
|
|
11
11
|
signature — args in, decoded body out, plus `.raw` for the Response. `RpcArgs`
|
|
12
12
|
drops the FormData upload variant exactly as writeRpcDts does; `RpcReturn`
|
|
13
13
|
reads the resolved body type. Written to `src/.abide/testRpc.d.ts` so the
|
|
@@ -25,7 +25,7 @@ import { rpc } from '../../_virtual/rpc.ts'
|
|
|
25
25
|
import { shell } from '../../_virtual/shell.ts'
|
|
26
26
|
// @ts-expect-error virtual module resolved by abideResolverPlugin
|
|
27
27
|
import { sockets } from '../../_virtual/sockets.ts'
|
|
28
|
-
import {
|
|
28
|
+
import { rpcRegistry } from '../server/rpc/rpcRegistry.ts'
|
|
29
29
|
import { createServer } from '../server/runtime/createServer.ts'
|
|
30
30
|
import { ensureRegistriesLoaded } from '../server/runtime/registryManifests.ts'
|
|
31
31
|
import { requestContext } from '../server/runtime/requestContext.ts'
|
|
@@ -46,8 +46,8 @@ import type { RemoteFunction } from '../shared/types/RemoteFunction.ts'
|
|
|
46
46
|
import { createTestSocketChannel } from './createTestSocketChannel.ts'
|
|
47
47
|
|
|
48
48
|
/*
|
|
49
|
-
Augmentable
|
|
50
|
-
build's writeTestRpcDts / writeTestSocketsDts emit one entry per
|
|
49
|
+
Augmentable rpc/socket maps for `app.rpc.<rpc>` / `app.sockets.<name>`. The
|
|
50
|
+
build's writeTestRpcDts / writeTestSocketsDts emit one entry per rpc/socket
|
|
51
51
|
(into src/.abide/), so the keys + signatures are the project's real surface
|
|
52
52
|
with no imports. Empty here; types arrive once the app has been built. Mirrors
|
|
53
53
|
url's RpcRoutes / health's AppHealthMap.
|
|
@@ -58,7 +58,7 @@ export interface RpcClient {}
|
|
|
58
58
|
// biome-ignore lint/suspicious/noEmptyInterface: augmented by the generated testSockets.d.ts
|
|
59
59
|
export interface SocketClient {}
|
|
60
60
|
|
|
61
|
-
/* The booted app under test. Every named subsystem is reachable as the
|
|
61
|
+
/* The booted app under test. Every named subsystem is reachable as the rpc
|
|
62
62
|
you call, the socket you iterate, or a path you fetch — over the real
|
|
63
63
|
server, so the full pipeline (CSRF, cookies, base path) runs. */
|
|
64
64
|
export type TestApp = {
|
|
@@ -69,7 +69,7 @@ export type TestApp = {
|
|
|
69
69
|
matches routes at raw paths — the APP_URL mount base is stripped by an
|
|
70
70
|
external proxy in production, absent here — so paths carry no base. */
|
|
71
71
|
fetch: (path: string, init?: RequestInit) => Promise<Response>
|
|
72
|
-
/*
|
|
72
|
+
/* Rpc calls over HTTP, keyed by command name: `app.rpc.getProduct({ id })`. */
|
|
73
73
|
rpc: RpcClient
|
|
74
74
|
/* Sockets keyed by name: `app.sockets.ticker` is the Socket — iterate it for
|
|
75
75
|
the live stream, `.tail(n)` to seed, `.publish(m)` to send. */
|
|
@@ -88,7 +88,7 @@ export type TestApp = {
|
|
|
88
88
|
Boots the real app on an ephemeral port — the same wiring serverEntry performs,
|
|
89
89
|
minus the standalone-binary env layers. Imports the framework's virtual
|
|
90
90
|
manifests (resolved by abideResolverPlugin, registered via `abide/preload`
|
|
91
|
-
in the consumer's bunfig), so the routes,
|
|
91
|
+
in the consumer's bunfig), so the routes, rpcs, and sockets are the project's
|
|
92
92
|
real surface, not a fixture. Pass nothing: `await createTestApp()` is the app
|
|
93
93
|
exactly as `bun start` would serve it.
|
|
94
94
|
|
|
@@ -138,19 +138,19 @@ export async function createTestApp(): Promise<TestApp> {
|
|
|
138
138
|
return fetch(`${origin}${path}`, init)
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
/*
|
|
141
|
+
/* Rpc modules loaded once so the registry holds every RemoteFunction; the
|
|
142
142
|
proxy maps command name → an HTTP call against the booted server. */
|
|
143
143
|
await ensureRegistriesLoaded()
|
|
144
144
|
const remotes = new Map<string, RemoteFunction<unknown, unknown>>(
|
|
145
|
-
Array.from(
|
|
145
|
+
Array.from(rpcRegistry.values()).map((entry) => [
|
|
146
146
|
commandNameForUrl(entry.remote.url),
|
|
147
147
|
entry.remote,
|
|
148
148
|
]),
|
|
149
149
|
)
|
|
150
150
|
|
|
151
151
|
function send(remote: RemoteFunction<unknown, unknown>, args: unknown): Promise<Response> {
|
|
152
|
-
/* Same-origin Origin header so the CSRF gate admits mutating
|
|
153
|
-
server serves
|
|
152
|
+
/* Same-origin Origin header so the CSRF gate admits mutating rpcs; the
|
|
153
|
+
server serves rpcs at their raw url (no mount base applied here). */
|
|
154
154
|
const request = buildRpcRequest({
|
|
155
155
|
method: remote.method,
|
|
156
156
|
url: remote.url,
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { resolveBranches } from './resolveBranches.ts'
|
|
2
|
+
import type { TemplateNode } from './types/TemplateNode.ts'
|
|
3
|
+
|
|
4
|
+
/* The structural shape of an `await` block, resolved ONCE from the node so the build and
|
|
5
|
+
SSR back-ends share one reading of it and only own emission. Both previously recomputed
|
|
6
|
+
branch resolution, the `finallyChildren` default, the blocking-vs-streaming split of the
|
|
7
|
+
resolved content/binding, and the surface-the-rejection rule — the parts that can silently
|
|
8
|
+
diverge. Same single-source-of-truth model the positional walks already use. */
|
|
9
|
+
export type AwaitPlan = {
|
|
10
|
+
/* `then` on the tag → blocking: no pending branch, the children ARE the resolved content
|
|
11
|
+
(settled before first flush). Absent → streaming: pending flushes, `then` resolves later. */
|
|
12
|
+
blocking: boolean
|
|
13
|
+
/* The pending content (streaming) — `[]` when blocking. */
|
|
14
|
+
pending: TemplateNode[]
|
|
15
|
+
/* The resolved content + its bound name: the children-minus-branch bound to `node.as`
|
|
16
|
+
when blocking, the `then` branch bound to its `as` when streaming. `_value` default. */
|
|
17
|
+
resolvedChildren: TemplateNode[]
|
|
18
|
+
resolvedAs: string
|
|
19
|
+
/* The catch content + its bound name (`_error` default); empty when no catch branch. */
|
|
20
|
+
catchChildren: TemplateNode[]
|
|
21
|
+
catchAs: string
|
|
22
|
+
finallyChildren: TemplateNode[]
|
|
23
|
+
/* Neither catch nor finally → a rejection must surface (re-throw / `undefined` catch thunk)
|
|
24
|
+
instead of rendering an empty branch. */
|
|
25
|
+
surfaceRejection: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/* Resolves an `await` node's branches into the shared structural plan. */
|
|
29
|
+
export function awaitPlan(node: Extract<TemplateNode, { kind: 'await' }>): AwaitPlan {
|
|
30
|
+
const [thenBranch, catchBranch, finallyBranch] = resolveBranches(
|
|
31
|
+
node,
|
|
32
|
+
'then',
|
|
33
|
+
'catch',
|
|
34
|
+
'finally',
|
|
35
|
+
)
|
|
36
|
+
const finallyChildren = finallyBranch?.children ?? []
|
|
37
|
+
const nonBranch = node.children.filter((child) => child.kind !== 'branch')
|
|
38
|
+
return {
|
|
39
|
+
blocking: node.blocking,
|
|
40
|
+
pending: node.blocking ? [] : nonBranch,
|
|
41
|
+
resolvedChildren: node.blocking ? nonBranch : (thenBranch?.children ?? []),
|
|
42
|
+
resolvedAs: (node.blocking ? node.as : thenBranch?.as) ?? '_value',
|
|
43
|
+
catchChildren: catchBranch?.children ?? [],
|
|
44
|
+
catchAs: catchBranch?.as ?? '_error',
|
|
45
|
+
finallyChildren,
|
|
46
|
+
surfaceRejection: catchBranch === undefined && finallyChildren.length === 0,
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -282,6 +282,14 @@ function scopeLineFor(
|
|
|
282
282
|
propsShapes.push(shape === undefined ? 'Record<string, any>' : verbatim(shape))
|
|
283
283
|
return { text: `const ${verbatim(declaration)};`, segments: [span(declaration, 6)] }
|
|
284
284
|
}
|
|
285
|
+
/* The rewrite drops the callee from the line, so hovering `state`/`computed`/
|
|
286
|
+
`linked` at its call site has nothing to resolve. Append the callee as a
|
|
287
|
+
trailing reference statement (`…; state;`), mapped back to its source span,
|
|
288
|
+
so it resolves to the same primitive the destructure does. */
|
|
289
|
+
const withCalleeRef = (line: ScopeLine): ScopeLine => ({
|
|
290
|
+
text: `${line.text} ${verbatim(call.expression)};`,
|
|
291
|
+
segments: [...line.segments, span(call.expression, line.text.length + 1)],
|
|
292
|
+
})
|
|
285
293
|
if (callee === 'state') {
|
|
286
294
|
/* state<T>(initial): T is the value type — carry it onto the `let` so an
|
|
287
295
|
explicit annotation isn't lost to `any`/`any[]` inference of the initial. */
|
|
@@ -297,13 +305,16 @@ function scopeLineFor(
|
|
|
297
305
|
then correctly flagged possibly-undefined; a guard narrows cleanly. */
|
|
298
306
|
const valueType = annotation === '' ? ': unknown' : `${annotation} | undefined`
|
|
299
307
|
/* map the binding name (offset 4, past `let `) so hover/go-to resolve on it */
|
|
300
|
-
return {
|
|
308
|
+
return withCalleeRef({
|
|
309
|
+
text: `let ${name}!${valueType};`,
|
|
310
|
+
segments: [span(declaration.name, 4)],
|
|
311
|
+
})
|
|
301
312
|
}
|
|
302
313
|
const prefix = `let ${name}${annotation} = (`
|
|
303
|
-
return {
|
|
314
|
+
return withCalleeRef({
|
|
304
315
|
text: `${prefix}${verbatim(init)});`,
|
|
305
316
|
segments: [span(declaration.name, 4), span(init, prefix.length)],
|
|
306
|
-
}
|
|
317
|
+
})
|
|
307
318
|
}
|
|
308
319
|
/* computed<T>(compute) / linked<T>(seed) — the only callees left: T is the value
|
|
309
320
|
type — the call's first arg is a thunk, so invoking it yields the value. Annotate
|
|
@@ -317,16 +328,16 @@ function scopeLineFor(
|
|
|
317
328
|
/* binding-name map offset = past the keyword + space (`let ` = 4, `const ` = 6) */
|
|
318
329
|
const keywordOffset = keyword.length + 1
|
|
319
330
|
if (fn === undefined) {
|
|
320
|
-
return {
|
|
331
|
+
return withCalleeRef({
|
|
321
332
|
text: `${keyword} ${name} = undefined;`,
|
|
322
333
|
segments: [span(declaration.name, keywordOffset)],
|
|
323
|
-
}
|
|
334
|
+
})
|
|
324
335
|
}
|
|
325
336
|
const prefix = `${keyword} ${name}${annotation} = (`
|
|
326
|
-
return {
|
|
337
|
+
return withCalleeRef({
|
|
327
338
|
text: `${prefix}${verbatim(fn)})();`,
|
|
328
339
|
segments: [span(declaration.name, keywordOffset), span(fn, prefix.length)],
|
|
329
|
-
}
|
|
340
|
+
})
|
|
330
341
|
}
|
|
331
342
|
|
|
332
343
|
/* Emits a sibling list — each node standalone via `emitNode`. */
|
|
@@ -359,23 +370,29 @@ function emitNode(node: TemplateNode, builder: Builder): void {
|
|
|
359
370
|
emitNodes(node.children, builder)
|
|
360
371
|
return
|
|
361
372
|
case 'component': {
|
|
362
|
-
/*
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
373
|
+
/* The imported tag resolves (via the shadow host) to the child's
|
|
374
|
+
`(props: Props) => …` default, so `Parameters<typeof Child>[0]` is its prop
|
|
375
|
+
shape. `on*` / `bind:` / `attach` props are framework-handled passthrough
|
|
376
|
+
(not part of the declared shape), so they are checked leniently — each value
|
|
377
|
+
against its own declared key type — never flagged as excess. */
|
|
378
|
+
const handled = (prop: { name: string; spread?: boolean }): boolean =>
|
|
379
|
+
prop.spread === true ||
|
|
380
|
+
prop.name.startsWith('on') ||
|
|
381
|
+
prop.name.startsWith('bind:') ||
|
|
382
|
+
prop.name.startsWith('class:') ||
|
|
383
|
+
prop.name.startsWith('style:') ||
|
|
384
|
+
prop.name === 'attach'
|
|
385
|
+
const hasSpread = node.props.some((prop) => prop.spread)
|
|
386
|
+
for (const prop of node.props.filter(handled)) {
|
|
387
|
+
/* Lead with a defensive `;`: an IIFE/object-literal arg starts with `(` or
|
|
388
|
+
`{`, so without it a preceding scope statement left unterminated (a script
|
|
389
|
+
ending in a call with no trailing semicolon, e.g. `effect(() => …)`)
|
|
390
|
+
merges across the newline into `effect(…)(…)` — a spurious "not callable". */
|
|
374
391
|
if (prop.spread) {
|
|
375
392
|
/* A `{...expr}` spread contributes a SUBSET of the props (required ones
|
|
376
393
|
may come from another spread/explicit prop), so check it against
|
|
377
|
-
`Partial<Props>` — every key it does carry must match
|
|
378
|
-
|
|
394
|
+
`Partial<Props>` — every key it does carry must match, without
|
|
395
|
+
demanding completeness. */
|
|
379
396
|
builder.raw(`;((__spread: Partial<Parameters<typeof ${node.name}>[0]>) => {})(`)
|
|
380
397
|
} else {
|
|
381
398
|
builder.raw(
|
|
@@ -385,6 +402,43 @@ function emitNode(node: TemplateNode, builder: Builder): void {
|
|
|
385
402
|
builder.expr(prop.code, prop.loc)
|
|
386
403
|
builder.raw(');\n')
|
|
387
404
|
}
|
|
405
|
+
/* The plain data props as one object-literal argument typed to the child's whole
|
|
406
|
+
prop shape: a missing required prop errors on the literal (anchored at the tag
|
|
407
|
+
via the mapped `{`), an unknown prop errors on its key, a wrong type on its
|
|
408
|
+
value. Skipped when a spread is present — a spread may supply required props,
|
|
409
|
+
so completeness can't be demanded; the data props fall back to lenient per-key
|
|
410
|
+
checks instead. */
|
|
411
|
+
const dataProps = node.props.filter((prop) => !handled(prop))
|
|
412
|
+
if (hasSpread) {
|
|
413
|
+
for (const prop of dataProps) {
|
|
414
|
+
builder.raw(
|
|
415
|
+
`;((__prop: Parameters<typeof ${node.name}>[0][${JSON.stringify(prop.name)}]) => {})(`,
|
|
416
|
+
)
|
|
417
|
+
builder.expr(prop.code, prop.loc)
|
|
418
|
+
builder.raw(');\n')
|
|
419
|
+
}
|
|
420
|
+
} else {
|
|
421
|
+
builder.raw(`;((__c: Parameters<typeof ${node.name}>[0]): void => { void __c })({`)
|
|
422
|
+
/* A zero-length anchor right after `{`, pointing at the tag: a missing-
|
|
423
|
+
required-prop error spans the literal from `{`, so it overlaps this and
|
|
424
|
+
maps to the tag (an empty span trivially satisfies the source-text ==
|
|
425
|
+
shadow-text invariant). */
|
|
426
|
+
builder.mapped('', node.loc)
|
|
427
|
+
builder.raw('\n')
|
|
428
|
+
for (const prop of dataProps) {
|
|
429
|
+
/* The key mapped to its source name (excess-prop errors land on the key);
|
|
430
|
+
the value verbatim-mapped (wrong-type errors land on the value). */
|
|
431
|
+
if (prop.nameLoc !== undefined) {
|
|
432
|
+
builder.mapped(prop.name, prop.nameLoc)
|
|
433
|
+
} else {
|
|
434
|
+
builder.raw(prop.name)
|
|
435
|
+
}
|
|
436
|
+
builder.raw(': ')
|
|
437
|
+
builder.expr(prop.code, prop.loc)
|
|
438
|
+
builder.raw(',\n')
|
|
439
|
+
}
|
|
440
|
+
builder.raw('});\n')
|
|
441
|
+
}
|
|
388
442
|
emitNodes(node.children, builder)
|
|
389
443
|
return
|
|
390
444
|
}
|
|
@@ -4,20 +4,56 @@ import { REACTIVE_CALLEES } from './REACTIVE_CALLEES.ts'
|
|
|
4
4
|
const factory = ts.factory
|
|
5
5
|
|
|
6
6
|
/* The reactive primitives that must be reached through a scope. A bare call to one of
|
|
7
|
-
these is a compile error: reactive
|
|
8
|
-
it (`scope().state(...)`), so a reader always sees the scope interaction.
|
|
9
|
-
|
|
7
|
+
these is a compile error: a reactive primitive is owned by a scope and the surface must
|
|
8
|
+
show it (`scope().state(...)`), so a reader always sees the scope interaction. `effect`
|
|
9
|
+
is here too — a reaction is scope-owned (it tears down with the scope), so it joins the
|
|
10
|
+
one surface; unlike the cells it stays a runtime call (`scope().effect(...)` passes
|
|
11
|
+
through to the `effect` helper), not a doc slot. */
|
|
12
|
+
const SCOPE_PRIMITIVES: ReadonlySet<string> = new Set(['state', 'linked', 'computed', 'effect'])
|
|
13
|
+
|
|
14
|
+
/* The primitive names a top-level `const { state, computed } = scope()` destructure binds.
|
|
15
|
+
Such a name is scope-bound — its bare call below is the destructured method, not a stray
|
|
16
|
+
global — so it is exempt from the bare-primitive error. Only a destructure of a `scope()`
|
|
17
|
+
call counts (receiver-agnostic on the callee name, matching signalCallee); an aliased
|
|
18
|
+
binding (`{ state: s }`) is not recognised, so the canonical name must be kept. */
|
|
19
|
+
function scopeDestructuredPrimitives(source: ts.SourceFile): Set<string> {
|
|
20
|
+
const bound = new Set<string>()
|
|
21
|
+
for (const statement of source.statements) {
|
|
22
|
+
if (!ts.isVariableStatement(statement)) {
|
|
23
|
+
continue
|
|
24
|
+
}
|
|
25
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
26
|
+
if (
|
|
27
|
+
signalCallee(declaration) === 'scope' &&
|
|
28
|
+
ts.isObjectBindingPattern(declaration.name)
|
|
29
|
+
) {
|
|
30
|
+
for (const element of declaration.name.elements) {
|
|
31
|
+
if (
|
|
32
|
+
element.propertyName === undefined &&
|
|
33
|
+
ts.isIdentifier(element.name) &&
|
|
34
|
+
SCOPE_PRIMITIVES.has(element.name.text)
|
|
35
|
+
) {
|
|
36
|
+
bound.add(element.name.text)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return bound
|
|
43
|
+
}
|
|
10
44
|
|
|
11
45
|
/* Throws on a bare scope primitive (`state(0)` instead of `scope().state(0)`) or on the
|
|
12
46
|
removed `prop(...)` reader — props are now read by destructuring `props()`. Walks all
|
|
13
|
-
calls, so a stray one nested in a function is caught too, not just top-level declarations.
|
|
47
|
+
calls, so a stray one nested in a function is caught too, not just top-level declarations.
|
|
48
|
+
A primitive destructured from `scope()` at the top is scope-bound and exempt. */
|
|
14
49
|
function assertScopedPrimitives(source: ts.SourceFile): void {
|
|
50
|
+
const scopeBound = scopeDestructuredPrimitives(source)
|
|
15
51
|
const visit = (node: ts.Node): void => {
|
|
16
52
|
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
|
|
17
53
|
const name = node.expression.text
|
|
18
|
-
if (SCOPE_PRIMITIVES.has(name)) {
|
|
54
|
+
if (SCOPE_PRIMITIVES.has(name) && !scopeBound.has(name)) {
|
|
19
55
|
throw new Error(
|
|
20
|
-
`abide: bare \`${name}(...)\` is not allowed — reactive
|
|
56
|
+
`abide: bare \`${name}(...)\` is not allowed — a reactive primitive lives on a scope. Use \`scope().${name}(...)\` (or a captured handle: \`const s = scope(); s.${name}(...)\`).`,
|
|
21
57
|
)
|
|
22
58
|
}
|
|
23
59
|
if (name === 'prop') {
|