@abide/abide 0.103.0 → 0.105.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 +29 -0
- package/bin/abide.ts +80 -173
- package/docs/REGISTRY.md +27 -27
- package/docs/RULEBOOK.md +68 -24
- package/docs/abide.md +31 -46
- package/package.json +1 -1
- package/src/compiler/Lowering.ts +15 -5
- package/src/compiler/Refusals.ts +57 -0
- package/src/compiler/TypeSession.ts +46 -89
- package/src/compiler/appLayout.ts +447 -0
- package/src/compiler/appMap.ts +21 -25
- package/src/compiler/checkApp.ts +61 -97
- package/src/compiler/checkedShadow.ts +12 -10
- package/src/compiler/classifyRead.ts +3 -6
- package/src/compiler/compileApp.ts +183 -478
- package/src/compiler/componentGraph.ts +2 -71
- package/src/compiler/declaredExports.ts +79 -60
- package/src/compiler/emitClient.ts +11 -13
- package/src/compiler/emitComponent.ts +21 -6
- package/src/compiler/emitSource.ts +20 -20
- package/src/compiler/handlersUnder.ts +46 -36
- package/src/compiler/hookCalls.ts +152 -0
- package/src/compiler/mountsOf.ts +31 -54
- package/src/compiler/projectShadow.ts +1 -1
- package/src/compiler/readSources.ts +27 -42
- package/src/compiler/readsIn.ts +5 -16
- package/src/compiler/resolverIn.ts +77 -0
- package/src/server/abideModules.ts +152 -146
- package/src/server/answerRequest.ts +18 -4
- package/src/server/answerSocket.ts +136 -76
- package/src/server/answerValue.ts +28 -15
- package/src/server/appTree.ts +59 -73
- package/src/server/claudeApi.ts +2 -5
- package/src/server/clientBundle.ts +198 -0
- package/src/server/coerceArgs.ts +1 -1
- package/src/server/commands.ts +93 -57
- package/src/server/createApp.ts +90 -16
- package/src/server/deferredBuilds.ts +175 -0
- package/src/server/devWorker.ts +107 -319
- package/src/server/faceOf.ts +120 -0
- package/src/server/fenceServer.ts +10 -18
- package/src/server/generatedSurfaces.ts +12 -53
- package/src/server/index.ts +3 -0
- package/src/server/placement.ts +167 -0
- package/src/server/remote.ts +14 -42
- package/src/server/render.ts +25 -17
- package/src/server/renderComponent.ts +9 -5
- package/src/server/requestScope.ts +10 -4
- package/src/server/responseRelay.ts +389 -0
- package/src/server/runCompiled.ts +9 -12
- package/src/server/runDesktop.ts +6 -9
- package/src/server/serveBuild.ts +26 -73
- package/src/server/serveDev.ts +19 -176
- package/src/server/streamEvent.ts +29 -0
- package/src/server/versionOf.ts +1 -2
- package/src/server/withAbideSources.ts +2 -1
- package/src/shared/HandlerRoute.ts +68 -0
- package/src/shared/REFUSAL_KEY.ts +5 -0
- package/src/shared/RESERVED_DIRECTORIES.ts +6 -0
- package/src/shared/answerOf.ts +105 -78
- package/src/shared/filledPath.ts +1 -9
- package/src/shared/framing.ts +127 -128
- package/src/shared/queryText.ts +15 -7
- package/src/shared/reactive/Cursor.ts +69 -124
- package/src/shared/reactive/Reactive.ts +296 -162
- package/src/shared/reactive/callLoad.ts +4 -18
- package/src/shared/reactive/index.ts +11 -8
- package/src/shared/reactive/pairReactive.ts +1 -1
- package/src/shared/reactive/scoped.ts +4 -3
- package/src/shared/reactive/share.ts +11 -1
- package/src/shared/routePattern.ts +0 -110
- package/src/shared/rpc.ts +17 -24
- package/src/shared/rpcRequest.ts +18 -47
- package/src/shared/takeEarlyCall.ts +12 -37
- package/src/shared/url.ts +1 -1
- package/src/ui/Level.ts +276 -0
- package/src/ui/List.ts +17 -17
- package/src/ui/Router.ts +20 -87
- package/src/ui/applyHead.ts +92 -79
- package/src/ui/index.ts +4 -2
- package/src/ui/reconcile.ts +4 -110
- package/src/ui/rows.ts +123 -2
- package/src/ui/wire.ts +12 -27
- package/src/compiler/hooksCalled.ts +0 -35
- package/src/server/clientBuildOptions.ts +0 -54
- package/src/server/refuseAddressCollision.ts +0 -37
- package/src/shared/reactive/chunksOfStream.ts +0 -57
- package/src/ui/MOUNTING_LEVEL.ts +0 -18
package/src/server/devWorker.ts
CHANGED
|
@@ -12,55 +12,44 @@
|
|
|
12
12
|
// module's imports resolve from the source that wrote them in either.
|
|
13
13
|
//
|
|
14
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)
|
|
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.24).
|
|
15
|
+
// 38.36) — `responseRelay`'s worker half, `serveOver`.
|
|
18
16
|
//
|
|
19
17
|
// 18.1, 18.3 — AND SO DOES A SOCKET. The listener holds the browser's web socket, which has to
|
|
20
18
|
// outlive this worker; what crosses is each frame, tagged with the connection it came over, and
|
|
21
19
|
// what goes back is each frame this worker's mux sends. The mux is the one `abide start` runs.
|
|
22
20
|
|
|
23
|
-
import {
|
|
21
|
+
import { DEV_ADDRESS } from '#shared/ADDRESSES.ts'
|
|
24
22
|
import { escapeText } from '#shared/escapeText.ts'
|
|
25
23
|
import {
|
|
26
|
-
abideModules,
|
|
27
24
|
type Compilation,
|
|
28
25
|
serveCompilation,
|
|
29
26
|
servedTable,
|
|
30
27
|
} from './abideModules.ts'
|
|
31
28
|
import { type AppSources, appSources } from './answerRequest.ts'
|
|
32
29
|
import type { Mux, MuxConnection } from './answerSocket.ts'
|
|
33
|
-
import { appPlugins } from './appPlugins.ts'
|
|
34
30
|
import { publishedUnder } from './appTree.ts'
|
|
35
|
-
import { CLIENT_ENTRY } from './CLIENT_BUILD.ts'
|
|
36
31
|
import {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
32
|
+
bundleServer,
|
|
33
|
+
type ClientBundle,
|
|
34
|
+
clientBundle,
|
|
35
|
+
type ServedFile,
|
|
36
|
+
} from './clientBundle.ts'
|
|
40
37
|
import { type BootedApp, bootApp, originUnder } from './createApp.ts'
|
|
41
38
|
import { keepServing } from './faultPolicy.ts'
|
|
42
|
-
import {
|
|
43
|
-
import { type Manifest, manifestOf, NO_MANIFEST } from './manifestOf.ts'
|
|
39
|
+
import { type Manifest, NO_MANIFEST } from './manifestOf.ts'
|
|
44
40
|
import { admitsMux } from './muxUpgrade.ts'
|
|
45
|
-
import { refuseAddressCollision } from './refuseAddressCollision.ts'
|
|
46
41
|
import { LISTENING } from './requestScope.ts'
|
|
42
|
+
import {
|
|
43
|
+
describe,
|
|
44
|
+
type FromRelay,
|
|
45
|
+
serveOver,
|
|
46
|
+
type ToRelay,
|
|
47
|
+
} from './responseRelay.ts'
|
|
47
48
|
import { withAbideSources } from './withAbideSources.ts'
|
|
48
49
|
|
|
49
50
|
// The reload client, which the worker serves and every document it answers loads.
|
|
50
51
|
const RELOAD_CLIENT_ADDRESS = `${DEV_ADDRESS}/client.js`
|
|
51
52
|
|
|
52
|
-
// The bundle this worker answers `/__abide/client/…` out of, the type each name is served as, and
|
|
53
|
-
// the chunks each route module needs (38.28).
|
|
54
|
-
type Bundle = {
|
|
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
|
-
>
|
|
61
|
-
manifest: Manifest
|
|
62
|
-
}
|
|
63
|
-
|
|
64
53
|
/**
|
|
65
54
|
* A message from the listener to this worker.
|
|
66
55
|
*
|
|
@@ -77,9 +66,7 @@ export type ToWorker =
|
|
|
77
66
|
published: Map<string, string>
|
|
78
67
|
failure: string
|
|
79
68
|
}
|
|
80
|
-
|
|
|
81
|
-
| { kind: 'pull'; id: number }
|
|
82
|
-
| { kind: 'cancel'; id: number }
|
|
69
|
+
| ToRelay
|
|
83
70
|
| {
|
|
84
71
|
kind: 'socket-open'
|
|
85
72
|
connection: number
|
|
@@ -92,37 +79,12 @@ export type ToWorker =
|
|
|
92
79
|
/**
|
|
93
80
|
* A message from this worker to the listener.
|
|
94
81
|
*
|
|
95
|
-
* Out: ready, a frame the mux sends a browser,
|
|
96
|
-
* chunk, and its end.
|
|
82
|
+
* Out: ready, a frame the mux sends a browser, and what answers a request (`FromRelay`).
|
|
97
83
|
*/
|
|
98
84
|
export type FromWorker =
|
|
99
85
|
| { kind: 'ready' }
|
|
100
86
|
| { kind: 'socket-send'; connection: number; text: string }
|
|
101
|
-
|
|
|
102
|
-
| {
|
|
103
|
-
kind: 'head'
|
|
104
|
-
id: number
|
|
105
|
-
status: number
|
|
106
|
-
statusText: string
|
|
107
|
-
headers: [string, string][]
|
|
108
|
-
empty: boolean
|
|
109
|
-
/** 37.3 — whether the listener upgrades this request to the mux (`admitsMux`). */
|
|
110
|
-
admitted: boolean
|
|
111
|
-
/** A body whose length the answer declared, sent with its head rather than pulled. */
|
|
112
|
-
whole: ArrayBuffer | undefined
|
|
113
|
-
}
|
|
114
|
-
| { kind: 'chunk'; id: number; bytes: ArrayBuffer }
|
|
115
|
-
| { kind: 'end'; id: number }
|
|
116
|
-
| { kind: 'fail'; id: number; message: string }
|
|
117
|
-
|
|
118
|
-
/** A request as it crosses to the worker: its id, and what the worker rebuilds it from. */
|
|
119
|
-
export type Ask = {
|
|
120
|
-
id: number
|
|
121
|
-
method: string
|
|
122
|
-
url: string
|
|
123
|
-
headers: [string, string][]
|
|
124
|
-
body: ArrayBuffer | null
|
|
125
|
-
}
|
|
87
|
+
| FromRelay
|
|
126
88
|
|
|
127
89
|
/**
|
|
128
90
|
* Sends a message to the listener.
|
|
@@ -132,27 +94,6 @@ export type Ask = {
|
|
|
132
94
|
function toListener(message: FromWorker): void {
|
|
133
95
|
process.send?.(message)
|
|
134
96
|
}
|
|
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)
|
|
156
97
|
|
|
157
98
|
// THE COMPILE, AS SOMETHING TO WAIT ON. A request that arrives while the worker is still
|
|
158
99
|
// compiling has to wait for the app rather than be told there is none: the listener forwards
|
|
@@ -170,6 +111,38 @@ const connections = new Map<number, MuxConnection>()
|
|
|
170
111
|
// keeps it up through a bundle that failed, so the failure is something to SERVE.
|
|
171
112
|
let failure = ''
|
|
172
113
|
|
|
114
|
+
// Each request the listener sends, answered by the app this worker booted.
|
|
115
|
+
const relay = serveOver(
|
|
116
|
+
{ postMessage: toListener },
|
|
117
|
+
{
|
|
118
|
+
async fetch(request) {
|
|
119
|
+
await opening
|
|
120
|
+
return app === undefined
|
|
121
|
+
? failedDocument(failure)
|
|
122
|
+
: await app.fetch(request)
|
|
123
|
+
},
|
|
124
|
+
admits: (request, response) =>
|
|
125
|
+
carried !== undefined && admitsMux(request, response, carried),
|
|
126
|
+
failed: failedDocument,
|
|
127
|
+
failureMarkup,
|
|
128
|
+
},
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
// 16.22 — THE LISTENER HOLDS THE CONNECTION, so the idle timeout a call raises is raised there, on
|
|
132
|
+
// the request bun handed it.
|
|
133
|
+
LISTENING.timeout = relay.timeout
|
|
134
|
+
|
|
135
|
+
// EVERY REQUEST IN FLIGHT HANGS UP WITH THIS WORKER, as each would with `abide start`'s process: a
|
|
136
|
+
// handler that spawned under `request().signal` takes its child down, where an exit alone left it
|
|
137
|
+
// running under launchd. The listener ends this worker on every reload, and its own end — killed
|
|
138
|
+
// rather than ended — is a disconnect.
|
|
139
|
+
function hangUpAndExit(): void {
|
|
140
|
+
relay.hangUpAll()
|
|
141
|
+
process.exit(0)
|
|
142
|
+
}
|
|
143
|
+
process.on('disconnect', hangUpAndExit)
|
|
144
|
+
process.on('SIGTERM', hangUpAndExit)
|
|
145
|
+
|
|
173
146
|
keepServing()
|
|
174
147
|
|
|
175
148
|
process.on('message', (message: ToWorker) => void receive(message))
|
|
@@ -181,15 +154,13 @@ async function receive(message: ToWorker): Promise<void> {
|
|
|
181
154
|
toListener({ kind: 'ready' })
|
|
182
155
|
return
|
|
183
156
|
}
|
|
184
|
-
if (
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
return
|
|
190
|
-
}
|
|
157
|
+
if (
|
|
158
|
+
message.kind === 'request' ||
|
|
159
|
+
message.kind === 'pull' ||
|
|
160
|
+
message.kind === 'cancel'
|
|
161
|
+
)
|
|
162
|
+
return relay.take(message)
|
|
191
163
|
await opening
|
|
192
|
-
if (message.kind === 'request') return answerAsk(message)
|
|
193
164
|
if (message.kind === 'socket-open') {
|
|
194
165
|
const numbered = message.connection
|
|
195
166
|
const connection = mux?.connect(
|
|
@@ -218,123 +189,6 @@ async function receive(message: ToWorker): Promise<void> {
|
|
|
218
189
|
connection.close()
|
|
219
190
|
}
|
|
220
191
|
|
|
221
|
-
// Each answer whose head has gone out, by request id: what its chunks are read from, and whether a
|
|
222
|
-
// failure in the middle can still be shown as markup.
|
|
223
|
-
const bodies = new Map<
|
|
224
|
-
number,
|
|
225
|
-
{ reader: ReadableStreamDefaultReader<Uint8Array>; html: boolean }
|
|
226
|
-
>()
|
|
227
|
-
|
|
228
|
-
// 16.22 — THE LISTENER HOLDS THE CONNECTION, so the idle timeout a call raises is raised there, on
|
|
229
|
-
// the request bun handed it: each request built here is known by the ask it was built from.
|
|
230
|
-
const ASKS = new WeakMap<Request, number>()
|
|
231
|
-
LISTENING.timeout = (received, seconds) => {
|
|
232
|
-
const id = ASKS.get(received)
|
|
233
|
-
if (id !== undefined) toListener({ kind: 'timeout', id, seconds })
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// A status whose response has no body to send, which `Response` refuses to be built with one.
|
|
237
|
-
const BODILESS = new Set([101, 204, 205, 304])
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Answers one request the listener sent, sending back its head and holding its body for pulls.
|
|
241
|
-
*
|
|
242
|
-
* A THROW BEFORE THE HEAD IS AN ANSWER, not a dropped message. The listener is waiting on a head
|
|
243
|
-
* for this id and nothing else will send one, so an escape left the reader's tab hanging until
|
|
244
|
-
* they gave up.
|
|
245
|
-
*/
|
|
246
|
-
async function answerAsk(ask: Ask): Promise<void> {
|
|
247
|
-
const hangUp = new AbortController()
|
|
248
|
-
HANG_UPS.set(ask.id, hangUp)
|
|
249
|
-
const request = new Request(ask.url, {
|
|
250
|
-
method: ask.method,
|
|
251
|
-
headers: ask.headers,
|
|
252
|
-
body: ask.body,
|
|
253
|
-
signal: hangUp.signal,
|
|
254
|
-
})
|
|
255
|
-
ASKS.set(request, ask.id)
|
|
256
|
-
let response: Response
|
|
257
|
-
try {
|
|
258
|
-
response =
|
|
259
|
-
app === undefined
|
|
260
|
-
? failedDocument(failure)
|
|
261
|
-
: await app.fetch(request)
|
|
262
|
-
} catch (thrown) {
|
|
263
|
-
response = failedDocument(describe(thrown))
|
|
264
|
-
}
|
|
265
|
-
const body = BODILESS.has(response.status) ? null : response.body
|
|
266
|
-
// A DECLARED LENGTH IS A BODY ALREADY WHOLE, and it crosses with its head: re-streamed through the
|
|
267
|
-
// listener, bun meets a first read that has to wait on this worker and answers it chunked,
|
|
268
|
-
// dropping the length the app declared.
|
|
269
|
-
const whole =
|
|
270
|
-
body !== null && response.headers.has('content-length')
|
|
271
|
-
? await response.arrayBuffer()
|
|
272
|
-
: undefined
|
|
273
|
-
toListener({
|
|
274
|
-
kind: 'head',
|
|
275
|
-
id: ask.id,
|
|
276
|
-
status: response.status,
|
|
277
|
-
statusText: response.statusText,
|
|
278
|
-
headers: [...response.headers.entries()],
|
|
279
|
-
empty: body === null,
|
|
280
|
-
admitted:
|
|
281
|
-
carried !== undefined && admitsMux(request, response, carried),
|
|
282
|
-
whole,
|
|
283
|
-
})
|
|
284
|
-
if (body === null || whole !== undefined) {
|
|
285
|
-
answered(ask.id)
|
|
286
|
-
return
|
|
287
|
-
}
|
|
288
|
-
bodies.set(ask.id, {
|
|
289
|
-
reader: body.getReader(),
|
|
290
|
-
html: (response.headers.get('content-type') ?? '').startsWith(
|
|
291
|
-
'text/html',
|
|
292
|
-
),
|
|
293
|
-
})
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
/**
|
|
297
|
-
* Sends the listener the next chunk of an answer's body, or its end.
|
|
298
|
-
*
|
|
299
|
-
* ONE CHUNK PER ASK. A throw in the middle ends the stream in an error, as `abide start` does —
|
|
300
|
-
* and where the answer is html a handler streams, what broke goes out as markup first, a reader
|
|
301
|
-
* shown a blank tab otherwise being told nothing. A page is never here: it is built whole (41.1).
|
|
302
|
-
*/
|
|
303
|
-
async function pull(id: number): Promise<void> {
|
|
304
|
-
const reading = bodies.get(id)
|
|
305
|
-
if (reading === undefined) return
|
|
306
|
-
try {
|
|
307
|
-
const { done, value } = await reading.reader.read()
|
|
308
|
-
if (done) {
|
|
309
|
-
bodies.delete(id)
|
|
310
|
-
answered(id)
|
|
311
|
-
toListener({ kind: 'end', id })
|
|
312
|
-
return
|
|
313
|
-
}
|
|
314
|
-
// THE VIEW'S OWN BYTES. A chunk may be a view on a buffer its producer still owns — a
|
|
315
|
-
// compressor's `Buffer` is a slice of a shared pool, and `Buffer#slice` copies nothing — so
|
|
316
|
-
// sending `value.buffer` sent the whole pool.
|
|
317
|
-
const bytes = new Uint8Array(value).buffer as ArrayBuffer
|
|
318
|
-
toListener({ kind: 'chunk', id, bytes })
|
|
319
|
-
} catch (thrown) {
|
|
320
|
-
bodies.delete(id)
|
|
321
|
-
answered(id)
|
|
322
|
-
const why = describe(thrown)
|
|
323
|
-
if (reading.html) {
|
|
324
|
-
const shown = new TextEncoder().encode(failureMarkup(why))
|
|
325
|
-
.buffer as ArrayBuffer
|
|
326
|
-
toListener({ kind: 'chunk', id, bytes: shown })
|
|
327
|
-
}
|
|
328
|
-
toListener({ kind: 'fail', id, message: why })
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
function describe(thrown: unknown): string {
|
|
333
|
-
return thrown instanceof Error
|
|
334
|
-
? (thrown.stack ?? thrown.message)
|
|
335
|
-
: String(thrown)
|
|
336
|
-
}
|
|
337
|
-
|
|
338
192
|
/**
|
|
339
193
|
* Boots the app over the modules the listener sent, or records why it could not.
|
|
340
194
|
*
|
|
@@ -352,10 +206,6 @@ async function open(
|
|
|
352
206
|
const root = start.root
|
|
353
207
|
const compilation = start.compilation
|
|
354
208
|
try {
|
|
355
|
-
// A page among what compiled — an app of handlers alone has the tables and no client to
|
|
356
|
-
// bundle.
|
|
357
|
-
const paged = compilation.tables[`${CLIENT_ENTRY}.ts`] !== undefined
|
|
358
|
-
|
|
359
209
|
// ONE PLUGIN, TWO CALLERS: the runtime resolves this worker's `import()` through it and
|
|
360
210
|
// the bundler resolves the browser's graph through it, so the two lanes cannot disagree
|
|
361
211
|
// about what a specifier means. Registering it is process-wide — which is a worker rather
|
|
@@ -370,36 +220,10 @@ async function open(
|
|
|
370
220
|
// ONE LISTING of the public directories — the compile's — for the bundle's externals and for
|
|
371
221
|
// what is served. 38.19 — OUT OF THE SOURCE TREE. `abide build` copies these into its output
|
|
372
222
|
// and dev has no output, so the address answers where the author's file actually is.
|
|
373
|
-
const
|
|
374
|
-
const
|
|
375
|
-
const publicAddresses = [...publicSources.values()]
|
|
376
|
-
// 38.43 — the app's own plugins, imported beside the reads below rather than ahead of them.
|
|
377
|
-
const bundling = appPlugins(root).then((own) =>
|
|
378
|
-
bundleInMemory(
|
|
379
|
-
root,
|
|
380
|
-
// 16.28, 16.29 — the browser's view of the server, as `abide build` gives it.
|
|
381
|
-
[
|
|
382
|
-
fenceServer(root, compilation.mounted),
|
|
383
|
-
abideModules(root, compilation),
|
|
384
|
-
...own,
|
|
385
|
-
],
|
|
386
|
-
paged,
|
|
387
|
-
publicAddresses,
|
|
388
|
-
compilation,
|
|
389
|
-
),
|
|
390
|
-
)
|
|
223
|
+
const published = publishedUnder(root, start.published)
|
|
224
|
+
const bundling = bundleInMemory(root, compilation, start.published)
|
|
391
225
|
const table = await servedTable(root)
|
|
392
226
|
const handlers = table.handlers
|
|
393
|
-
// 36.13, IN THIS LANE TOO. A throw here lands in `failure`, which is what 38.4 asks
|
|
394
|
-
// for: the command keeps answering and what it answers names both halves. Without it
|
|
395
|
-
// `answerRequest` below would silently prefer the file — its public-before-routes
|
|
396
|
-
// order rests on this collision being impossible.
|
|
397
|
-
refuseAddressCollision(
|
|
398
|
-
table.routes,
|
|
399
|
-
handlers,
|
|
400
|
-
publicSources,
|
|
401
|
-
'abide dev',
|
|
402
|
-
)
|
|
403
227
|
|
|
404
228
|
// THE SAME `App` `abide start` boots, so an app's hooks and middleware run in dev too — and its
|
|
405
229
|
// sources are read INSIDE the boot, as `abide start` reads them: a document's head carries
|
|
@@ -415,7 +239,16 @@ async function open(
|
|
|
415
239
|
client: bundling.then((bundle) => ({
|
|
416
240
|
manifest: bundle.manifest,
|
|
417
241
|
head: `<script type="module" src="${RELOAD_CLIENT_ADDRESS}"></script>`,
|
|
418
|
-
framework: (pathname) =>
|
|
242
|
+
framework: (pathname, request) =>
|
|
243
|
+
pathname === RELOAD_CLIENT_ADDRESS
|
|
244
|
+
? new Response(RELOAD_CLIENT, {
|
|
245
|
+
headers: {
|
|
246
|
+
'content-type':
|
|
247
|
+
'text/javascript; charset=utf-8',
|
|
248
|
+
'cache-control': 'no-store',
|
|
249
|
+
},
|
|
250
|
+
})
|
|
251
|
+
: bundle.framework(pathname, request),
|
|
419
252
|
})),
|
|
420
253
|
explained: (thrown) => failedDocument(describe(thrown)),
|
|
421
254
|
})
|
|
@@ -430,103 +263,58 @@ async function open(
|
|
|
430
263
|
}
|
|
431
264
|
}
|
|
432
265
|
|
|
433
|
-
function served(bundle: Bundle, pathname: string): Response | undefined {
|
|
434
|
-
if (pathname === RELOAD_CLIENT_ADDRESS)
|
|
435
|
-
return new Response(RELOAD_CLIENT, {
|
|
436
|
-
headers: {
|
|
437
|
-
'content-type': 'text/javascript; charset=utf-8',
|
|
438
|
-
'cache-control': 'no-store',
|
|
439
|
-
},
|
|
440
|
-
})
|
|
441
|
-
if (!pathname.startsWith(`${CLIENT_ADDRESS}/`)) return undefined
|
|
442
|
-
const found = bundle.files.get(pathname.slice(CLIENT_ADDRESS.length + 1))
|
|
443
|
-
if (found === undefined) return undefined
|
|
444
|
-
const { bytes } = found
|
|
445
|
-
const body =
|
|
446
|
-
typeof bytes === 'function'
|
|
447
|
-
? new ReadableStream({
|
|
448
|
-
async start(controller) {
|
|
449
|
-
controller.enqueue(
|
|
450
|
-
new TextEncoder().encode(await bytes()),
|
|
451
|
-
)
|
|
452
|
-
controller.close()
|
|
453
|
-
},
|
|
454
|
-
})
|
|
455
|
-
: bytes
|
|
456
|
-
// NO CACHING AT ALL, which is where dev parts company with 21.5: the names are stable across
|
|
457
|
-
// rebuilds, so a browser told to keep `entry.js` for a year keeps the app it had at boot.
|
|
458
|
-
return new Response(body, {
|
|
459
|
-
headers: { 'content-type': found.type, 'cache-control': 'no-store' },
|
|
460
|
-
})
|
|
461
|
-
}
|
|
462
|
-
|
|
463
266
|
/**
|
|
464
|
-
* The client bundle, built into memory.
|
|
465
|
-
*
|
|
466
|
-
* 38.16 — Unminified, and the entry named by its source rather than content-hashed, so what a
|
|
467
|
-
* browser loads and what a stack trace names are the module the author wrote. 38.5's hashes and
|
|
468
|
-
* manifest are the BUILD's, and the difference is deliberate: a dev chunk that changed has to be
|
|
469
|
-
* fetched again, and a name that changes with it is a cache the reload cannot clear.
|
|
267
|
+
* The client bundle (`clientBundle`), built into memory, and what answers it.
|
|
470
268
|
*
|
|
471
|
-
* A FAILURE IS AN EMPTY BUNDLE AND NOT A THROW (38.17). The pages still render, the document
|
|
472
|
-
*
|
|
269
|
+
* A FAILURE IS AN EMPTY BUNDLE AND NOT A THROW (38.17). The pages still render, the document still
|
|
270
|
+
* loads the reload client, and the next save is what fixes it — the error goes to THE TERMINAL
|
|
271
|
+
* RATHER THAN THE DOCUMENT, where the author started the command.
|
|
473
272
|
*/
|
|
474
273
|
async function bundleInMemory(
|
|
475
274
|
root: string,
|
|
476
|
-
plugins: Bun.BunPlugin[],
|
|
477
|
-
paged: boolean,
|
|
478
|
-
publicAddresses: string[],
|
|
479
275
|
compilation: Compilation,
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
sourcemap: 'linked',
|
|
488
|
-
naming: {
|
|
489
|
-
entry: '[name].[ext]',
|
|
490
|
-
chunk: '[name]-[hash].[ext]',
|
|
491
|
-
asset: '[name]-[hash].[ext]',
|
|
492
|
-
},
|
|
493
|
-
throw: false,
|
|
494
|
-
})
|
|
495
|
-
// THE TERMINAL RATHER THAN THE DOCUMENT. 38.17 keeps the server up, and a page that still
|
|
496
|
-
// paints is what it keeps up — so the pages stay served, unhydrated, and the author reads the
|
|
497
|
-
// build error where they started the command.
|
|
498
|
-
if (!built.success) {
|
|
276
|
+
published: Map<string, string>,
|
|
277
|
+
): Promise<{ manifest: Manifest; framework: AppSources['framework'] }> {
|
|
278
|
+
let bundle: ClientBundle
|
|
279
|
+
try {
|
|
280
|
+
bundle = await clientBundle(root, compilation, published, 'dev')
|
|
281
|
+
} catch (thrown) {
|
|
282
|
+
const logs = thrown instanceof AggregateError ? thrown.errors : []
|
|
499
283
|
console.error(
|
|
500
|
-
|
|
284
|
+
`${(thrown as Error).message}\n${logs.map((log) => String(log)).join('\n')}`,
|
|
501
285
|
)
|
|
502
|
-
|
|
286
|
+
bundle = { files: new Map(), manifest: NO_MANIFEST }
|
|
503
287
|
}
|
|
504
|
-
|
|
505
|
-
const
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
const name = output.path.slice(output.path.lastIndexOf('/') + 1)
|
|
511
|
-
const bytes = read[index] as ArrayBuffer
|
|
512
|
-
let served: ArrayBuffer | string | (() => Promise<string>) = bytes
|
|
513
|
-
if (name.endsWith('.css'))
|
|
514
|
-
served = stylesheetUnderMount(new TextDecoder().decode(bytes))
|
|
515
|
-
// 38.97 — PLACED ON THE FIRST ASK: only a debugger reads a map, and placing every chunk's
|
|
516
|
-
// up front was ~45 ms of each reload on the docs app, whether or not one was open.
|
|
517
|
-
else if (output.kind === 'sourcemap') {
|
|
288
|
+
const files = new Map<string, ServedFile>()
|
|
289
|
+
for (const [name, { bytes, type, kind }] of bundle.files) {
|
|
290
|
+
let body: () => BodyInit = () => bytes
|
|
291
|
+
// 38.97 — PLACED ON THE FIRST ASK: only a debugger reads a map, and placing every chunk's up
|
|
292
|
+
// front was ~45 ms of each reload on the docs app, whether or not one was open.
|
|
293
|
+
if (kind === 'sourcemap') {
|
|
518
294
|
let placed: Promise<string> | undefined
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
295
|
+
body = () =>
|
|
296
|
+
new ReadableStream({
|
|
297
|
+
async start(controller) {
|
|
298
|
+
placed ??= withAbideSources(
|
|
299
|
+
new TextDecoder().decode(bytes),
|
|
300
|
+
compilation,
|
|
301
|
+
root,
|
|
302
|
+
)
|
|
303
|
+
controller.enqueue(
|
|
304
|
+
new TextEncoder().encode(await placed),
|
|
305
|
+
)
|
|
306
|
+
controller.close()
|
|
307
|
+
},
|
|
308
|
+
})
|
|
525
309
|
}
|
|
526
|
-
|
|
310
|
+
files.set(name, { type, body, encoded: {} })
|
|
311
|
+
}
|
|
312
|
+
// NO CACHING AT ALL, which is where dev parts company with 21.5: the names are stable across
|
|
313
|
+
// rebuilds, so a browser told to keep `entry.js` for a year keeps the app it had at boot.
|
|
314
|
+
return {
|
|
315
|
+
manifest: bundle.manifest,
|
|
316
|
+
framework: bundleServer(files, 'no-store'),
|
|
527
317
|
}
|
|
528
|
-
bundle.manifest = manifestOf(built.metafile as Bun.BuildMetafile, root)
|
|
529
|
-
return bundle
|
|
530
318
|
}
|
|
531
319
|
|
|
532
320
|
/**
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// WHICH FACE AN IMPORT OF THE APP'S IS ANSWERED WITH — one decision, of the file an import resolved
|
|
2
|
+
// to, the importer asking and the lane it is asked in, and nothing of bun's: `abideModules` is the
|
|
3
|
+
// adapter that hands bun's hooks to it, because bun decides which hook asks (an `onResolve` for a
|
|
4
|
+
// specifier, an `onLoad` for a path), and this decides what the answer is.
|
|
5
|
+
|
|
6
|
+
import { HANDLER_SUFFIX } from './MODULE_QUERIES.ts'
|
|
7
|
+
|
|
8
|
+
// A VIRTUAL FACE'S PATH: its file's, with a suffix that is not a query. Both registries key a module
|
|
9
|
+
// by its path whatever its namespace, and the runtime's drops a query from it — so a file's text, its
|
|
10
|
+
// stylesheet or its bound face under `?…` took the place of the module the file compiles to.
|
|
11
|
+
export const BOUND = '::bound'
|
|
12
|
+
export const CLIENT = '::client'
|
|
13
|
+
export const TEXT = '::text'
|
|
14
|
+
export const STYLE = '::style'
|
|
15
|
+
|
|
16
|
+
/** The namespace a bundle's component module is answered in, at its file's own path. */
|
|
17
|
+
export const COMPONENT_NAMESPACE = 'abide-component'
|
|
18
|
+
/** The namespaces a file's text and a component's stylesheet are loaded in. */
|
|
19
|
+
export const TEXT_NAMESPACE = 'abide-text'
|
|
20
|
+
export const STYLE_NAMESPACE = 'abide-style'
|
|
21
|
+
/** The namespaces a browser module asked for outside a bundle, and a bound face, are loaded in. */
|
|
22
|
+
export const CLIENT_NAMESPACE = 'abide-client'
|
|
23
|
+
export const BOUND_NAMESPACE = 'abide-bound'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Where an import is asked: in a bundle or by the runtime, and for a browser or a server. The runtime
|
|
27
|
+
* is never a browser's, save for what a browser module it was asked for imports (`CLIENT`).
|
|
28
|
+
*/
|
|
29
|
+
export type Lane = { bundling: boolean; browser: boolean }
|
|
30
|
+
|
|
31
|
+
/** What a resolve answers an import with: the face's path, and the namespace it loads in. */
|
|
32
|
+
export type Face = { path: string; namespace: string }
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* What an import asks of a file, by its spelling: its text (`?text`), a component's stylesheet
|
|
36
|
+
* (`?style`), the module it compiles to (`?component`), its browser module (`?client`), a `.abide`
|
|
37
|
+
* named bare, or a module that may be a handler file.
|
|
38
|
+
*/
|
|
39
|
+
export type Asked =
|
|
40
|
+
| 'text'
|
|
41
|
+
| 'style'
|
|
42
|
+
| 'component'
|
|
43
|
+
| 'client'
|
|
44
|
+
| 'abide'
|
|
45
|
+
| 'handler'
|
|
46
|
+
|
|
47
|
+
/** One handler file, as `Compilation.handlers` holds it. */
|
|
48
|
+
export type HandlerFace = { scope: string; client: string }
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The face an import of `file` is answered with, asked `asked` from `importer` in `lane` — or
|
|
52
|
+
* `undefined` where the file is no face of abide's and bun's own resolve answers, or the refusal.
|
|
53
|
+
*
|
|
54
|
+
* `importer` is the path the importer is named by, its face's suffix kept: a browser module the
|
|
55
|
+
* runtime was asked for (`CLIENT`) imports in a browser's lane, and a bound face (`BOUND`) imports the
|
|
56
|
+
* component it binds.
|
|
57
|
+
*
|
|
58
|
+
* - 34.21 — a file's text, and 34.4 — a component's stylesheet, each at a path of its own, since a
|
|
59
|
+
* file is often imported as code and as text at once;
|
|
60
|
+
* - 32.17 — the module a `.abide` compiles to: in a browser's lane its browser module, else its
|
|
61
|
+
* server module — in a namespace of its own in a bundle, where bun refuses a `file` path with `..`
|
|
62
|
+
* in it, which a `[...rest]` directory writes (23.5);
|
|
63
|
+
* - 34.16 — a `.abide` named bare is a module abide did not compile asking: on a server its bound
|
|
64
|
+
* face, and in a browser the component module itself;
|
|
65
|
+
* - 16.2, 16.74 — a handler file, however its import is spelled and whoever imports it — the app or a
|
|
66
|
+
* package whose directory the app mounts (23.19) — is the one face the server mounts, and in a
|
|
67
|
+
* browser the wrapper; 20.4 — one withholding itself from the browser is no browser import.
|
|
68
|
+
*/
|
|
69
|
+
export function faceOf(
|
|
70
|
+
asked: Asked,
|
|
71
|
+
file: string,
|
|
72
|
+
importer: string,
|
|
73
|
+
lane: Lane,
|
|
74
|
+
handlers: ReadonlyMap<string, HandlerFace>,
|
|
75
|
+
): Face | Error | undefined {
|
|
76
|
+
const inBrowser = lane.browser || importer.endsWith(CLIENT)
|
|
77
|
+
const component = (): Face =>
|
|
78
|
+
inBrowser && !lane.browser
|
|
79
|
+
? { path: `${file}${CLIENT}`, namespace: CLIENT_NAMESPACE }
|
|
80
|
+
: { path: file, namespace: componentNamespaceOf(lane) }
|
|
81
|
+
switch (asked) {
|
|
82
|
+
case 'text':
|
|
83
|
+
return { path: `${file}${TEXT}`, namespace: TEXT_NAMESPACE }
|
|
84
|
+
case 'style':
|
|
85
|
+
return { path: `${file}${STYLE}`, namespace: STYLE_NAMESPACE }
|
|
86
|
+
case 'component':
|
|
87
|
+
return component()
|
|
88
|
+
case 'client':
|
|
89
|
+
return lane.browser
|
|
90
|
+
? { path: file, namespace: COMPONENT_NAMESPACE }
|
|
91
|
+
: { path: `${file}${CLIENT}`, namespace: CLIENT_NAMESPACE }
|
|
92
|
+
case 'abide':
|
|
93
|
+
return inBrowser || importer === `${file}${BOUND}`
|
|
94
|
+
? component()
|
|
95
|
+
: { path: `${file}${BOUND}`, namespace: BOUND_NAMESPACE }
|
|
96
|
+
case 'handler': {
|
|
97
|
+
const face = handlers.get(file)
|
|
98
|
+
if (face === undefined) return undefined
|
|
99
|
+
if (inBrowser && face.client === '')
|
|
100
|
+
return new Error(
|
|
101
|
+
`abide: ${realFileOf(importer)} imports ${file}, whose handler withholds itself from the browser (\`clients: { ui: false }\`). Call it from a <script server>, or let the browser have it. RULEBOOK 20.4.`,
|
|
102
|
+
)
|
|
103
|
+
return { path: `${file}${HANDLER_SUFFIX}`, namespace: 'file' }
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The namespace a component's own module loads in: abide's in a bundle, bun's own outside one. */
|
|
109
|
+
export function componentNamespaceOf(lane: Lane): string {
|
|
110
|
+
return lane.bundling ? COMPONENT_NAMESPACE : 'file'
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The file a face's path stands for: the path with its face's suffix taken off, or as given. */
|
|
114
|
+
export function realFileOf(path: string): string {
|
|
115
|
+
for (const suffix of FACE_SUFFIXES)
|
|
116
|
+
if (path.endsWith(suffix)) return path.slice(0, -suffix.length)
|
|
117
|
+
return path
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const FACE_SUFFIXES = [BOUND, CLIENT, TEXT, STYLE, HANDLER_SUFFIX]
|