@abide/abide 0.104.0 → 0.105.1

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 (85) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/bin/abide.ts +80 -173
  3. package/docs/REGISTRY.md +17 -17
  4. package/docs/RULEBOOK.md +50 -12
  5. package/docs/abide.md +9 -9
  6. package/package.json +1 -1
  7. package/src/compiler/Lowering.ts +15 -5
  8. package/src/compiler/Refusals.ts +57 -0
  9. package/src/compiler/TypeSession.ts +45 -86
  10. package/src/compiler/appLayout.ts +447 -0
  11. package/src/compiler/appMap.ts +21 -20
  12. package/src/compiler/checkApp.ts +61 -97
  13. package/src/compiler/checkedShadow.ts +11 -9
  14. package/src/compiler/classifyRead.ts +3 -6
  15. package/src/compiler/compileApp.ts +180 -512
  16. package/src/compiler/componentGraph.ts +2 -71
  17. package/src/compiler/declaredExports.ts +79 -60
  18. package/src/compiler/emitClient.ts +11 -13
  19. package/src/compiler/emitComponent.ts +21 -6
  20. package/src/compiler/emitSource.ts +20 -20
  21. package/src/compiler/handlersUnder.ts +46 -36
  22. package/src/compiler/hookCalls.ts +152 -0
  23. package/src/compiler/mountsOf.ts +31 -54
  24. package/src/compiler/readSources.ts +27 -42
  25. package/src/compiler/readsIn.ts +5 -16
  26. package/src/compiler/resolverIn.ts +77 -0
  27. package/src/server/abideModules.ts +92 -123
  28. package/src/server/answerRequest.ts +18 -4
  29. package/src/server/answerSocket.ts +136 -76
  30. package/src/server/answerValue.ts +28 -15
  31. package/src/server/appTree.ts +49 -71
  32. package/src/server/claudeApi.ts +2 -5
  33. package/src/server/clientBundle.ts +198 -0
  34. package/src/server/coerceArgs.ts +1 -1
  35. package/src/server/commands.ts +93 -57
  36. package/src/server/createApp.ts +90 -16
  37. package/src/server/deferredBuilds.ts +175 -0
  38. package/src/server/devWorker.ts +107 -319
  39. package/src/server/faceOf.ts +120 -0
  40. package/src/server/fenceServer.ts +10 -18
  41. package/src/server/index.ts +3 -0
  42. package/src/server/placement.ts +167 -0
  43. package/src/server/remote.ts +14 -42
  44. package/src/server/render.ts +25 -17
  45. package/src/server/renderComponent.ts +9 -5
  46. package/src/server/requestScope.ts +10 -4
  47. package/src/server/responseRelay.ts +389 -0
  48. package/src/server/runCompiled.ts +9 -12
  49. package/src/server/runDesktop.ts +6 -9
  50. package/src/server/serveBuild.ts +26 -73
  51. package/src/server/serveDev.ts +19 -176
  52. package/src/server/streamEvent.ts +29 -0
  53. package/src/server/withAbideSources.ts +2 -1
  54. package/src/shared/HandlerRoute.ts +68 -0
  55. package/src/shared/REFUSAL_KEY.ts +5 -0
  56. package/src/shared/answerOf.ts +105 -78
  57. package/src/shared/filledPath.ts +1 -9
  58. package/src/shared/framing.ts +127 -128
  59. package/src/shared/queryText.ts +15 -7
  60. package/src/shared/reactive/Cursor.ts +4 -2
  61. package/src/shared/reactive/Reactive.ts +285 -197
  62. package/src/shared/reactive/index.ts +11 -8
  63. package/src/shared/reactive/pairReactive.ts +1 -1
  64. package/src/shared/reactive/scoped.ts +4 -3
  65. package/src/shared/reactive/share.ts +11 -1
  66. package/src/shared/routePattern.ts +0 -123
  67. package/src/shared/rpc.ts +17 -24
  68. package/src/shared/rpcRequest.ts +18 -47
  69. package/src/shared/takeEarlyCall.ts +12 -37
  70. package/src/shared/url.ts +1 -1
  71. package/src/ui/Level.ts +276 -0
  72. package/src/ui/List.ts +17 -17
  73. package/src/ui/Router.ts +20 -87
  74. package/src/ui/applyHead.ts +92 -79
  75. package/src/ui/index.ts +4 -2
  76. package/src/ui/leave.ts +5 -0
  77. package/src/ui/reconcile.ts +23 -104
  78. package/src/ui/regions.ts +27 -2
  79. package/src/ui/rows.ts +123 -2
  80. package/src/ui/wire.ts +12 -27
  81. package/src/compiler/hooksCalled.ts +0 -35
  82. package/src/server/clientBuildOptions.ts +0 -54
  83. package/src/server/refuseAddressCollision.ts +0 -37
  84. package/src/shared/reactive/chunksOfStream.ts +0 -33
  85. package/src/ui/MOUNTING_LEVEL.ts +0 -18
