@abide/abide 0.123.2 → 0.124.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/ABIDE.md +8 -5
- package/CHANGELOG.md +58 -0
- package/bin/abide.ts +36 -65
- package/docs/REGISTRY.md +45 -12
- package/docs/RULEBOOK.md +24 -14
- package/docs/abide.md +999 -459
- package/package.json +1 -1
- package/src/compiler/ARRAY_MUTATORS.ts +20 -0
- package/src/compiler/Diagnostics.ts +198 -0
- package/src/compiler/KeptChecker.ts +97 -0
- package/src/compiler/LoweredFile.ts +609 -0
- package/src/compiler/Lowering.ts +18 -1
- package/src/compiler/PositionedRefusal.ts +1 -1
- package/src/compiler/SHARED_NAMES.ts +2 -0
- package/src/compiler/ShadowHost.ts +670 -0
- package/src/compiler/ShadowMap.ts +1 -1
- package/src/compiler/TypeSession.ts +76 -1743
- package/src/compiler/appLayout.ts +28 -20
- package/src/compiler/appLints.ts +109 -0
- package/src/compiler/argsDefaultsIn.ts +1 -1
- package/src/compiler/checkApp.ts +64 -68
- package/src/compiler/checkedShadow.ts +2 -2
- package/src/compiler/classifyRead.ts +16 -1
- package/src/compiler/compileApp.ts +102 -71
- package/src/compiler/declaredExports.ts +46 -21
- package/src/compiler/diagnoseSources.ts +232 -0
- package/src/compiler/emitClient.ts +1 -1
- package/src/compiler/formatAbide.ts +7 -8
- package/src/compiler/handlersUnder.ts +32 -63
- package/src/compiler/lineOf.ts +0 -19
- package/src/compiler/matchersOf.ts +3 -3
- package/src/compiler/mountsOf.ts +3 -3
- package/src/compiler/reactiveReads.ts +235 -0
- package/src/compiler/reactiveTypes.ts +13 -0
- package/src/compiler/readSources.ts +16 -18
- package/src/compiler/readsIn.ts +181 -82
- package/src/compiler/routesDeclaration.ts +46 -0
- package/src/compiler/scaffold.ts +7 -2
- package/src/compiler/serveLanguage.ts +46 -74
- package/src/compiler/sharedModuleWrites.ts +1 -1
- package/src/compiler/splitImports.ts +2 -2
- 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/DesktopSession.ts +213 -0
- package/src/server/Followers.ts +399 -0
- package/src/server/MODULE_QUERIES.ts +6 -2
- package/src/server/PageRender.ts +201 -0
- package/src/server/RESUME_MILLISECONDS.ts +2 -0
- package/src/server/abideModules.ts +7 -7
- package/src/server/answerFailure.ts +3 -2
- package/src/server/answerRequest.ts +16 -369
- package/src/server/answerRpc.ts +48 -302
- package/src/server/answerSocket.ts +41 -490
- package/src/server/appBuild.ts +377 -0
- package/src/server/appKit.ts +1 -1
- 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 +38 -66
- package/src/server/csp.ts +1 -1
- package/src/server/deferredBuilds.ts +3 -16
- package/src/server/devWorker.ts +11 -15
- package/src/server/faceOf.ts +1 -1
- package/src/server/generatedSurfaces.ts +2 -1
- package/src/server/handlerFileMount.ts +40 -0
- package/src/server/index.ts +3 -3
- package/src/server/installServer.ts +5 -9
- package/src/server/invokeHandler.ts +230 -0
- package/src/server/mountHandlers.ts +184 -0
- package/src/server/openDesktopWindow.ts +308 -0
- package/src/server/packageDirectory.ts +14 -0
- package/src/server/preload.ts +21 -2
- package/src/server/principal.ts +5 -4
- package/src/server/remote.ts +2 -2
- package/src/server/render.ts +31 -59
- package/src/server/requestScope.ts +5 -36
- package/src/server/runCompiled.ts +1 -1
- package/src/server/runDesktop.ts +20 -448
- package/src/server/seedBuffer.ts +12 -12
- package/src/server/serveDev.ts +24 -58
- package/src/server/servedApp.ts +390 -0
- package/src/server/shell.ts +1 -1
- package/src/server/signInChanged.ts +20 -0
- package/src/server/surfaceOf.ts +3 -2
- package/src/server/versionOf.ts +7 -16
- package/src/shared/RPC_SERVER.ts +27 -31
- package/src/shared/Schema.ts +1 -2
- package/src/shared/applyPatch.ts +1 -1
- package/src/shared/index.ts +1 -1
- package/src/shared/{note.ts → issueReport.ts} +22 -0
- package/src/shared/loadCall.ts +66 -0
- package/src/shared/muxFrames.ts +1 -1
- package/src/shared/reactive/Keyed.ts +24 -16
- package/src/shared/reactive/Reactive.ts +37 -991
- package/src/shared/reactive/argsContract.ts +131 -4
- package/src/shared/reactive/byOf.ts +2 -5
- package/src/shared/reactive/callLoad.ts +8 -14
- package/src/shared/reactive/cellShape.ts +734 -0
- package/src/shared/reactive/index.ts +12 -23
- package/src/shared/reactive/pairReactive.ts +5 -3
- package/src/shared/reactive/patchLog.ts +79 -0
- package/src/shared/reactive/patchMarks.ts +34 -0
- package/src/shared/reactive/producer.ts +121 -0
- package/src/shared/reactive/recordWrites.ts +11 -14
- package/src/shared/reactive/scoped.ts +3 -2
- package/src/shared/reactive/selections.ts +209 -0
- package/src/shared/reactive/share.ts +4 -3
- package/src/shared/reactive/watch.ts +3 -4
- package/src/shared/rpc.ts +20 -24
- package/src/shared/validateJson.ts +1 -2
- package/src/shared/writePath.ts +29 -0
- 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 +106 -313
- 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 +23 -11
- package/src/ui/outcomeOf.ts +120 -0
- 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 +28 -13
- package/src/ui/wire.ts +9 -10
- 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 -26
- package/src/server/runBuild.ts +0 -24
- package/src/server/serveBuild.ts +0 -144
- package/src/server/testBuild.ts +0 -220
- package/src/shared/LOAD_OFFLINE.ts +0 -15
- package/src/shared/LOAD_SIGNAL.ts +0 -10
- package/src/shared/issuesOf.ts +0 -18
- package/src/shared/reactive/ENTRY_LIVES.ts +0 -7
- 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
package/src/server/argsFor.ts
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
// WHAT A CALL'S ARGS ARE BEFORE ITS HANDLER SEES THEM, for every lane that lets a call to a handler in
|
|
2
|
-
// off the wire: over http (16.2) and a `GET` followed over the socket mux (18.3). Each lane reads the
|
|
3
|
-
// args off its own transport and answers a refusal in its own form.
|
|
4
|
-
|
|
5
|
-
import { DERIVED_ARGS } from '#shared/DERIVED_ARGS.ts'
|
|
6
|
-
import { normalized } from '#shared/reactive/argsContract.ts'
|
|
7
|
-
import { validationError } from '#shared/refuse.ts'
|
|
8
|
-
|
|
9
|
-
import type { Mounted } from './answerRpc.ts'
|
|
10
|
-
import { coerceArgs } from './coerceArgs.ts'
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* The args a call over the wire is answered with, out of the args it sent: the text a URL or a form
|
|
14
|
-
* carried coerced where `query` says the args are text, the handler's parameter defaults laid in, and
|
|
15
|
-
* the result held to the handler's annotation. Throws the refusal where it is not held.
|
|
16
|
-
*
|
|
17
|
-
* 19.7 — the text as the types the handler's `args` declares. 11.27 — then the defaults, ahead of
|
|
18
|
-
* every key taken after it: the seed a render filed and the `global` entry standing were both filed
|
|
19
|
-
* under them (11.29). 19.9 — THE WIRE HELD TO THE ANNOTATION, where no `args` schema is declared to
|
|
20
|
-
* hold it — the check the handler carries (`derivedArgs`), which its in-process call makes too (15.8).
|
|
21
|
-
*/
|
|
22
|
-
export function argsFor(
|
|
23
|
-
found: Pick<Mounted, 'handler' | 'argsDocument'>,
|
|
24
|
-
sent: unknown,
|
|
25
|
-
query: string | undefined,
|
|
26
|
-
): unknown {
|
|
27
|
-
const coerced =
|
|
28
|
-
query === undefined
|
|
29
|
-
? sent
|
|
30
|
-
: coerceArgs(
|
|
31
|
-
sent as Record<string, string | string[]> | undefined,
|
|
32
|
-
found.argsDocument,
|
|
33
|
-
query,
|
|
34
|
-
)
|
|
35
|
-
const args = normalized(found.handler, coerced)
|
|
36
|
-
const issues = found.handler[DERIVED_ARGS]?.(args) ?? null
|
|
37
|
-
if (issues !== null) throw validationError(issues)
|
|
38
|
-
return args
|
|
39
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { dirname } from 'node:path' // bun exposes no path helpers of its own
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Where an app's build goes: `.abide` in the directory above its source root.
|
|
5
|
-
*
|
|
6
|
-
* BESIDE THE APP RATHER THAN BESIDE THE SHELL: an app's build belongs to the app.
|
|
7
|
-
*/
|
|
8
|
-
export function buildDirectory(root: string): string {
|
|
9
|
-
return `${dirname(root)}/.abide`
|
|
10
|
-
}
|
package/src/server/coerceArgs.ts
DELETED
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
// 19.7 — A `GET` OR `DELETE`'s TEXT ARGS, COERCED FROM THE TYPE THE `JsonSchema` DECLARES, before
|
|
2
|
-
// anything validates them. A URL carries text, so `?id=42` is `'42'` until the schema says the member
|
|
3
|
-
// is a number. Coercion is the pipeline's and not the schema's (19.6): the schema is handed what this
|
|
4
|
-
// made of the text, and refuses what would not coerce.
|
|
5
|
-
//
|
|
6
|
-
// Read off a `Schema` built by `jsonSchema`, where the document is exposed; a schema with no document
|
|
7
|
-
// has declared no types, and its args stay the text they arrived as.
|
|
8
|
-
|
|
9
|
-
import { numberFromText } from '#shared/numberFromText.ts'
|
|
10
|
-
import { bareName } from '#shared/queryText.ts'
|
|
11
|
-
import { type Gated, gateOf } from '#shared/reactive/argsContract.ts'
|
|
12
|
-
|
|
13
|
-
export type Document = {
|
|
14
|
-
type?: string | string[]
|
|
15
|
-
properties?: Record<string, Document>
|
|
16
|
-
items?: Document
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** The document a handler's `args` schema declares, or `undefined`. */
|
|
20
|
-
export function argsDocumentOf(handler: Gated<unknown>): Document | undefined {
|
|
21
|
-
const gate = gateOf(handler) as
|
|
22
|
-
{ '~standard'?: { jsonSchema?: { input?: () => unknown } } } | undefined
|
|
23
|
-
const document = gate?.['~standard']?.jsonSchema?.input?.()
|
|
24
|
-
return document !== null && typeof document === 'object'
|
|
25
|
-
? (document as Document)
|
|
26
|
-
: undefined
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Name and value pairs as args: a name given more than once is a list, one given once its text.
|
|
31
|
-
*
|
|
32
|
-
* 16.18 — a query's and a form's members, and 38.61 — `abide call`'s `name=value` words, alike.
|
|
33
|
-
*/
|
|
34
|
-
export function collapsed(
|
|
35
|
-
entries: URLSearchParams | FormData,
|
|
36
|
-
): Record<string, string | string[]> {
|
|
37
|
-
const args: Record<string, string | string[]> = {}
|
|
38
|
-
for (const name of new Set(entries.keys())) {
|
|
39
|
-
const values = entries.getAll(name) as string[]
|
|
40
|
-
args[name] = values.length === 1 ? (values[0] as string) : values
|
|
41
|
-
}
|
|
42
|
-
return args
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* The text args, each member coerced to the type `document` declares.
|
|
47
|
-
*
|
|
48
|
-
* `query` is the raw `?…` the args were read from: `URLSearchParams` reads `rpcRequest`'s empty
|
|
49
|
-
* list, a bare name, as `''` — the same as `tags=`, the list `['']` — so a declared array tells the
|
|
50
|
-
* two apart there. A text member keeps the `''`.
|
|
51
|
-
*/
|
|
52
|
-
export function coerceArgs(
|
|
53
|
-
args: Record<string, string | string[]> | undefined,
|
|
54
|
-
document: Document | undefined,
|
|
55
|
-
query: string,
|
|
56
|
-
): unknown {
|
|
57
|
-
if (args === undefined || document?.properties === undefined) return args
|
|
58
|
-
const coerced: Record<string, unknown> = {}
|
|
59
|
-
for (const [name, text] of Object.entries(args)) {
|
|
60
|
-
const member = document.properties[name]
|
|
61
|
-
if (member === undefined) coerced[name] = text
|
|
62
|
-
else if (
|
|
63
|
-
text === '' &&
|
|
64
|
-
typesOf(member).includes('array') &&
|
|
65
|
-
`&${query.slice(1)}&`.includes(`&${bareName(name)}&`)
|
|
66
|
-
)
|
|
67
|
-
coerced[name] = []
|
|
68
|
-
else coerced[name] = coerceMember(text, member)
|
|
69
|
-
}
|
|
70
|
-
return coerced
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function typesOf(member: Document): string[] {
|
|
74
|
-
if (member.type === undefined) return []
|
|
75
|
-
return Array.isArray(member.type) ? member.type : [member.type]
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function coerceMember(text: string | string[], member: Document): unknown {
|
|
79
|
-
const types = typesOf(member)
|
|
80
|
-
// A list is its member repeated, and one given once still answers a declared array.
|
|
81
|
-
if (types.includes('array')) {
|
|
82
|
-
const items = Array.isArray(text) ? text : [text]
|
|
83
|
-
const item = member.items
|
|
84
|
-
return item === undefined
|
|
85
|
-
? items
|
|
86
|
-
: items.map((each) => coerceScalar(each, item))
|
|
87
|
-
}
|
|
88
|
-
if (Array.isArray(text)) return text
|
|
89
|
-
return coerceScalar(text, member)
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* The text as the first declared type it reads as, in the order the schema lists them.
|
|
94
|
-
*
|
|
95
|
-
* Text that reads as none stays text, for the schema to refuse.
|
|
96
|
-
*/
|
|
97
|
-
function coerceScalar(text: string, member: Document): unknown {
|
|
98
|
-
for (const type of typesOf(member)) {
|
|
99
|
-
if (type === 'string') return text
|
|
100
|
-
if (type === 'number' || type === 'integer') {
|
|
101
|
-
const number = numberFromText(text)
|
|
102
|
-
if (number !== undefined) return number
|
|
103
|
-
}
|
|
104
|
-
if (type === 'boolean' && (text === 'true' || text === 'false'))
|
|
105
|
-
return text === 'true'
|
|
106
|
-
if (type === 'null' && text === 'null') return null
|
|
107
|
-
}
|
|
108
|
-
return text
|
|
109
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { DERIVED_ARGS } from '#shared/DERIVED_ARGS.ts'
|
|
2
|
-
import type { JsonValue } from '#shared/JsonValue.ts'
|
|
3
|
-
import type { Gated } from '#shared/reactive/argsContract.ts'
|
|
4
|
-
import type { HeldToAnnotation } from '#shared/rpc.ts'
|
|
5
|
-
import { validateJson } from '#shared/validateJson.ts'
|
|
6
|
-
|
|
7
|
-
import { argsDocumentOf } from './coerceArgs.ts'
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Holds `handler`'s args to `schema`, the one the build derived off its annotation, for every call
|
|
11
|
-
* of it from now on: in process and over the wire (15.8, 19.9).
|
|
12
|
-
*
|
|
13
|
-
* What the module a handler is loaded through calls once its handler is built — a handler file's
|
|
14
|
-
* face and a `<script server>`'s module — so the schema is there wherever the handler can be
|
|
15
|
-
* called, whether or not an app mounted it. Not where its `args` gate publishes a document of its
|
|
16
|
-
* own, which holds the args itself (19.7). Args that did not come are held as an empty record: a
|
|
17
|
-
* schema is only derived where the handler's args are a required record, so a required member
|
|
18
|
-
* missing is refused, not read off nothing.
|
|
19
|
-
*/
|
|
20
|
-
export function derivedArgs(
|
|
21
|
-
handler: HeldToAnnotation & Gated<unknown>,
|
|
22
|
-
schema: JsonValue,
|
|
23
|
-
): void {
|
|
24
|
-
if (argsDocumentOf(handler) !== undefined) return
|
|
25
|
-
handler[DERIVED_ARGS] = (args) => validateJson(schema, args ?? {})
|
|
26
|
-
}
|
package/src/server/runBuild.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
// The CLI, spawned rather than imported: the compiler is not reachable from the server seam.
|
|
2
|
-
const ABIDE_BIN = Bun.fileURLToPath(
|
|
3
|
-
new URL('../../bin/abide.ts', import.meta.url),
|
|
4
|
-
)
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* `abide build` of the app under `root` into `out`, run as the CLI.
|
|
8
|
-
*
|
|
9
|
-
* SPAWNED RATHER THAN IMPORTED: the compiler is not reachable from the server seam.
|
|
10
|
-
*/
|
|
11
|
-
export async function runBuild(root: string, out: string): Promise<void> {
|
|
12
|
-
const child = Bun.spawn(
|
|
13
|
-
[process.execPath, ABIDE_BIN, 'build', '--root', root, '--out', out],
|
|
14
|
-
{ stdout: 'ignore', stderr: 'pipe' },
|
|
15
|
-
)
|
|
16
|
-
const [exitCode, said] = await Promise.all([
|
|
17
|
-
child.exited,
|
|
18
|
-
new Response(child.stderr).text(),
|
|
19
|
-
])
|
|
20
|
-
if (exitCode !== 0)
|
|
21
|
-
throw new Error(
|
|
22
|
-
`abide: could not build the app under ${root}.\n${said}`,
|
|
23
|
-
)
|
|
24
|
-
}
|
package/src/server/serveBuild.ts
DELETED
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
// 38.6 and 38.10 — `abide start` and a compiled app: the built app, over http. The request path is
|
|
2
|
-
// `answerRequest`'s, shared with `abide dev`; what this module is, is the SUPPLIER — where each part
|
|
3
|
-
// comes from when the parts are what `abide build` produced.
|
|
4
|
-
//
|
|
5
|
-
// THE ROUTE TABLE IS THE BUILD'S, not this module's. `abide build` walks the tree once and writes
|
|
6
|
-
// what it found, so the addresses the server answers and the addresses the client router knows
|
|
7
|
-
// are two readings of one walk rather than two walks that have to agree (30.8).
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
type Compilation,
|
|
11
|
-
serveCompilation,
|
|
12
|
-
servedTable,
|
|
13
|
-
} from './abideModules.ts'
|
|
14
|
-
import {
|
|
15
|
-
type AppSources,
|
|
16
|
-
appSources,
|
|
17
|
-
type SourceParts,
|
|
18
|
-
} from './answerRequest.ts'
|
|
19
|
-
import {
|
|
20
|
-
CLIENT_DIRECTORY,
|
|
21
|
-
COMPILATION,
|
|
22
|
-
ENCODING_SUFFIX,
|
|
23
|
-
MANIFEST,
|
|
24
|
-
PUBLIC_DIRECTORY,
|
|
25
|
-
} from './CLIENT_BUILD.ts'
|
|
26
|
-
import { bundleServer, type ServedFile } from './clientBundle.ts'
|
|
27
|
-
import { settingOn } from './config.ts'
|
|
28
|
-
import type { Manifest } from './manifestOf.ts'
|
|
29
|
-
import { namesUnder } from './namesUnder.ts'
|
|
30
|
-
import { ENCODINGS } from './negotiateEncoding.ts'
|
|
31
|
-
import { runBuild } from './runBuild.ts'
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* What `abide build` produced, as the parts an `App` is booted over.
|
|
35
|
-
*
|
|
36
|
-
* Read off disk by `abide start` (`loadBuild`), and out of the executable itself by a compiled app
|
|
37
|
-
* (38.10). ONE SHAPE, so the two are answered by one path and differ only in where a byte is read
|
|
38
|
-
* from — a file is a PATH in both, and `Bun.file` reads the executable's own the same way it reads
|
|
39
|
-
* the disk's.
|
|
40
|
-
*/
|
|
41
|
-
export type BuildParts = Omit<SourceParts, 'client' | 'explained'> & {
|
|
42
|
-
manifest: Manifest
|
|
43
|
-
/** Every file under `client/`, the precompressed copies among them, by name to its path. */
|
|
44
|
-
client: Map<string, string>
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Serves the compilation `abide build` kept in `out` to this process's imports; `false` where there
|
|
49
|
-
* is none, because the app under `root` holds nothing to build.
|
|
50
|
-
*
|
|
51
|
-
* 38.6 — what `abide start` boots: no compiler is loaded, and every `.abide` the app imports is
|
|
52
|
-
* answered out of what the build compiled. 38.100 — AN APP NOBODY BUILT IS BUILT, by `build`, rather
|
|
53
|
-
* than refused, so every caller boots the same way whether or not a build came first. The CLI passes
|
|
54
|
-
* its own build, in-process; anything else spawns it.
|
|
55
|
-
*/
|
|
56
|
-
export async function serveBuild(
|
|
57
|
-
root: string,
|
|
58
|
-
out: string,
|
|
59
|
-
build: (root: string, out: string) => Promise<unknown> = runBuild,
|
|
60
|
-
): Promise<boolean> {
|
|
61
|
-
const path = `${out}/${COMPILATION}`
|
|
62
|
-
if (!(await Bun.file(path).exists())) {
|
|
63
|
-
await build(root, out)
|
|
64
|
-
// A FRESH HANDLE: one taken before the build wrote the file reads it as empty.
|
|
65
|
-
if (!(await Bun.file(path).exists())) return false
|
|
66
|
-
}
|
|
67
|
-
serveCompilation(root, (await Bun.file(path).json()) as Compilation)
|
|
68
|
-
return true
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* What `abide build` left on disk, as the sources an `App` is booted over — its compilation served
|
|
73
|
-
* already (`serveBuild`).
|
|
74
|
-
*/
|
|
75
|
-
export async function loadBuild(options: {
|
|
76
|
-
/**
|
|
77
|
-
* The app's source root — the directory its pages sit under.
|
|
78
|
-
*/
|
|
79
|
-
root: string
|
|
80
|
-
/** Where `abide build` put its output. */
|
|
81
|
-
out: string
|
|
82
|
-
}): Promise<AppSources> {
|
|
83
|
-
// THE BOOT READS OVERLAP: the tables and the manifest are independent of each other. The
|
|
84
|
-
// manifest names the stylesheet only where the bundler collected one, so an app that imports
|
|
85
|
-
// none links none.
|
|
86
|
-
const [table, manifest] = await Promise.all([
|
|
87
|
-
servedTable(options.root),
|
|
88
|
-
Bun.file(`${options.out}/${MANIFEST}`).json() as Promise<Manifest>,
|
|
89
|
-
])
|
|
90
|
-
return builtSources({
|
|
91
|
-
table,
|
|
92
|
-
handlers: table.handlers,
|
|
93
|
-
manifest,
|
|
94
|
-
client: pathsUnder(`${options.out}/${CLIENT_DIRECTORY}`),
|
|
95
|
-
published: pathsUnder(`${options.out}/${PUBLIC_DIRECTORY}`),
|
|
96
|
-
})
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/** Every file under `directory`, by the name below it to its path. */
|
|
100
|
-
export function pathsUnder(directory: string): Map<string, string> {
|
|
101
|
-
const paths = new Map<string, string>()
|
|
102
|
-
for (const name of namesUnder(directory))
|
|
103
|
-
paths.set(name, `${directory}/${name}`)
|
|
104
|
-
return paths
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/** The sources an `App` is booted over, out of the parts `abide build` produced. */
|
|
108
|
-
export function builtSources(parts: BuildParts): Promise<AppSources> {
|
|
109
|
-
// THE PRECOMPRESSED COPIES ARE NOT ADDRESSES OF THEIR OWN. They are the same file in another
|
|
110
|
-
// encoding (38.5), so they are found under the name they encode and served as that name.
|
|
111
|
-
const suffixes = Object.values(ENCODING_SUFFIX) as string[]
|
|
112
|
-
const files = new Map<string, ServedFile>()
|
|
113
|
-
for (const [name, path] of parts.client) {
|
|
114
|
-
if (suffixes.some((suffix) => name.endsWith(suffix))) continue
|
|
115
|
-
const encoded: ServedFile['encoded'] = {}
|
|
116
|
-
for (const encoding of ENCODINGS) {
|
|
117
|
-
const copy = parts.client.get(name + ENCODING_SUFFIX[encoding])
|
|
118
|
-
if (copy !== undefined) encoded[encoding] = () => Bun.file(copy)
|
|
119
|
-
}
|
|
120
|
-
files.set(name, {
|
|
121
|
-
// THE TYPE IS THE NAME'S, not the path's: a compiled app's path carries a hash the
|
|
122
|
-
// bundler put before the extension, and a `.br`'s extension names nothing.
|
|
123
|
-
type: Bun.file(name).type,
|
|
124
|
-
body: () => Bun.file(path),
|
|
125
|
-
encoded,
|
|
126
|
-
})
|
|
127
|
-
}
|
|
128
|
-
return appSources({
|
|
129
|
-
...parts,
|
|
130
|
-
explained: undefined,
|
|
131
|
-
client: {
|
|
132
|
-
manifest: parts.manifest,
|
|
133
|
-
head: '',
|
|
134
|
-
// 21.5 — EVERY NAME IN THE BUNDLE CARRIES A HASH OF ITS BYTES, the entry's too (38.5), so
|
|
135
|
-
// a name never answers two different files and a year is safe for all of them. A deploy
|
|
136
|
-
// is new names in a new document, never new bytes at an old address.
|
|
137
|
-
framework: bundleServer(
|
|
138
|
-
files,
|
|
139
|
-
'public, max-age=31536000, immutable',
|
|
140
|
-
),
|
|
141
|
-
offline: settingOn('ABIDE_OFFLINE', true),
|
|
142
|
-
},
|
|
143
|
-
})
|
|
144
|
-
}
|
package/src/server/testBuild.ts
DELETED
|
@@ -1,220 +0,0 @@
|
|
|
1
|
-
// 42.16 — UNDER `bun test`, `createApp` BUILDS THE APP rather than reading whatever `abide build` last
|
|
2
|
-
// left, ONCE PER STATE OF ITS SOURCES: the build's directory is named after a hash of what made it,
|
|
3
|
-
// so a build either matches the sources or does not exist, and every test process — every
|
|
4
|
-
// `--parallel` worker, every spawned child — reuses the one that does. Eight workers each building
|
|
5
|
-
// their own copy of a 1.3 s build waited 7–10 s apiece.
|
|
6
|
-
|
|
7
|
-
import { mkdir, rename, rm, utimes, writeFile } from 'node:fs/promises' // bun exposes no directory creation, rename, removal, mtime write or exclusive create of its own
|
|
8
|
-
import { dirname } from 'node:path' // bun exposes no path helpers of its own
|
|
9
|
-
|
|
10
|
-
import { namesUnder } from './namesUnder.ts'
|
|
11
|
-
import { runBuild } from './runBuild.ts'
|
|
12
|
-
|
|
13
|
-
// The framework's own source is part of what made a build: a compiler change is a new build.
|
|
14
|
-
const FRAMEWORK = Bun.fileURLToPath(new URL('../..', import.meta.url))
|
|
15
|
-
const POLL_MS = 50
|
|
16
|
-
// A build no test process has reused for this long is swept by the next one that builds.
|
|
17
|
-
const UNUSED_FOR_MS = 24 * 60 * 60 * 1000
|
|
18
|
-
|
|
19
|
-
// One lookup per app per test process, however many files in it call `createApp`.
|
|
20
|
-
const TEST_BUILDS = new Map<string, Promise<string>>()
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* The build of the app under `root` for its sources as they stand, made if no process has made it.
|
|
24
|
-
*
|
|
25
|
-
* UNDER `node_modules`, so a bare import in the built modules resolves as it does from the app, and
|
|
26
|
-
* so it is ignored wherever `node_modules` is.
|
|
27
|
-
*/
|
|
28
|
-
export function testBuild(root: string): Promise<string> {
|
|
29
|
-
let built = TEST_BUILDS.get(root)
|
|
30
|
-
if (built === undefined) {
|
|
31
|
-
built = sourceHash(root).then((key) =>
|
|
32
|
-
sharedBuild(
|
|
33
|
-
`${dirname(root)}/node_modules/.cache/abide`,
|
|
34
|
-
`build-${key}`,
|
|
35
|
-
(out) => runBuild(root, out),
|
|
36
|
-
),
|
|
37
|
-
)
|
|
38
|
-
TEST_BUILDS.set(root, built)
|
|
39
|
-
}
|
|
40
|
-
return built
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* `builds/key`, made by `build` in exactly one process however many ask at once.
|
|
45
|
-
*
|
|
46
|
-
* THE FIRST TO CREATE `key.lock` BUILDS — an exclusive create is atomic, so one process wins — into
|
|
47
|
-
* a staging directory renamed into place, so nothing reads a half-written build. The rest wait for
|
|
48
|
-
* it, and take the lock over if the process holding it has ended.
|
|
49
|
-
*/
|
|
50
|
-
export async function sharedBuild(
|
|
51
|
-
builds: string,
|
|
52
|
-
key: string,
|
|
53
|
-
build: (out: string) => Promise<void>,
|
|
54
|
-
): Promise<string> {
|
|
55
|
-
const out = `${builds}/${key}`
|
|
56
|
-
const lock = `${out}.lock`
|
|
57
|
-
await mkdir(builds, { recursive: true })
|
|
58
|
-
for (;;) {
|
|
59
|
-
if (await isDirectory(out)) {
|
|
60
|
-
const now = new Date()
|
|
61
|
-
await utimes(out, now, now)
|
|
62
|
-
return out
|
|
63
|
-
}
|
|
64
|
-
if (await claim(lock)) {
|
|
65
|
-
try {
|
|
66
|
-
if (!(await isDirectory(out))) await buildInto(out, build)
|
|
67
|
-
} finally {
|
|
68
|
-
await rm(lock, { force: true })
|
|
69
|
-
}
|
|
70
|
-
await sweep(builds, key)
|
|
71
|
-
return out
|
|
72
|
-
}
|
|
73
|
-
if (await holderEnded(lock)) await rm(lock, { force: true })
|
|
74
|
-
else await Bun.sleep(POLL_MS)
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
async function buildInto(
|
|
79
|
-
out: string,
|
|
80
|
-
build: (out: string) => Promise<void>,
|
|
81
|
-
): Promise<void> {
|
|
82
|
-
const staging = `${out}.tmp-${process.pid}`
|
|
83
|
-
await rm(staging, { recursive: true, force: true })
|
|
84
|
-
try {
|
|
85
|
-
await build(staging)
|
|
86
|
-
await rename(staging, out)
|
|
87
|
-
} catch (thrown) {
|
|
88
|
-
await rm(staging, { recursive: true, force: true })
|
|
89
|
-
// TWO HOLDERS, where one took over a lock the other had just taken over: the first rename
|
|
90
|
-
// stands, and it is the same build.
|
|
91
|
-
if (await isDirectory(out)) return
|
|
92
|
-
throw thrown
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* A hash of everything a build of the app under `root` is made from.
|
|
98
|
-
*
|
|
99
|
-
* The app's files, the package and lockfile above it, the framework's own source, and `root`
|
|
100
|
-
* itself, since a build carries the paths it was made at.
|
|
101
|
-
*/
|
|
102
|
-
export async function sourceHash(root: string): Promise<string> {
|
|
103
|
-
const app = dirname(root)
|
|
104
|
-
const paths = [
|
|
105
|
-
...filesUnder(root),
|
|
106
|
-
...filesUnder(`${FRAMEWORK}src`),
|
|
107
|
-
...filesUnder(`${FRAMEWORK}bin`),
|
|
108
|
-
`${app}/package.json`,
|
|
109
|
-
`${app}/tsconfig.json`,
|
|
110
|
-
]
|
|
111
|
-
const lockfile = await lockfileAbove(app)
|
|
112
|
-
if (lockfile !== undefined) paths.push(lockfile)
|
|
113
|
-
const contents = await Promise.all(
|
|
114
|
-
paths.map((path) =>
|
|
115
|
-
Bun.file(path)
|
|
116
|
-
.bytes()
|
|
117
|
-
.catch(() => undefined),
|
|
118
|
-
),
|
|
119
|
-
)
|
|
120
|
-
const hasher = new Bun.CryptoHasher('sha256')
|
|
121
|
-
hasher.update(root)
|
|
122
|
-
for (let at = 0; at < paths.length; at += 1) {
|
|
123
|
-
const bytes = contents[at]
|
|
124
|
-
if (bytes === undefined) continue
|
|
125
|
-
hasher.update(paths[at] as string)
|
|
126
|
-
hasher.update(bytes)
|
|
127
|
-
}
|
|
128
|
-
return hasher.digest('hex').slice(0, 16)
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Sorted, so the hash does not depend on the order a directory lists in. Dot entries and
|
|
132
|
-
// `node_modules` are skipped: neither is the app's source, and the builds live in one.
|
|
133
|
-
function filesUnder(directory: string): string[] {
|
|
134
|
-
const found: string[] = []
|
|
135
|
-
for (const name of namesUnder(directory))
|
|
136
|
-
if (!`/${name}`.includes('/node_modules/'))
|
|
137
|
-
found.push(`${directory}/${name}`)
|
|
138
|
-
return found.sort()
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
async function lockfileAbove(directory: string): Promise<string | undefined> {
|
|
142
|
-
for (let at = directory; ; at = dirname(at)) {
|
|
143
|
-
for (const name of ['bun.lock', 'bun.lockb'])
|
|
144
|
-
if (await Bun.file(`${at}/${name}`).exists()) return `${at}/${name}`
|
|
145
|
-
if (dirname(at) === at) return undefined
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
async function claim(lock: string): Promise<boolean> {
|
|
150
|
-
try {
|
|
151
|
-
await writeFile(lock, String(process.pid), { flag: 'wx' })
|
|
152
|
-
return true
|
|
153
|
-
} catch (thrown) {
|
|
154
|
-
if ((thrown as { code?: string }).code === 'EEXIST') return false
|
|
155
|
-
throw thrown
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
async function holderEnded(lock: string): Promise<boolean> {
|
|
160
|
-
const text = await Bun.file(lock)
|
|
161
|
-
.text()
|
|
162
|
-
.catch(() => undefined)
|
|
163
|
-
// Gone is not ended: the holder finished, and the next pass finds its build.
|
|
164
|
-
if (text === undefined) return false
|
|
165
|
-
// Empty is a holder between creating the lock and writing its pid.
|
|
166
|
-
return text !== '' && !isRunning(Number(text))
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/** Removes builds unused for a day, and what an ended process left half-made. */
|
|
170
|
-
async function sweep(builds: string, kept: string): Promise<void> {
|
|
171
|
-
const names = new Bun.Glob('*').scanSync({ cwd: builds, onlyFiles: false })
|
|
172
|
-
const removals: Promise<void>[] = []
|
|
173
|
-
const now = Date.now()
|
|
174
|
-
for (const name of names) {
|
|
175
|
-
if (name === kept) continue
|
|
176
|
-
const staged = /\.tmp-(\d+)$/.exec(name)
|
|
177
|
-
if (staged !== null) {
|
|
178
|
-
if (!isRunning(Number(staged[1])))
|
|
179
|
-
removals.push(
|
|
180
|
-
rm(`${builds}/${name}`, { recursive: true, force: true }),
|
|
181
|
-
)
|
|
182
|
-
continue
|
|
183
|
-
}
|
|
184
|
-
if (name.endsWith('.lock')) continue
|
|
185
|
-
removals.push(
|
|
186
|
-
Bun.file(`${builds}/${name}`)
|
|
187
|
-
.stat()
|
|
188
|
-
.then((found) =>
|
|
189
|
-
now - found.mtimeMs > UNUSED_FOR_MS
|
|
190
|
-
? rm(`${builds}/${name}`, {
|
|
191
|
-
recursive: true,
|
|
192
|
-
force: true,
|
|
193
|
-
})
|
|
194
|
-
: undefined,
|
|
195
|
-
)
|
|
196
|
-
.catch(() => undefined),
|
|
197
|
-
)
|
|
198
|
-
}
|
|
199
|
-
await Promise.all(removals)
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
async function isDirectory(path: string): Promise<boolean> {
|
|
203
|
-
return (
|
|
204
|
-
(
|
|
205
|
-
await Bun.file(path)
|
|
206
|
-
.stat()
|
|
207
|
-
.catch(() => undefined)
|
|
208
|
-
)?.isDirectory() === true
|
|
209
|
-
)
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function isRunning(pid: number): boolean {
|
|
213
|
-
try {
|
|
214
|
-
process.kill(pid, 0)
|
|
215
|
-
return true
|
|
216
|
-
} catch (thrown) {
|
|
217
|
-
// EPERM is a process that exists and is someone else's.
|
|
218
|
-
return (thrown as { code?: string }).code === 'EPERM'
|
|
219
|
-
}
|
|
220
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The `offline` of the call that is starting a load, while it starts.
|
|
3
|
-
*
|
|
4
|
-
* 16.81 — SET AROUND THE REACH as `LOAD_SIGNAL` is, and read by the browser's wire as its body
|
|
5
|
-
* starts: what stands in for an answer the call could not get. A leaf, because `callLoad` sets it
|
|
6
|
-
* and the wire reads it.
|
|
7
|
-
*/
|
|
8
|
-
export const LOAD_OFFLINE: {
|
|
9
|
-
current: ((args: unknown) => unknown) | undefined
|
|
10
|
-
/**
|
|
11
|
-
* Where the body that queues the call asks for the value the call reached, which `callLoad`
|
|
12
|
-
* fills once the reach returns: what the delivered answer lands in (46.7).
|
|
13
|
-
*/
|
|
14
|
-
standing: { value: unknown } | undefined
|
|
15
|
-
} = { current: undefined, standing: undefined }
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The abort signal of the call that is starting a load, while it starts.
|
|
3
|
-
*
|
|
4
|
-
* 16.49 — THE SIGNAL OF THE CALL THAT IS STARTING A LOAD, set around the reach and read by a
|
|
5
|
-
* transport's body as it starts: the browser's wire hands it to `fetch`, so a load every caller
|
|
6
|
-
* aborted ends its request too. A leaf, because `callLoad` sets it and the wire reads it.
|
|
7
|
-
*/
|
|
8
|
-
export const LOAD_SIGNAL: { current: AbortSignal | undefined } = {
|
|
9
|
-
current: undefined,
|
|
10
|
-
}
|
package/src/shared/issuesOf.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Issues in the shape `Issues<T>` gives them for the value refused: by path for an object or an
|
|
3
|
-
* array, the bare list for anything else.
|
|
4
|
-
*
|
|
5
|
-
* DECIDED BY THE VALUE, because the value is all the runtime holds — `T` is erased. A scalar has
|
|
6
|
-
* no path to key by, so every issue about one stands on `''`, and handing back `{ '': [...] }`
|
|
7
|
-
* there is the wrapper `Issues<T>` says a reader never unwraps: `error().data[0]` read `undefined`.
|
|
8
|
-
*/
|
|
9
|
-
export function issuesOf(
|
|
10
|
-
found: Record<string, string[]>,
|
|
11
|
-
value: unknown,
|
|
12
|
-
): Record<string, string[]> | string[] {
|
|
13
|
-
if (typeof value === 'object' && value !== null) return found
|
|
14
|
-
const listed: string[] = []
|
|
15
|
-
for (const path in found)
|
|
16
|
-
for (const said of found[path] as string[]) listed.push(said)
|
|
17
|
-
return listed
|
|
18
|
-
}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* When each entry of a `Keyed` declaring `ttl` last produced, and how long it lives, by entry cell.
|
|
3
|
-
*
|
|
4
|
-
* 11.45. A leaf because `Keyed.ts` writes it and `Reactive.ts` reads it (`deadlineOf`), and the
|
|
5
|
-
* second cannot import the first.
|
|
6
|
-
*/
|
|
7
|
-
export const ENTRY_LIVES = new WeakMap<object, { at: number; ms: number }>()
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* What an invalidate of a value the server feeds over the mux sends on, by the cell its past is on.
|
|
3
|
-
*
|
|
4
|
-
* 7.10. The core calls these for every cell it invalidates, however the trigger was spelled — the
|
|
5
|
-
* value's own, a `Keyed`'s, an `Rpc`'s or a tag's — and only a value a browser follows has any, so
|
|
6
|
-
* every other invalidate is one miss. A leaf, for `PATCH_SINKS`'s reason.
|
|
7
|
-
*/
|
|
8
|
-
export const FOLLOWED = new WeakMap<object, Set<() => void>>()
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Which objects an `s.patch` wrote to, and when.
|
|
3
|
-
*
|
|
4
|
-
* 32.33. `epoch` moves once per patch and `written` holds the epoch an object was last written at,
|
|
5
|
-
* so a row that took its item at epoch `e` is stale exactly where its item's entry is past `e`. A leaf, because
|
|
6
|
-
* `reconcile` reads it from the `@abide/abide/runtime` barrel.
|
|
7
|
-
*/
|
|
8
|
-
export const PATCHES: { epoch: number; written: WeakMap<object, number> } = {
|
|
9
|
-
epoch: 0,
|
|
10
|
-
written: new WeakMap(),
|
|
11
|
-
}
|