@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.
Files changed (150) hide show
  1. package/ABIDE.md +8 -5
  2. package/CHANGELOG.md +58 -0
  3. package/bin/abide.ts +36 -65
  4. package/docs/REGISTRY.md +45 -12
  5. package/docs/RULEBOOK.md +24 -14
  6. package/docs/abide.md +999 -459
  7. package/package.json +1 -1
  8. package/src/compiler/ARRAY_MUTATORS.ts +20 -0
  9. package/src/compiler/Diagnostics.ts +198 -0
  10. package/src/compiler/KeptChecker.ts +97 -0
  11. package/src/compiler/LoweredFile.ts +609 -0
  12. package/src/compiler/Lowering.ts +18 -1
  13. package/src/compiler/PositionedRefusal.ts +1 -1
  14. package/src/compiler/SHARED_NAMES.ts +2 -0
  15. package/src/compiler/ShadowHost.ts +670 -0
  16. package/src/compiler/ShadowMap.ts +1 -1
  17. package/src/compiler/TypeSession.ts +76 -1743
  18. package/src/compiler/appLayout.ts +28 -20
  19. package/src/compiler/appLints.ts +109 -0
  20. package/src/compiler/argsDefaultsIn.ts +1 -1
  21. package/src/compiler/checkApp.ts +64 -68
  22. package/src/compiler/checkedShadow.ts +2 -2
  23. package/src/compiler/classifyRead.ts +16 -1
  24. package/src/compiler/compileApp.ts +102 -71
  25. package/src/compiler/declaredExports.ts +46 -21
  26. package/src/compiler/diagnoseSources.ts +232 -0
  27. package/src/compiler/emitClient.ts +1 -1
  28. package/src/compiler/formatAbide.ts +7 -8
  29. package/src/compiler/handlersUnder.ts +32 -63
  30. package/src/compiler/lineOf.ts +0 -19
  31. package/src/compiler/matchersOf.ts +3 -3
  32. package/src/compiler/mountsOf.ts +3 -3
  33. package/src/compiler/reactiveReads.ts +235 -0
  34. package/src/compiler/reactiveTypes.ts +13 -0
  35. package/src/compiler/readSources.ts +16 -18
  36. package/src/compiler/readsIn.ts +181 -82
  37. package/src/compiler/routesDeclaration.ts +46 -0
  38. package/src/compiler/scaffold.ts +7 -2
  39. package/src/compiler/serveLanguage.ts +46 -74
  40. package/src/compiler/sharedModuleWrites.ts +1 -1
  41. package/src/compiler/splitImports.ts +2 -2
  42. package/src/compiler/templateWarnings.ts +1 -2
  43. package/src/compiler/trackingWarnings.ts +1 -1
  44. package/src/server/CLIENT_BUILD.ts +4 -32
  45. package/src/server/DesktopSession.ts +213 -0
  46. package/src/server/Followers.ts +399 -0
  47. package/src/server/MODULE_QUERIES.ts +6 -2
  48. package/src/server/PageRender.ts +201 -0
  49. package/src/server/RESUME_MILLISECONDS.ts +2 -0
  50. package/src/server/abideModules.ts +7 -7
  51. package/src/server/answerFailure.ts +3 -2
  52. package/src/server/answerRequest.ts +16 -369
  53. package/src/server/answerRpc.ts +48 -302
  54. package/src/server/answerSocket.ts +41 -490
  55. package/src/server/appBuild.ts +377 -0
  56. package/src/server/appKit.ts +1 -1
  57. package/src/server/appPlugins.ts +15 -17
  58. package/src/server/appTree.ts +6 -5
  59. package/src/server/argsContract.ts +209 -0
  60. package/src/server/bridgeRelay.ts +153 -65
  61. package/src/server/claudeCode.ts +30 -127
  62. package/src/server/config.ts +6 -7
  63. package/src/server/createApp.ts +38 -66
  64. package/src/server/csp.ts +1 -1
  65. package/src/server/deferredBuilds.ts +3 -16
  66. package/src/server/devWorker.ts +11 -15
  67. package/src/server/faceOf.ts +1 -1
  68. package/src/server/generatedSurfaces.ts +2 -1
  69. package/src/server/handlerFileMount.ts +40 -0
  70. package/src/server/index.ts +3 -3
  71. package/src/server/installServer.ts +5 -9
  72. package/src/server/invokeHandler.ts +230 -0
  73. package/src/server/mountHandlers.ts +184 -0
  74. package/src/server/openDesktopWindow.ts +308 -0
  75. package/src/server/packageDirectory.ts +14 -0
  76. package/src/server/preload.ts +21 -2
  77. package/src/server/principal.ts +5 -4
  78. package/src/server/remote.ts +2 -2
  79. package/src/server/render.ts +31 -59
  80. package/src/server/requestScope.ts +5 -36
  81. package/src/server/runCompiled.ts +1 -1
  82. package/src/server/runDesktop.ts +20 -448
  83. package/src/server/seedBuffer.ts +12 -12
  84. package/src/server/serveDev.ts +24 -58
  85. package/src/server/servedApp.ts +390 -0
  86. package/src/server/shell.ts +1 -1
  87. package/src/server/signInChanged.ts +20 -0
  88. package/src/server/surfaceOf.ts +3 -2
  89. package/src/server/versionOf.ts +7 -16
  90. package/src/shared/RPC_SERVER.ts +27 -31
  91. package/src/shared/Schema.ts +1 -2
  92. package/src/shared/applyPatch.ts +1 -1
  93. package/src/shared/index.ts +1 -1
  94. package/src/shared/{note.ts → issueReport.ts} +22 -0
  95. package/src/shared/loadCall.ts +66 -0
  96. package/src/shared/muxFrames.ts +1 -1
  97. package/src/shared/reactive/Keyed.ts +24 -16
  98. package/src/shared/reactive/Reactive.ts +37 -991
  99. package/src/shared/reactive/argsContract.ts +131 -4
  100. package/src/shared/reactive/byOf.ts +2 -5
  101. package/src/shared/reactive/callLoad.ts +8 -14
  102. package/src/shared/reactive/cellShape.ts +734 -0
  103. package/src/shared/reactive/index.ts +12 -23
  104. package/src/shared/reactive/pairReactive.ts +5 -3
  105. package/src/shared/reactive/patchLog.ts +79 -0
  106. package/src/shared/reactive/patchMarks.ts +34 -0
  107. package/src/shared/reactive/producer.ts +121 -0
  108. package/src/shared/reactive/recordWrites.ts +11 -14
  109. package/src/shared/reactive/scoped.ts +3 -2
  110. package/src/shared/reactive/selections.ts +209 -0
  111. package/src/shared/reactive/share.ts +4 -3
  112. package/src/shared/reactive/watch.ts +3 -4
  113. package/src/shared/rpc.ts +20 -24
  114. package/src/shared/validateJson.ts +1 -2
  115. package/src/shared/writePath.ts +29 -0
  116. package/src/ui/ArmRegion.ts +10 -4
  117. package/src/ui/Level.ts +11 -20
  118. package/src/ui/LevelChain.ts +272 -0
  119. package/src/ui/Router.ts +106 -313
  120. package/src/ui/Scope.ts +17 -3
  121. package/src/ui/TryRegion.ts +22 -23
  122. package/src/ui/bindSpread.ts +2 -4
  123. package/src/ui/listenForRoutes.ts +3 -10
  124. package/src/ui/outboxQueue.ts +23 -11
  125. package/src/ui/outcomeOf.ts +120 -0
  126. package/src/ui/reconcile.ts +4 -6
  127. package/src/ui/routing.ts +2 -1
  128. package/src/ui/rows.ts +2 -2
  129. package/src/ui/setAttribute.ts +12 -3
  130. package/src/ui/socketWire.ts +28 -13
  131. package/src/ui/wire.ts +9 -10
  132. package/src/compiler/Finding.ts +0 -2
  133. package/src/compiler/Refusals.ts +0 -65
  134. package/src/compiler/formatDiagnosis.ts +0 -20
  135. package/src/server/appDirectories.ts +0 -9
  136. package/src/server/argsFor.ts +0 -39
  137. package/src/server/buildDirectory.ts +0 -10
  138. package/src/server/coerceArgs.ts +0 -109
  139. package/src/server/derivedArgs.ts +0 -26
  140. package/src/server/runBuild.ts +0 -24
  141. package/src/server/serveBuild.ts +0 -144
  142. package/src/server/testBuild.ts +0 -220
  143. package/src/shared/LOAD_OFFLINE.ts +0 -15
  144. package/src/shared/LOAD_SIGNAL.ts +0 -10
  145. package/src/shared/issuesOf.ts +0 -18
  146. package/src/shared/reactive/ENTRY_LIVES.ts +0 -7
  147. package/src/shared/reactive/FOLLOWED.ts +0 -8
  148. package/src/shared/reactive/PATCHES.ts +0 -11
  149. package/src/shared/reactive/PATCH_SINKS.ts +0 -26
  150. package/src/shared/reactive/canonicalKey.ts +0 -95
@@ -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
- }
@@ -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
- }
@@ -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
- }
@@ -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
- }
@@ -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
- }
@@ -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
- }