@abide/abide 0.101.0 → 0.102.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/CHANGELOG.md +16 -0
- package/bin/abide.ts +223 -164
- package/docs/REGISTRY.md +726 -0
- package/docs/RULEBOOK.md +2753 -0
- package/docs/abide.md +12878 -0
- package/package.json +5 -3
- package/scaffold/AGENTS.md +31 -0
- package/scaffold/CLAUDE.md +1 -0
- package/scaffold/src/layout.abide +18 -0
- package/src/compiler/Lowering.ts +78 -9
- package/src/compiler/ShadowMap.ts +2 -2
- package/src/compiler/TypeSession.ts +262 -67
- package/src/compiler/argsSchemaOf.ts +1 -1
- package/src/compiler/checkApp.ts +43 -15
- package/src/compiler/checkedShadow.ts +39 -5
- package/src/compiler/classifyRead.ts +6 -4
- package/src/compiler/classifyTemplate.ts +1 -1
- package/src/compiler/compileApp.ts +433 -193
- package/src/compiler/compiledEntry.ts +43 -39
- package/src/compiler/componentGraph.ts +58 -71
- package/src/compiler/declaredExports.ts +24 -7
- package/src/compiler/emitClient.ts +141 -71
- package/src/compiler/emitComponent.ts +37 -8
- package/src/compiler/emitSource.ts +106 -13
- package/src/compiler/handlersUnder.ts +106 -59
- package/src/compiler/index.ts +7 -0
- package/src/compiler/mountsOf.ts +138 -0
- package/src/compiler/nearest.ts +14 -0
- package/src/compiler/parseAbide.ts +157 -20
- package/src/compiler/projectShadow.ts +213 -7
- package/src/compiler/reactiveTypes.ts +20 -7
- package/src/compiler/readSources.ts +15 -2
- package/src/compiler/readsIn.ts +61 -7
- package/src/compiler/renderMarkup.ts +3 -3
- package/src/compiler/scaffold.ts +30 -11
- package/src/compiler/serveLanguage.ts +125 -34
- package/src/compiler/sourceMapOf.ts +193 -0
- package/src/compiler/splitImports.ts +205 -83
- package/src/compiler/syncTypes.ts +32 -0
- package/src/compiler/tokenize.ts +14 -0
- package/src/compiler/writeDefaultProject.ts +1 -1
- package/src/compiler/writeTypes.ts +82 -0
- package/src/server/CLIENT_BUILD.ts +11 -13
- package/src/server/DEFAULT_PORT.ts +1 -1
- package/src/server/LAUNCHER_FILES.ts +9 -0
- package/src/server/MODULE_QUERIES.ts +18 -0
- package/src/server/TABLES.ts +7 -0
- package/src/server/VIRTUAL_DIRECTORY.ts +4 -5
- package/src/server/abideModules.ts +391 -0
- package/src/server/answerFailure.ts +16 -6
- package/src/server/answerRequest.ts +235 -106
- package/src/server/answerRpc.ts +102 -30
- package/src/server/answerSocket.ts +65 -20
- package/src/server/answerValue.ts +22 -9
- package/src/server/appDirectories.ts +1 -1
- package/src/server/appPlugins.ts +1 -1
- package/src/server/appTree.ts +179 -0
- package/src/server/argsFor.ts +2 -2
- package/src/server/assistant.ts +2 -2
- package/src/server/bridgeRelay.ts +6 -8
- package/src/server/claudeCode.ts +0 -6
- package/src/server/clientBuildOptions.ts +8 -3
- package/src/server/coerceArgs.ts +1 -1
- package/src/server/commands.ts +37 -37
- package/src/server/componentModules.d.ts +3 -1
- package/src/server/compressAnswer.ts +4 -4
- package/src/server/config.ts +25 -29
- package/src/server/configFields.ts +31 -25
- package/src/server/createApp.ts +59 -73
- package/src/server/devWorker.ts +114 -181
- package/src/server/executableName.ts +4 -0
- package/src/server/fenceServer.ts +37 -35
- package/src/server/generatedSurfaces.ts +99 -66
- package/src/server/hooks.ts +124 -52
- package/src/server/iconAccent.ts +191 -0
- package/src/server/index.ts +11 -7
- package/src/server/installSourceMaps.ts +88 -0
- package/src/server/launcherOptions.ts +34 -31
- package/src/server/launcherPage.ts +107 -64
- package/src/server/manifestOf.ts +16 -13
- package/src/server/namesUnder.ts +2 -2
- package/src/server/nativeMenu.ts +5 -5
- package/src/server/negotiateEncoding.ts +1 -1
- package/src/server/principal.ts +10 -5
- package/src/server/realOrAsGiven.ts +10 -0
- package/src/server/refuseAddressCollision.ts +21 -14
- package/src/server/remote.ts +49 -52
- package/src/server/render.ts +65 -94
- package/src/server/renderComponent.ts +5 -0
- package/src/server/reportHealth.ts +2 -2
- package/src/server/requestScope.ts +12 -1
- package/src/server/runBuild.ts +24 -0
- package/src/server/runCompiled.ts +5 -5
- package/src/server/runDesktop.ts +52 -27
- package/src/server/serveBuild.ts +43 -52
- package/src/server/serveDev.ts +73 -37
- package/src/server/shell.ts +7 -22
- package/src/server/sourceMappings.ts +139 -0
- package/src/server/surfaceOf.ts +18 -3
- package/src/server/testBuild.ts +1 -19
- package/src/server/withAbideSources.ts +117 -0
- package/src/shared/ADDRESSES.ts +7 -18
- package/src/shared/APP_META.ts +7 -0
- package/src/shared/CopyOptions.ts +1 -1
- package/src/shared/DEBUG_META.ts +1 -1
- package/src/shared/RESERVED_DIRECTORIES.ts +15 -0
- package/src/shared/cookies.ts +2 -2
- package/src/shared/csp.ts +1 -1
- package/src/shared/escapeText.ts +1 -1
- package/src/shared/filledPath.ts +85 -0
- package/src/shared/framing.ts +79 -6
- package/src/shared/headKeyOf.ts +2 -0
- package/src/shared/index.ts +1 -1
- package/src/shared/log.ts +27 -13
- package/src/shared/matchRoute.ts +23 -4
- package/src/shared/menuItem.ts +3 -3
- package/src/shared/reactive/Keyed.ts +2 -2
- package/src/shared/reactive/NotLanded.ts +1 -1
- package/src/shared/reactive/Reactive.ts +43 -43
- package/src/shared/reactive/argsContract.ts +4 -4
- package/src/shared/reactive/canonicalKey.ts +4 -4
- package/src/shared/reactive/chunksOfStream.ts +6 -0
- package/src/shared/reactive/index.ts +1 -1
- package/src/shared/reactive/recordWrites.ts +103 -5
- package/src/shared/reactive/structural.ts +1 -1
- package/src/shared/reactive/tracking.ts +1 -1
- package/src/shared/reactive/watch.ts +3 -3
- package/src/shared/refuse.ts +1 -1
- package/src/shared/routePattern.ts +82 -46
- package/src/shared/rpc.ts +37 -8
- package/src/shared/rpcRequest.ts +27 -3
- package/src/shared/url.ts +4 -44
- package/src/shared/writePath.ts +1 -1
- package/src/ui/LAYER_NAMES.ts +1 -1
- package/src/ui/List.ts +3 -3
- package/src/ui/MOUNTING_LEVEL.ts +18 -0
- package/src/ui/Mismatch.ts +1 -1
- package/src/ui/RawRun.ts +1 -1
- package/src/ui/Router.ts +38 -31
- package/src/ui/Scope.ts +1 -1
- package/src/ui/TryRegion.ts +2 -2
- package/src/ui/applyHead.ts +33 -0
- package/src/ui/index.ts +1 -1
- package/src/ui/menuBridge.ts +1 -1
- package/src/ui/moveNode.ts +1 -1
- package/src/ui/reconcile.ts +2 -2
- package/src/ui/reportMenu.ts +2 -2
- package/src/ui/routing.ts +2 -2
- package/src/ui/setAttribute.ts +1 -1
- package/src/ui/setText.ts +1 -1
- package/src/ui/socketWire.ts +9 -7
- package/src/ui/wire.ts +1 -1
- package/scaffold/src/server/app.ts +0 -6
- package/scaffold/src/shared/.gitkeep +0 -0
- package/scaffold/src/ui/app.html +0 -8
- package/src/server/componentImports.ts +0 -56
- /package/scaffold/src/{ui/pages/page.abide → page.abide} +0 -0
package/src/server/serveDev.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// 38.4 — `abide dev`: the project, watched, with the app kept up across every save. What is here
|
|
2
|
-
// is the LISTENER and nothing else (38.
|
|
2
|
+
// is the LISTENER and nothing else (38.14): the socket is bound once in this process and survives
|
|
3
3
|
// every reload, so a browser holding a connection open meets a server that is still there when
|
|
4
4
|
// the app behind it has been thrown away and rebuilt.
|
|
5
5
|
//
|
|
@@ -12,13 +12,14 @@
|
|
|
12
12
|
// reload, where a compiler imported into each new app cost 4.5 s a save against the 80 ms the
|
|
13
13
|
// compile takes — and the clause asks for the APP to be in the child, which the compiler is not.
|
|
14
14
|
//
|
|
15
|
-
// 38.
|
|
15
|
+
// 38.15 — A RELOAD IS A NEW PROCESS, not a module cache to invalidate. An app leaves things behind
|
|
16
16
|
// that no invalidation reaches — a timer, a `share`, a module that ran once, and bun's own memory of
|
|
17
17
|
// a file it failed to find — and a process is the one boundary that takes all of them with it. Every live socket is force-closed with it, which
|
|
18
18
|
// is also the whole of the reload protocol: the reload client reconnects and reloads, so nothing
|
|
19
19
|
// here has a message to send.
|
|
20
20
|
|
|
21
21
|
import { watch } from 'node:fs' // bun exposes no file watcher of its own
|
|
22
|
+
import { dirname, resolve } from 'node:path' // bun exposes no path helpers of its own
|
|
22
23
|
import {
|
|
23
24
|
type AppChecker,
|
|
24
25
|
compileApp,
|
|
@@ -26,23 +27,27 @@ import {
|
|
|
26
27
|
} from '#compiler/compileApp.ts'
|
|
27
28
|
import { formatDiagnosis } from '#compiler/formatDiagnosis.ts'
|
|
28
29
|
import { DEV_ADDRESS } from '#shared/ADDRESSES.ts'
|
|
30
|
+
import { placeOf } from '#shared/routePattern.ts'
|
|
31
|
+
import type { Compilation } from './abideModules.ts'
|
|
29
32
|
import { DEFAULT_PORT } from './DEFAULT_PORT.ts'
|
|
30
33
|
import type { Ask, FromWorker, ToWorker } from './devWorker.ts'
|
|
31
34
|
import { upgraded } from './muxUpgrade.ts'
|
|
32
35
|
import { plainAnswer } from './plainAnswer.ts'
|
|
33
36
|
import { SETTLE_MILLISECONDS } from './SETTLE_MILLISECONDS.ts'
|
|
34
|
-
import { VIRTUAL_DIRECTORY } from './VIRTUAL_DIRECTORY.ts'
|
|
35
37
|
|
|
36
38
|
/** What `abide dev` serves, and where. */
|
|
37
39
|
export type DevOptions = {
|
|
38
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* The app's source root — where its pages sit, and the tree that is watched — as its real path
|
|
42
|
+
* (`appRoot`), which is how the bundler names what it built.
|
|
43
|
+
*/
|
|
39
44
|
root: string
|
|
40
45
|
port?: number | undefined
|
|
41
46
|
}
|
|
42
47
|
|
|
43
48
|
// A request in flight: the request itself — because a worker replaced before it answered answers
|
|
44
|
-
// nothing, and the ask has to be put to the app that exists (38.
|
|
45
|
-
// back, the stream the body is fed into and the pull waiting on the worker's next chunk (38.
|
|
49
|
+
// nothing, and the ask has to be put to the app that exists (38.24) — and, once its head has come
|
|
50
|
+
// back, the stream the body is fed into and the pull waiting on the worker's next chunk (38.35).
|
|
46
51
|
type InFlight = Omit<Ask, 'id' | 'method' | 'url'> & {
|
|
47
52
|
// 16.22 — the request bun handed the listener, which is the one its idle timeout is raised on.
|
|
48
53
|
request: Request
|
|
@@ -60,7 +65,12 @@ type DevWorker = {
|
|
|
60
65
|
terminate(): void
|
|
61
66
|
}
|
|
62
67
|
|
|
63
|
-
type Compiled = {
|
|
68
|
+
type Compiled = {
|
|
69
|
+
compilation: Compilation
|
|
70
|
+
/** The public files the compile's walk found (`appFiles`), which the worker answers. */
|
|
71
|
+
published: Map<string, string>
|
|
72
|
+
failure: string
|
|
73
|
+
}
|
|
64
74
|
|
|
65
75
|
const LAST_DEV_PORT = 3099
|
|
66
76
|
|
|
@@ -128,21 +138,26 @@ export async function serveDev(
|
|
|
128
138
|
if (checker !== undefined && !checker.current())
|
|
129
139
|
await closeChecker()
|
|
130
140
|
checker ??= await openChecker(options.root)
|
|
131
|
-
const compiled = await compileApp(
|
|
132
|
-
options.root,
|
|
133
|
-
`${options.root}/${VIRTUAL_DIRECTORY}`,
|
|
134
|
-
checker,
|
|
135
|
-
)
|
|
141
|
+
const compiled = await compileApp(options.root, checker)
|
|
136
142
|
// 14 and 32.14 — what the compiler has to say, on every compile.
|
|
137
143
|
for (const warning of compiled.warnings)
|
|
138
144
|
console.warn(formatDiagnosis(warning, process.cwd()))
|
|
139
|
-
|
|
145
|
+
watchDependencies(compiled.files)
|
|
146
|
+
return {
|
|
147
|
+
compilation: { files: compiled.files, tables: compiled.tables },
|
|
148
|
+
published: compiled.published,
|
|
149
|
+
failure: '',
|
|
150
|
+
}
|
|
140
151
|
} catch (thrown) {
|
|
141
152
|
await closeChecker()
|
|
142
153
|
const failure =
|
|
143
154
|
thrown instanceof Error ? thrown.message : String(thrown)
|
|
144
155
|
console.error(`abide dev: ${failure}`)
|
|
145
|
-
return {
|
|
156
|
+
return {
|
|
157
|
+
compilation: { files: {}, tables: {} },
|
|
158
|
+
published: new Map(),
|
|
159
|
+
failure,
|
|
160
|
+
}
|
|
146
161
|
}
|
|
147
162
|
}
|
|
148
163
|
|
|
@@ -156,9 +171,34 @@ export async function serveDev(
|
|
|
156
171
|
}
|
|
157
172
|
}
|
|
158
173
|
|
|
174
|
+
// A path a watcher saw move — or a move nobody could name — and the replacement it schedules.
|
|
175
|
+
let settling: ReturnType<typeof setTimeout> | undefined
|
|
176
|
+
function moved(path: string | undefined): void {
|
|
177
|
+
checker?.saw(path)
|
|
178
|
+
if (settling !== undefined) clearTimeout(settling)
|
|
179
|
+
settling = setTimeout(replace, SETTLE_MILLISECONDS)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// 38.99 — A DEPENDENCY'S `.abide` IS THE APP'S TOO, and a linked package's lies outside the root's
|
|
183
|
+
// watcher: each directory one was compiled from is watched once, for the session.
|
|
184
|
+
const watchedDependencies = new Set<string>()
|
|
185
|
+
function watchDependencies(files: Compilation['files']): void {
|
|
186
|
+
for (const relative of Object.keys(files)) {
|
|
187
|
+
if (placeOf(relative) !== 'dependency') continue
|
|
188
|
+
const directory = dirname(resolve(options.root, relative))
|
|
189
|
+
if (watchedDependencies.has(directory)) continue
|
|
190
|
+
watchedDependencies.add(directory)
|
|
191
|
+
watch(directory, (_event, filename) =>
|
|
192
|
+
moved(
|
|
193
|
+
filename === null ? undefined : `${directory}/${filename}`,
|
|
194
|
+
),
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
159
199
|
// The worker, compiled and answering.
|
|
160
|
-
function open({
|
|
161
|
-
// 38.
|
|
200
|
+
function open({ compilation, published, failure }: Compiled): void {
|
|
201
|
+
// 38.14 — A PROCESS, NOT A THREAD, for the header's reason: bun remembers a file it failed
|
|
162
202
|
// to find for the life of the process, and a thread inherits the miss.
|
|
163
203
|
//
|
|
164
204
|
// THE ENVIRONMENT AS IT STANDS, `NODE_ENV` included (36.3): the child is handed what this
|
|
@@ -182,7 +222,7 @@ export async function serveDev(
|
|
|
182
222
|
server.timeout(asked, message.seconds)
|
|
183
223
|
} else answer(started, message)
|
|
184
224
|
},
|
|
185
|
-
// 38.
|
|
225
|
+
// 38.41 — AN APP THAT EXITS ON ITS OWN is replaced, and says so: a listener still
|
|
186
226
|
// forwarding to it leaves every tab hanging.
|
|
187
227
|
onExit() {
|
|
188
228
|
if (worker !== started) return
|
|
@@ -200,7 +240,7 @@ export async function serveDev(
|
|
|
200
240
|
if (!child.killed && child.exitCode === null)
|
|
201
241
|
child.send(message)
|
|
202
242
|
},
|
|
203
|
-
// 38.
|
|
243
|
+
// 38.96 — SIGTERM, which the worker answers by hanging up what it is answering; a worker
|
|
204
244
|
// wedged in a loop never reads it, so a second later it is killed outright.
|
|
205
245
|
terminate() {
|
|
206
246
|
child.kill()
|
|
@@ -211,7 +251,8 @@ export async function serveDev(
|
|
|
211
251
|
started.postMessage({
|
|
212
252
|
kind: 'start',
|
|
213
253
|
root: options.root,
|
|
214
|
-
|
|
254
|
+
compilation,
|
|
255
|
+
published,
|
|
215
256
|
failure,
|
|
216
257
|
})
|
|
217
258
|
// The worker holds every request behind its own compile, so what waited here only had
|
|
@@ -221,8 +262,8 @@ export async function serveDev(
|
|
|
221
262
|
}
|
|
222
263
|
|
|
223
264
|
// WHAT AN APP THAT IS GONE WAS HOLDING. A request whose head has not gone out is re-asked of the
|
|
224
|
-
// replacement (38.
|
|
225
|
-
// ended it (38.
|
|
265
|
+
// replacement (38.24), or answered where the app ended itself — the request in hand may be what
|
|
266
|
+
// ended it (38.41). 38.37 — one whose head has gone out cannot be answered again, and ends as
|
|
226
267
|
// `abide start`'s does.
|
|
227
268
|
function endInFlight(reask: boolean, why: string): void {
|
|
228
269
|
for (const held of inFlight.values()) {
|
|
@@ -235,11 +276,11 @@ export async function serveDev(
|
|
|
235
276
|
inFlight.clear()
|
|
236
277
|
}
|
|
237
278
|
|
|
238
|
-
// 38.
|
|
279
|
+
// 38.15 — THE REPLACEMENT. The old worker is terminated, every socket it could still be
|
|
239
280
|
// answering is force-closed, and the new one is opened. The order matters: a socket closed
|
|
240
281
|
// after the new worker is up is a reload client that reconnects to a server still compiling.
|
|
241
282
|
//
|
|
242
|
-
// 38.
|
|
283
|
+
// 38.24 — A REQUEST IN FLIGHT IS RE-ASKED rather than failed. Failing it serves a reader a
|
|
243
284
|
// network error for asking while somebody saved, and during a regeneration of a whole docs
|
|
244
285
|
// tree that is every request; re-asked, they wait and get the app the source now says.
|
|
245
286
|
function replace(): void {
|
|
@@ -258,24 +299,19 @@ export async function serveDev(
|
|
|
258
299
|
await compiling
|
|
259
300
|
|
|
260
301
|
// 38.4 — THE PROJECT, WATCHED. One recursive watcher over the source root: the pages, the
|
|
261
|
-
//
|
|
302
|
+
// layouts, the stylesheets and every `public` directory are all under it, and every one of them is something
|
|
262
303
|
// the worker read at boot.
|
|
263
304
|
//
|
|
264
305
|
// IT IS NOT UNREFFED. It holds the process open, which is what a command that IS a server
|
|
265
306
|
// wants: the only ways out of `abide dev` are a signal and a throw, exactly as for
|
|
266
307
|
// `abide start`.
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
filename === null ? undefined : `${options.root}/${filename}`,
|
|
271
|
-
)
|
|
272
|
-
if (settling !== undefined) clearTimeout(settling)
|
|
273
|
-
settling = setTimeout(replace, SETTLE_MILLISECONDS)
|
|
274
|
-
})
|
|
308
|
+
watch(options.root, { recursive: true }, (_event, filename) =>
|
|
309
|
+
moved(filename === null ? undefined : `${options.root}/${filename}`),
|
|
310
|
+
)
|
|
275
311
|
|
|
276
312
|
// A REQUEST AS A MESSAGE. `Request` does not survive a structured clone, so what crosses is
|
|
277
313
|
// the four things the worker rebuilds one from — and the request's body is collected here, which
|
|
278
|
-
// is what lets it be asked again of a replacement (38.
|
|
314
|
+
// is what lets it be asked again of a replacement (38.24). The answer's body streams (38.35).
|
|
279
315
|
//
|
|
280
316
|
// COLLECTED ONCE, HERE, rather than inside `forward`: a body can only be read off a
|
|
281
317
|
// `Request` once, and a re-asked request is the second read that is not there.
|
|
@@ -294,7 +330,7 @@ export async function serveDev(
|
|
|
294
330
|
})
|
|
295
331
|
}
|
|
296
332
|
|
|
297
|
-
// 38.
|
|
333
|
+
// 38.38 — ASKED WITHOUT `accept-encoding`, so the app answers uncompressed. A failure after the
|
|
298
334
|
// head is shown as markup appended to the stream (`pull` in the worker), and markup appended to a
|
|
299
335
|
// compressed stream is a stream no browser can decode. Localhost buys nothing from the bytes saved.
|
|
300
336
|
function identityOnly(headers: Headers): [string, string][] {
|
|
@@ -304,7 +340,7 @@ export async function serveDev(
|
|
|
304
340
|
return kept
|
|
305
341
|
}
|
|
306
342
|
|
|
307
|
-
// 38.
|
|
343
|
+
// 38.35, 38.36 — THE HEAD SETTLES THE RESPONSE, and its body pulls one chunk from the worker per
|
|
308
344
|
// read the connection makes: a high-water mark of 0 asks for nothing ahead of the reader.
|
|
309
345
|
function answer(
|
|
310
346
|
from: DevWorker,
|
|
@@ -478,7 +514,7 @@ export async function serveDev(
|
|
|
478
514
|
},
|
|
479
515
|
})
|
|
480
516
|
|
|
481
|
-
// 38.
|
|
517
|
+
// 38.64 — A PORT SOMEONE NAMED IS THE PORT, and one taken is the failure. `Bun.serve` given
|
|
482
518
|
// none reads `PORT` itself.
|
|
483
519
|
const server =
|
|
484
520
|
options.port !== undefined || process.env.PORT
|
|
@@ -487,7 +523,7 @@ export async function serveDev(
|
|
|
487
523
|
return server as unknown as Bun.Server<undefined>
|
|
488
524
|
}
|
|
489
525
|
|
|
490
|
-
// 38.
|
|
526
|
+
// 38.63 — NAMED BY NOBODY, the first free port from 3000. A port taken is the next one tried, and
|
|
491
527
|
// any other failure is the failure.
|
|
492
528
|
function listenOnFirstFree<Listener>(
|
|
493
529
|
listen: (port: number) => Listener,
|
package/src/server/shell.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { useColor } from '#shared/log.ts'
|
|
|
9
9
|
import { paint as painted } from '#shared/paint.ts'
|
|
10
10
|
import type { Mount } from './answerRpc.ts'
|
|
11
11
|
import { LogPanel } from './LogPanel.ts'
|
|
12
|
-
import { callHandler,
|
|
12
|
+
import { callHandler, watch } from './remote.ts'
|
|
13
13
|
import { type Listed, surfaceOf } from './surfaceOf.ts'
|
|
14
14
|
|
|
15
15
|
/**
|
|
@@ -48,8 +48,7 @@ function wordsOf(text: string): string[] {
|
|
|
48
48
|
* is in argv (38.8). Answers `exit` where the line said so and `ended` where the input did, so a
|
|
49
49
|
* host that owns the app can stop it on the one and keep serving on the other.
|
|
50
50
|
*
|
|
51
|
-
* `local` is the app in this process — the one `abide` alone serves — whose
|
|
52
|
-
* `logs` has nothing to add, and whose `exit` stops it.
|
|
51
|
+
* `local` is the app in this process — the one `abide` alone serves — whose `exit` stops it.
|
|
53
52
|
*/
|
|
54
53
|
export async function shell(options: {
|
|
55
54
|
url: string
|
|
@@ -78,7 +77,7 @@ export async function shell(options: {
|
|
|
78
77
|
),
|
|
79
78
|
})
|
|
80
79
|
// CTRL-C AS A SHELL TAKES IT — raw mode takes the key from the terminal, so no signal is sent for
|
|
81
|
-
// it: a call running is
|
|
80
|
+
// it: a call running is canceled, then every watch standing, a line typed is dropped, and on
|
|
82
81
|
// an empty line it asks once and leaves on the second, as `node`'s REPL does.
|
|
83
82
|
let exited = false
|
|
84
83
|
let closed = false
|
|
@@ -169,7 +168,6 @@ export async function shell(options: {
|
|
|
169
168
|
)
|
|
170
169
|
: undefined
|
|
171
170
|
if (panel === undefined) console.log(options.heading)
|
|
172
|
-
let tailing = false
|
|
173
171
|
try {
|
|
174
172
|
for await (const line of lines) {
|
|
175
173
|
const [word, address] = line.trim().split(/\s+/)
|
|
@@ -199,7 +197,7 @@ export async function shell(options: {
|
|
|
199
197
|
).catch((thrown: Error) => ({
|
|
200
198
|
code: 1,
|
|
201
199
|
text: running?.signal.aborted
|
|
202
|
-
? '(
|
|
200
|
+
? '(canceled)'
|
|
203
201
|
: thrown.message,
|
|
204
202
|
}))
|
|
205
203
|
running = undefined
|
|
@@ -210,14 +208,8 @@ export async function shell(options: {
|
|
|
210
208
|
)
|
|
211
209
|
} else if (word === 'call')
|
|
212
210
|
console.log('call: name the handler — call <address> [args]')
|
|
213
|
-
//
|
|
214
|
-
else if (word === '
|
|
215
|
-
tailing = true
|
|
216
|
-
void followLogs(url).then(() => {
|
|
217
|
-
tailing = false
|
|
218
|
-
})
|
|
219
|
-
} else if (word === 'watch' && address !== undefined) {
|
|
220
|
-
// Followed alongside the prompt, as `logs` is.
|
|
211
|
+
// FOLLOWED ALONGSIDE THE PROMPT rather than in place of it, so the shell still answers.
|
|
212
|
+
else if (word === 'watch' && address !== undefined) {
|
|
221
213
|
const following = new AbortController()
|
|
222
214
|
watching.add(following)
|
|
223
215
|
void watch(
|
|
@@ -235,12 +227,6 @@ export async function shell(options: {
|
|
|
235
227
|
.finally(() => watching.delete(following))
|
|
236
228
|
} else if (word === 'watch')
|
|
237
229
|
console.log('watch: name a GET — watch <address> [args]')
|
|
238
|
-
else if (word === 'logs')
|
|
239
|
-
console.log(
|
|
240
|
-
local
|
|
241
|
-
? "logs: the app's log already tails here."
|
|
242
|
-
: 'logs: already tailing.',
|
|
243
|
-
)
|
|
244
230
|
else
|
|
245
231
|
console.log(
|
|
246
232
|
`unknown: ${word} — help lists what the shell takes`,
|
|
@@ -456,7 +442,6 @@ export function shellWords(local: boolean): [usage: string, meaning: string][] {
|
|
|
456
442
|
['handlers', 'every handler call can reach, with its args'],
|
|
457
443
|
['watch <address> [args]', 'follow a GET — watch chat/room room=lobby'],
|
|
458
444
|
]
|
|
459
|
-
if (!local) words.push(['logs', "tail the app's log.records here"])
|
|
460
445
|
words.push(
|
|
461
446
|
['help', 'this list — Ctrl+C clears the line, Ctrl+L the panel'],
|
|
462
447
|
['exit', local ? 'stop the app and leave' : 'leave the shell'],
|
|
@@ -488,7 +473,7 @@ export function cliListing(handlers: Mount[]): Listed[] {
|
|
|
488
473
|
|
|
489
474
|
/**
|
|
490
475
|
* The handlers the shell lists: each address with the args it is typed with, and its description on
|
|
491
|
-
* the line under it where it declares one. 38.
|
|
476
|
+
* the line under it where it declares one. 38.62.
|
|
492
477
|
*/
|
|
493
478
|
export function handlersText(
|
|
494
479
|
listed: Listed[],
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// THE `mappings` FIELD OF A SOURCE MAP (revision 3), read and written. The compiler writes one per
|
|
2
|
+
// module it emits from a `.abide` (`sourceMapOf`), the server reads them to place a stack frame in
|
|
3
|
+
// the `.abide` (`installSourceMaps`), and the client build reads bun's to put the two together —
|
|
4
|
+
// bun reads no map it is handed, at run time or in the bundler, so every one of those is ours.
|
|
5
|
+
|
|
6
|
+
/** Where one generated column came from: `[column, source, line, sourceColumn]`, all from 0. */
|
|
7
|
+
export type Segment = [number, number, number, number]
|
|
8
|
+
|
|
9
|
+
const DIGITS =
|
|
10
|
+
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
|
|
11
|
+
const VALUES = new Map<string, number>()
|
|
12
|
+
for (let index = 0; index < DIGITS.length; index += 1)
|
|
13
|
+
VALUES.set(DIGITS[index] as string, index)
|
|
14
|
+
|
|
15
|
+
function vlq(value: number): string {
|
|
16
|
+
let rest = value < 0 ? (-value << 1) | 1 : value << 1
|
|
17
|
+
let out = ''
|
|
18
|
+
do {
|
|
19
|
+
let digit = rest & 31
|
|
20
|
+
rest >>>= 5
|
|
21
|
+
if (rest > 0) digit |= 32
|
|
22
|
+
out += DIGITS[digit]
|
|
23
|
+
} while (rest > 0)
|
|
24
|
+
return out
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Each generated line's segments, ascending by column, as the `mappings` string. */
|
|
28
|
+
export function encodeMappings(lines: readonly (readonly Segment[])[]): string {
|
|
29
|
+
let out = ''
|
|
30
|
+
let source = 0
|
|
31
|
+
let line = 0
|
|
32
|
+
let sourceColumn = 0
|
|
33
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
34
|
+
if (index > 0) out += ';'
|
|
35
|
+
let column = 0
|
|
36
|
+
const segments = lines[index] as readonly Segment[]
|
|
37
|
+
for (let at = 0; at < segments.length; at += 1) {
|
|
38
|
+
const segment = segments[at] as Segment
|
|
39
|
+
if (at > 0) out += ','
|
|
40
|
+
out +=
|
|
41
|
+
vlq(segment[0] - column) +
|
|
42
|
+
vlq(segment[1] - source) +
|
|
43
|
+
vlq(segment[2] - line) +
|
|
44
|
+
vlq(segment[3] - sourceColumn)
|
|
45
|
+
column = segment[0]
|
|
46
|
+
source = segment[1]
|
|
47
|
+
line = segment[2]
|
|
48
|
+
sourceColumn = segment[3]
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return out
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* A `mappings` string as each generated line's segments. A segment naming no source — one field
|
|
56
|
+
* long — places nothing, and is dropped.
|
|
57
|
+
*/
|
|
58
|
+
export function decodeMappings(mappings: string): Segment[][] {
|
|
59
|
+
const lines: Segment[][] = [[]]
|
|
60
|
+
const fields: number[] = []
|
|
61
|
+
let source = 0
|
|
62
|
+
let line = 0
|
|
63
|
+
let sourceColumn = 0
|
|
64
|
+
let column = 0
|
|
65
|
+
let value = 0
|
|
66
|
+
let shift = 0
|
|
67
|
+
const close = (): void => {
|
|
68
|
+
if (fields.length >= 4) {
|
|
69
|
+
column += fields[0] as number
|
|
70
|
+
source += fields[1] as number
|
|
71
|
+
line += fields[2] as number
|
|
72
|
+
sourceColumn += fields[3] as number
|
|
73
|
+
;(lines[lines.length - 1] as Segment[]).push([
|
|
74
|
+
column,
|
|
75
|
+
source,
|
|
76
|
+
line,
|
|
77
|
+
sourceColumn,
|
|
78
|
+
])
|
|
79
|
+
} else if (fields.length > 0) column += fields[0] as number
|
|
80
|
+
fields.length = 0
|
|
81
|
+
}
|
|
82
|
+
for (const character of mappings) {
|
|
83
|
+
if (character === ';') {
|
|
84
|
+
close()
|
|
85
|
+
lines.push([])
|
|
86
|
+
column = 0
|
|
87
|
+
continue
|
|
88
|
+
}
|
|
89
|
+
if (character === ',') {
|
|
90
|
+
close()
|
|
91
|
+
continue
|
|
92
|
+
}
|
|
93
|
+
const digit = VALUES.get(character) ?? 0
|
|
94
|
+
value |= (digit & 31) << shift
|
|
95
|
+
if (digit & 32) {
|
|
96
|
+
shift += 5
|
|
97
|
+
continue
|
|
98
|
+
}
|
|
99
|
+
fields.push(value & 1 ? -(value >>> 1) : value >>> 1)
|
|
100
|
+
value = 0
|
|
101
|
+
shift = 0
|
|
102
|
+
}
|
|
103
|
+
close()
|
|
104
|
+
return lines
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Where `column` on generated `line` came from: the last segment at or before it, or the line's
|
|
109
|
+
* first where the column is before every one — a frame's column points into an expression, and
|
|
110
|
+
* the segment is its start.
|
|
111
|
+
*/
|
|
112
|
+
export function segmentAt(
|
|
113
|
+
lines: readonly (readonly Segment[])[],
|
|
114
|
+
line: number,
|
|
115
|
+
column: number,
|
|
116
|
+
): Segment | undefined {
|
|
117
|
+
const segments = lines[line]
|
|
118
|
+
if (segments === undefined || segments.length === 0) return undefined
|
|
119
|
+
let found = segments[0] as Segment
|
|
120
|
+
for (const segment of segments) {
|
|
121
|
+
if (segment[0] > column) break
|
|
122
|
+
found = segment
|
|
123
|
+
}
|
|
124
|
+
return found
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The `[line, column]` generated `line` and `column` came from. Within a segment the run was copied
|
|
129
|
+
* as written, so the column moves with it.
|
|
130
|
+
*/
|
|
131
|
+
export function placeAt(
|
|
132
|
+
lines: readonly (readonly Segment[])[],
|
|
133
|
+
line: number,
|
|
134
|
+
column: number,
|
|
135
|
+
): [number, number] | undefined {
|
|
136
|
+
const segment = segmentAt(lines, line, column)
|
|
137
|
+
if (segment === undefined) return undefined
|
|
138
|
+
return [segment[2], segment[3] + Math.max(column - segment[0], 0)]
|
|
139
|
+
}
|
package/src/server/surfaceOf.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { RPC_ADDRESS } from '#shared/ADDRESSES.ts'
|
|
2
1
|
import type { Method } from '#shared/rpc.ts'
|
|
3
2
|
import type { Mount, Mounted } from './answerRpc.ts'
|
|
4
3
|
|
|
@@ -9,8 +8,10 @@ export type Surface = 'mcp' | 'openapi' | 'cli'
|
|
|
9
8
|
export type Listed = {
|
|
10
9
|
address: string
|
|
11
10
|
method: Method
|
|
12
|
-
/** Its
|
|
11
|
+
/** Its route without the leading `/` — what `call` spells it (38.8). */
|
|
13
12
|
path: string
|
|
13
|
+
/** What MCP and OpenAPI name it by. 20.13. */
|
|
14
|
+
name: string
|
|
14
15
|
description: string | undefined
|
|
15
16
|
/** Its args' JSON Schema, where it has one. 19.9, 20.11. */
|
|
16
17
|
schema: unknown
|
|
@@ -39,9 +40,23 @@ export function surfaceOf(
|
|
|
39
40
|
return {
|
|
40
41
|
address: mount.address,
|
|
41
42
|
method: mount.method,
|
|
42
|
-
path: mount.address.slice(
|
|
43
|
+
path: mount.address.slice(1),
|
|
44
|
+
name: surfaceName(mount.method, mount.address),
|
|
43
45
|
description:
|
|
44
46
|
live === undefined ? mount.description : live.handler.description,
|
|
45
47
|
schema: live === undefined ? mount.args : live.argsDocument,
|
|
46
48
|
}
|
|
47
49
|
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A handler's name on MCP and OpenAPI: its method and its route, in the letters a tool's name admits.
|
|
53
|
+
*
|
|
54
|
+
* 20.13 — the method first, so two handlers at one route are two names, and a segment's brackets
|
|
55
|
+
* and dots dropped, so `/media/[id]` reads `media_id` rather than `media__id_`.
|
|
56
|
+
*/
|
|
57
|
+
export function surfaceName(method: Method, route: string): string {
|
|
58
|
+
return `${method.toLowerCase()}${route === '/' ? '' : route.replace(/[[\].]/g, '')}`.replace(
|
|
59
|
+
/[^a-zA-Z0-9-]/g,
|
|
60
|
+
'_',
|
|
61
|
+
)
|
|
62
|
+
}
|
package/src/server/testBuild.ts
CHANGED
|
@@ -7,11 +7,8 @@
|
|
|
7
7
|
import { mkdir, rename, rm, utimes, writeFile } from 'node:fs/promises' // bun exposes no directory creation, rename, removal, mtime write or exclusive create of its own
|
|
8
8
|
import { dirname } from 'node:path' // bun exposes no path helpers of its own
|
|
9
9
|
import { namesUnder } from './namesUnder.ts'
|
|
10
|
+
import { runBuild } from './runBuild.ts'
|
|
10
11
|
|
|
11
|
-
// The CLI, spawned rather than imported: the compiler is not reachable from the server seam.
|
|
12
|
-
const ABIDE_BIN = Bun.fileURLToPath(
|
|
13
|
-
new URL('../../bin/abide.ts', import.meta.url),
|
|
14
|
-
)
|
|
15
12
|
// The framework's own source is part of what made a build: a compiler change is a new build.
|
|
16
13
|
const FRAMEWORK = Bun.fileURLToPath(new URL('../..', import.meta.url))
|
|
17
14
|
const POLL_MS = 50
|
|
@@ -95,21 +92,6 @@ async function buildInto(
|
|
|
95
92
|
}
|
|
96
93
|
}
|
|
97
94
|
|
|
98
|
-
async function runBuild(root: string, out: string): Promise<void> {
|
|
99
|
-
const child = Bun.spawn(
|
|
100
|
-
[process.execPath, ABIDE_BIN, 'build', '--root', root, '--out', out],
|
|
101
|
-
{ stdout: 'ignore', stderr: 'pipe' },
|
|
102
|
-
)
|
|
103
|
-
const [exitCode, said] = await Promise.all([
|
|
104
|
-
child.exited,
|
|
105
|
-
new Response(child.stderr).text(),
|
|
106
|
-
])
|
|
107
|
-
if (exitCode !== 0)
|
|
108
|
-
throw new Error(
|
|
109
|
-
`abide: createApp could not build the app under ${root} for this test run.\n${said}`,
|
|
110
|
-
)
|
|
111
|
-
}
|
|
112
|
-
|
|
113
95
|
/**
|
|
114
96
|
* A hash of everything a build of the app under `root` is made from.
|
|
115
97
|
*
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { relative, resolve } from 'node:path' // bun exposes no path helpers of its own
|
|
2
|
+
import { COMPONENT_NAMESPACE, type Compilation } from './abideModules.ts'
|
|
3
|
+
import { COMPONENT_QUERY } from './MODULE_QUERIES.ts'
|
|
4
|
+
import {
|
|
5
|
+
decodeMappings,
|
|
6
|
+
encodeMappings,
|
|
7
|
+
placeAt,
|
|
8
|
+
type Segment,
|
|
9
|
+
} from './sourceMappings.ts'
|
|
10
|
+
|
|
11
|
+
const NAMESPACED = `${COMPONENT_NAMESPACE}:`
|
|
12
|
+
|
|
13
|
+
// THE PARSER IS IMPORTED ON THE FIRST MAP ASKED FOR, not with the dev worker each reload boots:
|
|
14
|
+
// only a debugger reads one.
|
|
15
|
+
let parser: Promise<typeof import('#compiler/parseAbide.ts')> | undefined
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 38.97 — a client chunk's source map, with every position in a module compiled from a `.abide`
|
|
19
|
+
* placed in the `.abide` instead.
|
|
20
|
+
*
|
|
21
|
+
* Bun maps the chunk to the modules it bundled and reads no map those carry, so the second step is
|
|
22
|
+
* taken here. A position in the module's own scaffolding — what no `.abide` line wrote — stays in
|
|
23
|
+
* the compiled module, which is still listed, so a debugger steps through it rather than losing it.
|
|
24
|
+
*/
|
|
25
|
+
export async function withAbideSources(
|
|
26
|
+
mapText: string,
|
|
27
|
+
compilation: Compilation,
|
|
28
|
+
root: string,
|
|
29
|
+
): Promise<string> {
|
|
30
|
+
const map = JSON.parse(mapText) as {
|
|
31
|
+
sources: string[]
|
|
32
|
+
sourcesContent?: (string | null)[]
|
|
33
|
+
mappings: string
|
|
34
|
+
}
|
|
35
|
+
const sources = map.sources
|
|
36
|
+
const contents = map.sourcesContent ?? []
|
|
37
|
+
// Per bundled source, its module's map and the index its `.abide` is listed at.
|
|
38
|
+
const placed: ({ lines: Segment[][]; index: number } | undefined)[] = []
|
|
39
|
+
const listed = new Map<string, number>()
|
|
40
|
+
const reads: Promise<void>[] = []
|
|
41
|
+
const originals = sources.length
|
|
42
|
+
for (let index = 0; index < originals; index += 1) {
|
|
43
|
+
// Bun names a bundled source from where the command runs, and a component module — which
|
|
44
|
+
// the plugin served at its `.abide`'s path — by its namespace and that path.
|
|
45
|
+
const named = sources[index] as string
|
|
46
|
+
const component = named.startsWith(NAMESPACED)
|
|
47
|
+
const abide = resolve(
|
|
48
|
+
component ? named.slice(NAMESPACED.length) : named,
|
|
49
|
+
)
|
|
50
|
+
const mappings = component
|
|
51
|
+
? compilation.files[relative(root, abide)]?.clientMappings
|
|
52
|
+
: undefined
|
|
53
|
+
if (mappings === undefined) {
|
|
54
|
+
placed.push(undefined)
|
|
55
|
+
continue
|
|
56
|
+
}
|
|
57
|
+
// The module bun bundled is named apart from the `.abide` it is placed in, which shares
|
|
58
|
+
// its path.
|
|
59
|
+
sources[index] = `${relative(process.cwd(), abide)}${COMPONENT_QUERY}`
|
|
60
|
+
let at = listed.get(abide)
|
|
61
|
+
if (at === undefined) {
|
|
62
|
+
at = sources.length
|
|
63
|
+
listed.set(abide, at)
|
|
64
|
+
// Named as bun names its own, from where the command runs; read from where it is.
|
|
65
|
+
sources.push(relative(process.cwd(), abide))
|
|
66
|
+
const slot = at
|
|
67
|
+
parser ??= import('#compiler/parseAbide.ts')
|
|
68
|
+
reads.push(
|
|
69
|
+
Promise.all([Bun.file(abide).text(), parser]).then(
|
|
70
|
+
([text, { parseAbide }]) => {
|
|
71
|
+
contents[slot] = withoutServerBlocks(text, parseAbide)
|
|
72
|
+
},
|
|
73
|
+
),
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
placed.push({ lines: decodeMappings(mappings), index: at })
|
|
77
|
+
}
|
|
78
|
+
if (listed.size === 0) return mapText
|
|
79
|
+
await Promise.all(reads)
|
|
80
|
+
for (let index = contents.length; index < sources.length; index += 1)
|
|
81
|
+
contents[index] ??= null
|
|
82
|
+
const lines = decodeMappings(map.mappings)
|
|
83
|
+
for (const segments of lines)
|
|
84
|
+
for (const segment of segments) {
|
|
85
|
+
const module = placed[segment[1]]
|
|
86
|
+
if (module === undefined) continue
|
|
87
|
+
const from = placeAt(module.lines, segment[2], segment[3])
|
|
88
|
+
if (from === undefined) continue
|
|
89
|
+
segment[1] = module.index
|
|
90
|
+
segment[2] = from[0]
|
|
91
|
+
segment[3] = from[1]
|
|
92
|
+
}
|
|
93
|
+
map.sourcesContent = contents
|
|
94
|
+
map.mappings = encodeMappings(lines)
|
|
95
|
+
return JSON.stringify(map)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 16.28, 34.20 — the `.abide`'s text with every `<script server>` body blanked: the map is served
|
|
100
|
+
* to a browser, which reads none of one. Each line is kept, so every line the map places is still
|
|
101
|
+
* the line it names.
|
|
102
|
+
*/
|
|
103
|
+
function withoutServerBlocks(
|
|
104
|
+
text: string,
|
|
105
|
+
parseAbide: typeof import('#compiler/parseAbide.ts').parseAbide,
|
|
106
|
+
): string {
|
|
107
|
+
const file = parseAbide(text)
|
|
108
|
+
let blanked = ''
|
|
109
|
+
let at = 0
|
|
110
|
+
for (let index = 0; index < file.servers.length; index += 1) {
|
|
111
|
+
const start = file.origins.servers[index] as number
|
|
112
|
+
const end = start + (file.servers[index] as string).length
|
|
113
|
+
blanked += `${text.slice(at, start)}${text.slice(start, end).replace(/[^\n]/g, '')}`
|
|
114
|
+
at = end
|
|
115
|
+
}
|
|
116
|
+
return blanked + text.slice(at)
|
|
117
|
+
}
|