@abide/abide 0.44.0 → 0.45.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 +8 -6
- package/CHANGELOG.md +64 -0
- package/README.md +171 -181
- package/package.json +2 -1
- package/src/abideResolverPlugin.ts +6 -1
- package/src/devEntry.ts +43 -9
- package/src/lib/cli/parseArgvForRpc.ts +21 -24
- package/src/lib/cli/runCli.ts +9 -24
- package/src/lib/cli/tokenizeArgvFlags.ts +84 -0
- package/src/lib/server/error.ts +48 -53
- package/src/lib/server/json.ts +4 -3
- package/src/lib/server/jsonl.ts +1 -1
- package/src/lib/server/rpc/defineRpc.ts +11 -9
- package/src/lib/server/rpc/types/RemoteHandler.ts +5 -7
- package/src/lib/server/rpc/types/RpcHelper.ts +125 -35
- package/src/lib/server/rpc/types/TypedError.ts +18 -0
- package/src/lib/server/rpc/validationError.ts +3 -3
- package/src/lib/server/runtime/STATUS_TEXT.ts +22 -0
- package/src/lib/server/runtime/createServer.ts +36 -12
- package/src/lib/server/runtime/devHotModuleResponse.ts +1 -1
- package/src/lib/server/runtime/gzipResponse.ts +14 -16
- package/src/lib/server/runtime/installAmbientScopeStore.ts +33 -0
- package/src/lib/server/runtime/runWithRequestScope.ts +4 -1
- package/src/lib/server/runtime/snapshotEntryFromCache.ts +2 -0
- package/src/lib/server/runtime/typedErrorResponse.ts +26 -0
- package/src/lib/server/runtime/types/RequestStore.ts +25 -0
- package/src/lib/server/runtime/withResponseDefaults.ts +8 -3
- package/src/lib/server/sockets/defineSocket.ts +5 -1
- package/src/lib/server/sse.ts +1 -1
- package/src/lib/shared/HttpError.ts +1 -1
- package/src/lib/shared/RPC_ARGS_TYPE.ts +7 -0
- package/src/lib/shared/activeCacheStore.ts +5 -14
- package/src/lib/shared/activePage.ts +5 -19
- package/src/lib/shared/augmentModule.ts +18 -0
- package/src/lib/shared/basePath.ts +6 -5
- package/src/lib/shared/baseResolver.ts +10 -0
- package/src/lib/shared/baseSlot.ts +6 -12
- package/src/lib/shared/cache.ts +46 -4
- package/src/lib/shared/cacheEntryFromSnapshot.ts +21 -1
- package/src/lib/shared/cacheKeyOf.ts +7 -0
- package/src/lib/shared/cacheKeyStore.ts +8 -0
- package/src/lib/shared/cacheStoreResolver.ts +12 -0
- package/src/lib/shared/cacheStoreSlot.ts +5 -13
- package/src/lib/shared/changeAffectsClient.ts +35 -0
- package/src/lib/shared/createResolverSlot.ts +37 -0
- package/src/lib/shared/debugGate.ts +90 -0
- package/src/lib/shared/emitLogRecord.ts +18 -4
- package/src/lib/shared/globalCacheStoreResolver.ts +12 -0
- package/src/lib/shared/globalCacheStoreSlot.ts +6 -11
- package/src/lib/shared/httpErrorFor.ts +1 -1
- package/src/lib/shared/isDebugEnabled.ts +3 -10
- package/src/lib/shared/isDebugNegated.ts +4 -6
- package/src/lib/shared/pageResolver.ts +16 -0
- package/src/lib/shared/pageSlot.ts +5 -14
- package/src/lib/shared/recordCacheKey.ts +6 -0
- package/src/lib/shared/requestScopeResolver.ts +12 -0
- package/src/lib/shared/requestScopeSlot.ts +7 -12
- package/src/lib/shared/responseBodyKind.ts +35 -0
- package/src/lib/shared/setBaseResolver.ts +2 -4
- package/src/lib/shared/setCacheStoreResolver.ts +3 -5
- package/src/lib/shared/setGlobalCacheStoreResolver.ts +3 -5
- package/src/lib/shared/setPageResolver.ts +2 -5
- package/src/lib/shared/setRequestScopeResolver.ts +3 -5
- package/src/lib/shared/toTagSet.ts +3 -3
- package/src/lib/shared/types/CacheEntry.ts +15 -0
- package/src/lib/shared/types/CacheOptions.ts +5 -5
- package/src/lib/shared/types/CacheSnapshotEntry.ts +5 -0
- package/src/lib/shared/types/DebugGate.ts +10 -0
- package/src/lib/shared/types/ErrorSpec.ts +6 -6
- package/src/lib/shared/types/RemoteFunction.ts +8 -4
- package/src/lib/shared/types/ResolverSlot.ts +10 -0
- package/src/lib/shared/writeDts.ts +8 -1
- package/src/lib/shared/writePublicAssetsDts.ts +5 -9
- package/src/lib/shared/writeRoutesDts.ts +12 -16
- package/src/lib/shared/writeRpcDts.ts +13 -12
- package/src/lib/shared/writeTestRpcDts.ts +11 -11
- package/src/lib/shared/writeTestSocketsDts.ts +9 -9
- package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +1 -0
- package/src/lib/ui/compile/abideUiPlugin.ts +9 -7
- package/src/lib/ui/compile/assertRuntimeHelpersBound.ts +24 -12
- package/src/lib/ui/compile/compileModule.ts +37 -16
- package/src/lib/ui/compile/compileShadow.ts +75 -1
- package/src/lib/ui/compile/createShadowLanguageService.ts +29 -3
- package/src/lib/ui/compile/generateBuild.ts +7 -1
- package/src/lib/ui/compile/generateSSR.ts +10 -3
- package/src/lib/ui/compile/parseTemplate.ts +4 -1
- package/src/lib/ui/compile/types/TemplateNode.ts +1 -1
- package/src/lib/ui/createScope.ts +11 -4
- package/src/lib/ui/deferResume.ts +29 -0
- package/src/lib/ui/dom/appendText.ts +19 -4
- package/src/lib/ui/dom/awaitBlock.ts +74 -26
- package/src/lib/ui/dom/eachAsync.ts +11 -18
- package/src/lib/ui/dom/mountSwappableRange.ts +79 -0
- package/src/lib/ui/dom/skeleton.ts +10 -1
- package/src/lib/ui/dom/switchBlock.ts +15 -63
- package/src/lib/ui/dom/when.ts +11 -64
- package/src/lib/ui/installHotBridge.ts +2 -0
- package/src/lib/ui/matchRoute.ts +18 -1
- package/src/lib/ui/remoteProxy.ts +11 -1
- package/src/lib/ui/renderToStream.ts +13 -8
- package/src/lib/ui/resumeSeedScript.ts +2 -2
- package/src/lib/ui/runtime/CURRENT_SCOPE.ts +14 -1
- package/src/lib/ui/runtime/RESUME.ts +6 -0
- package/src/lib/ui/runtime/ambientScopeBacking.ts +26 -0
- package/src/lib/ui/runtime/createDoc.ts +102 -12
- package/src/lib/ui/runtime/createEffectNode.ts +7 -1
- package/src/lib/ui/runtime/flushEffects.ts +18 -0
- package/src/lib/ui/runtime/generationGuard.ts +40 -0
- package/src/lib/ui/runtime/runNode.ts +9 -1
- package/src/lib/ui/runtime/types/SsrRender.ts +4 -3
- package/src/lib/ui/scope.ts +6 -8
- package/src/lib/ui/tryEncodeResume.ts +5 -2
- package/src/lib/ui/types/Scope.ts +3 -4
- package/src/zodCjsPlugin.ts +16 -1
- package/src/lib/server/rpc/buildErrorConstructors.ts +0 -19
- package/src/lib/shared/matchesDebugPattern.ts +0 -16
- package/src/lib/shared/parseDebugPatterns.ts +0 -21
- package/src/lib/shared/types/ErrorConstructors.ts +0 -17
- package/src/lib/shared/types/ErrorDescriptor.ts +0 -10
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createResolverSlot } from './createResolverSlot.ts'
|
|
2
|
+
import type { RequestScopeInfo } from './types/RequestScopeInfo.ts'
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
Slot + setter for the request-scope resolver. The server installs an ALS-backed
|
|
6
|
+
resolver (createServer, reading the RequestStore); the client a module-singleton
|
|
7
|
+
seeded from __SSR__ (startClient). No fallback creator — an unset resolver (or one
|
|
8
|
+
returning undefined outside any request) means "no scope": callers read
|
|
9
|
+
`.resolver?.()` and treat undefined as absent. requestScopeSlot /
|
|
10
|
+
setRequestScopeResolver re-export the slot and setter.
|
|
11
|
+
*/
|
|
12
|
+
export const requestScopeResolver = createResolverSlot<RequestScopeInfo>()
|
|
@@ -1,15 +1,10 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { requestScopeResolver } from './requestScopeResolver.ts'
|
|
2
2
|
|
|
3
3
|
/*
|
|
4
|
-
Internal slot the runtime entries register their request-scope resolver into
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
lines print without the context prefix. Mirrors pageSlot / cacheStoreSlot.
|
|
4
|
+
Internal slot the runtime entries register their request-scope resolver into (see
|
|
5
|
+
requestScopeResolver). Exposed so callers read `.resolver?.()` and test helpers
|
|
6
|
+
snapshot/poke `.resolver` directly. Undefined resolver — or one returning
|
|
7
|
+
undefined outside any request — means "no scope": trace() returns undefined and
|
|
8
|
+
log lines print without the context prefix.
|
|
10
9
|
*/
|
|
11
|
-
export const requestScopeSlot
|
|
12
|
-
resolver: (() => RequestScopeInfo | undefined) | undefined
|
|
13
|
-
} = {
|
|
14
|
-
resolver: undefined,
|
|
15
|
-
}
|
|
10
|
+
export const requestScopeSlot = requestScopeResolver.slot
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { contentBodyKind } from './contentBodyKind.ts'
|
|
2
|
+
import { contentTypeOf } from './contentTypeOf.ts'
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
Compressible Content-Types — text and structured-text payloads. Binary or
|
|
6
|
+
already-compressed bodies (images, fonts, archives, zstd/gzip blobs) gain
|
|
7
|
+
nothing from a second pass and only burn CPU.
|
|
8
|
+
*/
|
|
9
|
+
const COMPRESSIBLE_TYPE =
|
|
10
|
+
/^(?:text\/|application\/(?:json|javascript|xml|[\w.-]+\+(?:json|xml))|image\/svg)/
|
|
11
|
+
|
|
12
|
+
/*
|
|
13
|
+
The post-handler body class that decides a dynamic response's wire handling:
|
|
14
|
+
`streaming` (SSE / JSONL — drained frame-by-frame, never gzip-buffered, opted
|
|
15
|
+
out of the idle timeout), `compressible` (text/structured-text — gzipped when
|
|
16
|
+
the client accepts it), or `opaque` (binary/already-encoded — passed through).
|
|
17
|
+
|
|
18
|
+
One header read + classify per response, threaded through the dispatch pipeline
|
|
19
|
+
so `gzipResponse` and the idle-timeout opt-out don't each re-derive it from the
|
|
20
|
+
Content-Type (the S2 finding: the same body was classified 3-4×). Mirrors the
|
|
21
|
+
shared `contentBodyKind` streaming bucket so the wire path and the decode path
|
|
22
|
+
can't disagree on what's a stream.
|
|
23
|
+
*/
|
|
24
|
+
export type ResponseBodyKind = 'streaming' | 'compressible' | 'opaque'
|
|
25
|
+
|
|
26
|
+
export function responseBodyKind(response: Response): ResponseBodyKind {
|
|
27
|
+
const contentType = contentTypeOf(response.headers)
|
|
28
|
+
if (contentBodyKind(contentType) === 'streaming') {
|
|
29
|
+
return 'streaming'
|
|
30
|
+
}
|
|
31
|
+
if (COMPRESSIBLE_TYPE.test(contentType)) {
|
|
32
|
+
return 'compressible'
|
|
33
|
+
}
|
|
34
|
+
return 'opaque'
|
|
35
|
+
}
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { baseResolver } from './baseResolver.ts'
|
|
2
2
|
|
|
3
3
|
// Registers the runtime's mount-base resolver. Called once per side at boot.
|
|
4
|
-
export
|
|
5
|
-
baseSlot.resolver = fn
|
|
6
|
-
}
|
|
4
|
+
export const setBaseResolver = baseResolver.set
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { CacheStore } from './types/CacheStore.ts'
|
|
1
|
+
import { cacheStoreResolver } from './cacheStoreResolver.ts'
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
}
|
|
3
|
+
// Registers the runtime's active-CacheStore resolver. Called once per side at boot.
|
|
4
|
+
export const setCacheStoreResolver = cacheStoreResolver.set
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { CacheStore } from './types/CacheStore.ts'
|
|
1
|
+
import { globalCacheStoreResolver } from './globalCacheStoreResolver.ts'
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
}
|
|
3
|
+
// Registers the process-level cache store resolver. Called once per side at boot.
|
|
4
|
+
export const setGlobalCacheStoreResolver = globalCacheStoreResolver.set
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { PageSnapshot } from './types/PageSnapshot.ts'
|
|
1
|
+
import { pageResolver } from './pageResolver.ts'
|
|
3
2
|
|
|
4
3
|
// Registers the runtime's page resolver. Called once per side at boot.
|
|
5
|
-
export
|
|
6
|
-
pageSlot.resolver = fn
|
|
7
|
-
}
|
|
4
|
+
export const setPageResolver = pageResolver.set
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { RequestScopeInfo } from './types/RequestScopeInfo.ts'
|
|
1
|
+
import { requestScopeResolver } from './requestScopeResolver.ts'
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
}
|
|
3
|
+
// Registers the runtime's request-scope resolver. Called once per side at boot.
|
|
4
|
+
export const setRequestScopeResolver = requestScopeResolver.set
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* Normalizes a tags option
|
|
2
|
-
export function toTagSet(tags: string
|
|
3
|
-
return new Set(
|
|
1
|
+
/* Normalizes a tags option to a Set for O(1) membership. */
|
|
2
|
+
export function toTagSet(tags: string[]): Set<string> {
|
|
3
|
+
return new Set(tags)
|
|
4
4
|
}
|
|
@@ -59,9 +59,24 @@ export type CacheEntry = {
|
|
|
59
59
|
ttl: number | undefined
|
|
60
60
|
expiresAt: number | undefined
|
|
61
61
|
value?: unknown
|
|
62
|
+
/*
|
|
63
|
+
Lazy warm materializer (Tier 2 deferred seed): a memoized thunk that decodes the
|
|
64
|
+
shipped body into the warm value on first call, so a deferred snapshot pays no decode
|
|
65
|
+
at hydration — only when a read actually needs the value. `value` stays undefined until
|
|
66
|
+
then; the read gate treats an entry with `warm` set as warm without invoking it. Eager
|
|
67
|
+
snapshots set `value` and leave this undefined.
|
|
68
|
+
*/
|
|
69
|
+
warm?: () => unknown
|
|
62
70
|
tags?: Set<string>
|
|
63
71
|
settled?: boolean
|
|
64
72
|
hydrated?: boolean
|
|
73
|
+
/*
|
|
74
|
+
Marks that the SSR resume path deferred this entry's `{#await cache()}` value — shipped a
|
|
75
|
+
`{ defer, key }` marker instead of inlining it. The post-stream snapshot drain reads it to
|
|
76
|
+
ship the body as a LAZY seed (decoded on first read, not at hydration). Set server-side
|
|
77
|
+
only, on the request-scoped entry, during the stream.
|
|
78
|
+
*/
|
|
79
|
+
deferred?: boolean
|
|
65
80
|
refreshing?: boolean
|
|
66
81
|
invalidation?: InvalidationState
|
|
67
82
|
}
|
|
@@ -6,10 +6,10 @@ milliseconds-past-resolve that the entry stays live: omitted = forever, 0 =
|
|
|
6
6
|
dedupe only (entry dropped once the promise settles — the mutation idiom:
|
|
7
7
|
in-flight coalescing and pending() visibility, nothing retained), any other
|
|
8
8
|
number = TTL.
|
|
9
|
-
`tags` is
|
|
10
|
-
`cache.invalidate({ tags })` drops every entry sharing any of them —
|
|
11
|
-
|
|
12
|
-
uuid) shared by a set of calls gives them their own private invalidation group.
|
|
9
|
+
`tags` is an array of free-form labels grouping unrelated calls so one
|
|
10
|
+
`cache.invalidate({ tags })` drops every entry sharing any of them — list
|
|
11
|
+
multiple when a call belongs to multiple invalidation groups. A unique tag (e.g.
|
|
12
|
+
a uuid) shared by a set of calls gives them their own private invalidation group.
|
|
13
13
|
|
|
14
14
|
`global` opts the entry into the process-level store instead of the default
|
|
15
15
|
request-scoped one (server) — so a value computed in one request is reused by
|
|
@@ -37,7 +37,7 @@ uncheckable — set `swr` only on a producer that is a pure read.
|
|
|
37
37
|
*/
|
|
38
38
|
export type CacheOptions = {
|
|
39
39
|
ttl?: number
|
|
40
|
-
tags?: string
|
|
40
|
+
tags?: string[]
|
|
41
41
|
global?: boolean
|
|
42
42
|
swr?: boolean | { throttle?: number; debounce?: number }
|
|
43
43
|
}
|
|
@@ -14,4 +14,9 @@ export type CacheSnapshotEntry = {
|
|
|
14
14
|
statusText: string
|
|
15
15
|
headers: Array<[string, string]>
|
|
16
16
|
body: string
|
|
17
|
+
/* Deferred seed (Tier 2): the client stores the body but does NOT decode it at boot —
|
|
18
|
+
the warm value is materialized lazily on the first read, off the hydration path. Set
|
|
19
|
+
by the server for a deferred `{#await cache()}` whose value ships via a `{defer,key}`
|
|
20
|
+
resume marker instead of inline, so hydration touches neither copy's decode. */
|
|
21
|
+
lazy?: boolean
|
|
17
22
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/*
|
|
2
|
+
The two questions a DEBUG string answers for one channel name. `enabled` gates a
|
|
3
|
+
diagnostic channel (inclusion, with exclusions winning); `negated` is the off
|
|
4
|
+
switch for always-on channels (a `-name` pattern silencing the app's own or
|
|
5
|
+
abide's framework voice). Returned by debugGate, already bound to one env value.
|
|
6
|
+
*/
|
|
7
|
+
export type DebugGate = {
|
|
8
|
+
enabled(name: string): boolean
|
|
9
|
+
negated(name: string): boolean
|
|
10
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { StandardSchemaV1 } from './StandardSchemaV1.ts'
|
|
2
2
|
|
|
3
3
|
/*
|
|
4
|
-
A rpc's
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
`
|
|
8
|
-
`
|
|
9
|
-
`
|
|
4
|
+
A rpc's error set, keyed by error NAME (not status, so two errors can share a
|
|
5
|
+
status). Each entry names its HTTP `status` and an optional `data` schema whose
|
|
6
|
+
inferred input the error constructor requires. Built at the type level from the
|
|
7
|
+
`error.typed(...)` constructors a handler RETURNS (see `TypedError` +
|
|
8
|
+
`InferredErrors` in RpcHelper); the client's `rpc.isError` narrows `.kind` /
|
|
9
|
+
`.data` off it (see `RpcErrorGuard`).
|
|
10
10
|
*/
|
|
11
11
|
export type ErrorSpec = Record<string, { status: number; data?: StandardSchemaV1 }>
|
|
@@ -43,6 +43,7 @@ export type RemoteFunction<
|
|
|
43
43
|
Args,
|
|
44
44
|
Return,
|
|
45
45
|
Errors extends ErrorSpec = Record<never, never>,
|
|
46
|
+
Durable extends boolean = false,
|
|
46
47
|
> = RemoteCallable<Args, Return> & {
|
|
47
48
|
readonly method: HttpMethod
|
|
48
49
|
readonly url: string
|
|
@@ -56,7 +57,10 @@ export type RemoteFunction<
|
|
|
56
57
|
per-rpc replacement for a global guard, since the error name → data type mapping
|
|
57
58
|
lives in the rpc's own spec. */
|
|
58
59
|
readonly isError: RpcErrorGuard<Errors>
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
} /* `outbox` presence follows the `outbox: true` opt: the mutating helper threads `Durable`
|
|
61
|
+
into the return type so a DURABLE rpc's `.outbox` is the required queue face (no optional
|
|
62
|
+
chain). A non-durable rpc keeps it optional — assignable to a durable one everywhere a
|
|
63
|
+
bare `RemoteFunction<Args, Return>` slot (cache selectors, registries) is expected, since
|
|
64
|
+
required→optional widens cleanly and no call site had to learn the `Durable` bit. */ & (Durable extends true
|
|
65
|
+
? { readonly outbox: Outbox<Args> }
|
|
66
|
+
: { readonly outbox?: Outbox<Args> })
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/*
|
|
2
|
+
The mutable cell a runtime entry registers its resolver into, plus the single
|
|
3
|
+
lazy/value fallback used when no resolver is registered. createServer and
|
|
4
|
+
startClient install side-specific resolvers; isolated tests poke `.resolver` /
|
|
5
|
+
`.fallback` directly, so both fields stay public and mutable.
|
|
6
|
+
*/
|
|
7
|
+
export type ResolverSlot<T> = {
|
|
8
|
+
resolver: (() => T | undefined) | undefined
|
|
9
|
+
fallback: T | undefined
|
|
10
|
+
}
|
|
@@ -8,5 +8,12 @@ src tsconfig include picks up), wrapping `body` in the shared banner + the
|
|
|
8
8
|
differ only in `body`, so the envelope lives here.
|
|
9
9
|
*/
|
|
10
10
|
export async function writeDts(cwd: string, name: string, body: string): Promise<void> {
|
|
11
|
-
|
|
11
|
+
const next = `${DTS_BANNER}\n${body}\n\nexport {}\n`
|
|
12
|
+
const file = Bun.file(`${cwd}/src/.abide/${name}.d.ts`)
|
|
13
|
+
// Skip the write when byte-identical — an unchanged rebuild (the common case)
|
|
14
|
+
// otherwise bumps mtime every save, forcing the editor's TS server to re-typecheck.
|
|
15
|
+
if ((await file.exists()) && (await file.text()) === next) {
|
|
16
|
+
return
|
|
17
|
+
}
|
|
18
|
+
await Bun.write(file, next)
|
|
12
19
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { augmentModule } from './augmentModule.ts'
|
|
1
2
|
import { writeDts } from './writeDts.ts'
|
|
2
3
|
|
|
3
4
|
/*
|
|
@@ -19,13 +20,8 @@ export async function writePublicAssetsDts({
|
|
|
19
20
|
importName: string
|
|
20
21
|
}): Promise<void> {
|
|
21
22
|
const entries = publicFiles
|
|
22
|
-
.map((file) =>
|
|
23
|
-
.toSorted()
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
interface PublicAssets {
|
|
27
|
-
${entries}
|
|
28
|
-
}
|
|
29
|
-
}`
|
|
30
|
-
await writeDts(cwd, 'publicAssets', body)
|
|
23
|
+
.map((file): [string, string] => [`/${file}`, 'true'])
|
|
24
|
+
.toSorted(([a], [b]) => (JSON.stringify(a) < JSON.stringify(b) ? -1 : 1))
|
|
25
|
+
const module = augmentModule(`${importName}/shared/url`, 'PublicAssets', entries)
|
|
26
|
+
await writeDts(cwd, 'publicAssets', module)
|
|
31
27
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { augmentModule } from './augmentModule.ts'
|
|
1
2
|
import { pageUrlForFile } from './pageUrlForFile.ts'
|
|
2
3
|
import { routeParamsShape } from './routeParamsShape.ts'
|
|
3
4
|
import { writeDts } from './writeDts.ts'
|
|
@@ -25,21 +26,16 @@ export async function writeRoutesDts({
|
|
|
25
26
|
const routes = pageFiles
|
|
26
27
|
.map((file) => ({ route: pageUrlForFile(file) }))
|
|
27
28
|
.toSorted((a, b) => a.route.localeCompare(b.route))
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
const routesModule = augmentModule(
|
|
30
|
+
`${importName}/shared/page`,
|
|
31
|
+
'Routes',
|
|
32
|
+
routes.map(({ route }): [string, string] => [route, routeParamsShape(route)]),
|
|
33
|
+
)
|
|
31
34
|
/* Keys-only mirror for url()'s autocomplete (values unused — PathParams derives the shape). */
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
declare module '${importName}/shared/url' {
|
|
40
|
-
interface PageRoutes {
|
|
41
|
-
${urlKeys}
|
|
42
|
-
}
|
|
43
|
-
}`
|
|
44
|
-
await writeDts(cwd, 'routes', body)
|
|
35
|
+
const urlModule = augmentModule(
|
|
36
|
+
`${importName}/shared/url`,
|
|
37
|
+
'PageRoutes',
|
|
38
|
+
routes.map(({ route }): [string, string] => [route, 'true']),
|
|
39
|
+
)
|
|
40
|
+
await writeDts(cwd, 'routes', `${routesModule}\n\n${urlModule}`)
|
|
45
41
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { augmentModule } from './augmentModule.ts'
|
|
1
2
|
import { carriesBodyArgs } from './carriesBodyArgs.ts'
|
|
2
3
|
import { detectRpcMethod } from './detectRpcMethod.ts'
|
|
3
4
|
import { fileStem } from './fileStem.ts'
|
|
5
|
+
import { RPC_ARGS_TYPE } from './RPC_ARGS_TYPE.ts'
|
|
4
6
|
import { rpcUrlForFile } from './rpcUrlForFile.ts'
|
|
5
7
|
import { writeDts } from './writeDts.ts'
|
|
6
8
|
|
|
@@ -26,24 +28,23 @@ export async function writeRpcDts({
|
|
|
26
28
|
rpcFiles: string[]
|
|
27
29
|
importName: string
|
|
28
30
|
}): Promise<void> {
|
|
29
|
-
const
|
|
30
|
-
rpcFiles.map(async (file) => {
|
|
31
|
+
const pairs = await Promise.all(
|
|
32
|
+
rpcFiles.map(async (file): Promise<[string, string] | undefined> => {
|
|
31
33
|
const method = detectRpcMethod(await Bun.file(`${rpcDir}/${file}`).text())
|
|
32
34
|
// A body rpc's args can't ride a URL — leave it out of the url() rpc map.
|
|
33
35
|
if (!method || carriesBodyArgs(method)) {
|
|
34
36
|
return undefined
|
|
35
37
|
}
|
|
36
38
|
const importPath = `../server/rpc/${file}`
|
|
37
|
-
return
|
|
39
|
+
return [
|
|
40
|
+
rpcUrlForFile(file),
|
|
41
|
+
`RpcArgs<typeof import(${JSON.stringify(importPath)}).${fileStem(file)}>`,
|
|
42
|
+
]
|
|
38
43
|
}),
|
|
39
44
|
)
|
|
40
|
-
const entries =
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
${entries.join('\n')}
|
|
46
|
-
}
|
|
47
|
-
}`
|
|
48
|
-
await writeDts(cwd, 'rpc', body)
|
|
45
|
+
const entries = pairs
|
|
46
|
+
.filter((pair) => pair !== undefined)
|
|
47
|
+
.toSorted(([a], [b]) => (JSON.stringify(a) < JSON.stringify(b) ? -1 : 1))
|
|
48
|
+
const module = augmentModule(`${importName}/shared/url`, 'RpcRoutes', entries)
|
|
49
|
+
await writeDts(cwd, 'rpc', `${RPC_ARGS_TYPE}\n\n${module}`)
|
|
49
50
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { augmentModule } from './augmentModule.ts'
|
|
1
2
|
import { commandNameForUrl } from './commandNameForUrl.ts'
|
|
2
3
|
import { fileStem } from './fileStem.ts'
|
|
4
|
+
import { RPC_ARGS_TYPE } from './RPC_ARGS_TYPE.ts'
|
|
3
5
|
import { rpcUrlForFile } from './rpcUrlForFile.ts'
|
|
4
6
|
import { writeDts } from './writeDts.ts'
|
|
5
7
|
|
|
@@ -24,22 +26,20 @@ export async function writeTestRpcDts({
|
|
|
24
26
|
importName: string
|
|
25
27
|
}): Promise<void> {
|
|
26
28
|
const entries = rpcFiles
|
|
27
|
-
.map((file) => {
|
|
29
|
+
.map((file): [string, string] => {
|
|
28
30
|
const name = commandNameForUrl(rpcUrlForFile(file))
|
|
29
31
|
const importPath = `../server/rpc/${file}`
|
|
30
|
-
return
|
|
32
|
+
return [
|
|
33
|
+
name,
|
|
34
|
+
`RpcInvoker<typeof import(${JSON.stringify(importPath)}).${fileStem(file)}>`,
|
|
35
|
+
]
|
|
31
36
|
})
|
|
32
|
-
.toSorted()
|
|
33
|
-
const
|
|
37
|
+
.toSorted(([a], [b]) => (JSON.stringify(a) < JSON.stringify(b) ? -1 : 1))
|
|
38
|
+
const helperTypes = `${RPC_ARGS_TYPE}
|
|
34
39
|
type RpcReturn<Fn> = Fn extends (...args: never[]) => Promise<infer Return> ? Return : never
|
|
35
40
|
type RpcInvoker<Fn> = ((args?: RpcArgs<Fn>) => Promise<RpcReturn<Fn>>) & {
|
|
36
41
|
raw: (args?: RpcArgs<Fn>) => Promise<Response>
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
declare module '${importName}/test/createTestApp' {
|
|
40
|
-
interface RpcClient {
|
|
41
|
-
${entries.join('\n')}
|
|
42
|
-
}
|
|
43
42
|
}`
|
|
44
|
-
|
|
43
|
+
const module = augmentModule(`${importName}/test/createTestApp`, 'RpcClient', entries)
|
|
44
|
+
await writeDts(cwd, 'testRpc', `${helperTypes}\n\n${module}`)
|
|
45
45
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { augmentModule } from './augmentModule.ts'
|
|
1
2
|
import { fileStem } from './fileStem.ts'
|
|
2
3
|
import { socketNameForFile } from './socketNameForFile.ts'
|
|
3
4
|
import { writeDts } from './writeDts.ts'
|
|
@@ -20,15 +21,14 @@ export async function writeTestSocketsDts({
|
|
|
20
21
|
importName: string
|
|
21
22
|
}): Promise<void> {
|
|
22
23
|
const entries = socketFiles
|
|
23
|
-
.map((file) => {
|
|
24
|
+
.map((file): [string, string] => {
|
|
24
25
|
const importPath = `../server/sockets/${file}`
|
|
25
|
-
return
|
|
26
|
+
return [
|
|
27
|
+
socketNameForFile(file),
|
|
28
|
+
`typeof import(${JSON.stringify(importPath)}).${fileStem(file)}`,
|
|
29
|
+
]
|
|
26
30
|
})
|
|
27
|
-
.toSorted()
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
${entries.join('\n')}
|
|
31
|
-
}
|
|
32
|
-
}`
|
|
33
|
-
await writeDts(cwd, 'testSockets', body)
|
|
31
|
+
.toSorted(([a], [b]) => (JSON.stringify(a) < JSON.stringify(b) ? -1 : 1))
|
|
32
|
+
const module = augmentModule(`${importName}/test/createTestApp`, 'SocketClient', entries)
|
|
33
|
+
await writeDts(cwd, 'testSockets', module)
|
|
34
34
|
}
|
|
@@ -35,6 +35,7 @@ export const UI_RUNTIME_IMPORTS: { name: string; specifier: string; alias?: stri
|
|
|
35
35
|
{ name: 'eachAsync', specifier: 'ui/dom/eachAsync', alias: '$$eachAsync' },
|
|
36
36
|
{ name: 'when', specifier: 'ui/dom/when', alias: '$$when' },
|
|
37
37
|
{ name: 'awaitBlock', specifier: 'ui/dom/awaitBlock', alias: '$$awaitBlock' },
|
|
38
|
+
{ name: 'deferResume', specifier: 'ui/deferResume', alias: '$$deferResume' },
|
|
38
39
|
{ name: 'tryBlock', specifier: 'ui/dom/tryBlock', alias: '$$tryBlock' },
|
|
39
40
|
{ name: 'switchBlock', specifier: 'ui/dom/switchBlock', alias: '$$switchBlock' },
|
|
40
41
|
{ name: 'mountSlot', specifier: 'ui/dom/mountSlot', alias: '$$mountSlot' },
|
|
@@ -3,7 +3,6 @@ import type { BunPlugin } from 'bun'
|
|
|
3
3
|
import { fileName } from '../../shared/fileName.ts'
|
|
4
4
|
import { messageFromError } from '../../shared/messageFromError.ts'
|
|
5
5
|
import { AbideCompileError } from './AbideCompileError.ts'
|
|
6
|
-
import { analyzeComponent } from './analyzeComponent.ts'
|
|
7
6
|
import { compileModule } from './compileModule.ts'
|
|
8
7
|
import { nearestProjectRoot } from './nearestProjectRoot.ts'
|
|
9
8
|
import { offsetToLineColumn } from './offsetToLineColumn.ts'
|
|
@@ -63,14 +62,17 @@ export const abideUiPlugin: BunPlugin = {
|
|
|
63
62
|
throw new Error(`${message.replace(/^\[abide\]\s*/, `[abide] ${at} — `)}`)
|
|
64
63
|
}
|
|
65
64
|
}
|
|
66
|
-
|
|
65
|
+
/* One compile pass yields both the module code and its scoped `<style>` blocks —
|
|
66
|
+
the styles come from the analysis `compileModule` already ran, so the loader no
|
|
67
|
+
longer re-analyzes the source just to recover them. */
|
|
68
|
+
const { code, styles } = compileAbide(() =>
|
|
69
|
+
compileModule(source, { isLayout, moduleId }),
|
|
70
|
+
)
|
|
67
71
|
/* Browser build with `<style>`(s): concatenate every scoped block's CSS and
|
|
68
72
|
pull it into the bundle via one virtual import, keyed by `moduleId` so the
|
|
69
|
-
registry id and the CSS id agree.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
)
|
|
73
|
-
if (styles.length === 0) {
|
|
73
|
+
registry id and the CSS id agree. Server builds skip the import (SSR styling
|
|
74
|
+
comes from the already-linked sheet). */
|
|
75
|
+
if (!toBrowser || styles.length === 0) {
|
|
74
76
|
return { contents: code, loader: 'ts' }
|
|
75
77
|
}
|
|
76
78
|
const virtual = `abide-style:${moduleId}`
|
|
@@ -15,20 +15,32 @@ and require that name to be imported or locally bound. A helper name inside a st
|
|
|
15
15
|
comment is not a `CallExpression` callee, so a docs component quoting framework code
|
|
16
16
|
(`mount(...)` in a snippet) never false-positives. Compile-time only; never on the hot path.
|
|
17
17
|
*/
|
|
18
|
-
|
|
18
|
+
/* `module` is either the source string (standalone use — parsed here) or the already-parsed
|
|
19
|
+
tree of the bodies WITHOUT the prepended import block (compile pipeline — shares the one
|
|
20
|
+
parse the dead-import filter made). In the latter case the import block's bindings aren't in
|
|
21
|
+
the tree, so `importedHelpers` supplies the helper names that block will bind. */
|
|
22
|
+
export function assertRuntimeHelpersBound(
|
|
23
|
+
module: string | ts.SourceFile,
|
|
24
|
+
importedHelpers: Set<string>,
|
|
25
|
+
context: string,
|
|
26
|
+
): void {
|
|
19
27
|
/* The EMITTED local names (the `$$` alias when set) — codegen calls those, and the
|
|
20
28
|
aliased import binds them, so the bound/called check must use the same form. */
|
|
21
29
|
const helperNames = new Set(UI_RUNTIME_IMPORTS.map((entry) => entry.alias ?? entry.name))
|
|
22
|
-
const source =
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
const source =
|
|
31
|
+
typeof module === 'string'
|
|
32
|
+
? ts.createSourceFile(
|
|
33
|
+
'module.ts',
|
|
34
|
+
module,
|
|
35
|
+
ts.ScriptTarget.Latest,
|
|
36
|
+
/* setParentNodes */ true,
|
|
37
|
+
)
|
|
38
|
+
: module
|
|
39
|
+
/* Names a bare call can resolve to: the import block's helper bindings (supplied when the
|
|
40
|
+
tree omits them) plus every import binding and declared name in the tree. Collected
|
|
41
|
+
generously (any identifier in a binding position) — over-approximating the bound set only
|
|
42
|
+
risks missing a defect, never raising a false alarm on valid output. */
|
|
43
|
+
const bound = new Set<string>(importedHelpers)
|
|
32
44
|
const calledHelpers: { name: string; position: number }[] = []
|
|
33
45
|
const visit = (node: ts.Node): void => {
|
|
34
46
|
if (
|
|
@@ -62,7 +74,7 @@ export function assertRuntimeHelpersBound(module: string, context: string): void
|
|
|
62
74
|
if (unbound !== undefined) {
|
|
63
75
|
const { line, character } = source.getLineAndCharacterOfPosition(unbound.position)
|
|
64
76
|
throw new Error(
|
|
65
|
-
`[abide] ${context} calls runtime helper \`${unbound.name}\` at line ${line + 1}:${character + 1} but never imports it — the dead-import filter dropped it. Please report this with the component source.\nOutput:\n${
|
|
77
|
+
`[abide] ${context} calls runtime helper \`${unbound.name}\` at line ${line + 1}:${character + 1} but never imports it — the dead-import filter dropped it. Please report this with the component source.\nOutput:\n${source.text}`,
|
|
66
78
|
)
|
|
67
79
|
}
|
|
68
80
|
}
|