@abide/abide 0.100.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 +31 -0
- package/bin/abide.ts +227 -163
- package/docs/REGISTRY.md +726 -0
- package/docs/RULEBOOK.md +2753 -0
- package/docs/abide.md +12878 -0
- package/package.json +10 -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 -56
- 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 +124 -39
- package/src/server/answerSocket.ts +141 -48
- 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 +48 -14
- 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 +144 -183
- 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 +81 -39
- 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/GLOBAL_GETS.ts +9 -3
- package/src/shared/RESERVED_DIRECTORIES.ts +15 -0
- package/src/shared/RPC_SERVER.ts +2 -2
- 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/Cursor.ts +3 -9
- package/src/shared/reactive/Keyed.ts +4 -3
- package/src/shared/reactive/NotLanded.ts +1 -1
- package/src/shared/reactive/Reactive.ts +81 -60
- 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 +155 -21
- package/src/shared/reactive/watch.ts +10 -6
- package/src/shared/refuse.ts +1 -1
- package/src/shared/routePattern.ts +82 -46
- package/src/shared/rpc.ts +47 -13
- 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/devWorker.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// 38.
|
|
1
|
+
// 38.14 — THE APP, IN A CHILD PROCESS. Everything an app's own code touches is in here: the modules
|
|
2
2
|
// it compiled to, the client bundle, and the render. The listener is the parent (`serveDev`), so
|
|
3
3
|
// replacing this process is how a reload happens and nothing an app left behind — a module in the
|
|
4
4
|
// registry, a timer, a `share`, a file bun once failed to find — survives one.
|
|
@@ -6,15 +6,15 @@
|
|
|
6
6
|
// THE COMPILER IS NOT THE APP, and it stays outside: `serveDev` compiles and sends the modules
|
|
7
7
|
// in, so the compiler is loaded once for every reload — `serveDev` says what that saves.
|
|
8
8
|
//
|
|
9
|
-
// NOTHING IS WRITTEN TO DISK (38.
|
|
10
|
-
// plugin
|
|
11
|
-
// bundle a browser fetches
|
|
12
|
-
//
|
|
9
|
+
// NOTHING IS WRITTEN TO DISK (38.16). The compilation arrives as text and is served through the one
|
|
10
|
+
// plugin `abide start` serves a build's through (`abideModules`): at runtime for the server modules
|
|
11
|
+
// this worker imports, and in the bundle a browser fetches — each `.abide` at its own path, so a
|
|
12
|
+
// module's imports resolve from the source that wrote them in either.
|
|
13
13
|
//
|
|
14
|
-
// A REQUEST ARRIVES AS A MESSAGE, and its answer leaves as a head and then a chunk per pull (38.
|
|
15
|
-
// 38.
|
|
14
|
+
// A REQUEST ARRIVES AS A MESSAGE, and its answer leaves as a head and then a chunk per pull (38.35,
|
|
15
|
+
// 38.36): the listener asks for the next chunk when its connection has taken the last, so a stream
|
|
16
16
|
// is read no faster than the browser reads it. A request BODY is still collected, which is what
|
|
17
|
-
// lets the listener re-ask a request of the replacement (38.
|
|
17
|
+
// lets the listener re-ask a request of the replacement (38.24).
|
|
18
18
|
//
|
|
19
19
|
// 18.1, 18.3 — AND SO DOES A SOCKET. The listener holds the browser's web socket, which has to
|
|
20
20
|
// outlive this worker; what crosses is each frame, tagged with the connection it came over, and
|
|
@@ -23,13 +23,16 @@
|
|
|
23
23
|
import { CLIENT_ADDRESS, DEV_ADDRESS } from '#shared/ADDRESSES.ts'
|
|
24
24
|
import { escapeText } from '#shared/escapeText.ts'
|
|
25
25
|
import {
|
|
26
|
-
|
|
27
|
-
type
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
abideModules,
|
|
27
|
+
type Compilation,
|
|
28
|
+
serveCompilation,
|
|
29
|
+
servedTable,
|
|
30
|
+
} from './abideModules.ts'
|
|
31
|
+
import { type AppSources, appSources } from './answerRequest.ts'
|
|
30
32
|
import type { Mux, MuxConnection } from './answerSocket.ts'
|
|
31
33
|
import { appPlugins } from './appPlugins.ts'
|
|
32
|
-
import {
|
|
34
|
+
import { publishedUnder } from './appTree.ts'
|
|
35
|
+
import { CLIENT_ENTRY } from './CLIENT_BUILD.ts'
|
|
33
36
|
import {
|
|
34
37
|
clientBuildOptions,
|
|
35
38
|
stylesheetUnderMount,
|
|
@@ -40,26 +43,28 @@ import { fenceServer } from './fenceServer.ts'
|
|
|
40
43
|
import { type Manifest, manifestOf, NO_MANIFEST } from './manifestOf.ts'
|
|
41
44
|
import { admitsMux } from './muxUpgrade.ts'
|
|
42
45
|
import { refuseAddressCollision } from './refuseAddressCollision.ts'
|
|
43
|
-
import { shellUnder } from './render.ts'
|
|
44
46
|
import { LISTENING } from './requestScope.ts'
|
|
45
|
-
import {
|
|
46
|
-
import { VIRTUAL_DIRECTORY } from './VIRTUAL_DIRECTORY.ts'
|
|
47
|
+
import { withAbideSources } from './withAbideSources.ts'
|
|
47
48
|
|
|
48
49
|
// The reload client, which the worker serves and every document it answers loads.
|
|
49
50
|
const RELOAD_CLIENT_ADDRESS = `${DEV_ADDRESS}/client.js`
|
|
50
51
|
|
|
51
52
|
// The bundle this worker answers `/__abide/client/…` out of, the type each name is served as, and
|
|
52
|
-
// the chunks each route module needs (38.
|
|
53
|
+
// the chunks each route module needs (38.28).
|
|
53
54
|
type Bundle = {
|
|
54
|
-
// A stylesheet is held as its text, rewritten (`stylesheetUnderMount`);
|
|
55
|
-
|
|
55
|
+
// A stylesheet is held as its text, rewritten (`stylesheetUnderMount`); a source map as the
|
|
56
|
+
// rewrite of it, taken on the first ask (`withAbideSources`); the rest as built.
|
|
57
|
+
files: Map<
|
|
58
|
+
string,
|
|
59
|
+
{ bytes: ArrayBuffer | string | (() => Promise<string>); type: string }
|
|
60
|
+
>
|
|
56
61
|
manifest: Manifest
|
|
57
62
|
}
|
|
58
63
|
|
|
59
64
|
/**
|
|
60
65
|
* A message from the listener to this worker.
|
|
61
66
|
*
|
|
62
|
-
* 38.
|
|
67
|
+
* 38.14 — WHAT CROSSES BETWEEN THE LISTENER AND THIS WORKER, one union per direction. In: the app
|
|
63
68
|
* compiled (and what stopped it compiling, where something did), a request, a pull or cancel of
|
|
64
69
|
* its body, and each browser web socket's frames for the mux.
|
|
65
70
|
*/
|
|
@@ -67,7 +72,9 @@ export type ToWorker =
|
|
|
67
72
|
| {
|
|
68
73
|
kind: 'start'
|
|
69
74
|
root: string
|
|
70
|
-
|
|
75
|
+
compilation: Compilation
|
|
76
|
+
/** Every public file, to its address, as the compile's walk found them. */
|
|
77
|
+
published: Map<string, string>
|
|
71
78
|
failure: string
|
|
72
79
|
}
|
|
73
80
|
| ({ kind: 'request' } & Ask)
|
|
@@ -120,13 +127,32 @@ export type Ask = {
|
|
|
120
127
|
/**
|
|
121
128
|
* Sends a message to the listener.
|
|
122
129
|
*
|
|
123
|
-
* 38.
|
|
130
|
+
* 38.14 — over bun's IPC: this is its child process (`serveDev` says why a process).
|
|
124
131
|
*/
|
|
125
132
|
function toListener(message: FromWorker): void {
|
|
126
133
|
process.send?.(message)
|
|
127
134
|
}
|
|
128
|
-
//
|
|
129
|
-
|
|
135
|
+
// 38.96 — EACH REQUEST'S SIGNAL, by request id, until its answer ends: the listener holds the
|
|
136
|
+
// connection, so a hang-up reaches this worker as a `cancel` rather than through bun.
|
|
137
|
+
const HANG_UPS = new Map<number, AbortController>()
|
|
138
|
+
|
|
139
|
+
/** Files away the hang-up of the request `id`, whose answer has ended, and hands it back. */
|
|
140
|
+
function answered(id: number): AbortController | undefined {
|
|
141
|
+
const hangUp = HANG_UPS.get(id)
|
|
142
|
+
HANG_UPS.delete(id)
|
|
143
|
+
return hangUp
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// EVERY REQUEST IN FLIGHT HANGS UP WITH THIS WORKER, as each would with `abide start`'s process: a
|
|
147
|
+
// handler that spawned under `request().signal` takes its child down, where an exit alone left it
|
|
148
|
+
// running under launchd. The listener ends this worker on every reload, and its own end — killed
|
|
149
|
+
// rather than ended — is a disconnect.
|
|
150
|
+
function hangUpAndExit(): void {
|
|
151
|
+
for (const hangUp of HANG_UPS.values()) hangUp.abort()
|
|
152
|
+
process.exit(0)
|
|
153
|
+
}
|
|
154
|
+
process.on('disconnect', hangUpAndExit)
|
|
155
|
+
process.on('SIGTERM', hangUpAndExit)
|
|
130
156
|
|
|
131
157
|
// THE COMPILE, AS SOMETHING TO WAIT ON. A request that arrives while the worker is still
|
|
132
158
|
// compiling has to wait for the app rather than be told there is none: the listener forwards
|
|
@@ -138,9 +164,9 @@ let app: BootedApp | undefined
|
|
|
138
164
|
// 18.1 — the mux over this worker's handlers, and each connection the listener holds.
|
|
139
165
|
let mux: Mux | undefined
|
|
140
166
|
// What the mux carries, which `admitsMux` reads: an app with no handler has no mux to open.
|
|
141
|
-
let carried:
|
|
167
|
+
let carried: AppSources | undefined
|
|
142
168
|
const connections = new Map<number, MuxConnection>()
|
|
143
|
-
// What stopped the app compiling, kept rather than thrown: 38.4 keeps the server up and 38.
|
|
169
|
+
// What stopped the app compiling, kept rather than thrown: 38.4 keeps the server up and 38.17
|
|
144
170
|
// keeps it up through a bundle that failed, so the failure is something to SERVE.
|
|
145
171
|
let failure = ''
|
|
146
172
|
|
|
@@ -159,6 +185,7 @@ async function receive(message: ToWorker): Promise<void> {
|
|
|
159
185
|
if (message.kind === 'cancel') {
|
|
160
186
|
const reading = bodies.get(message.id)
|
|
161
187
|
bodies.delete(message.id)
|
|
188
|
+
answered(message.id)?.abort()
|
|
162
189
|
return reading?.reader.cancel()
|
|
163
190
|
}
|
|
164
191
|
await opening
|
|
@@ -176,7 +203,7 @@ async function receive(message: ToWorker): Promise<void> {
|
|
|
176
203
|
})
|
|
177
204
|
},
|
|
178
205
|
// NOTHING TO CLOSE: the listener holds the browser's socket, and it outlives this
|
|
179
|
-
// worker (38.
|
|
206
|
+
// worker (38.14). Nothing here stops the mux, either.
|
|
180
207
|
close() {},
|
|
181
208
|
},
|
|
182
209
|
)
|
|
@@ -217,10 +244,13 @@ const BODILESS = new Set([101, 204, 205, 304])
|
|
|
217
244
|
* they gave up.
|
|
218
245
|
*/
|
|
219
246
|
async function answerAsk(ask: Ask): Promise<void> {
|
|
247
|
+
const hangUp = new AbortController()
|
|
248
|
+
HANG_UPS.set(ask.id, hangUp)
|
|
220
249
|
const request = new Request(ask.url, {
|
|
221
250
|
method: ask.method,
|
|
222
251
|
headers: ask.headers,
|
|
223
252
|
body: ask.body,
|
|
253
|
+
signal: hangUp.signal,
|
|
224
254
|
})
|
|
225
255
|
ASKS.set(request, ask.id)
|
|
226
256
|
let response: Response
|
|
@@ -251,7 +281,10 @@ async function answerAsk(ask: Ask): Promise<void> {
|
|
|
251
281
|
carried !== undefined && admitsMux(request, response, carried),
|
|
252
282
|
whole,
|
|
253
283
|
})
|
|
254
|
-
if (body === null || whole !== undefined)
|
|
284
|
+
if (body === null || whole !== undefined) {
|
|
285
|
+
answered(ask.id)
|
|
286
|
+
return
|
|
287
|
+
}
|
|
255
288
|
bodies.set(ask.id, {
|
|
256
289
|
reader: body.getReader(),
|
|
257
290
|
html: (response.headers.get('content-type') ?? '').startsWith(
|
|
@@ -274,6 +307,7 @@ async function pull(id: number): Promise<void> {
|
|
|
274
307
|
const { done, value } = await reading.reader.read()
|
|
275
308
|
if (done) {
|
|
276
309
|
bodies.delete(id)
|
|
310
|
+
answered(id)
|
|
277
311
|
toListener({ kind: 'end', id })
|
|
278
312
|
return
|
|
279
313
|
}
|
|
@@ -284,6 +318,7 @@ async function pull(id: number): Promise<void> {
|
|
|
284
318
|
toListener({ kind: 'chunk', id, bytes })
|
|
285
319
|
} catch (thrown) {
|
|
286
320
|
bodies.delete(id)
|
|
321
|
+
answered(id)
|
|
287
322
|
const why = describe(thrown)
|
|
288
323
|
if (reading.html) {
|
|
289
324
|
const shown = new TextEncoder().encode(failureMarkup(why))
|
|
@@ -315,79 +350,74 @@ async function open(
|
|
|
315
350
|
failure = start.failure
|
|
316
351
|
if (failure !== '') return
|
|
317
352
|
const root = start.root
|
|
353
|
+
const compilation = start.compilation
|
|
318
354
|
try {
|
|
319
|
-
const base = `${root}/${VIRTUAL_DIRECTORY}`
|
|
320
|
-
const modules = new Map<string, string>()
|
|
321
|
-
for (const [name, text] of start.modules)
|
|
322
|
-
modules.set(`${base}/${name}`, text)
|
|
323
355
|
// A page among what compiled — an app of handlers alone has the tables and no client to
|
|
324
356
|
// bundle.
|
|
325
|
-
const paged =
|
|
357
|
+
const paged = compilation.tables[`${CLIENT_ENTRY}.ts`] !== undefined
|
|
326
358
|
|
|
327
359
|
// ONE PLUGIN, TWO CALLERS: the runtime resolves this worker's `import()` through it and
|
|
328
360
|
// the bundler resolves the browser's graph through it, so the two lanes cannot disagree
|
|
329
361
|
// about what a specifier means. Registering it is process-wide — which is a worker rather
|
|
330
362
|
// than the command, so a reload is a new registry rather than a cache to bust.
|
|
331
|
-
|
|
332
|
-
Bun.plugin(plugin)
|
|
363
|
+
serveCompilation(root, compilation)
|
|
333
364
|
|
|
334
|
-
// ASKED FOR TOGETHER. The bundle, the
|
|
365
|
+
// ASKED FOR TOGETHER. The bundle, the public directory and the server's own
|
|
335
366
|
// tables depend on none of each other — the plugin is registered above, before any of
|
|
336
367
|
// them asks it anything — and on a reload every one of them is latency the author waits on,
|
|
337
368
|
// so the bundle is handed over still building (`SourceParts.client`).
|
|
338
369
|
//
|
|
339
|
-
// ONE LISTING of the public
|
|
340
|
-
// 38.
|
|
341
|
-
// output, so the address answers where the author's file actually is.
|
|
342
|
-
const
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
)
|
|
347
|
-
// 38.44 — the app's own plugins, imported beside the reads below rather than ahead of them.
|
|
370
|
+
// ONE LISTING of the public directories — the compile's — for the bundle's externals and for
|
|
371
|
+
// what is served. 38.19 — OUT OF THE SOURCE TREE. `abide build` copies these into its output
|
|
372
|
+
// and dev has no output, so the address answers where the author's file actually is.
|
|
373
|
+
const publicSources = start.published
|
|
374
|
+
const published = publishedUnder(root, publicSources)
|
|
375
|
+
const publicAddresses = [...publicSources.values()]
|
|
376
|
+
// 38.43 — the app's own plugins, imported beside the reads below rather than ahead of them.
|
|
348
377
|
const bundling = appPlugins(root).then((own) =>
|
|
349
378
|
bundleInMemory(
|
|
350
379
|
root,
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
[plugin, fenceServer(root, start.modules), ...own],
|
|
380
|
+
// 16.28, 16.29 — the browser's view of the server, as `abide build` gives it.
|
|
381
|
+
[fenceServer(root), abideModules(root, compilation), ...own],
|
|
354
382
|
paged,
|
|
355
383
|
publicAddresses,
|
|
384
|
+
compilation,
|
|
356
385
|
),
|
|
357
386
|
)
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
import(
|
|
361
|
-
Bun.pathToFileURL(`${base}/server.ts`).href
|
|
362
|
-
) as Promise<ServerTable>,
|
|
363
|
-
handlersIn(base),
|
|
364
|
-
])
|
|
365
|
-
// 38.18 — THE RELOAD CLIENT IS THIS WORKER'S, not the bundle's, and it goes in whether or
|
|
366
|
-
// not there is an entry beside it: a bundle that failed is exactly when a reader needs the
|
|
367
|
-
// next save to reach them. `clientTags` writes nothing for a manifest with no entry.
|
|
368
|
-
const sources = await appSources({
|
|
369
|
-
table,
|
|
370
|
-
shell,
|
|
371
|
-
handlers,
|
|
372
|
-
handlerModule: sourceModuleLoader(root, base),
|
|
373
|
-
published,
|
|
374
|
-
client: bundling.then((bundle) => ({
|
|
375
|
-
manifest: bundle.manifest,
|
|
376
|
-
head: `<script type="module" src="${RELOAD_CLIENT_ADDRESS}"></script>`,
|
|
377
|
-
framework: (pathname) => served(bundle, pathname),
|
|
378
|
-
})),
|
|
379
|
-
explained: (thrown) => failedDocument(describe(thrown)),
|
|
380
|
-
})
|
|
381
|
-
|
|
387
|
+
const table = await servedTable(root)
|
|
388
|
+
const handlers = table.handlers
|
|
382
389
|
// 36.13, IN THIS LANE TOO. A throw here lands in `failure`, which is what 38.4 asks
|
|
383
390
|
// for: the command keeps answering and what it answers names both halves. Without it
|
|
384
391
|
// `answerRequest` below would silently prefer the file — its public-before-routes
|
|
385
392
|
// order rests on this collision being impossible.
|
|
386
|
-
refuseAddressCollision(
|
|
393
|
+
refuseAddressCollision(
|
|
394
|
+
table.routes,
|
|
395
|
+
handlers,
|
|
396
|
+
publicSources,
|
|
397
|
+
'abide dev',
|
|
398
|
+
)
|
|
387
399
|
|
|
388
|
-
|
|
389
|
-
//
|
|
390
|
-
|
|
400
|
+
// THE SAME `App` `abide start` boots, so an app's hooks and middleware run in dev too — and its
|
|
401
|
+
// sources are read INSIDE the boot, as `abide start` reads them: a document's head carries
|
|
402
|
+
// what the resolved configuration says (28.4's app name), which is not resolved before it.
|
|
403
|
+
app = await bootApp(originUnder(root), async () => {
|
|
404
|
+
// 38.17 — THE RELOAD CLIENT IS THIS WORKER'S, not the bundle's, and it goes in whether or
|
|
405
|
+
// not there is an entry beside it: a bundle that failed is exactly when a reader needs the
|
|
406
|
+
// next save to reach them. `clientTags` writes nothing for a manifest with no entry.
|
|
407
|
+
const sources = await appSources({
|
|
408
|
+
table,
|
|
409
|
+
handlers,
|
|
410
|
+
published,
|
|
411
|
+
client: bundling.then((bundle) => ({
|
|
412
|
+
manifest: bundle.manifest,
|
|
413
|
+
head: `<script type="module" src="${RELOAD_CLIENT_ADDRESS}"></script>`,
|
|
414
|
+
framework: (pathname) => served(bundle, pathname),
|
|
415
|
+
})),
|
|
416
|
+
explained: (thrown) => failedDocument(describe(thrown)),
|
|
417
|
+
})
|
|
418
|
+
carried = sources
|
|
419
|
+
return sources
|
|
420
|
+
})
|
|
391
421
|
// 18.1 — the mux `App.listen` would open, over the app's own record: a frame's `rpc.raw` and
|
|
392
422
|
// MCP surface are this app's.
|
|
393
423
|
mux = app.openMux()
|
|
@@ -407,9 +437,21 @@ function served(bundle: Bundle, pathname: string): Response | undefined {
|
|
|
407
437
|
if (!pathname.startsWith(`${CLIENT_ADDRESS}/`)) return undefined
|
|
408
438
|
const found = bundle.files.get(pathname.slice(CLIENT_ADDRESS.length + 1))
|
|
409
439
|
if (found === undefined) return undefined
|
|
440
|
+
const { bytes } = found
|
|
441
|
+
const body =
|
|
442
|
+
typeof bytes === 'function'
|
|
443
|
+
? new ReadableStream({
|
|
444
|
+
async start(controller) {
|
|
445
|
+
controller.enqueue(
|
|
446
|
+
new TextEncoder().encode(await bytes()),
|
|
447
|
+
)
|
|
448
|
+
controller.close()
|
|
449
|
+
},
|
|
450
|
+
})
|
|
451
|
+
: bytes
|
|
410
452
|
// NO CACHING AT ALL, which is where dev parts company with 21.5: the names are stable across
|
|
411
453
|
// rebuilds, so a browser told to keep `entry.js` for a year keeps the app it had at boot.
|
|
412
|
-
return new Response(
|
|
454
|
+
return new Response(body, {
|
|
413
455
|
headers: { 'content-type': found.type, 'cache-control': 'no-store' },
|
|
414
456
|
})
|
|
415
457
|
}
|
|
@@ -417,30 +459,28 @@ function served(bundle: Bundle, pathname: string): Response | undefined {
|
|
|
417
459
|
/**
|
|
418
460
|
* The client bundle, built into memory.
|
|
419
461
|
*
|
|
420
|
-
* 38.
|
|
462
|
+
* 38.16 — Unminified, and the entry named by its source rather than content-hashed, so what a
|
|
421
463
|
* browser loads and what a stack trace names are the module the author wrote. 38.5's hashes and
|
|
422
464
|
* manifest are the BUILD's, and the difference is deliberate: a dev chunk that changed has to be
|
|
423
465
|
* fetched again, and a name that changes with it is a cache the reload cannot clear.
|
|
424
466
|
*
|
|
425
|
-
* A FAILURE IS AN EMPTY BUNDLE AND NOT A THROW (38.
|
|
467
|
+
* A FAILURE IS AN EMPTY BUNDLE AND NOT A THROW (38.17). The pages still render, the document
|
|
426
468
|
* still loads the reload client, and the next save is what fixes it.
|
|
427
469
|
*/
|
|
428
470
|
async function bundleInMemory(
|
|
429
471
|
root: string,
|
|
430
|
-
base: string,
|
|
431
472
|
plugins: Bun.BunPlugin[],
|
|
432
473
|
paged: boolean,
|
|
433
474
|
publicAddresses: string[],
|
|
475
|
+
compilation: Compilation,
|
|
434
476
|
): Promise<Bundle> {
|
|
435
477
|
const bundle: Bundle = { files: new Map(), manifest: NO_MANIFEST }
|
|
436
478
|
if (!paged) return bundle
|
|
437
479
|
const built = await Bun.build({
|
|
438
|
-
// 36.15's addresses off the source tree this lane answers them out of (38.
|
|
439
|
-
...clientBuildOptions(
|
|
440
|
-
//
|
|
441
|
-
|
|
442
|
-
// below makes them flat anyway.
|
|
443
|
-
root,
|
|
480
|
+
// 36.15's addresses off the source tree this lane answers them out of (38.19).
|
|
481
|
+
...clientBuildOptions(root, plugins, publicAddresses),
|
|
482
|
+
// 38.97 — a debugger in the browser reads the `.abide` (`withAbideSources`).
|
|
483
|
+
sourcemap: 'linked',
|
|
444
484
|
naming: {
|
|
445
485
|
entry: '[name].[ext]',
|
|
446
486
|
chunk: '[name]-[hash].[ext]',
|
|
@@ -448,7 +488,7 @@ async function bundleInMemory(
|
|
|
448
488
|
},
|
|
449
489
|
throw: false,
|
|
450
490
|
})
|
|
451
|
-
// THE TERMINAL RATHER THAN THE DOCUMENT. 38.
|
|
491
|
+
// THE TERMINAL RATHER THAN THE DOCUMENT. 38.17 keeps the server up, and a page that still
|
|
452
492
|
// paints is what it keeps up — so the pages stay served, unhydrated, and the author reads the
|
|
453
493
|
// build error where they started the command.
|
|
454
494
|
if (!built.success) {
|
|
@@ -465,103 +505,24 @@ async function bundleInMemory(
|
|
|
465
505
|
const output = built.outputs[index] as Bun.BuildArtifact
|
|
466
506
|
const name = output.path.slice(output.path.lastIndexOf('/') + 1)
|
|
467
507
|
const bytes = read[index] as ArrayBuffer
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
/**
|
|
482
|
-
* The plugin serving the compiled modules out of `modules`, for the runtime and the bundler alike.
|
|
483
|
-
*
|
|
484
|
-
* A module in the map wins; everything else is resolved from where that module PRETENDS to be,
|
|
485
|
-
* which is inside the app's package — so `abide`, `#ui/…` and a stylesheet all answer exactly as
|
|
486
|
-
* they do for a build on disk.
|
|
487
|
-
*/
|
|
488
|
-
function virtualModules(modules: Map<string, string>): Bun.BunPlugin {
|
|
489
|
-
return {
|
|
490
|
-
name: 'abide-dev',
|
|
491
|
-
setup(build) {
|
|
492
|
-
build.onResolve({ filter: /.*/ }, (args) => {
|
|
493
|
-
if (modules.has(args.path))
|
|
494
|
-
return { path: args.path, namespace: 'abide-dev' }
|
|
495
|
-
// The importer carries its namespace in front of the path when it is one of
|
|
496
|
-
// ours — and the runtime puts a `/` in front of that again, where the bundler
|
|
497
|
-
// does not. Both spellings are stripped here because a `file://` URL refuses to
|
|
498
|
-
// take either as a base, and the one that was missed cost the whole route table:
|
|
499
|
-
// every dynamic import in it read as a module nobody could find.
|
|
500
|
-
const importer = args.importer.replace(/^\/?abide-dev:/, '')
|
|
501
|
-
if (!modules.has(importer)) return undefined
|
|
502
|
-
const from = importer.slice(0, importer.lastIndexOf('/'))
|
|
503
|
-
if (args.path.startsWith('.')) {
|
|
504
|
-
const beside = Bun.fileURLToPath(
|
|
505
|
-
new URL(args.path, Bun.pathToFileURL(`${from}/`)),
|
|
506
|
-
)
|
|
507
|
-
if (modules.has(beside))
|
|
508
|
-
return { path: beside, namespace: 'abide-dev' }
|
|
509
|
-
}
|
|
510
|
-
// OUT OF THE MAP AND INTO THE PACKAGE. Resolved from the virtual directory rather
|
|
511
|
-
// than left to the default, which has an `abide-dev:` prefix to walk up from and
|
|
512
|
-
// finds nothing.
|
|
513
|
-
try {
|
|
514
|
-
return {
|
|
515
|
-
path: Bun.resolveSync(args.path, from),
|
|
516
|
-
namespace: 'file',
|
|
517
|
-
}
|
|
518
|
-
} catch {
|
|
519
|
-
return undefined
|
|
520
|
-
}
|
|
521
|
-
})
|
|
522
|
-
// ONLY THE RUNTIME'S LOADS ARE REWRITTEN: the bundler asks `onResolve` above for a `#`
|
|
523
|
-
// specifier itself, and a reload's loads are nearly all the bundler's — 181 of 183 on
|
|
524
|
-
// the docs app, where the rewrite's parse was ~25 ms of each reload.
|
|
525
|
-
const bundling = 'config' in build
|
|
526
|
-
build.onLoad({ filter: /.*/, namespace: 'abide-dev' }, (args) => ({
|
|
527
|
-
contents: bundling
|
|
528
|
-
? (modules.get(args.path) ?? '')
|
|
529
|
-
: withSeamsResolved(
|
|
530
|
-
modules.get(args.path) ?? '',
|
|
531
|
-
args.path,
|
|
532
|
-
),
|
|
533
|
-
// 34.4 — a component's scoped styles are a stylesheet the bundle collects.
|
|
534
|
-
loader: args.path.endsWith('.css') ? 'css' : 'ts',
|
|
535
|
-
}))
|
|
536
|
-
},
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
// A `#` SPECIFIER IN A MODULE FROM THE MAP, resolved before the module is handed over. The runtime
|
|
541
|
-
// takes a `#` specifier to the package's `imports` itself rather than through this plugin, and from
|
|
542
|
-
// a module in the map it takes the target as an exact file — so the extensionless
|
|
543
|
-
// `#server/rpc/contacts` a component writes found nothing, where the bundler, which does ask the
|
|
544
|
-
// plugin, probed its way to the `.ts` through the same `Bun.resolveSync` — so the rewrite is the
|
|
545
|
-
// runtime's alone, and both lanes land on the same path.
|
|
546
|
-
const TRANSPILER = new Bun.Transpiler({ loader: 'ts' })
|
|
547
|
-
|
|
548
|
-
function withSeamsResolved(source: string, path: string): string {
|
|
549
|
-
if (!path.endsWith('.ts')) return source
|
|
550
|
-
const from = path.slice(0, path.lastIndexOf('/'))
|
|
551
|
-
let resolved = source
|
|
552
|
-
for (const { path: specifier } of TRANSPILER.scanImports(source)) {
|
|
553
|
-
if (!specifier.startsWith('#')) continue
|
|
554
|
-
let target: string
|
|
555
|
-
try {
|
|
556
|
-
target = Bun.resolveSync(specifier, from)
|
|
557
|
-
} catch {
|
|
558
|
-
continue
|
|
508
|
+
let served: ArrayBuffer | string | (() => Promise<string>) = bytes
|
|
509
|
+
if (name.endsWith('.css'))
|
|
510
|
+
served = stylesheetUnderMount(new TextDecoder().decode(bytes))
|
|
511
|
+
// 38.97 — PLACED ON THE FIRST ASK: only a debugger reads a map, and placing every chunk's
|
|
512
|
+
// up front was ~45 ms of each reload on the docs app, whether or not one was open.
|
|
513
|
+
else if (output.kind === 'sourcemap') {
|
|
514
|
+
let placed: Promise<string> | undefined
|
|
515
|
+
served = () =>
|
|
516
|
+
(placed ??= withAbideSources(
|
|
517
|
+
new TextDecoder().decode(bytes),
|
|
518
|
+
compilation,
|
|
519
|
+
root,
|
|
520
|
+
))
|
|
559
521
|
}
|
|
560
|
-
|
|
561
|
-
.replaceAll(`'${specifier}'`, `'${target}'`)
|
|
562
|
-
.replaceAll(`"${specifier}"`, `"${target}"`)
|
|
522
|
+
bundle.files.set(name, { bytes: served, type: output.type })
|
|
563
523
|
}
|
|
564
|
-
|
|
524
|
+
bundle.manifest = manifestOf(built.metafile as Bun.BuildMetafile, root)
|
|
525
|
+
return bundle
|
|
565
526
|
}
|
|
566
527
|
|
|
567
528
|
/**
|
|
@@ -592,7 +553,7 @@ function failureMarkup(why: string): string {
|
|
|
592
553
|
return `<main><h1>abide dev</h1><pre>${escapeText(why)}</pre></main>`
|
|
593
554
|
}
|
|
594
555
|
|
|
595
|
-
// 38.
|
|
556
|
+
// 38.15's other end. The listener force-closes every live socket when it replaces this worker, so
|
|
596
557
|
// a CLOSE is the rebuild and the reconnect that follows is the new app — there is no message to
|
|
597
558
|
// send and nothing to version. A reader who never opened the socket is told nothing, which is the
|
|
598
559
|
// same as before the save.
|
|
@@ -1,48 +1,50 @@
|
|
|
1
|
-
// 16.
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// (
|
|
5
|
-
// the server barrel cannot ride into the browser by being imported from the wrong file.
|
|
1
|
+
// 16.29 — WHAT THE BROWSER'S BUNDLE SEES OF THE SERVER: nothing under a `server` directory, at any
|
|
2
|
+
// depth, so a secret or a database client cannot ride into the browser by being imported from the
|
|
3
|
+
// wrong file. A handler crosses as the wrapper its file's browser module carries in place of the
|
|
4
|
+
// server block (34.23), which the compiler wrote; this is the fence around everything else.
|
|
6
5
|
//
|
|
7
6
|
// A PLUGIN ON THE BUNDLE, NOT A REWRITE OF THE PAGES. A rewrite reaches the specifiers the compiler
|
|
8
|
-
// emitted; the bundle reaches every edge — a
|
|
7
|
+
// emitted; the bundle reaches every edge — a helper the page imports, importing a server module,
|
|
8
|
+
// included.
|
|
9
|
+
//
|
|
10
|
+
// REGISTERED AHEAD OF `abideModules`, AND IN ITS NAMESPACES: a load is answered by the first plugin
|
|
11
|
+
// that answers it, and a component or a file's text is loaded there rather than as a `file`.
|
|
12
|
+
|
|
13
|
+
import { SERVER_DIRECTORY } from '#shared/RESERVED_DIRECTORIES.ts'
|
|
14
|
+
import { placeOf } from '#shared/routePattern.ts'
|
|
15
|
+
import {
|
|
16
|
+
COMPONENT_NAMESPACE,
|
|
17
|
+
STYLE_NAMESPACE,
|
|
18
|
+
TEXT_NAMESPACE,
|
|
19
|
+
} from './abideModules.ts'
|
|
9
20
|
|
|
10
|
-
|
|
11
|
-
|
|
21
|
+
// Every namespace a browser bundle loads a file of the app's in: its modules, its components, a
|
|
22
|
+
// file's text and a component's stylesheet.
|
|
23
|
+
const FENCED = ['file', COMPONENT_NAMESPACE, TEXT_NAMESPACE, STYLE_NAMESPACE]
|
|
12
24
|
|
|
13
25
|
/**
|
|
14
|
-
* The bundler plugin
|
|
26
|
+
* The bundler plugin refusing everything under a `server` directory to the browser's bundle.
|
|
15
27
|
*
|
|
16
|
-
* `
|
|
17
|
-
*
|
|
28
|
+
* `root` is its real path (`appRoot`), which is how the bundler names a file. Listed before
|
|
29
|
+
* `abideModules`.
|
|
18
30
|
*/
|
|
19
|
-
export function fenceServer(
|
|
20
|
-
root
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const roots = new Set([root, realpathSync(root)])
|
|
31
|
+
export function fenceServer(root: string): Bun.BunPlugin {
|
|
32
|
+
// Anything with a `server` directory in its path below the root, and `placeOf` decides — a load
|
|
33
|
+
// it passes on falls through to the next plugin's, or the bundler's own.
|
|
34
|
+
const filter = new RegExp(
|
|
35
|
+
`^${RegExp.escape(root)}/(?:.*/)?${SERVER_DIRECTORY}/`,
|
|
36
|
+
)
|
|
26
37
|
return {
|
|
27
38
|
name: 'abide-fence',
|
|
28
39
|
setup(build) {
|
|
29
|
-
for (const
|
|
30
|
-
build.onLoad(
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
`${WIRE_DIRECTORY}/${source}`,
|
|
38
|
-
)
|
|
39
|
-
if (wrapper === undefined)
|
|
40
|
-
throw new Error(
|
|
41
|
-
`abide: ${source} is server code, and the browser's bundle imports it. A browser may import a handler from a module under server/rpc and nothing else under server/. RULEBOOK 16.29.`,
|
|
42
|
-
)
|
|
43
|
-
return { contents: wrapper, loader: 'ts' }
|
|
44
|
-
},
|
|
45
|
-
)
|
|
40
|
+
for (const namespace of FENCED)
|
|
41
|
+
build.onLoad({ filter, namespace }, (args) => {
|
|
42
|
+
const below = args.path.slice(root.length + 1)
|
|
43
|
+
if (placeOf(below) !== 'server') return undefined
|
|
44
|
+
throw new Error(
|
|
45
|
+
`abide: ${below.replace(/::\w+$/, '')} is under a server directory, and the browser's bundle imports it. RULEBOOK 16.29.`,
|
|
46
|
+
)
|
|
47
|
+
})
|
|
46
48
|
},
|
|
47
49
|
}
|
|
48
50
|
}
|