@abide/abide 0.123.1 → 0.123.3
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 +33 -0
- package/bin/abide.ts +36 -65
- package/docs/REGISTRY.md +2 -2
- package/docs/RULEBOOK.md +2 -2
- package/docs/abide.md +2 -2
- package/package.json +1 -1
- package/src/compiler/Diagnostics.ts +198 -0
- package/src/compiler/KeptChecker.ts +97 -0
- package/src/compiler/Lowering.ts +1 -1
- package/src/compiler/PositionedRefusal.ts +1 -1
- package/src/compiler/TypeSession.ts +62 -85
- package/src/compiler/appLayout.ts +28 -20
- package/src/compiler/argsDefaultsIn.ts +1 -1
- package/src/compiler/checkApp.ts +64 -68
- package/src/compiler/checkedShadow.ts +1 -1
- package/src/compiler/compileApp.ts +101 -70
- package/src/compiler/declaredExports.ts +2 -4
- package/src/compiler/emitClient.ts +1 -1
- package/src/compiler/formatAbide.ts +2 -2
- package/src/compiler/handlersUnder.ts +17 -29
- package/src/compiler/lineOf.ts +0 -19
- package/src/compiler/matchersOf.ts +3 -3
- package/src/compiler/mountsOf.ts +3 -3
- package/src/compiler/readSources.ts +16 -18
- package/src/compiler/readsIn.ts +1 -1
- package/src/compiler/serveLanguage.ts +23 -57
- package/src/compiler/sharedModuleWrites.ts +1 -1
- package/src/compiler/templateWarnings.ts +1 -2
- package/src/compiler/trackingWarnings.ts +1 -1
- package/src/server/CLIENT_BUILD.ts +4 -32
- package/src/server/RESUME_MILLISECONDS.ts +2 -0
- package/src/server/answerRequest.ts +11 -300
- package/src/server/answerRpc.ts +12 -54
- package/src/server/answerSocket.ts +8 -35
- package/src/server/appBuild.ts +377 -0
- package/src/server/appPlugins.ts +15 -17
- package/src/server/appTree.ts +6 -5
- package/src/server/argsContract.ts +209 -0
- package/src/server/bridgeRelay.ts +153 -65
- package/src/server/claudeCode.ts +30 -127
- package/src/server/config.ts +6 -7
- package/src/server/createApp.ts +30 -64
- package/src/server/devWorker.ts +11 -15
- package/src/server/handlerFileMount.ts +40 -0
- package/src/server/index.ts +2 -2
- package/src/server/packageDirectory.ts +14 -0
- package/src/server/preload.ts +3 -2
- package/src/server/principal.ts +4 -3
- package/src/server/remote.ts +1 -1
- package/src/server/runCompiled.ts +1 -1
- package/src/server/runDesktop.ts +1 -1
- package/src/server/seedBuffer.ts +1 -1
- package/src/server/serveDev.ts +24 -58
- package/src/server/servedApp.ts +390 -0
- package/src/server/signInChanged.ts +20 -0
- package/src/server/surfaceOf.ts +2 -1
- package/src/server/versionOf.ts +6 -15
- package/src/shared/applyPatch.ts +1 -1
- package/src/shared/muxFrames.ts +1 -1
- package/src/shared/reactive/Keyed.ts +1 -1
- package/src/shared/reactive/Reactive.ts +21 -8
- package/src/shared/reactive/argsContract.ts +131 -4
- package/src/shared/reactive/index.ts +1 -0
- package/src/shared/reactive/patchLog.ts +79 -0
- package/src/shared/reactive/patchMarks.ts +34 -0
- package/src/shared/reactive/recordWrites.ts +11 -14
- package/src/shared/reactive/share.ts +4 -3
- package/src/shared/rpc.ts +12 -19
- package/src/shared/validateJson.ts +14 -3
- package/src/ui/ArmRegion.ts +10 -4
- package/src/ui/Level.ts +11 -20
- package/src/ui/LevelChain.ts +272 -0
- package/src/ui/Router.ts +54 -245
- package/src/ui/Scope.ts +17 -3
- package/src/ui/TryRegion.ts +22 -23
- package/src/ui/bindSpread.ts +2 -4
- package/src/ui/listenForRoutes.ts +3 -10
- package/src/ui/outboxQueue.ts +21 -3
- package/src/ui/reconcile.ts +4 -6
- package/src/ui/routing.ts +2 -1
- package/src/ui/rows.ts +2 -2
- package/src/ui/setAttribute.ts +12 -3
- package/src/ui/socketWire.ts +26 -11
- package/src/compiler/Finding.ts +0 -2
- package/src/compiler/Refusals.ts +0 -65
- package/src/compiler/formatDiagnosis.ts +0 -20
- package/src/server/appDirectories.ts +0 -9
- package/src/server/argsFor.ts +0 -39
- package/src/server/buildDirectory.ts +0 -10
- package/src/server/coerceArgs.ts +0 -109
- package/src/server/derivedArgs.ts +0 -95
- package/src/server/runBuild.ts +0 -24
- package/src/server/serveBuild.ts +0 -144
- package/src/server/testBuild.ts +0 -220
- package/src/shared/reactive/FOLLOWED.ts +0 -8
- package/src/shared/reactive/PATCHES.ts +0 -11
- package/src/shared/reactive/PATCH_SINKS.ts +0 -26
- package/src/shared/reactive/canonicalKey.ts +0 -95
|
@@ -1,18 +1,19 @@
|
|
|
1
|
-
import { existsSync
|
|
1
|
+
import { existsSync } from 'node:fs' // bun exposes no sync existence check of its own
|
|
2
2
|
import { dirname } from 'node:path' // bun exposes no path helpers of its own
|
|
3
3
|
|
|
4
4
|
import { realOrAsGiven } from '#server/realOrAsGiven.ts'
|
|
5
5
|
|
|
6
6
|
import { checkSource, lowerApp } from './checkApp.ts'
|
|
7
7
|
import { citedClauses } from './citedClauses.ts'
|
|
8
|
-
import {
|
|
8
|
+
import type { AppChecker } from './compileApp.ts'
|
|
9
|
+
import { type Diagnosis, formatDiagnosis } from './Diagnostics.ts'
|
|
9
10
|
import { DOCS_SITE } from './DOCS_SITE.ts'
|
|
10
11
|
import { formatAbide } from './formatAbide.ts'
|
|
11
|
-
import { formatDiagnosis } from './formatDiagnosis.ts'
|
|
12
12
|
import { isNamedAlone } from './isNamedAlone.ts'
|
|
13
|
+
import { KeptChecker } from './KeptChecker.ts'
|
|
13
14
|
import { lineOf, offsetOf } from './lineOf.ts'
|
|
14
15
|
import { nearest } from './nearest.ts'
|
|
15
|
-
import type {
|
|
16
|
+
import type { Inspection, Place } from './TypeSession.ts'
|
|
16
17
|
|
|
17
18
|
// 38.11 — `abide lsp`: THE `.abide` LANGUAGE SERVER, OVER STDIO. It is `abide check` (38.3) asked
|
|
18
19
|
// about one open document at a time, so the squiggle an editor draws and the line CI fails on are
|
|
@@ -64,8 +65,7 @@ export async function serveLanguage(
|
|
|
64
65
|
): Promise<void> {
|
|
65
66
|
const documents = new Map<string, Document>()
|
|
66
67
|
const timers = new Map<string, ReturnType<typeof setTimeout>>()
|
|
67
|
-
const checkers = new Map<string,
|
|
68
|
-
const watchers = new Map<string, FSWatcher>()
|
|
68
|
+
const checkers = new Map<string, KeptChecker>()
|
|
69
69
|
// Each app's real root, to the root the editor reached it by.
|
|
70
70
|
const links = new Map<string, string>()
|
|
71
71
|
let queue: Promise<void> = Promise.resolve()
|
|
@@ -133,37 +133,19 @@ export async function serveLanguage(
|
|
|
133
133
|
const held = (path: string): string | undefined =>
|
|
134
134
|
documents.get(Bun.pathToFileURL(spelled(path)).href)?.text
|
|
135
135
|
|
|
136
|
-
// An app's checker,
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
|
|
140
|
-
const checkerOf = async (app: string): Promise<AppChecker> => {
|
|
136
|
+
// An app's checker, kept for the session — and TOLD WHAT MOVED ON DISK, or it answers every
|
|
137
|
+
// check from the text it read when it opened: an export added to a `lib` module the page
|
|
138
|
+
// imports is "no exported member" until the editor restarts this (`KeptChecker`).
|
|
139
|
+
const checkerOf = (app: string): KeptChecker => {
|
|
141
140
|
let checker = checkers.get(app)
|
|
142
|
-
if (checker !== undefined && !checker.current()) {
|
|
143
|
-
checkers.delete(app)
|
|
144
|
-
await checker.close().catch(() => {})
|
|
145
|
-
checker = undefined
|
|
146
|
-
}
|
|
147
141
|
if (checker === undefined) {
|
|
148
|
-
checker =
|
|
142
|
+
checker = new KeptChecker(app, () => {
|
|
143
|
+
// What an open page says may be decided by the file that moved.
|
|
144
|
+
for (const uri of documents.keys())
|
|
145
|
+
if (placed(uri)?.app === app) check(uri)
|
|
146
|
+
})
|
|
149
147
|
checkers.set(app, checker)
|
|
150
148
|
}
|
|
151
|
-
if (!watchers.has(app))
|
|
152
|
-
watchers.set(
|
|
153
|
-
app,
|
|
154
|
-
watch(app, { recursive: true }, (_event, filename) => {
|
|
155
|
-
checkers
|
|
156
|
-
.get(app)
|
|
157
|
-
?.saw(
|
|
158
|
-
filename === null
|
|
159
|
-
? undefined
|
|
160
|
-
: `${app}/${filename}`,
|
|
161
|
-
)
|
|
162
|
-
// What an open page says may be decided by the file that moved.
|
|
163
|
-
for (const uri of documents.keys())
|
|
164
|
-
if (placed(uri)?.app === app) check(uri)
|
|
165
|
-
}),
|
|
166
|
-
)
|
|
167
149
|
return checker
|
|
168
150
|
}
|
|
169
151
|
|
|
@@ -172,11 +154,9 @@ export async function serveLanguage(
|
|
|
172
154
|
// keeps one for the session. Queued behind the checks, so none is in flight when it closes.
|
|
173
155
|
const closeIfUnopened = async (app: string): Promise<void> => {
|
|
174
156
|
for (const uri of documents.keys()) if (placed(uri)?.app === app) return
|
|
175
|
-
watchers.get(app)?.close()
|
|
176
|
-
watchers.delete(app)
|
|
177
157
|
const checker = checkers.get(app)
|
|
178
158
|
checkers.delete(app)
|
|
179
|
-
await checker?.close()
|
|
159
|
+
await checker?.close()
|
|
180
160
|
}
|
|
181
161
|
|
|
182
162
|
const run = async (uri: string): Promise<void> => {
|
|
@@ -189,17 +169,11 @@ export async function serveLanguage(
|
|
|
189
169
|
const { app, path } = at
|
|
190
170
|
let found: Diagnosis[]
|
|
191
171
|
try {
|
|
192
|
-
found = await
|
|
193
|
-
|
|
194
|
-
path,
|
|
195
|
-
document.text,
|
|
196
|
-
held,
|
|
172
|
+
found = await checkerOf(app).run((checker) =>
|
|
173
|
+
checkSource(checker, path, document.text, held),
|
|
197
174
|
)
|
|
198
175
|
} catch (thrown) {
|
|
199
176
|
// The checker's own failure, not the page's: the next check opens another.
|
|
200
|
-
const failed = checkers.get(app)
|
|
201
|
-
checkers.delete(app)
|
|
202
|
-
await failed?.close().catch(() => {})
|
|
203
177
|
found = [
|
|
204
178
|
{
|
|
205
179
|
path,
|
|
@@ -269,7 +243,7 @@ export async function serveLanguage(
|
|
|
269
243
|
const at = placed(uri)
|
|
270
244
|
if (document === undefined || at === undefined) return undefined
|
|
271
245
|
return question(
|
|
272
|
-
await checkerOf(at.app),
|
|
246
|
+
await checkerOf(at.app).live(),
|
|
273
247
|
at.path,
|
|
274
248
|
document.text,
|
|
275
249
|
offsetOf(document.text, position),
|
|
@@ -544,24 +518,16 @@ export async function serveLanguage(
|
|
|
544
518
|
) as Message
|
|
545
519
|
buffer = buffer.slice(start + length)
|
|
546
520
|
if (!(await handle(message))) {
|
|
547
|
-
await closeAll(checkers
|
|
521
|
+
await closeAll(checkers)
|
|
548
522
|
return
|
|
549
523
|
}
|
|
550
524
|
}
|
|
551
525
|
}
|
|
552
|
-
await closeAll(checkers
|
|
526
|
+
await closeAll(checkers)
|
|
553
527
|
}
|
|
554
528
|
|
|
555
|
-
async function closeAll(
|
|
556
|
-
checkers
|
|
557
|
-
watchers: Map<string, FSWatcher>,
|
|
558
|
-
): Promise<void> {
|
|
559
|
-
for (const watcher of watchers.values()) watcher.close()
|
|
560
|
-
await Promise.all(
|
|
561
|
-
[...checkers.values()].map((checker) =>
|
|
562
|
-
checker.close().catch(() => {}),
|
|
563
|
-
),
|
|
564
|
-
)
|
|
529
|
+
async function closeAll(checkers: Map<string, KeptChecker>): Promise<void> {
|
|
530
|
+
await Promise.all([...checkers.values()].map((checker) => checker.close()))
|
|
565
531
|
}
|
|
566
532
|
|
|
567
533
|
/**
|
|
@@ -9,7 +9,7 @@ import type {
|
|
|
9
9
|
} from 'typescript/unstable/async'
|
|
10
10
|
|
|
11
11
|
import { classifyRead, FUNCTIONS } from './classifyRead.ts'
|
|
12
|
-
import type { Finding } from './
|
|
12
|
+
import type { Finding } from './Diagnostics.ts'
|
|
13
13
|
import type { Span } from './projectShadow.ts'
|
|
14
14
|
|
|
15
15
|
// 34.18 — A PLAIN `let` IN A `<script module>` THAT A `<script>` WRITES AS IT SETS UP. The block is
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { CheckerAnswer } from './CheckerAnswer.ts'
|
|
2
2
|
import type { Read } from './classifyRead.ts'
|
|
3
3
|
import { isComponentTag } from './classifyTemplate.ts'
|
|
4
|
+
import { type Diagnosis, diagnosisAt } from './Diagnostics.ts'
|
|
4
5
|
import { isNamedAlone } from './isNamedAlone.ts'
|
|
5
|
-
import { diagnosisAt } from './lineOf.ts'
|
|
6
6
|
import { Lowering } from './Lowering.ts'
|
|
7
7
|
import type { AbideFile } from './parseAbide.ts'
|
|
8
8
|
import {
|
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
type TemplateNode,
|
|
13
13
|
visitBodies,
|
|
14
14
|
} from './TemplateNode.ts'
|
|
15
|
-
import type { Diagnosis } from './TypeSession.ts'
|
|
16
15
|
|
|
17
16
|
// 32.14 — A `{#for}` WITH NO KEY OVER A BODY THAT HOLDS STATE. Unkeyed, a row is matched to the
|
|
18
17
|
// item at its index, so a reorder hands one row's state — a component's own values, a field's text,
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
} from 'typescript/unstable/ast/is'
|
|
17
17
|
|
|
18
18
|
import { FUNCTIONS, TRANSPARENT } from './classifyRead.ts'
|
|
19
|
-
import type { Finding } from './
|
|
19
|
+
import type { Finding } from './Diagnostics.ts'
|
|
20
20
|
import type { PositionKind, PositionTable } from './PositionTable.ts'
|
|
21
21
|
|
|
22
22
|
// 14.14, 14.15, 14.16 — WHERE A READ STANDS IN A BODY THAT TRACKS, and the three ways it can stand
|
|
@@ -1,13 +1,9 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// in both is a build that silently serves nothing. The addresses it is answered at are
|
|
1
|
+
// WHAT THE BROWSER BUNDLE IS NAMED, once for the seams that have to agree about it: the compiler
|
|
2
|
+
// writes the entry, `abide build` precompresses every file, and the server answers both (38.5,
|
|
3
|
+
// 38.20). A leaf, with no imports of its own, for the reason every constant crossing a seam is one.
|
|
4
|
+
// Where the bundle lands in a build is `appBuild.ts`'s; the addresses it is answered at are
|
|
6
5
|
// `#shared/ADDRESSES.ts`.
|
|
7
6
|
|
|
8
|
-
/** The directory the bundle lands in, below the build's own output directory. */
|
|
9
|
-
export const CLIENT_DIRECTORY = 'client'
|
|
10
|
-
|
|
11
7
|
/**
|
|
12
8
|
* The module that mounts the app — the one bundle file whose name the shell writes out.
|
|
13
9
|
*
|
|
@@ -19,21 +15,6 @@ export const CLIENT_DIRECTORY = 'client'
|
|
|
19
15
|
*/
|
|
20
16
|
export const CLIENT_ENTRY = 'entry'
|
|
21
17
|
|
|
22
|
-
/**
|
|
23
|
-
* The manifest file `abide build` writes beside the client directory.
|
|
24
|
-
*
|
|
25
|
-
* 38.5 — the manifest `manifestOf` reads off the bundle, below the build's own output directory
|
|
26
|
-
* and BESIDE the client directory rather than in it: everything in there is answered under
|
|
27
|
-
* `CLIENT_ADDRESS` (38.20), and the manifest is the server's, not the browser's.
|
|
28
|
-
*/
|
|
29
|
-
export const MANIFEST = 'manifest.json'
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* The compilation `abide build` keeps beside the client directory: every `.abide` compiled, and
|
|
33
|
-
* the build's own tables (`Compilation`), which `abide start` serves without the compiler.
|
|
34
|
-
*/
|
|
35
|
-
export const COMPILATION = 'compilation.json'
|
|
36
|
-
|
|
37
18
|
/**
|
|
38
19
|
* The encodings a response can be compressed in, each with its precompressed file's suffix.
|
|
39
20
|
*
|
|
@@ -43,12 +24,3 @@ export const COMPILATION = 'compilation.json'
|
|
|
43
24
|
* a build that precompresses into files nothing ever serves.
|
|
44
25
|
*/
|
|
45
26
|
export const ENCODING_SUFFIX = { br: '.br', gzip: '.gz' } as const
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Where `abide build` copies every `public` directory, each file at its address, below its own
|
|
49
|
-
* output directory. The source's own name for one is `#shared/RESERVED_DIRECTORIES.ts`'s.
|
|
50
|
-
*
|
|
51
|
-
* 38.18 — a directory rather than the source tree: `abide start` boots what the build produced and
|
|
52
|
-
* reaches for nothing an app might have moved since.
|
|
53
|
-
*/
|
|
54
|
-
export const PUBLIC_DIRECTORY = 'public'
|
|
@@ -8,75 +8,43 @@
|
|
|
8
8
|
|
|
9
9
|
import {
|
|
10
10
|
ABIDE_ADDRESS,
|
|
11
|
-
APP_MANIFEST_ADDRESS,
|
|
12
11
|
BRIDGE_ADDRESS,
|
|
13
|
-
CLIENT_ADDRESS,
|
|
14
12
|
HEALTH_ADDRESS,
|
|
15
13
|
MCP_ADDRESS,
|
|
16
14
|
OPENAPI_ADDRESS,
|
|
17
15
|
PRIVATE_HANDLERS_ADDRESS,
|
|
18
|
-
WORKER_ADDRESS,
|
|
19
16
|
} from '#shared/ADDRESSES.ts'
|
|
20
|
-
import { APP_META } from '#shared/APP_META.ts'
|
|
21
|
-
import { DEBUG_META } from '#shared/DEBUG_META.ts'
|
|
22
17
|
import { decodedOrKept } from '#shared/decodedOrKept.ts'
|
|
23
|
-
import { ERROR_META } from '#shared/ERROR_META.ts'
|
|
24
|
-
import { escapeAttribute } from '#shared/escapeText.ts'
|
|
25
18
|
import { isFailed } from '#shared/Failed.ts'
|
|
26
19
|
import { HandlerRoute } from '#shared/HandlerRoute.ts'
|
|
27
20
|
import { health } from '#shared/health.ts'
|
|
28
21
|
import { KEEP_HEADER } from '#shared/KEEP_HEADER.ts'
|
|
29
22
|
import { KEEP_OPENED } from '#shared/KEEP_OPENED.ts'
|
|
30
|
-
import { ICON_ADDRESSES } from '#shared/LAUNCHER_FILES.ts'
|
|
31
|
-
import { appName } from '#shared/log.ts'
|
|
32
23
|
import { matchOf, type Params, partsOf } from '#shared/matchRoute.ts'
|
|
33
24
|
import { compose } from '#shared/Middleware.ts'
|
|
34
|
-
import {
|
|
35
|
-
import { MOUNT_META } from '#shared/MOUNT_META.ts'
|
|
25
|
+
import { mounted } from '#shared/mount.ts'
|
|
36
26
|
import { NAVIGATION_HEADER } from '#shared/NAVIGATION_HEADER.ts'
|
|
37
|
-
import { OFFLINE_META } from '#shared/OFFLINE_META.ts'
|
|
38
27
|
import { notFound as refusedNotFound, refuse } from '#shared/refuse.ts'
|
|
39
28
|
import { arrive } from '#shared/route.ts'
|
|
40
|
-
import {
|
|
41
|
-
parseRoutePattern,
|
|
42
|
-
rankRoutes,
|
|
43
|
-
type Segment,
|
|
44
|
-
} from '#shared/routePattern.ts'
|
|
29
|
+
import type { Segment } from '#shared/routePattern.ts'
|
|
45
30
|
import type { Method, Rpc } from '#shared/rpc.ts'
|
|
46
31
|
import { rpcRequest } from '#shared/rpcRequest.ts'
|
|
47
32
|
|
|
48
33
|
import { answerFailure, laneFailure } from './answerFailure.ts'
|
|
49
|
-
import {
|
|
50
|
-
|
|
51
|
-
type Mounted,
|
|
52
|
-
mountedOwn,
|
|
53
|
-
mountHandlers,
|
|
54
|
-
type MountRow,
|
|
55
|
-
} from './answerRpc.ts'
|
|
56
|
-
import { answerBlobTagged, answerValue, contentEtag } from './answerValue.ts'
|
|
57
|
-
import { answerWorker } from './answerWorker.ts'
|
|
58
|
-
import { appManifest } from './appManifest.ts'
|
|
34
|
+
import { answerRpc, type Mounted, mountedOwn } from './answerRpc.ts'
|
|
35
|
+
import { answerBlobTagged, answerValue } from './answerValue.ts'
|
|
59
36
|
import { answerBridge } from './bridgeRelay.ts'
|
|
60
37
|
import { callerOrMint } from './callerOf.ts'
|
|
61
|
-
import { CLIENT_ENTRY } from './CLIENT_BUILD.ts'
|
|
62
38
|
import { compressAnswer } from './compressAnswer.ts'
|
|
63
|
-
import { settingOn } from './config.ts'
|
|
64
39
|
import { noteCallInRender } from './deferredBuilds.ts'
|
|
65
40
|
import { DOCUMENT_HEADERS } from './DOCUMENT_HEADERS.ts'
|
|
66
41
|
import { answerMcp, openApi } from './generatedSurfaces.ts'
|
|
67
|
-
import { failureOf, type LaneRung
|
|
68
|
-
import { chunksOf, type Manifest } from './manifestOf.ts'
|
|
42
|
+
import { failureOf, type LaneRung } from './hooks.ts'
|
|
69
43
|
import { muxAdmission } from './muxUpgrade.ts'
|
|
70
44
|
import { OFFLINE_STALE } from './OFFLINE_STALE.ts'
|
|
71
45
|
import { plainAnswer } from './plainAnswer.ts'
|
|
72
46
|
import { PRINCIPAL_MOUNT } from './principal.ts'
|
|
73
|
-
import {
|
|
74
|
-
boundPage,
|
|
75
|
-
type Component,
|
|
76
|
-
DEFAULT_DOCUMENT,
|
|
77
|
-
intoHead,
|
|
78
|
-
renderDocument,
|
|
79
|
-
} from './render.ts'
|
|
47
|
+
import { boundPage, type Component, renderDocument } from './render.ts'
|
|
80
48
|
import {
|
|
81
49
|
adoptRequest,
|
|
82
50
|
type AnsweringApp,
|
|
@@ -107,9 +75,9 @@ type ServerRoute = {
|
|
|
107
75
|
// 35.2 — an error page, and the directory it answers for.
|
|
108
76
|
type ServerError = Omit<ServerRoute, 'clientModules'>
|
|
109
77
|
|
|
110
|
-
type RankedError = ServerError & { segments: Segment[] }
|
|
78
|
+
export type RankedError = ServerError & { segments: Segment[] }
|
|
111
79
|
|
|
112
|
-
type Ranked = RankedError & {
|
|
80
|
+
export type Ranked = RankedError & {
|
|
113
81
|
// The document this route is painted into, with the tags that load the app already in its
|
|
114
82
|
// head (38.22, 38.23) and the preloads for this route's own modules (38.28). One per route and
|
|
115
83
|
// built at boot, so a request pays for none of it.
|
|
@@ -122,7 +90,7 @@ type Ranked = RankedError & {
|
|
|
122
90
|
* 19.1 — one file per method per route, so a route is a row and a method is a column, and 19.20's
|
|
123
91
|
* "the most specific route answering the method" is a walk down the rows asking each column.
|
|
124
92
|
*/
|
|
125
|
-
type RouteRow = {
|
|
93
|
+
export type RouteRow = {
|
|
126
94
|
pattern: string
|
|
127
95
|
matchers: Record<string, string>
|
|
128
96
|
segments: Segment[]
|
|
@@ -141,7 +109,7 @@ type RouteRow = {
|
|
|
141
109
|
}
|
|
142
110
|
|
|
143
111
|
/** A public file, read once at boot: where the bytes are, the etag off them, and its type. */
|
|
144
|
-
type PublicFile = { path: string; etag: string; type: string }
|
|
112
|
+
export type PublicFile = { path: string; etag: string; type: string }
|
|
145
113
|
|
|
146
114
|
/** Everything `answerRequest` answers from: the ranked routes, the lanes and the handlers. */
|
|
147
115
|
export type AppSources = {
|
|
@@ -486,168 +454,8 @@ export function reachedInRender(
|
|
|
486
454
|
*/
|
|
487
455
|
export type ServerTable = { routes: ServerRoute[]; errors: ServerError[] }
|
|
488
456
|
|
|
489
|
-
/**
|
|
490
|
-
* What a command hands `appSources`: the parts it read, built or compiled, and the client lane.
|
|
491
|
-
*
|
|
492
|
-
* ONE SHAPE FOR EVERY SUPPLIER — `abide start` off disk, a compiled app out of itself, `abide dev` out
|
|
493
|
-
* of memory, and a test handing over modules it already holds — so what differs between them is
|
|
494
|
-
* where a part came from and never how the app is assembled from it.
|
|
495
|
-
*/
|
|
496
|
-
export type SourceParts = {
|
|
497
|
-
table: ServerTable
|
|
498
|
-
/** The handler table the build wrote, each row with its module's loader. 16.2. */
|
|
499
|
-
handlers: MountRow[]
|
|
500
|
-
/** Every public file, by its name below the directory to its path. 38.18, 38.19. */
|
|
501
|
-
published: Map<string, string>
|
|
502
|
-
/**
|
|
503
|
-
* The bundle's manifest, its lane, and `head` — `abide dev`'s reload client (38.17), after the
|
|
504
|
-
* app's own tags.
|
|
505
|
-
*
|
|
506
|
-
* A PROMISE WHERE THE BUNDLE IS STILL BUILDING: `abide dev`'s handlers mount and its public files
|
|
507
|
-
* hash while the bundler runs, rather than after it — ~40 ms of a docs-app reload.
|
|
508
|
-
*/
|
|
509
|
-
client: ClientLane | Promise<ClientLane>
|
|
510
|
-
explained: AppSources['explained']
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
type ClientLane = {
|
|
514
|
-
manifest: Manifest
|
|
515
|
-
framework: AppSources['framework']
|
|
516
|
-
head: string
|
|
517
|
-
/**
|
|
518
|
-
* 45.1, 45.9 — whether the worker saves `manifest`'s bundle and the pages register it: `abide
|
|
519
|
-
* build`'s, unless `ABIDE_OFFLINE` is off. Where not, the worker address answers the removing one.
|
|
520
|
-
*/
|
|
521
|
-
offline: boolean
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
/**
|
|
525
|
-
* The sources an `App` is booted over, off the parts a command supplies.
|
|
526
|
-
*
|
|
527
|
-
* 30.9 — MOST SPECIFIC FIRST, ranked off the table rather than in it. The ordering IS the client
|
|
528
|
-
* router's own: both seams compile through `rankRoutes` and walk a route with `matchOf` — the
|
|
529
|
-
* server past a route that has no answer for the method asked, which the router never is — so
|
|
530
|
-
* "the two cannot resolve one address differently" is a grep rather than a promise. 35.2 — the
|
|
531
|
-
* error pages deepest directory first, so the nearest answers.
|
|
532
|
-
*/
|
|
533
|
-
export async function appSources(parts: SourceParts): Promise<AppSources> {
|
|
534
|
-
const [mounted, published, client] = await Promise.all([
|
|
535
|
-
mountHandlers(parts.handlers),
|
|
536
|
-
publicFilesAt(parts.published),
|
|
537
|
-
parts.client,
|
|
538
|
-
])
|
|
539
|
-
// 35.6 — every page is served in abide's own document; what it carries beyond that, the root
|
|
540
|
-
// `layout.abide` writes (35.15).
|
|
541
|
-
// 45.15 — installable where the pages are kept offline and the app has an icon to install by.
|
|
542
|
-
const icon = published.get(ICON_ADDRESSES.png)
|
|
543
|
-
const install =
|
|
544
|
-
icon !== undefined && settingOn('ABIDE_OFFLINE', true)
|
|
545
|
-
? await appManifest(icon.path)
|
|
546
|
-
: undefined
|
|
547
|
-
const documentOf = (modules: string[]): string =>
|
|
548
|
-
intoHead(
|
|
549
|
-
DEFAULT_DOCUMENT,
|
|
550
|
-
clientTags(client.manifest, modules) +
|
|
551
|
-
(client.offline ? `<meta name="${OFFLINE_META}">` : '') +
|
|
552
|
-
(install?.tags ?? '') +
|
|
553
|
-
client.head,
|
|
554
|
-
)
|
|
555
|
-
const pages: Omit<Ranked, 'segments'>[] = []
|
|
556
|
-
for (const route of parts.table.routes)
|
|
557
|
-
pages.push({
|
|
558
|
-
pattern: route.pattern,
|
|
559
|
-
load: route.load,
|
|
560
|
-
layouts: route.layouts,
|
|
561
|
-
matchers: route.matchers,
|
|
562
|
-
document: documentOf(route.clientModules),
|
|
563
|
-
})
|
|
564
|
-
// 19.1 — the build refused two files at one route and method, so a route is one row however
|
|
565
|
-
// many methods answer at it — and 23.27 one matcher per position, so whichever file opens the
|
|
566
|
-
// row brings what every file at it would.
|
|
567
|
-
const rows = new Map<string, Omit<RouteRow, 'segments'>>()
|
|
568
|
-
const rowAt = (
|
|
569
|
-
pattern: string,
|
|
570
|
-
matchers: Record<string, string>,
|
|
571
|
-
): Omit<RouteRow, 'segments'> => {
|
|
572
|
-
let row = rows.get(pattern)
|
|
573
|
-
if (row === undefined) {
|
|
574
|
-
row = {
|
|
575
|
-
pattern,
|
|
576
|
-
matchers,
|
|
577
|
-
page: undefined,
|
|
578
|
-
handlers: new Map(),
|
|
579
|
-
answers: new Map(),
|
|
580
|
-
}
|
|
581
|
-
rows.set(pattern, row)
|
|
582
|
-
}
|
|
583
|
-
return row
|
|
584
|
-
}
|
|
585
|
-
for (const page of rankRoutes(pages))
|
|
586
|
-
rowAt(page.pattern, page.matchers).page = page
|
|
587
|
-
for (const handler of mounted.handlers.values())
|
|
588
|
-
rowAt(handler.address, handler.matchers).handlers.set(
|
|
589
|
-
handler.method,
|
|
590
|
-
handler,
|
|
591
|
-
)
|
|
592
|
-
const errors: RankedError[] = []
|
|
593
|
-
for (const error of parts.table.errors)
|
|
594
|
-
errors.push({
|
|
595
|
-
...error,
|
|
596
|
-
segments: parseRoutePattern(error.pattern, error.matchers),
|
|
597
|
-
})
|
|
598
|
-
errors.sort((a, b) => b.segments.length - a.segments.length)
|
|
599
|
-
const sources: AppSources = {
|
|
600
|
-
routes: rankRoutes([...rows.values()]),
|
|
601
|
-
errors,
|
|
602
|
-
// 35.18 — marked, so the browser hydrates no page over what an error page painted.
|
|
603
|
-
errorDocument: intoHead(documentOf([]), `<meta name="${ERROR_META}">`),
|
|
604
|
-
framework: (pathname, request) => {
|
|
605
|
-
if (pathname === WORKER_ADDRESS)
|
|
606
|
-
return answerWorker(
|
|
607
|
-
client.offline ? client.manifest : undefined,
|
|
608
|
-
)
|
|
609
|
-
if (pathname === APP_MANIFEST_ADDRESS && install !== undefined)
|
|
610
|
-
return new Response(install.body, {
|
|
611
|
-
headers: {
|
|
612
|
-
'content-type': 'application/manifest+json',
|
|
613
|
-
'cache-control': 'no-cache',
|
|
614
|
-
},
|
|
615
|
-
})
|
|
616
|
-
return client.framework(pathname, request)
|
|
617
|
-
},
|
|
618
|
-
published,
|
|
619
|
-
explained: parts.explained,
|
|
620
|
-
offline: client.offline,
|
|
621
|
-
...mounted,
|
|
622
|
-
}
|
|
623
|
-
for (const row of sources.routes) {
|
|
624
|
-
if (row.page !== undefined)
|
|
625
|
-
row.answers.set(
|
|
626
|
-
row.page,
|
|
627
|
-
answerInside(
|
|
628
|
-
sources,
|
|
629
|
-
row.page,
|
|
630
|
-
// 37.14 — a page's lane paints a refusal as its error page does (35.2).
|
|
631
|
-
rungsOver(row.pattern, (failure, request) =>
|
|
632
|
-
answerFailure(
|
|
633
|
-
sources,
|
|
634
|
-
new URL(request.url).pathname,
|
|
635
|
-
failure,
|
|
636
|
-
),
|
|
637
|
-
),
|
|
638
|
-
),
|
|
639
|
-
)
|
|
640
|
-
for (const handler of row.handlers.values())
|
|
641
|
-
row.answers.set(
|
|
642
|
-
handler,
|
|
643
|
-
answerInside(sources, handler, handler.rungs),
|
|
644
|
-
)
|
|
645
|
-
}
|
|
646
|
-
return sources
|
|
647
|
-
}
|
|
648
|
-
|
|
649
457
|
/** `found`'s answer inside the rungs filed over its route. 16.14, 37.2. */
|
|
650
|
-
function answerInside(
|
|
458
|
+
export function answerInside(
|
|
651
459
|
app: AppSources,
|
|
652
460
|
found: Ranked | Mounted,
|
|
653
461
|
rungs: LaneRung[],
|
|
@@ -655,103 +463,6 @@ function answerInside(
|
|
|
655
463
|
return compose(rungs, ({ request }) => answerMatched(app, request, found))
|
|
656
464
|
}
|
|
657
465
|
|
|
658
|
-
/**
|
|
659
|
-
* The head tags that load the app: its metas, stylesheet, entry script and the route's preloads.
|
|
660
|
-
*
|
|
661
|
-
* 38.22, and 38.23 for the stylesheet beside it. An app does not write either: the bundle's
|
|
662
|
-
* address is the build's, and a hand-written one is a path that goes stale the first time the
|
|
663
|
-
* output layout moves.
|
|
664
|
-
*
|
|
665
|
-
* THE STYLESHEET GOES FIRST, and ahead of the page's own head contributions for the same reason:
|
|
666
|
-
* a document that parses its script before its styles paints once unstyled. It is a plain `<link>`
|
|
667
|
-
* rather than anything the client does, so a page the server painted is styled with no JS at all.
|
|
668
|
-
*
|
|
669
|
-
* ONE STYLESHEET FOR THE APP: the bundler collects every stylesheet reachable from the entry — each
|
|
670
|
-
* component's scoped `<style>` (34.4) and each one a component imports (34.6) — into one file, so a
|
|
671
|
-
* page that renders none of those components is served them too. For a `<style>` block that costs
|
|
672
|
-
* bytes rather than correctness: every rule requires its component's scope token, so on a page the
|
|
673
|
-
* component is not on it matches nothing. An imported stylesheet carries no token and applies as
|
|
674
|
-
* written, wherever it is served.
|
|
675
|
-
*
|
|
676
|
-
* 38.28 — THE ROUTE'S CHUNKS ARE PRELOADED BESIDE THE ENTRY. Left to the client, the page's chunk
|
|
677
|
-
* and the shared runtime it imports are only asked for once the entry has arrived and run the
|
|
678
|
-
* router — a whole round trip after the document, for the largest file in the bundle. Named here,
|
|
679
|
-
* they are requested while the document is still being parsed.
|
|
680
|
-
*
|
|
681
|
-
* EVERY NAME IS THE MANIFEST'S, the entry's included: a build hashes it (38.5). A manifest with no
|
|
682
|
-
* entry is a bundle that failed under `abide dev` (38.17), and gets no tags rather than a
|
|
683
|
-
* `<script>` at a 404.
|
|
684
|
-
*/
|
|
685
|
-
export function clientTags(manifest: Manifest, modules: string[]): string {
|
|
686
|
-
if (manifest.entry === undefined) return ''
|
|
687
|
-
// 28.10 — `DEBUG`, through the document rather than the bundle, where the server has one.
|
|
688
|
-
const debug = process.env.DEBUG
|
|
689
|
-
let tags =
|
|
690
|
-
debug === undefined || debug === ''
|
|
691
|
-
? ''
|
|
692
|
-
: `<meta name="${DEBUG_META}" content="${escapeAttribute(debug)}">`
|
|
693
|
-
// 28.4 — the app's name, so a browser's `log.named` prefixes the one the server's does.
|
|
694
|
-
tags += `<meta name="${APP_META}" content="${escapeAttribute(appName())}">`
|
|
695
|
-
// 16.10 — the mount the browser resolves every address against, the root's `''` included: a
|
|
696
|
-
// lookup that finds no tag has walked the whole document to say so.
|
|
697
|
-
tags += `<meta name="${MOUNT_META}" content="${escapeAttribute(MOUNT.current())}">`
|
|
698
|
-
const client = mounted(CLIENT_ADDRESS)
|
|
699
|
-
tags +=
|
|
700
|
-
manifest.stylesheet === undefined
|
|
701
|
-
? ''
|
|
702
|
-
: `<link rel="stylesheet" href="${client}/${manifest.stylesheet}">`
|
|
703
|
-
tags += `<script type="module" src="${client}/${manifest.entry}"></script>`
|
|
704
|
-
for (const chunk of chunksOf([`${CLIENT_ENTRY}.ts`, ...modules], manifest))
|
|
705
|
-
tags += `<link rel="modulepreload" href="${client}/${chunk}">`
|
|
706
|
-
return tags
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
/**
|
|
710
|
-
* The public files by address, off each file's name and where its bytes are.
|
|
711
|
-
*
|
|
712
|
-
* 38.18 and 38.19's directory, as an address per file — or a compiled app's, whose bytes are
|
|
713
|
-
* inside itself. Read at boot rather than per request, and the ETAG IS READ WITH IT: 21.13
|
|
714
|
-
* derives it from the bytes, so there is no cheaper moment than the one read that is happening
|
|
715
|
-
* anyway. The bytes are hashed and dropped — `Bun.file` streams the body when somebody asks, so a
|
|
716
|
-
* large file is not held in memory for the life of the process.
|
|
717
|
-
*/
|
|
718
|
-
async function publicFilesAt(
|
|
719
|
-
paths: Map<string, string>,
|
|
720
|
-
): Promise<Map<string, PublicFile>> {
|
|
721
|
-
const files = new Map<string, PublicFile>()
|
|
722
|
-
// THE READS OVERLAP. They are independent, and `abide dev` runs this again on every
|
|
723
|
-
// reload rather than once at boot — so it is reload latency an author waits on, not just
|
|
724
|
-
// a boot cost. Each is hashed as it lands, so no more than the reads in flight are held.
|
|
725
|
-
const names = [...paths.keys()]
|
|
726
|
-
const etags = await Promise.all(
|
|
727
|
-
names.map(async (name) =>
|
|
728
|
-
contentEtag(await Bun.file(paths.get(name) as string).bytes()),
|
|
729
|
-
),
|
|
730
|
-
)
|
|
731
|
-
for (let index = 0; index < names.length; index += 1) {
|
|
732
|
-
const name = names[index] as string
|
|
733
|
-
files.set(`/${name}`, {
|
|
734
|
-
path: paths.get(name) as string,
|
|
735
|
-
etag: etags[index] as string,
|
|
736
|
-
type: publicType(name),
|
|
737
|
-
})
|
|
738
|
-
}
|
|
739
|
-
return files
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
/**
|
|
743
|
-
* A public file's content type, off its name's extension.
|
|
744
|
-
*
|
|
745
|
-
* 36.14, 36.17 — a text type without a charset is given UTF-8's rather than whatever a browser
|
|
746
|
-
* guesses.
|
|
747
|
-
*/
|
|
748
|
-
function publicType(name: string): string {
|
|
749
|
-
const type = Bun.file(name).type
|
|
750
|
-
return type.startsWith('text/') && !type.includes('charset')
|
|
751
|
-
? `${type}; charset=utf-8`
|
|
752
|
-
: type
|
|
753
|
-
}
|
|
754
|
-
|
|
755
466
|
function notFound(): Response {
|
|
756
467
|
return plainAnswer('not found', 404)
|
|
757
468
|
}
|