@abide/abide 0.44.0 → 0.44.1
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/CHANGELOG.md +24 -0
- package/package.json +1 -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/rpc/defineRpc.ts +9 -1
- package/src/lib/server/runtime/createServer.ts +29 -12
- package/src/lib/server/runtime/devHotModuleResponse.ts +1 -1
- package/src/lib/server/runtime/gzipResponse.ts +14 -16
- package/src/lib/server/runtime/runWithRequestScope.ts +4 -1
- package/src/lib/server/runtime/types/RequestStore.ts +16 -0
- package/src/lib/server/runtime/withResponseDefaults.ts +8 -3
- package/src/lib/server/sockets/defineSocket.ts +5 -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 +21 -1
- 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/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/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/types/DebugGate.ts +10 -0
- 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/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/parseTemplate.ts +4 -1
- package/src/lib/ui/compile/types/TemplateNode.ts +1 -1
- 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/runtime/createDoc.ts +102 -12
- package/src/zodCjsPlugin.ts +16 -1
- package/src/lib/shared/matchesDebugPattern.ts +0 -16
- package/src/lib/shared/parseDebugPatterns.ts +0 -21
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# abide
|
|
2
2
|
|
|
3
|
+
## 0.44.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 767b415: 02-zod-plugin-memo ([`0e00b21`](https://github.com/briancray/abide/commit/0e00b217f99352a7d3def4841172c5bff287bc5c))
|
|
8
|
+
- 767b415: 06-wakeSubtree-prefix-index ([`23ab920`](https://github.com/briancray/abide/commit/23ab9202fd0cb41e01811e78b76eac2843c9cf8f))
|
|
9
|
+
- 767b415: 09-when-via-switch ([`48f8d50`](https://github.com/briancray/abide/commit/48f8d5063cf74060004ad38691f584abcff0cab2))
|
|
10
|
+
- 767b415: 08-dts-codegen ([`5170542`](https://github.com/briancray/abide/commit/51705426e55a700140cc7bc38f7e21f30759fce3))
|
|
11
|
+
- 767b415: 03-socket-publish-gate ([`56fae49`](https://github.com/briancray/abide/commit/56fae49bf393fdfb3aa35cecc618a39443354ce4))
|
|
12
|
+
- 767b415: 10-cli-tokenizer ([`5e305c1`](https://github.com/briancray/abide/commit/5e305c154efd5f77d0a57e78831b72a30b4d9155))
|
|
13
|
+
- 767b415: 14-resolver-slot-factory-JUDGMENT ([`6e00809`](https://github.com/briancray/abide/commit/6e00809c343e24599d54b2304a50a9cecd90619f))
|
|
14
|
+
- 767b415: catch top-level await in a .abide <script> during abide check ([`76ff64e`](https://github.com/briancray/abide/commit/76ff64e9fb9ffed108ae2fde53f2ded2bf3a0abe))
|
|
15
|
+
- 767b415: 12-warm-clone-PARTIAL ([`8e58e0b`](https://github.com/briancray/abide/commit/8e58e0b2c1891174cc809797f949f01619529b89))
|
|
16
|
+
- 767b415: 05-compiler-double-pass ([`9216dae`](https://github.com/briancray/abide/commit/9216dae4c26e27fce49f39b116bd8b1dcf1dd226))
|
|
17
|
+
- 767b415: 04-lsp-shadow-cache ([`9f9f5fc`](https://github.com/briancray/abide/commit/9f9f5fc84792204259d591fa4aff261b2a19c46d))
|
|
18
|
+
- 767b415: 13-dev-rebuild-scoping-JUDGMENT ([`b5a1d49`](https://github.com/briancray/abide/commit/b5a1d49e6607a8fcf37efe03e9fe6f6b2c31ea45))
|
|
19
|
+
- 767b415: gate skeleton fragment-staging on a connected parent ([`c9380ee`](https://github.com/briancray/abide/commit/c9380eebdd9f6773ccffd446665b1acbc78b1123))
|
|
20
|
+
- 767b415: 07-debug-gate ([`c96466e`](https://github.com/briancray/abide/commit/c96466e73ea1f521d728b11aac44262530c3b09c))
|
|
21
|
+
- 767b415: rename paint's escape var to avoid shadowing the global ([`caf1322`](https://github.com/briancray/abide/commit/caf1322adc6598bc6ea32e0ba9047ba768b6f5e1))
|
|
22
|
+
- 767b415: reflow generateBuild attr-kind union to biome's multiline form ([`f9b3669`](https://github.com/briancray/abide/commit/f9b36690e4add2c955abb8f0127a9af9d513be53))
|
|
23
|
+
- 767b415: 01-bugs-B1-B2 ([`fa8659d`](https://github.com/briancray/abide/commit/fa8659d276265c0fbb4f7ca0740fc74702f00168))
|
|
24
|
+
- 767b415: address code-review findings ([`fb02df5`](https://github.com/briancray/abide/commit/fb02df58c924d7206fce35874cc80309949c9e59))
|
|
25
|
+
- 767b415: 11-server-log-micro ([`fb689bc`](https://github.com/briancray/abide/commit/fb689bc05bee9ba127ddf206e925e3a9e04841fd))
|
|
26
|
+
|
|
3
27
|
## 0.44.0
|
|
4
28
|
|
|
5
29
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -257,10 +257,15 @@ export function abideResolverPlugin({
|
|
|
257
257
|
return {
|
|
258
258
|
name: 'abide-resolver',
|
|
259
259
|
setup(build) {
|
|
260
|
-
/* Fresh edge graph each build (dev watch reuses the
|
|
260
|
+
/* Fresh edge graph + resolution cache each build (dev watch reuses the
|
|
261
|
+
plugin instance). The resolution cache memoises (path → resolved file)
|
|
262
|
+
within a build; clearing it per build keeps the memo but stops a stale
|
|
263
|
+
miss — a path resolved before its file existed — surviving into the next
|
|
264
|
+
build, so a freshly-created file resolves instead of staying "not found".
|
|
261
265
|
onStart is build-time only — absent in the runtime/preload plugin context. */
|
|
262
266
|
build.onStart?.(() => {
|
|
263
267
|
importerOf.clear()
|
|
268
|
+
resolveExtensionCache.clear()
|
|
264
269
|
})
|
|
265
270
|
|
|
266
271
|
/*
|
package/src/devEntry.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { DEV_READY_MESSAGE } from './lib/server/runtime/DEV_READY_MESSAGE.ts'
|
|
|
6
6
|
import { DEV_REBUILD_MESSAGE } from './lib/server/runtime/DEV_REBUILD_MESSAGE.ts'
|
|
7
7
|
import { findOpenPort } from './lib/server/runtime/findOpenPort.ts'
|
|
8
8
|
import { abideLog } from './lib/shared/abideLog.ts'
|
|
9
|
+
import { changeAffectsClient } from './lib/shared/changeAffectsClient.ts'
|
|
9
10
|
|
|
10
11
|
/*
|
|
11
12
|
Dev orchestrator. Replaces `bun --watch` (which only watches the import graph,
|
|
@@ -174,30 +175,51 @@ async function replaceServer(port: number): Promise<void> {
|
|
|
174
175
|
|
|
175
176
|
let building = false
|
|
176
177
|
let queued = false
|
|
178
|
+
// True once any change collapsed into the queued run needs the client rebuilt;
|
|
179
|
+
// a client-affecting change can never be downgraded to a server-only restart.
|
|
180
|
+
let queuedNeedsClient = false
|
|
177
181
|
|
|
178
182
|
/*
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
183
|
+
Apply a change: rebuild the client (unless `skipClientBuild` — a server/MCP-only
|
|
184
|
+
change leaves the client bundle byte-identical), then on success swap in a fresh
|
|
185
|
+
server child. The worker restart always runs: SSR renders through Bun's module
|
|
186
|
+
cache, so a new process is the only reliable way to reflect any source edit,
|
|
187
|
+
client-affecting or not.
|
|
188
|
+
|
|
189
|
+
Serialized: a change arriving mid-build sets `queued` so exactly one more run
|
|
190
|
+
follows, collapsing further changes in between; `queuedNeedsClient` records
|
|
191
|
+
whether any of them needs the client, so the collapsed run never skips a client
|
|
192
|
+
rebuild a queued change required. A failed build leaves the current child
|
|
193
|
+
untouched — the error is logged and the last-good server keeps serving.
|
|
184
194
|
*/
|
|
185
|
-
async function rebuild(port: number): Promise<void> {
|
|
195
|
+
async function rebuild(port: number, skipClientBuild = false): Promise<void> {
|
|
186
196
|
if (building) {
|
|
187
197
|
queued = true
|
|
198
|
+
queuedNeedsClient ||= !skipClientBuild
|
|
188
199
|
return
|
|
189
200
|
}
|
|
190
201
|
building = true
|
|
191
202
|
try {
|
|
192
|
-
const succeeded = await build(buildOptions)
|
|
203
|
+
const succeeded = skipClientBuild ? true : await build(buildOptions)
|
|
193
204
|
if (succeeded) {
|
|
194
205
|
await replaceServer(port)
|
|
195
206
|
}
|
|
207
|
+
/* A server/MCP-only change reuses the existing client bundle. That's correct for
|
|
208
|
+
an rpc/socket handler BODY edit, but changing an rpc's method, its export name,
|
|
209
|
+
or `outbox: true` changes the client proxy stub too — the stale bundle then
|
|
210
|
+
calls with the old shape (e.g. a 405). Flag the skip so that case isn't silent. */
|
|
211
|
+
if (skipClientBuild) {
|
|
212
|
+
abideLog.info(
|
|
213
|
+
'server-only change — kept the existing client bundle; if you changed an rpc method, export name, or outbox, save a client file (or restart dev) for a full rebuild',
|
|
214
|
+
)
|
|
215
|
+
}
|
|
196
216
|
} finally {
|
|
197
217
|
building = false
|
|
198
218
|
if (queued) {
|
|
199
219
|
queued = false
|
|
200
|
-
|
|
220
|
+
const needsClient = queuedNeedsClient
|
|
221
|
+
queuedNeedsClient = false
|
|
222
|
+
void rebuild(port, !needsClient)
|
|
201
223
|
}
|
|
202
224
|
}
|
|
203
225
|
}
|
|
@@ -223,14 +245,26 @@ an agent editing the app's own source — isn't yanked mid-run by a save.
|
|
|
223
245
|
const manualRebuild = Bun.env.ABIDE_DEV_NO_WATCH === '1'
|
|
224
246
|
|
|
225
247
|
let debounce: ReturnType<typeof setTimeout> | undefined
|
|
248
|
+
/*
|
|
249
|
+
True only while every change collapsed into the pending debounce is server/MCP-
|
|
250
|
+
only; one client-affecting change in the burst flips it false (latches until the
|
|
251
|
+
debounce fires), so a multi-file save that touches the client never skips the
|
|
252
|
+
client rebuild.
|
|
253
|
+
*/
|
|
254
|
+
let pendingSkipClientBuild = true
|
|
226
255
|
const watcher = manualRebuild
|
|
227
256
|
? undefined
|
|
228
257
|
: watch(SOURCE_DIR, { recursive: true }, (_event, filename) => {
|
|
229
258
|
if (!filename || isGenerated(filename)) {
|
|
230
259
|
return
|
|
231
260
|
}
|
|
261
|
+
pendingSkipClientBuild &&= !changeAffectsClient(filename)
|
|
232
262
|
clearTimeout(debounce)
|
|
233
|
-
debounce = setTimeout(() =>
|
|
263
|
+
debounce = setTimeout(() => {
|
|
264
|
+
const skipClientBuild = pendingSkipClientBuild
|
|
265
|
+
pendingSkipClientBuild = true
|
|
266
|
+
void rebuild(port, skipClientBuild)
|
|
267
|
+
}, REBUILD_DEBOUNCE_MS)
|
|
234
268
|
})
|
|
235
269
|
if (manualRebuild) {
|
|
236
270
|
abideLog.info(
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { tokenizeArgvFlags } from './tokenizeArgvFlags.ts'
|
|
2
|
+
|
|
1
3
|
/*
|
|
2
4
|
Parses an argv tail into the JSON args bag for an RPC. The JSON Schema
|
|
3
5
|
on the manifest entry (when present) drives flag typing:
|
|
@@ -47,44 +49,39 @@ export async function parseArgvForRpc(
|
|
|
47
49
|
}
|
|
48
50
|
}
|
|
49
51
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
/* The shared tokenizer owns the flag-consumption grammar (boolean / inline /
|
|
53
|
+
`--json` / `--no-` negation). This loop layers RPC value semantics on each
|
|
54
|
+
yielded token: JSON-blob merge, Number coercion, array accumulation. */
|
|
55
|
+
for (const token of tokenizeArgvFlags(argv, jsonSchema)) {
|
|
56
|
+
if (token.positional !== undefined) {
|
|
57
|
+
throw new Error(`unexpected positional argument: ${token.positional}`)
|
|
58
|
+
}
|
|
59
|
+
if (token.isJson) {
|
|
60
|
+
if (token.missingValue || token.value === undefined) {
|
|
55
61
|
throw new Error('--json requires a value')
|
|
56
62
|
}
|
|
57
|
-
const parsed = JSON.parse(
|
|
63
|
+
const parsed = JSON.parse(token.value)
|
|
58
64
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
59
65
|
throw new Error('--json value must be a JSON object')
|
|
60
66
|
}
|
|
61
67
|
Object.assign(args, parsed)
|
|
62
68
|
continue
|
|
63
69
|
}
|
|
64
|
-
|
|
65
|
-
|
|
70
|
+
// The tokenizer treats `--help` / `-h` as a help token; in RPC parsing
|
|
71
|
+
// they are an unrecognised flag, surfaced loudly like any other.
|
|
72
|
+
if (token.isHelp || token.name === undefined) {
|
|
73
|
+
throw new Error('unexpected --help flag')
|
|
66
74
|
}
|
|
67
|
-
const
|
|
68
|
-
const
|
|
69
|
-
? [stripped.slice(0, stripped.indexOf('=')), stripped.slice(stripped.indexOf('=') + 1)]
|
|
70
|
-
: [stripped, undefined]
|
|
71
|
-
/* `--no-x` negates only a known boolean property x; otherwise the literal
|
|
72
|
-
name wins, so a property legitimately named `no-…` stays reachable. */
|
|
73
|
-
const negatedName = literalName.startsWith('no-')
|
|
74
|
-
? literalName.slice('no-'.length)
|
|
75
|
-
: undefined
|
|
76
|
-
const isNegated = negatedName !== undefined && properties[negatedName]?.type === 'boolean'
|
|
77
|
-
const name = isNegated ? (negatedName as string) : literalName
|
|
78
|
-
const prop = properties[name]
|
|
79
|
-
const propType = prop?.type
|
|
75
|
+
const name = token.name
|
|
76
|
+
const propType = properties[name]?.type
|
|
80
77
|
if (propType === 'boolean') {
|
|
81
|
-
args[name] = !
|
|
78
|
+
args[name] = !token.negated
|
|
82
79
|
continue
|
|
83
80
|
}
|
|
84
|
-
|
|
85
|
-
if (value === undefined) {
|
|
81
|
+
if (token.missingValue || token.value === undefined) {
|
|
86
82
|
throw new Error(`--${name} requires a value`)
|
|
87
83
|
}
|
|
84
|
+
const value = token.value
|
|
88
85
|
if (propType === 'number' || propType === 'integer') {
|
|
89
86
|
// Reject a blank value explicitly — `Number('')` / `Number(' ')` is 0,
|
|
90
87
|
// not NaN, so the NaN guard alone would silently coerce it to zero.
|
package/src/lib/cli/runCli.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { printTrimmed } from './printTrimmed.ts'
|
|
|
9
9
|
import { resolveCliTarget } from './resolveCliTarget.ts'
|
|
10
10
|
import { runSession } from './runSession.ts'
|
|
11
11
|
import { startLocalInstance } from './startLocalInstance.ts'
|
|
12
|
+
import { tokenizeArgvFlags } from './tokenizeArgvFlags.ts'
|
|
12
13
|
import type { CliManifest } from './types/CliManifest.ts'
|
|
13
14
|
import type { CliTarget } from './types/CliTarget.ts'
|
|
14
15
|
|
|
@@ -16,37 +17,21 @@ const isHelpFlag = (arg: string): boolean => arg === '--help' || arg === '-h'
|
|
|
16
17
|
|
|
17
18
|
/*
|
|
18
19
|
Whether a command's argv tail genuinely requests help, i.e. a help flag appears
|
|
19
|
-
at a flag position rather than as the value of a value-expecting flag.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
at a flag position rather than as the value of a value-expecting flag. Shares
|
|
21
|
+
tokenizeArgvFlags' grammar with parseArgvForRpc so the two can't drift: the
|
|
22
|
+
tokenizer applies the flag-consumption rule (boolean / `--name=…` / `--json`
|
|
23
|
+
consume their value), so a `--help` that was a flag's value is already swallowed
|
|
24
|
+
and never surfaces as a help token — `--title --help` passes `--help` as the
|
|
25
|
+
title value, not a help request.
|
|
23
26
|
*/
|
|
24
27
|
function commandArgvRequestsHelp(
|
|
25
28
|
argvTail: string[],
|
|
26
29
|
jsonSchema: Record<string, unknown> | undefined,
|
|
27
30
|
): boolean {
|
|
28
|
-
const
|
|
29
|
-
(
|
|
30
|
-
for (let index = 0; index < argvTail.length; index += 1) {
|
|
31
|
-
const token = argvTail[index] as string
|
|
32
|
-
if (isHelpFlag(token)) {
|
|
31
|
+
for (const token of tokenizeArgvFlags(argvTail, jsonSchema)) {
|
|
32
|
+
if (token.isHelp) {
|
|
33
33
|
return true
|
|
34
34
|
}
|
|
35
|
-
if (!token.startsWith('--')) {
|
|
36
|
-
continue
|
|
37
|
-
}
|
|
38
|
-
if (token === '--json') {
|
|
39
|
-
index += 1 // consumes its value
|
|
40
|
-
continue
|
|
41
|
-
}
|
|
42
|
-
const rawName = token.slice('--'.length)
|
|
43
|
-
if (rawName.includes('=')) {
|
|
44
|
-
continue // inline value, consumes no following token
|
|
45
|
-
}
|
|
46
|
-
const name = rawName.startsWith('no-') ? rawName.slice('no-'.length) : rawName
|
|
47
|
-
if (properties[name]?.type !== 'boolean') {
|
|
48
|
-
index += 1 // a value-expecting flag consumes the next token
|
|
49
|
-
}
|
|
50
35
|
}
|
|
51
36
|
return false
|
|
52
37
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/*
|
|
2
|
+
The single argv-tail flag grammar shared by help-detection (runCli) and RPC arg
|
|
3
|
+
parsing (parseArgvForRpc) so the two can never drift. Walks the tail and yields
|
|
4
|
+
one token per flag occurrence, honouring the schema-driven consumption rule:
|
|
5
|
+
|
|
6
|
+
- `--help` / `-h` → { isHelp: true }, consumes no following token
|
|
7
|
+
- `--json <blob>` → { isJson: true, value }, consumes the next token
|
|
8
|
+
(the value is the verbatim JSON args bag)
|
|
9
|
+
- boolean props (and `--no-x`) → { name, value: undefined }, consumes no token
|
|
10
|
+
- `--name=value` → { name, value }, inline, consumes no token
|
|
11
|
+
- everything else → { name, value }, consumes the next token
|
|
12
|
+
|
|
13
|
+
`--no-x` negates only a known boolean property x; otherwise the literal `no-…`
|
|
14
|
+
name is kept so a property legitimately named `no-…` stays reachable. Reports
|
|
15
|
+
`negated` and `missingValue` so the parser can apply boolean/coercion semantics
|
|
16
|
+
and throw precise errors. A bare positional (no leading `--`) yields
|
|
17
|
+
{ positional: token } so each consumer rejects or ignores it as it sees fit.
|
|
18
|
+
|
|
19
|
+
Pure; reads the schema's `properties` only to classify boolean props.
|
|
20
|
+
*/
|
|
21
|
+
export function* tokenizeArgvFlags(
|
|
22
|
+
argvTail: string[],
|
|
23
|
+
jsonSchema: Record<string, unknown> | undefined,
|
|
24
|
+
): Generator<{
|
|
25
|
+
isHelp?: boolean
|
|
26
|
+
isJson?: boolean
|
|
27
|
+
positional?: string
|
|
28
|
+
name?: string
|
|
29
|
+
value?: string
|
|
30
|
+
negated?: boolean
|
|
31
|
+
missingValue?: boolean
|
|
32
|
+
}> {
|
|
33
|
+
const properties =
|
|
34
|
+
(jsonSchema?.properties as Record<string, { type?: string }> | undefined) ?? {}
|
|
35
|
+
for (let index = 0; index < argvTail.length; index += 1) {
|
|
36
|
+
const token = argvTail[index] as string
|
|
37
|
+
if (token === '--help' || token === '-h') {
|
|
38
|
+
yield { isHelp: true }
|
|
39
|
+
continue
|
|
40
|
+
}
|
|
41
|
+
if (!token.startsWith('--')) {
|
|
42
|
+
yield { positional: token }
|
|
43
|
+
continue
|
|
44
|
+
}
|
|
45
|
+
// `--json <blob>`: the next token is the verbatim JSON args bag.
|
|
46
|
+
if (token === '--json') {
|
|
47
|
+
const value = argvTail[index + 1]
|
|
48
|
+
if (value === undefined) {
|
|
49
|
+
yield { isJson: true, missingValue: true }
|
|
50
|
+
} else {
|
|
51
|
+
index += 1
|
|
52
|
+
yield { isJson: true, value }
|
|
53
|
+
}
|
|
54
|
+
continue
|
|
55
|
+
}
|
|
56
|
+
const stripped = token.slice('--'.length)
|
|
57
|
+
const [literalName, inlineValue] = stripped.includes('=')
|
|
58
|
+
? [stripped.slice(0, stripped.indexOf('=')), stripped.slice(stripped.indexOf('=') + 1)]
|
|
59
|
+
: [stripped, undefined]
|
|
60
|
+
/* `--no-x` negates only a known boolean property x; otherwise the literal
|
|
61
|
+
name wins, so a property legitimately named `no-…` stays reachable. */
|
|
62
|
+
const negatedName = literalName.startsWith('no-')
|
|
63
|
+
? literalName.slice('no-'.length)
|
|
64
|
+
: undefined
|
|
65
|
+
const negated = negatedName !== undefined && properties[negatedName]?.type === 'boolean'
|
|
66
|
+
const name = negated ? (negatedName as string) : literalName
|
|
67
|
+
// Boolean props and inline `--name=value` consume no following token.
|
|
68
|
+
if (properties[name]?.type === 'boolean') {
|
|
69
|
+
yield { name, negated }
|
|
70
|
+
continue
|
|
71
|
+
}
|
|
72
|
+
if (inlineValue !== undefined) {
|
|
73
|
+
yield { name, value: inlineValue }
|
|
74
|
+
continue
|
|
75
|
+
}
|
|
76
|
+
const value = argvTail[index + 1]
|
|
77
|
+
if (value === undefined) {
|
|
78
|
+
yield { name, missingValue: true }
|
|
79
|
+
} else {
|
|
80
|
+
index += 1
|
|
81
|
+
yield { name, value }
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -100,7 +100,15 @@ export function defineRpc<Args, Return>(
|
|
|
100
100
|
function buildRequest(args: Args | undefined): Request {
|
|
101
101
|
const store = requestContext.getStore()
|
|
102
102
|
const baseUrl = store ? store.url.href : 'http://localhost/'
|
|
103
|
-
|
|
103
|
+
/* The forwarded allowlist is request-invariant — derive it once per scope
|
|
104
|
+
and clone per call (buildRpcRequest mutates the Headers). */
|
|
105
|
+
let headers: Headers
|
|
106
|
+
if (store) {
|
|
107
|
+
store.forwardedHeaders ??= forwardHeaders(store.req.headers)
|
|
108
|
+
headers = new Headers(store.forwardedHeaders)
|
|
109
|
+
} else {
|
|
110
|
+
headers = new Headers()
|
|
111
|
+
}
|
|
104
112
|
return buildRpcRequest({ method, url, args, baseUrl, headers })
|
|
105
113
|
}
|
|
106
114
|
|
|
@@ -17,6 +17,7 @@ import { isDebugNegated } from '../../shared/isDebugNegated.ts'
|
|
|
17
17
|
import { logClosingRecord } from '../../shared/logClosingRecord.ts'
|
|
18
18
|
import { OFFLINE_HEADER } from '../../shared/OFFLINE_HEADER.ts'
|
|
19
19
|
import { parseBoundedEnvInt } from '../../shared/parseBoundedEnvInt.ts'
|
|
20
|
+
import { responseBodyKind } from '../../shared/responseBodyKind.ts'
|
|
20
21
|
import { SOCKETS_PATH } from '../../shared/SOCKETS_PATH.ts'
|
|
21
22
|
import { setAppName } from '../../shared/setAppName.ts'
|
|
22
23
|
import { setBaseResolver } from '../../shared/setBaseResolver.ts'
|
|
@@ -242,8 +243,11 @@ export async function createServer({
|
|
|
242
243
|
isn't installed. Resolved at boot so the fetch route below can branch on it.
|
|
243
244
|
*/
|
|
244
245
|
const inspectorHandler = await maybeMountInspector({ name: appName, version: appVersion })
|
|
245
|
-
/* Built on first request, then reused — the rpc registry is frozen after load.
|
|
246
|
-
|
|
246
|
+
/* Built on first request, then reused — the rpc registry is frozen after load.
|
|
247
|
+
Memoised as a promise so two concurrent cold requests share one build instead
|
|
248
|
+
of both building (the second otherwise clobbering the first). A rejected build
|
|
249
|
+
clears the memo so the next request retries rather than caching the failure. */
|
|
250
|
+
let openApiSpec: Promise<ReturnType<typeof buildOpenApiSpec>> | undefined
|
|
247
251
|
const cliCwd = process.cwd()
|
|
248
252
|
|
|
249
253
|
/* Request closing records are on by default — DEBUG=-abide is the off switch (negation, like the abide channel itself). */
|
|
@@ -362,12 +366,19 @@ export async function createServer({
|
|
|
362
366
|
const response = app?.handle
|
|
363
367
|
? await app.handle(req, (next) => handler(next, pathParams, store))
|
|
364
368
|
: await handler(req, pathParams, store)
|
|
369
|
+
/* Classify the body once (S2) and thread it into both downstream
|
|
370
|
+
steps — gzip + the closing-record stream monitor — instead of
|
|
371
|
+
each re-deriving from the Content-Type. */
|
|
372
|
+
const kind = responseBodyKind(response)
|
|
373
|
+
store.responseStreaming = kind === 'streaming'
|
|
374
|
+
// Streaming bodies (sse/jsonl, socket tail) opt out of the idle timeout.
|
|
375
|
+
if (kind === 'streaming') {
|
|
376
|
+
server.timeout(req, 0)
|
|
377
|
+
}
|
|
365
378
|
/* Gzip compressible dynamic bodies (SSR HTML, rpc/json, 404) when the
|
|
366
379
|
client accepts it; streaming frame protocols and static assets are
|
|
367
380
|
passed through untouched (see gzipResponse). */
|
|
368
|
-
|
|
369
|
-
// Streaming bodies (sse/jsonl, socket tail) opt out of the idle timeout.
|
|
370
|
-
return disableIdleTimeoutForStream(server, req, encoded)
|
|
381
|
+
return gzipResponse(req, response, kind)
|
|
371
382
|
})
|
|
372
383
|
}
|
|
373
384
|
|
|
@@ -583,14 +594,20 @@ export async function createServer({
|
|
|
583
594
|
req,
|
|
584
595
|
{},
|
|
585
596
|
async () => {
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
597
|
+
openApiSpec ??= ensureRegistriesLoaded()
|
|
598
|
+
.then(() =>
|
|
599
|
+
buildOpenApiSpec({
|
|
600
|
+
title: appName,
|
|
601
|
+
version: appVersion,
|
|
602
|
+
}),
|
|
603
|
+
)
|
|
604
|
+
.catch((error) => {
|
|
605
|
+
// Don't cache a failed build — clear the memo so a
|
|
606
|
+
// later request retries instead of 500-ing forever.
|
|
607
|
+
openApiSpec = undefined
|
|
608
|
+
throw error
|
|
591
609
|
})
|
|
592
|
-
|
|
593
|
-
return Response.json(openApiSpec, {
|
|
610
|
+
return Response.json(await openApiSpec, {
|
|
594
611
|
headers: { 'Cache-Control': NO_STORE },
|
|
595
612
|
})
|
|
596
613
|
},
|
|
@@ -26,7 +26,7 @@ export async function devHotModuleResponse(moduleId: string): Promise<Response>
|
|
|
26
26
|
if (source === undefined) {
|
|
27
27
|
return new Response('not found', { status: 404 })
|
|
28
28
|
}
|
|
29
|
-
const code = compileModule(source, {
|
|
29
|
+
const { code } = compileModule(source, {
|
|
30
30
|
isLayout: moduleId.endsWith('layout.abide'),
|
|
31
31
|
moduleId,
|
|
32
32
|
hot: true,
|
|
@@ -1,17 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import type { ResponseBodyKind } from '../../shared/responseBodyKind.ts'
|
|
2
|
+
import { responseBodyKind } from '../../shared/responseBodyKind.ts'
|
|
3
3
|
import { acceptsGzip } from './acceptsGzip.ts'
|
|
4
4
|
import { flushingGzipStream } from './flushingGzipStream.ts'
|
|
5
5
|
import { STREAMED_HTML_HEADER } from './STREAMED_HTML_HEADER.ts'
|
|
6
6
|
|
|
7
|
-
/*
|
|
8
|
-
Compressible Content-Types — text and structured-text payloads. Binary or
|
|
9
|
-
already-compressed bodies (images, fonts, archives, zstd/gzip blobs) gain
|
|
10
|
-
nothing from a second pass and only burn CPU.
|
|
11
|
-
*/
|
|
12
|
-
const COMPRESSIBLE_TYPE =
|
|
13
|
-
/^(?:text\/|application\/(?:json|javascript|xml|[\w.-]+\+(?:json|xml))|image\/svg)/
|
|
14
|
-
|
|
15
7
|
/*
|
|
16
8
|
Gzips a dynamic response (SSR HTML, rpc/json replies, the plain 404) when the
|
|
17
9
|
client accepts it. Static assets never reach here — they already carry a
|
|
@@ -23,6 +15,12 @@ expose a string body's length before send, and measuring would mean buffering th
|
|
|
23
15
|
body — the framing overhead on the rare tiny body is negligible against
|
|
24
16
|
compressing every page and rpc payload.
|
|
25
17
|
|
|
18
|
+
`kind` is the body class the dispatch pipeline already computed (S2: classify
|
|
19
|
+
once, thread it in) — only `compressible` bodies gzip; `streaming` and `opaque`
|
|
20
|
+
pass through. Standalone callers (the health probe) omit it and it's derived
|
|
21
|
+
here. The streamed-HTML marker is handled independent of `kind` since it's
|
|
22
|
+
stripped even on a skip path.
|
|
23
|
+
|
|
26
24
|
Buffered bodies take the web CompressionStream (best ratio, one flush at close).
|
|
27
25
|
The streamed SSR document self-marks (STREAMED_HTML_HEADER) and takes a
|
|
28
26
|
per-chunk-flushing gzip instead: the plain CompressionStream buffers the head
|
|
@@ -30,7 +28,7 @@ until its deflate window fills, which defeats streaming (the browser can't
|
|
|
30
28
|
preload-scan the head or paint the pending shell until the stream nearly closes).
|
|
31
29
|
The marker is stripped so it never reaches the client.
|
|
32
30
|
*/
|
|
33
|
-
export function gzipResponse(req: Request, response: Response): Response {
|
|
31
|
+
export function gzipResponse(req: Request, response: Response, kind?: ResponseBodyKind): Response {
|
|
34
32
|
const streamedHtml = response.headers.has(STREAMED_HTML_HEADER)
|
|
35
33
|
if (streamedHtml) {
|
|
36
34
|
response.headers.delete(STREAMED_HTML_HEADER)
|
|
@@ -38,11 +36,11 @@ export function gzipResponse(req: Request, response: Response): Response {
|
|
|
38
36
|
if (!response.body || response.headers.has('Content-Encoding')) {
|
|
39
37
|
return response
|
|
40
38
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
if (!
|
|
39
|
+
/* A streamed-HTML document classifies as `compressible` (text/html) but the
|
|
40
|
+
marker was already stripped above, so re-derive after the strip when the
|
|
41
|
+
pipeline didn't hand a kind in. */
|
|
42
|
+
const bodyKind = kind ?? responseBodyKind(response)
|
|
43
|
+
if (!acceptsGzip(req) || bodyKind !== 'compressible') {
|
|
46
44
|
return response
|
|
47
45
|
}
|
|
48
46
|
const headers = new Headers(response.headers)
|
|
@@ -120,7 +120,10 @@ export function runWithRequestScope(
|
|
|
120
120
|
})
|
|
121
121
|
})
|
|
122
122
|
}
|
|
123
|
-
|
|
123
|
+
/* Reuse the dispatch pipeline's single S2 classification when it stashed
|
|
124
|
+
one; callers that don't (mcp, the fetch fallback, tests) classify here. */
|
|
125
|
+
const streaming = store.responseStreaming ?? isStreamingResponse(response)
|
|
126
|
+
if (response.body && streaming) {
|
|
124
127
|
/*
|
|
125
128
|
A hand-pumped wrap rather than a TransformStream: streams typically
|
|
126
129
|
end by client disconnect (an SSE tab closing), which cancels the
|
|
@@ -52,4 +52,20 @@ export type RequestStore = {
|
|
|
52
52
|
carried no file parts.
|
|
53
53
|
*/
|
|
54
54
|
files?: Record<string, File[]>
|
|
55
|
+
/*
|
|
56
|
+
The allowlisted headers forwarded onto in-process rpc Requests during this
|
|
57
|
+
request's SSR pass — derived once from the inbound headers (which don't change
|
|
58
|
+
within the scope) and reused by every defineRpc call. Each call clones it,
|
|
59
|
+
since buildRpcRequest mutates the Headers (content-type/ref-json) before
|
|
60
|
+
constructing the Request. Undefined until the first in-process rpc.
|
|
61
|
+
*/
|
|
62
|
+
forwardedHeaders?: Headers
|
|
63
|
+
/*
|
|
64
|
+
The body class of the response the dispatch pipeline produced, stashed so
|
|
65
|
+
runWithRequestScope's closing-record stream monitor reuses the single S2
|
|
66
|
+
classification instead of re-deriving from the Content-Type. Undefined for
|
|
67
|
+
callers that don't classify (mcp, the fetch fallback, tests) — those fall
|
|
68
|
+
back to classifying the response themselves.
|
|
69
|
+
*/
|
|
70
|
+
responseStreaming?: boolean
|
|
55
71
|
}
|
|
@@ -17,8 +17,13 @@ export function withResponseDefaults(
|
|
|
17
17
|
status?: number,
|
|
18
18
|
): ResponseInit {
|
|
19
19
|
const headers = new Headers(defaultHeaders)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
/* The dominant case carries no overrides — skip the second Headers
|
|
21
|
+
allocation + per-key copy and ship the defaults alone. Only merge when
|
|
22
|
+
the caller actually supplied headers. */
|
|
23
|
+
if (init?.headers !== undefined) {
|
|
24
|
+
new Headers(init.headers).forEach((value, key) => {
|
|
25
|
+
headers.set(key, value)
|
|
26
|
+
})
|
|
27
|
+
}
|
|
23
28
|
return { ...init, headers, ...(status !== undefined && { status }) }
|
|
24
29
|
}
|
|
@@ -119,7 +119,11 @@ export function defineSocket<T>(name: string, opts: SocketOptions = {}): Socket<
|
|
|
119
119
|
cachedServer = getActiveServer()
|
|
120
120
|
}
|
|
121
121
|
const server = cachedServer
|
|
122
|
-
|
|
122
|
+
/* Remote fan-out is the only consumer of the encoded frame: in-process iterators
|
|
123
|
+
already received the live object via notify() above. Skip the full message-graph
|
|
124
|
+
walk (encodeRefJson) + native publish when zero ws clients are subscribed to the
|
|
125
|
+
topic — subscriberCount is a cheap C-side check, encodeRefJson is not. */
|
|
126
|
+
if (server && server.subscriberCount(topic) > 0) {
|
|
123
127
|
/* Typed against the shared wire contract so a `SocketServerFrame` change can't
|
|
124
128
|
silently drift from this construction site (the dispatcher's `send` is already
|
|
125
129
|
typed; this was the last `msg` frame built through an unchecked JSON.stringify). */
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/*
|
|
2
|
+
The `RpcArgs<Fn>` type alias both rpc d.ts writers emit verbatim: it lifts the
|
|
3
|
+
args type out of a RemoteFunction, dropping the FormData upload variant so a
|
|
4
|
+
url()/test-client call types against the rpc's plain args.
|
|
5
|
+
*/
|
|
6
|
+
export const RPC_ARGS_TYPE =
|
|
7
|
+
'type RpcArgs<Fn> = Fn extends (args: infer Args) => unknown ? Exclude<Args, FormData> : never'
|
|
@@ -1,20 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { createCacheStore } from './createCacheStore.ts'
|
|
1
|
+
import { cacheStoreResolver } from './cacheStoreResolver.ts'
|
|
3
2
|
import type { CacheStore } from './types/CacheStore.ts'
|
|
4
3
|
|
|
5
4
|
/*
|
|
6
|
-
Resolves the active CacheStore
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
a single fallback store is created lazily so isolated tests still work.
|
|
5
|
+
Resolves the active CacheStore: the registered resolver's store, or a single
|
|
6
|
+
lazily-created fallback when none is registered (so isolated tests work). The
|
|
7
|
+
fallback creator guarantees a value, hence the non-null assertion.
|
|
10
8
|
*/
|
|
11
9
|
export function activeCacheStore(): CacheStore {
|
|
12
|
-
|
|
13
|
-
if (fromResolver) {
|
|
14
|
-
return fromResolver
|
|
15
|
-
}
|
|
16
|
-
if (!cacheStoreSlot.fallback) {
|
|
17
|
-
cacheStoreSlot.fallback = createCacheStore()
|
|
18
|
-
}
|
|
19
|
-
return cacheStoreSlot.fallback
|
|
10
|
+
return cacheStoreResolver.get()!
|
|
20
11
|
}
|