@@ -12,6 +12,7 @@ import { MOUNT, unmounted } from '#shared/mount.ts'
12
12
  import { RPC_SERVER } from '#shared/RPC_SERVER.ts'
13
13
  import { servedTable } from './abideModules.ts'
14
14
  import { type AppSources, answerRequest } from './answerRequest.ts'
15
+ import type { Mount } from './answerRpc.ts'
15
16
  import { Mux, type MuxConnection } from './answerSocket.ts'
16
17
  import { appDirectories } from './appDirectories.ts'
17
18
  import { holdsApp } from './appTree.ts'
@@ -61,17 +62,69 @@ export interface App {
61
62
  */
62
63
  export async function createApp(): Promise<App> {
63
64
  const root = appRoot(undefined)
64
- if (root === undefined)
65
- throw new Error(
66
- `abide: createApp found no app. Looked for a .abide under ${resolve('src')}, or under ${process.cwd()} where it has no src.`,
67
- )
68
- const out = await servedBuild(root)
69
- if (out === undefined)
70
- throw new Error(
71
- `abide: createApp found nothing under ${root} to build.`,
72
- )
73
- const { loadBuild } = await import('./serveBuild.ts')
74
- return bootApp(originUnder(root), () => loadBuild({ root, out }))
65
+ const app = await appOnDisk(
66
+ root === undefined ? undefined : { root, out: buildDirectory(root) },
67
+ (root) => servedBuild(root),
68
+ ).boot()
69
+ if (typeof app === 'string')
70
+ throw new Error(`abide: createApp found ${app}`)
71
+ return app
72
+ }
73
+
74
+ /**
75
+ * One app, as one invocation means it: FOUND ONCE, and everything a command derives — the banner's
76
+ * identity, the shell's handler table, the boot — read off this rather than found again from where
77
+ * the process happens to stand. An app on disk (`appOnDisk`), or the one an executable holds.
78
+ */
79
+ export type AppHandle = {
80
+ /** 27.3's name and version: the banner's, and what a script `run` resolves its configuration with. */
81
+ package: AppPackage
82
+ /**
83
+ * The handler table of a build already there, NEVER ONE MADE FOR THE ASKING: `call` and the
84
+ * shell are aimed at a running app (38.7), which this tree may not even be. Empty where none.
85
+ */
86
+ handlers: () => Promise<Mount[]>
87
+ /** The app's build made this process's, and booted — or why there is nothing to boot. */
88
+ boot: () => Promise<App | string>
89
+ }
90
+
91
+ /** Why there is no app where none was named: where 38.25 looked. 38.26. */
92
+ export function noAppHere(): string {
93
+ return `no app here. Looked for a .abide under ${resolve('src')}, or under ${process.cwd()} where it has no src.`
94
+ }
95
+
96
+ /**
97
+ * The app on disk at `found` — its source root and build directory, both absolute — or the
98
+ * refusal where nothing was found. `built` makes its build this process's (`serveBuild`, which
99
+ * builds one nobody built — 38.100) and answers the directory it is in, or `undefined` where
100
+ * nothing under the root builds.
101
+ */
102
+ export function appOnDisk(
103
+ found: { root: string; out: string } | undefined,
104
+ built: (root: string, out: string) => Promise<string | undefined>,
105
+ ): AppHandle {
106
+ if (found === undefined)
107
+ return {
108
+ package: packageUnder(process.cwd()),
109
+ handlers: async () => [],
110
+ boot: async () => noAppHere(),
111
+ }
112
+ const { root, out } = found
113
+ const origin = originUnder(root)
114
+ return {
115
+ package: origin.package,
116
+ handlers: async () => {
117
+ const { serveBuild } = await import('./serveBuild.ts')
118
+ if (!(await serveBuild(root, out, async () => undefined))) return []
119
+ return (await servedTable(root)).handlers
120
+ },
121
+ boot: async () => {
122
+ const served = await built(root, out)
123
+ if (served === undefined) return `nothing under ${root} to build.`
124
+ const { loadBuild } = await import('./serveBuild.ts')
125
+ return bootApp(origin, () => loadBuild({ root, out: served }))
126
+ },
127
+ }
75
128
  }
76
129
 
77
130
  /**
@@ -152,22 +205,43 @@ export function originUnder(root: string): AppOrigin {
152
205
  */
153
206
  export type BootedApp = App & { openMux(): Mux }
154
207
 
208
+ /**
209
+ * Enters the app as this process's server — every slot `#shared` leaves for one filled, the app's
210
+ * identity set, every server block imported — and re-resolves the configuration: the one preamble
211
+ * `bootApp`, a script `run` and the desktop launcher each start with.
212
+ *
213
+ * 37.1 — EVERY SERVER BLOCK, imported for what it CALLS: every hook it runs is filed by the time the
214
+ * import settles, which is before `onStart` is composed around a boot. 38.57 — RE-RESOLVED: a module
215
+ * this process loaded first may already have read the configuration, under no identity.
216
+ */
217
+ export async function enterApp(origin: AppOrigin): Promise<void> {
218
+ installServer()
219
+ APP_PACKAGE.current = origin.package
220
+ if (origin.hooks !== undefined) await origin.hooks()
221
+ config.invalidate()
222
+ }
223
+
155
224
  /** An `App` booted over the sources `load` supplies, inside the app's `onStart` hooks. */
156
225
  export async function bootApp(
157
226
  origin: AppOrigin,
158
227
  load: () => Promise<AppSources>,
159
228
  ): Promise<BootedApp> {
160
- installServer()
161
- // 37.1 — EVERY SERVER BLOCK, imported for what it CALLS: every hook it runs is filed by the
162
- // time the import settles, which is before `onStart` is composed around the boot.
163
- if (origin.hooks !== undefined) await origin.hooks()
229
+ await enterApp(origin)
230
+ return bootEntered(load)
231
+ }
164
232
 
233
+ /**
234
+ * `bootApp` in a process `enterApp` already entered the app in: the desktop launcher's, which enters
235
+ * once for the hooks its own page reads and boots again on every start.
236
+ */
237
+ export async function bootEntered(
238
+ load: () => Promise<AppSources>,
239
+ ): Promise<BootedApp> {
165
240
  // 37.4 — THE BOOT, INSIDE `onStart`. 37.6 — a throw out of a hook or out of the boot rejects
166
241
  // `createApp`, and nothing is served.
167
242
  //
168
243
  // 42.2 — THE CONFIGURATION IS READ AS PART OF THE BOOT, so a configuration that does not resolve
169
244
  // is a boot that fails rather than a first request that does.
170
- APP_PACKAGE.current = origin.package
171
245
  let sources: AppSources | undefined
172
246
  await compose(inOrder(HOOKS.starts), async () => {
173
247
  config.invalidate()
@@ -0,0 +1,175 @@
1
+ // 41.28 — A CHILD WHOSE SETUP MADE A CALL IS BUILT AS A PAGE IS: after the turn.
2
+ // A component is set up where its parent's markup invokes it, so its build cannot wait inline; it is
3
+ // left as a marker in the parent's head and body, built once the turn has run, and written in where
4
+ // its marker stands. A child whose setup made no call builds where it stands, as before, and pays a
5
+ // counter read on either side of its setup for the difference.
6
+ //
7
+ // THE MARKERS ARE NEVER IN WHAT IS SENT. Every one is replaced before `composePage` answers, and the
8
+ // nonce is never written anywhere, so no text an author or a caller supplies can stand in for one.
9
+
10
+ import { regionMarkup } from '#shared/anchorMarkup.ts'
11
+ import { isReactive, isStreaming, loadOf } from '#shared/reactive/index.ts'
12
+ import type { Rendered } from './render.ts'
13
+ import type { PageCall } from './requestScope.ts'
14
+
15
+ // SYNCHRONOUS SETUP IS WHAT MAKES ONE COUNTER ENOUGH. 34.3 keeps a setup from awaiting, so no other
16
+ // request's render runs between a component's two reads, however many renders are in flight.
17
+ let callsInRenders = 0
18
+
19
+ /** How many calls renders have made, read on either side of a component's setup. */
20
+ export function callsInRender(): number {
21
+ return callsInRenders
22
+ }
23
+
24
+ /** Counts a call a page's render made (`reachedInRender`). */
25
+ export function noteCallInRender(): void {
26
+ callsInRenders += 1
27
+ }
28
+
29
+ /** A deferred child: its record, and the page's calls its setup made, marked with its build. */
30
+ type Deferred = { rendered: Rendered; callsFrom: number; callsTo: number }
31
+
32
+ type Build = { deferred: Deferred[]; calls: PageCall[] }
33
+
34
+ /**
35
+ * The render whose markup is being built. Set only while `pass` runs, which is synchronous, so
36
+ * renders in flight never see each other's.
37
+ */
38
+ let building: Build | undefined
39
+
40
+ const NONCE = crypto.randomUUID()
41
+ const MARKER = new RegExp(`\u0000${NONCE}([hb])(\\d+)\u0000`, 'g')
42
+
43
+ /** The page's calls so far, for a child to note where its setup's begin; `-1` outside a build. */
44
+ export function callsMadeSoFar(): number {
45
+ return building === undefined ? -1 : building.calls.length
46
+ }
47
+
48
+ /**
49
+ * Where a child that deferred stands in its parent's markup: a marker in the head and in the body,
50
+ * or `undefined` where no page is being built and the child builds where it stands.
51
+ */
52
+ export function deferBuild(
53
+ rendered: Rendered,
54
+ heads: string[],
55
+ callsFrom: number,
56
+ ): string | undefined {
57
+ if (building === undefined) return undefined
58
+ const at = building.deferred.length
59
+ building.deferred.push({
60
+ rendered,
61
+ callsFrom,
62
+ callsTo: building.calls.length,
63
+ })
64
+ heads.push(`\u0000${NONCE}h${at}\u0000`)
65
+ return regionMarkup(`\u0000${NONCE}b${at}\u0000`)
66
+ }
67
+
68
+ /**
69
+ * `build`'s markup, with every child it deferred built after the turn — and the children those
70
+ * defer after another — each written in where its marker stands.
71
+ */
72
+ export async function buildDeferred(
73
+ calls: PageCall[],
74
+ build: () => { head: string; body: string },
75
+ ): Promise<{ head: string; body: string }> {
76
+ const state: Build = { deferred: [], calls }
77
+ let built = { head: '', body: '' }
78
+ pass(state, () => {
79
+ built = build()
80
+ })
81
+ if (state.deferred.length === 0) return built
82
+ const fills: { head: string; body: string }[] = []
83
+ let from = 0
84
+ while (from < state.deferred.length) {
85
+ await new Promise((settled) => setImmediate(settled))
86
+ const to = state.deferred.length
87
+ pass(state, () => {
88
+ for (let at = from; at < to; at++)
89
+ fills.push(filled((state.deferred[at] as Deferred).rendered))
90
+ })
91
+ from = to
92
+ }
93
+ return {
94
+ head: expanded(built.head, fills),
95
+ body: expanded(built.body, fills),
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Marks every call not yet marked: settled or not, as the markup just built read it.
101
+ *
102
+ * 41.25 — READ WHERE THE MARKUP HAS JUST BEEN BUILT, before anything has run since: a call that
103
+ * settles a microtask later was painted in flight, and marked settled it has hydration choose an
104
+ * arm the server never painted.
105
+ */
106
+ export function markPainted(calls: PageCall[]): void {
107
+ for (const call of calls)
108
+ if (call.painted === undefined) call.painted = paintedSettled(call)
109
+ }
110
+
111
+ // One synchronous build pass, and the calls it built from marked at its end — all but those a
112
+ // child it deferred made, which its own pass marks.
113
+ function pass(state: Build, run: () => void): void {
114
+ const deferredFrom = state.deferred.length
115
+ building = state
116
+ try {
117
+ run()
118
+ } finally {
119
+ building = undefined
120
+ }
121
+ const { calls, deferred } = state
122
+ for (let at = 0; at < calls.length; at++) {
123
+ const call = calls[at] as PageCall
124
+ if (
125
+ call.painted !== undefined ||
126
+ ownedByDeferred(at, deferred, deferredFrom)
127
+ )
128
+ continue
129
+ call.painted = paintedSettled(call)
130
+ }
131
+ }
132
+
133
+ function ownedByDeferred(
134
+ call: number,
135
+ deferred: Deferred[],
136
+ from: number,
137
+ ): boolean {
138
+ for (let at = from; at < deferred.length; at++) {
139
+ const child = deferred[at] as Deferred
140
+ if (call >= child.callsFrom && call < child.callsTo) return true
141
+ }
142
+ return false
143
+ }
144
+
145
+ // 41.26 — a call that streams is never marked.
146
+ function paintedSettled({ reached }: PageCall): boolean {
147
+ return (
148
+ isReactive(reached) &&
149
+ loadOf(reached) === undefined &&
150
+ !isStreaming(reached)
151
+ )
152
+ }
153
+
154
+ /** 35.16 — a document is written by the outermost component alone, and `rendered` is placed. */
155
+ export function refuseDocument(rendered: Rendered): void {
156
+ if (rendered.document !== undefined)
157
+ throw new Error(
158
+ 'abide: a component that writes <html> is placed inside another, and only the outermost component writes the document. RULEBOOK 35.16.',
159
+ )
160
+ }
161
+
162
+ function filled(rendered: Rendered): { head: string; body: string } {
163
+ refuseDocument(rendered)
164
+ return { head: rendered.head, body: rendered.body }
165
+ }
166
+
167
+ function expanded(
168
+ markup: string,
169
+ fills: { head: string; body: string }[],
170
+ ): string {
171
+ return markup.replace(MARKER, (_, kind: string, at: string) => {
172
+ const fill = fills[Number(at)] as { head: string; body: string }
173
+ return expanded(kind === 'h' ? fill.head : fill.body, fills)
174
+ })
175
+ }