@abide/abide 0.102.0 → 0.104.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 (73) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/bin/abide.ts +31 -28
  3. package/docs/REGISTRY.md +40 -36
  4. package/docs/RULEBOOK.md +113 -92
  5. package/docs/abide.md +788 -554
  6. package/package.json +2 -1
  7. package/scaffold/AGENTS.md +13 -6
  8. package/src/compiler/TypeSession.ts +108 -35
  9. package/src/compiler/appMap.ts +91 -0
  10. package/src/compiler/argsSchemaOf.ts +1 -1
  11. package/src/compiler/checkApp.ts +35 -18
  12. package/src/compiler/checkedShadow.ts +40 -7
  13. package/src/compiler/compileApp.ts +271 -76
  14. package/src/compiler/compiledEntry.ts +10 -6
  15. package/src/compiler/componentGraph.ts +6 -7
  16. package/src/compiler/declaredExports.ts +3 -3
  17. package/src/compiler/emitClient.ts +3 -3
  18. package/src/compiler/emitComponent.ts +9 -9
  19. package/src/compiler/emitSource.ts +5 -9
  20. package/src/compiler/handlersUnder.ts +114 -73
  21. package/src/compiler/hooksCalled.ts +35 -0
  22. package/src/compiler/mountsOf.ts +30 -18
  23. package/src/compiler/parseAbide.ts +12 -12
  24. package/src/compiler/projectShadow.ts +71 -46
  25. package/src/compiler/readsIn.ts +2 -2
  26. package/src/compiler/scaffold.ts +24 -21
  27. package/src/compiler/serveLanguage.ts +0 -24
  28. package/src/compiler/sharedModuleWrites.ts +2 -2
  29. package/src/compiler/splitImports.ts +4 -4
  30. package/src/compiler/writeDefaultProject.ts +28 -1
  31. package/src/server/MODULE_QUERIES.ts +12 -3
  32. package/src/server/abideModules.ts +156 -53
  33. package/src/server/answerRequest.ts +9 -4
  34. package/src/server/answerRpc.ts +18 -16
  35. package/src/server/appTree.ts +92 -25
  36. package/src/server/commands.ts +5 -4
  37. package/src/server/componentModules.d.ts +1 -2
  38. package/src/server/createApp.ts +23 -11
  39. package/src/server/devWorker.ts +5 -1
  40. package/src/server/fenceServer.ts +36 -13
  41. package/src/server/generatedSurfaces.ts +12 -53
  42. package/src/server/hooks.ts +17 -4
  43. package/src/server/index.ts +1 -0
  44. package/src/server/installSourceMaps.ts +6 -1
  45. package/src/server/launcherOptions.ts +12 -13
  46. package/src/server/launcherPage.ts +2 -2
  47. package/src/server/nativeMenu.ts +3 -3
  48. package/src/server/preload.ts +13 -0
  49. package/src/server/runDesktop.ts +11 -16
  50. package/src/server/serveDev.ts +8 -2
  51. package/src/server/surfaceOf.ts +2 -0
  52. package/src/server/versionOf.ts +1 -2
  53. package/src/server/withAbideSources.ts +1 -1
  54. package/src/shared/ADDRESSES.ts +3 -0
  55. package/src/shared/LAUNCHER_FILES.ts +9 -0
  56. package/src/shared/RESERVED_DIRECTORIES.ts +19 -4
  57. package/src/shared/index.ts +2 -1
  58. package/src/{ui → shared}/menuBridge.ts +1 -1
  59. package/src/shared/menuItem.ts +2 -2
  60. package/src/shared/onMenu.ts +58 -0
  61. package/src/shared/reactive/Cursor.ts +66 -123
  62. package/src/shared/reactive/Reactive.ts +74 -28
  63. package/src/shared/reactive/callLoad.ts +4 -18
  64. package/src/shared/reactive/chunksOfStream.ts +10 -34
  65. package/src/{ui → shared}/reportMenu.ts +7 -9
  66. package/src/shared/routePattern.ts +74 -23
  67. package/src/shared/rpc.ts +1 -1
  68. package/src/ui/Router.ts +0 -6
  69. package/src/compiler/syncTypes.ts +0 -32
  70. package/src/compiler/writeTypes.ts +0 -82
  71. package/src/server/LAUNCHER_FILES.ts +0 -9
  72. /package/scaffold/src/{page.abide → index.abide} +0 -0
  73. /package/src/{compiler → shared}/SPECIFIERS.ts +0 -0
@@ -19,7 +19,13 @@ import { type Found, runSchema, type Schema } from '../Schema.ts'
19
19
  import { UNEXPECTED } from '../UNEXPECTED.ts'
20
20
  import { WORK } from '../WORK.ts'
21
21
  import { CELL } from './CELL.ts'
22
- import { Cursor, hasCursorBehind, keepPlaces, type Retained } from './Cursor.ts'
22
+ import {
23
+ CAUGHT_UP,
24
+ Cursor,
25
+ hasCursorBehind,
26
+ keepPlaces,
27
+ type Retained,
28
+ } from './Cursor.ts'
23
29
  import { ENTRY_LIVES } from './ENTRY_LIVES.ts'
24
30
  import { armTimer, type Expiry, expiryFor } from './Expiry.ts'
25
31
  import { FOLLOWED } from './FOLLOWED.ts'
@@ -257,7 +263,7 @@ export type Reactive<
257
263
  invalidate(): void
258
264
  // biome-ignore lint/suspicious/noConfusingVoidType: `void` is the RETURN POSITION of an author's effect, and it is load-bearing. `undefined` there refuses `(v) => { read(v) }` — a block body returning nothing infers `void`, which is not assignable to `undefined` — so the rule's fix would reject the ordinary effect.
259
265
  watch(effect: (value: Stored) => void | (() => void)): () => void
260
- /** The production in flight, and then what follows it (2.5). */
266
+ /** What one run produced: the settled value once, or a stream's chunks to its close (2.5, 2.6). */
261
267
  [Symbol.asyncIterator](): AsyncIterator<Produced>
262
268
  then<Result>(
263
269
  onSettled?: (value: Stored) => Result | PromiseLike<Result>,
@@ -459,7 +465,7 @@ type Stream = {
459
465
  * Whether what is served is a run that closed.
460
466
  *
461
467
  * WHETHER WHAT IS SERVED IS A RUN THAT CLOSED, which is the only accumulation a deadline
462
- * reaches (6.11). A run in flight has minted nothing, so `expiry.at` says nothing about it —
468
+ * reaches (6.10). A run in flight has minted nothing, so `expiry.at` says nothing about it —
463
469
  * without this the standing arm reads `at` as 0 and drops the value on its first read.
464
470
  */
465
471
  closed: boolean
@@ -494,7 +500,7 @@ function becomeStream(cell: Cell, source: Cell | null): Stream {
494
500
  }
495
501
 
496
502
  /**
497
- * Starts a closed run's clock: the close is a streaming producer's mint (6.11), as `produce` is
503
+ * Starts a closed run's clock: the close is a streaming producer's mint (6.10), as `produce` is
498
504
  * every other producer's. A failed or superseded run never reaches here, so it starts no clock.
499
505
  */
500
506
  function closeRun(cell: Cell, stream: Stream): void {
@@ -651,8 +657,8 @@ class Cell {
651
657
  */
652
658
  declare schema: Schema<unknown> | null
653
659
  /**
654
- * A streaming producer's chunk buffer, for a cursor to walk (2.5); `EMPTY_RING` on every
655
- * other value, whose cursor answers the standing production from `value`.
660
+ * A streaming producer's chunk buffer, for a cursor to walk (2.6); `EMPTY_RING` on every
661
+ * other value.
656
662
  */
657
663
  declare ring: unknown[]
658
664
  /**
@@ -1519,9 +1525,7 @@ function produce(cell: Cell, produced: unknown): number {
1519
1525
  // `ttl` must not pay.
1520
1526
  const expiry = cell.expiry
1521
1527
  const mintedAt = expiry === null ? 0 : Date.now()
1522
- // 6.10: AN `undefined` IS SERVED AND MOVES NO ORDINAL, so no cursor is handed one — it is not a
1523
- // message, and not a JSON value either.
1524
- if (produced !== undefined) cell.ordinal += 1
1528
+ cell.ordinal += 1
1525
1529
  if (expiry !== null) {
1526
1530
  expiry.at = mintedAt
1527
1531
  // A production is what 6.7 released being replaced, so the value is no longer
@@ -2336,7 +2340,7 @@ function adoptAnswer(cell: Cell, produced: unknown): void {
2336
2340
  *
2337
2341
  * 4.16: AN OPENING `undefined` IS LOADED AND IS NOT A PRODUCTION THROUGH THE PIPELINE. No `schema`
2338
2342
  * judges it, no `transform` shapes it, no window holds it and no `store` writes it — it is the value
2339
- * being empty, and the first real production is what those are for. It mints no ordinal (6.10).
2343
+ * being empty, and the first real production is what those are for. It mints no ordinal.
2340
2344
  */
2341
2345
  function landOpening(cell: Cell): void {
2342
2346
  const moved = settleProbes(cell)
@@ -2495,7 +2499,7 @@ async function pump(
2495
2499
  cell.loading = false
2496
2500
  // 3.7: `busy` held until the close, and this is the close.
2497
2501
  if (moved) probesMoved(cell)
2498
- // 6.11, AFTER THE CASCADE ABOVE: the entry is what a read would answer with, so it is taken
2502
+ // 6.10, AFTER THE CASCADE ABOVE: the entry is what a read would answer with, so it is taken
2499
2503
  // once the last chunk has landed and the value is servable.
2500
2504
  closeRun(cell, stream)
2501
2505
  // 8.9: a stream is written once, at the close, with the accumulation and never a partial.
@@ -2773,7 +2777,7 @@ function beginLoad(cell: Cell): void {
2773
2777
  *
2774
2778
  * AND WHAT IS SERVED WAS MINTED, which every producer but a streaming one answers yes to the moment
2775
2779
  * it is servable. A stream serves the run it is building (9.1), and that run has minted nothing
2776
- * until it closes (6.11) — so a deadline has nothing to measure from and must not drop it.
2780
+ * until it closes (6.10) — so a deadline has nothing to measure from and must not drop it.
2777
2781
  */
2778
2782
  function standingLive(cell: Cell, expiry: Expiry): boolean {
2779
2783
  if (!reloads(cell) || expiry.dropped || !cell.servable) return false
@@ -2936,6 +2940,53 @@ function failureOf(cell: Cell): unknown {
2936
2940
  return at.failure
2937
2941
  }
2938
2942
 
2943
+ /**
2944
+ * What one run of `cell` produced, as `for await` walks it.
2945
+ *
2946
+ * 2.5 AND 2.6 — the settling, yielded once, or a stream's chunks from the first to the close. Which
2947
+ * of the two is asked of the settling, since a value may only turn out to stream once its load
2948
+ * resolves (9.6).
2949
+ *
2950
+ * THE CURSOR IS THE WAKE AND THE SETTLING IS THE END (`Cursor.closeOn`).
2951
+ */
2952
+ async function* producedRun(cell: Cell): AsyncGenerator<unknown> {
2953
+ const first = await settled(cell, true)
2954
+ if (first !== STREAMING) {
2955
+ yield first
2956
+ return
2957
+ }
2958
+ // THE RUN IS ITS RING: a new run's first chunk replaces the array, where a patch mutates it in
2959
+ // place (9.13) and only moves the base. A run with no chunk yet holds `EMPTY_RING`.
2960
+ let ring = cell.ring
2961
+ // 9.15 — a failure already standing is a revalidation's (2.4), and not this run's.
2962
+ const standing = failureOf(cell)
2963
+ const cursor = new Cursor(cell, cell.ringBase)
2964
+ cursor.closeOn(settled(cell))
2965
+ try {
2966
+ for (;;) {
2967
+ if (cell.ring !== ring) {
2968
+ // 9.16.
2969
+ if (ring !== EMPTY_RING) return
2970
+ ring = cell.ring
2971
+ }
2972
+ const chunk = cursor.take()
2973
+ if (chunk !== CAUGHT_UP) {
2974
+ yield chunk
2975
+ continue
2976
+ }
2977
+ if (cursor.closed) break
2978
+ await cursor.turn()
2979
+ }
2980
+ } finally {
2981
+ cursor.stop()
2982
+ }
2983
+ if (cursor.failure !== undefined) throw cursor.failure
2984
+ // 9.15 AND 16.59 — a run that failed after its first chunk, or one its timeout ended, settles
2985
+ // with what it had; the refusal stands beside it.
2986
+ const ended = failureOf(cell)
2987
+ if (ended !== undefined && ended !== standing) throw ended
2988
+ }
2989
+
2939
2990
  /**
2940
2991
  * `busy` without subscribing.
2941
2992
  *
@@ -4034,20 +4085,7 @@ const MEMBERS = {
4034
4085
  return watchCell(this[CELL], effect)
4035
4086
  },
4036
4087
  [Symbol.asyncIterator](this: Callable): AsyncIterator<unknown> {
4037
- const cell = this[CELL]
4038
- // 2.5: "the production in flight, from its start". Starting one ordinal back is
4039
- // what includes it rather than skipping to whatever comes next — and clamped at
4040
- // zero, where a value nothing has produced into must WAIT rather than hand over
4041
- // the `undefined` it is holding.
4042
- // A STREAM'S PRODUCTION IN FLIGHT IS THE STREAM, so its start is the run's first
4043
- // chunk rather than the latest one.
4044
- const from =
4045
- cell.stream !== null
4046
- ? cell.ringBase
4047
- : cell.ordinal > 0
4048
- ? cell.ordinal - 1
4049
- : 0
4050
- return new Cursor(cell, from)
4088
+ return producedRun(this[CELL])
4051
4089
  },
4052
4090
  // biome-ignore lint/suspicious/noThenProperty: RULEBOOK 1.4 — a `Reactive` MUST be thenable, which is what this rule warns about. Banning it buys a discrimination rather than a semantics, and 16.4's in-process `Rpc` arm is what that discrimination would have cost. 1.6 is the guard that makes thenability safe — the brand is read before `then`.
4053
4091
  then(
@@ -4166,6 +4204,15 @@ export function isStreaming(value: unknown): boolean {
4166
4204
  return stream !== null && stream !== undefined
4167
4205
  }
4168
4206
 
4207
+ /**
4208
+ * A cursor over `value` that reads nothing and only wakes when it changes.
4209
+ *
4210
+ * PLACED PAST EVERY CHUNK, so it holds no place a patch has to keep (5.7) and is never behind.
4211
+ */
4212
+ export function wakeOf(value: unknown): Cursor {
4213
+ return new Cursor((value as Callable)[CELL], Number.POSITIVE_INFINITY)
4214
+ }
4215
+
4169
4216
  /**
4170
4217
  * The chunks a streaming value has told its readers of, from index `from` on.
4171
4218
  *
@@ -4347,8 +4394,7 @@ const SIMPLE_PROTOTYPE = brandedPrototype({
4347
4394
  return undefined
4348
4395
  }
4349
4396
  cell.value = value
4350
- // 6.10: an `undefined` is served and moves no ordinal.
4351
- if (value !== undefined) cell.ordinal += 1
4397
+ cell.ordinal += 1
4352
4398
  // `setDirect`, with `plain` known true by `simple`.
4353
4399
  cell.direct = cell.state === CLEAN
4354
4400
  if (cell.readers.length !== 0) markReaders(cell, DIRTY)
@@ -161,33 +161,19 @@ function timeLoad(attended: Attended, milliseconds: number | undefined): void {
161
161
  }, milliseconds)
162
162
  }
163
163
  let settled = false
164
- let wake = (): void => {}
165
164
  const end = (): void => {
166
165
  settled = true
167
166
  clearTimeout(timer)
168
- wake()
169
167
  }
170
- // A CHUNK IS PROGRESS. The live cursor wakes on each, and outlives the run — it waits on the
171
- // next one — so the run settling is a wake of its own, and is what lets the cursor go.
168
+ // A CHUNK IS PROGRESS, and the run's close ends the loop (2.6). What the run failed with is the
169
+ // load's to answer, so the loop only stops on it.
172
170
  const followChunks = async (): Promise<void> => {
173
- const cursor = (reached as AsyncIterable<unknown>)[
174
- Symbol.asyncIterator
175
- ]()
176
171
  try {
177
- while (!settled) {
178
- await new Promise<void>((resolve) => {
179
- wake = resolve
180
- cursor.next().then(
181
- () => resolve(),
182
- () => resolve(),
183
- )
184
- })
172
+ for await (const _ of reached as AsyncIterable<unknown>) {
185
173
  if (settled || loadOf(reached) !== load) return
186
174
  arm()
187
175
  }
188
- } finally {
189
- void cursor.return?.(undefined)
190
- }
176
+ } catch {}
191
177
  }
192
178
  arm()
193
179
  // 9.6 — a value behind rungs only turns out to stream once the onion resolves, so the question
@@ -1,54 +1,30 @@
1
1
  import { isFailed } from '../Failed.ts'
2
- import { chunksFrom } from './Reactive.ts'
2
+ import { chunksFrom, wakeOf } from './Reactive.ts'
3
3
 
4
4
  /**
5
5
  * The chunks of a streaming value, as they land, ending when its run closes.
6
6
  *
7
- * The live cursor is the wake and not the source: a cursor outlives the run (it waits on the next
8
- * one), and a chunk landing in the same turn as the close could be lost to whichever settled first
9
- * — so each wake sends what the accumulation holds past what has already gone, and the close sends
10
- * the rest.
7
+ * WHAT A READ WOULD SERVE, chunk by chunk (16.32), rather than the ring `for await` walks: a gated
8
+ * value's chunks are its gated accumulation's (9.8). A cursor is the wake and the settling is the
9
+ * end (`Cursor.closeOn`).
11
10
  */
12
11
  export async function* chunksOfStream(value: unknown): AsyncGenerator<unknown> {
13
- const stream = value as PromiseLike<unknown> & AsyncIterable<unknown>
14
- let closed = false
15
- let failure: unknown
16
- // ONE REACTION ON THE CLOSE FOR THE WHOLE STREAM, which wakes whichever wait is current. A
17
- // `Promise.race` against the close per chunk left one reaction per chunk on it, held until the
18
- // stream ended.
19
- let wake: () => void = () => {}
20
- void Promise.resolve(stream).then(
21
- () => {
22
- closed = true
23
- wake()
24
- },
25
- (error: unknown) => {
26
- closed = true
27
- failure = error
28
- wake()
29
- },
30
- )
31
- const cursor = stream[Symbol.asyncIterator]()
12
+ const cursor = wakeOf(value)
13
+ cursor.closeOn(Promise.resolve(value as PromiseLike<unknown>))
32
14
  let sent = 0
33
- let woken = cursor.next()
34
15
  try {
35
16
  for (;;) {
36
- if (!closed)
37
- await new Promise<void>((resolve, reject) => {
38
- wake = resolve
39
- woken.then(() => resolve(), reject)
40
- })
41
17
  for (const chunk of chunksFrom(value, sent)) {
42
18
  sent += 1
43
19
  yield chunk
44
20
  }
45
- if (closed) break
46
- woken = cursor.next()
21
+ if (cursor.closed) break
22
+ await cursor.turn()
47
23
  }
48
24
  } finally {
49
- void cursor.return?.(undefined)
25
+ cursor.stop()
50
26
  }
51
- if (failure !== undefined) throw failure
27
+ if (cursor.failure !== undefined) throw cursor.failure
52
28
  // 16.59, 16.60 — A RUN ENDED BY ITS TIMEOUT SETTLES with the chunks it had and holds its refusal
53
29
  // rather than rejecting, so the refusal is read off the value and thrown after them: a stream
54
30
  // answered over http then ends on it (16.68) rather than as though it had finished.
@@ -1,9 +1,9 @@
1
- // 38.88 — A ROUTE'S MENU, TOLD TO THE WINDOW IT IS SHOWN IN. Loaded only inside a desktop app, where
2
- // the window has opened `abideMenu` to the page; anywhere else the router never imports it.
1
+ // 38.88 — A ROUTE'S MENUS, TOLD TO THE WINDOW IT IS SHOWN IN. Loaded only inside a desktop app, where
2
+ // the window has opened `abideMenu` to the page; anywhere else `onMenu` never imports it.
3
3
 
4
- import { MenuEntry, type MenuItem } from '#shared/menuItem.ts'
5
- import { navigate } from '#shared/navigate.ts'
6
4
  import { menuBridge } from './menuBridge.ts'
5
+ import { MenuEntry, type MenuItem } from './menuItem.ts'
6
+ import { navigate } from './navigate.ts'
7
7
 
8
8
  /** A menu item as the window builds it: an item the page answers carries the `id` it is told by. */
9
9
  type Told =
@@ -20,15 +20,13 @@ type Told =
20
20
  let numbered: Exclude<MenuItem, 'separator'>[] = []
21
21
  let listening = false
22
22
 
23
- /** Tells the window the menus `chain` exports, layouts outermost first and the page last. */
24
- export function reportMenu(chain: { menu?: unknown }[]): void {
23
+ /** Tells the window `menus`, the layouts' outermost first and the page's last. */
24
+ export function reportMenu(menus: readonly MenuItem[]): void {
25
25
  const bridge = menuBridge()
26
26
  if (bridge === undefined) return
27
27
  numbered = []
28
28
  const told: Told[] = []
29
- for (const level of chain)
30
- if (Array.isArray(level.menu))
31
- for (const menu of level.menu as MenuItem[]) told.push(tell(menu))
29
+ for (const menu of menus) told.push(tell(menu))
32
30
  if (!listening) {
33
31
  listening = true
34
32
  // 38.90, 38.91 — the window names the item picked; the page does what it means.
@@ -1,5 +1,5 @@
1
1
  // A ROUTE LITERAL, PARSED. One grammar for two things that would otherwise drift: the file a
2
- // page is filed as (`invoices/[id]/page.abide`) and the literal an author writes
2
+ // page is filed as (`invoices/[id].abide`) and the literal an author writes
3
3
  // (`'/invoices/[id]'`). The bracket spelling is the file system's, so the address is read off the
4
4
  // tree rather than declared beside it and the two cannot disagree.
5
5
  //
@@ -10,7 +10,12 @@
10
10
  // A LEAF: nothing imported but another leaf, because the compiler, the client router and a server
11
11
  // router all read it and a constant crossing a seam lives where nothing pulls a graph in behind it.
12
12
 
13
- import { PUBLIC_DIRECTORY, SERVER_DIRECTORY } from './RESERVED_DIRECTORIES.ts'
13
+ import { LAUNCHER_PAGE } from './LAUNCHER_FILES.ts'
14
+ import {
15
+ HOLDS_SERVER,
16
+ RESERVED_DIRECTORIES,
17
+ SERVER_DIRECTORY,
18
+ } from './RESERVED_DIRECTORIES.ts'
14
19
 
15
20
  type SegmentKind = 'static' | 'required' | 'optional' | 'rest'
16
21
 
@@ -48,9 +53,9 @@ export type FileKind = 'page' | 'layout' | 'error' | 'component'
48
53
  /**
49
54
  * Whether the `.abide` at `path` is a page, a layout, an error page or a component.
50
55
  *
51
- * AN ADDRESS RATHER THAN A NAME. The file name says which of the three it is, and only a file
52
- * whose place is an `address` (`placeOf`) is one: a `page.abide` under a `server` directory is a
53
- * component.
56
+ * 23.21 — WHERE IT SITS, THEN ITS NAME. Every `.abide` directly in an address directory is a page
57
+ * but a `layout.abide`, an `error.abide` and the root's `launcher.abide`; one under a fence is a
58
+ * component whatever it is called.
54
59
  * One test for every reader: the build's routes, the checker's `Routes` and the `route.params` a
55
60
  * page is typed by.
56
61
  *
@@ -58,40 +63,56 @@ export type FileKind = 'page' | 'layout' | 'error' | 'component'
58
63
  * read the machine's directories as the app's.
59
64
  */
60
65
  export function fileKind(path: string): FileKind {
61
- if (placeOf(path) !== 'address') return 'component'
62
- // The WHOLE basename: `homepage.abide` ends with `page.abide` and is a component.
66
+ if (placeOf(path) !== 'address' || path === LAUNCHER_PAGE)
67
+ return 'component'
63
68
  const name = path.slice(path.lastIndexOf('/') + 1)
64
- if (name === 'page.abide') return 'page'
65
69
  if (name === 'layout.abide') return 'layout'
66
- return name === 'error.abide' ? 'error' : 'component'
70
+ if (name === 'error.abide') return 'error'
71
+ return 'page'
67
72
  }
68
73
 
69
74
  /** Where a file sits in an app, which is what it is to every reader (`placeOf`). */
70
- export type Place = 'address' | 'server' | 'public' | 'dependency' | 'skipped'
75
+ export type Place =
76
+ | 'address'
77
+ | 'server'
78
+ | 'public'
79
+ | 'lib'
80
+ | 'tests'
81
+ | 'dependency'
82
+ | 'skipped'
71
83
 
72
84
  /**
73
85
  * Where the file at `path`, relative to the app's root, sits — THE ONE ANSWER every walk, fence and
74
86
  * emit reads, so no two of them disagree about a path.
75
87
  *
76
- * - `dependency` — 34.28: a package's, outside the root or under `node_modules`, whose directories
88
+ * - `dependency` — 34.24: a package's, outside the root or under `node_modules`, whose directories
77
89
  * are its package's layout and none of the app's;
78
90
  * - `skipped` — under a dot directory, which no walk of the app enters;
79
- * - `server` or `public` — 23.15: under that reserved directory, the nearest to the root deciding
80
- * (23.16 refuses one inside the other);
91
+ * - `server`, `public`, `lib` or `tests` — 23.15: under that fence, the nearest to the root deciding,
92
+ * save a `server` under a `lib` or a `tests`, which is `server` — the fence the browser's bundle
93
+ * is checked against (16.29) is never hidden by one above it (23.16);
81
94
  * - `address` — 23.14: everywhere else, its directories the segments of its route.
82
95
  */
83
96
  export function placeOf(path: string): Place {
84
97
  if (path.startsWith('../') || path.startsWith('/')) return 'dependency'
85
98
  const parts = path.split('/')
99
+ // A `lib` or a `tests` met first, which a `server` below it still overrides.
100
+ let fence: Place | undefined
86
101
  // The last part is the file itself, which is never a directory.
87
102
  for (let index = 0; index < parts.length - 1; index += 1) {
88
103
  const part = parts[index] as string
89
- if (part === 'node_modules') return 'dependency'
90
- if (part.startsWith('.')) return 'skipped'
91
- if (part === SERVER_DIRECTORY) return 'server'
92
- if (part === PUBLIC_DIRECTORY) return 'public'
104
+ if (skipped(part))
105
+ return (
106
+ fence ?? (part === 'node_modules' ? 'dependency' : 'skipped')
107
+ )
108
+ if (fence !== undefined) {
109
+ if (part === SERVER_DIRECTORY) return 'server'
110
+ } else if (RESERVED_DIRECTORIES.has(part)) {
111
+ if (!HOLDS_SERVER.has(part)) return part as Place
112
+ fence = part as Place
113
+ }
93
114
  }
94
- return 'address'
115
+ return fence ?? 'address'
95
116
  }
96
117
 
97
118
  /** Whether a walk of the app passes over the entry `name`: a dot file or directory, or `node_modules`. */
@@ -100,18 +121,48 @@ export function skipped(name: string): boolean {
100
121
  }
101
122
 
102
123
  /**
103
- * The route a file answers at — its DIRECTORY's below the root, whatever its name.
124
+ * The route a file answers at — its path below the root without its extension.
104
125
  *
105
- * 23.14 — `media/[id]/page.abide` is `/media/[id]`, and so is every other file beside it, which is
106
- * what lets a directory's handlers sit in as many files as an author likes (16.2). `path` is
107
- * relative to the app's root, as `fileKind`'s is.
126
+ * 23.14 — `media/[id].abide` and `media/getGrid.ts` are `/media/[id]` and `/media/getGrid`; an
127
+ * `index.abide`, a `layout.abide` and an `error.abide` are their directory's. `path` is relative to
128
+ * the app's root, as `fileKind`'s is.
108
129
  */
109
130
  export function routeOf(path: string): string {
110
131
  const parts = path.split('/')
111
- parts.pop()
132
+ const file = parts.pop() as string
133
+ // The extension alone: `[...rest].abide` carries dots of its own.
134
+ if (!DIRECTORY_FILES.has(file)) parts.push(file.replace(EXTENSION, ''))
112
135
  return `/${parts.filter((part) => part !== '').join('/')}`
113
136
  }
114
137
 
138
+ const EXTENSION = /\.(?:abide|ts)$/
139
+
140
+ /** 23.14, 23.18 — the names whose route is their directory's, and so a route's wherever they stand. */
141
+ export const DIRECTORY_FILES = new Set([
142
+ 'index.abide',
143
+ 'layout.abide',
144
+ 'error.abide',
145
+ ])
146
+
147
+ /**
148
+ * The route the hooks and private handlers of the file at `path` are filed at.
149
+ *
150
+ * 37.1 — an address's own route; under a `lib` or a `tests` directory, the route of the directory
151
+ * holding it, so a component an admin page renders runs under the admin route's rungs; under a
152
+ * `server` or a `public` directory, and for a dependency's file, the root's.
153
+ */
154
+ export function scopeOf(path: string): string {
155
+ // 38.82 — the launcher answers at no route, and files what it calls at the root's.
156
+ if (path === LAUNCHER_PAGE) return '/'
157
+ const place = placeOf(path)
158
+ if (place === 'address') return routeOf(path)
159
+ if (place !== 'lib' && place !== 'tests') return '/'
160
+ const parts = path.split('/')
161
+ // The fence `placeOf` named is the first one, and its name is its place.
162
+ const fence = parts.indexOf(place)
163
+ return `/${parts.slice(0, fence).join('/')}`
164
+ }
165
+
115
166
  /**
116
167
  * A `sort` comparator over two parsed patterns: negative where `a` is the more specific.
117
168
  *
package/src/shared/rpc.ts CHANGED
@@ -501,7 +501,7 @@ export const DELETE: Declare = declare('DELETE')
501
501
  function addressOf(rpc: { address: string | undefined }): string {
502
502
  if (rpc.address === undefined)
503
503
  throw new Error(
504
- 'abide: this handler has no address yet. A handler is given one where it is mounted, exported from a <script server> (16.2).',
504
+ "abide: this handler has no address yet. A handler is given one where it is mounted: a handler file's default (16.2), or a `const` in a <script server> (16.70).",
505
505
  )
506
506
  return rpc.address
507
507
  }
package/src/ui/Router.ts CHANGED
@@ -12,7 +12,6 @@ import { applyHead, withdrawHead } from './applyHead.ts'
12
12
  import { LAYER_MOUNT, LAYER_NAVIGATE } from './LAYER_NAMES.ts'
13
13
  import { MOUNTING_LEVEL } from './MOUNTING_LEVEL.ts'
14
14
  import { beginLayer, endLayer } from './measureLayer.ts'
15
- import { menuBridge } from './menuBridge.ts'
16
15
  import { fillRegion, pageRegion, type Region } from './regions.ts'
17
16
  import type { Scope, Stoppable } from './Scope.ts'
18
17
 
@@ -568,11 +567,6 @@ export class Router {
568
567
  ): void {
569
568
  arrive(target.href, found.route.pattern, found.params)
570
569
  this.chain = modules
571
- // 38.88 — inside a desktop app, the window shows the menus this chain exports.
572
- if (menuBridge() !== undefined)
573
- void import('./reportMenu.ts').then((loaded) =>
574
- loaded.reportMenu(modules as { menu?: unknown }[]),
575
- )
576
570
  this.standing = found.route
577
571
  this.mounted = target.pathname
578
572
  if (into === undefined) return
@@ -1,32 +0,0 @@
1
- // 34.24 — THE TYPES A PLAIN `.ts` READS A `.abide` BY, written with no checker and no build: the
2
- // files parsed and projected, and nothing else. What `abide sync` runs — and the scaffold's `prepare`
3
- // with it, so a project is typed as soon as it is installed — and what the language server runs as
4
- // a file is edited.
5
-
6
- import { sourcesUnder } from './compileApp.ts'
7
- import { parseSources } from './readSources.ts'
8
- import type { Diagnosis } from './TypeSession.ts'
9
- import { writeTypes } from './writeTypes.ts'
10
-
11
- /**
12
- * Writes the types of every `.abide` under `appRoot`, `held` standing in for the text on disk of
13
- * the files an editor has open; answers how many files it typed and what it refused. A file that
14
- * does not parse, or whose projection is refused, is left as it last was and the rest are typed.
15
- */
16
- export async function syncTypes(
17
- appRoot: string,
18
- held: ReadonlyMap<string, string> = new Map(),
19
- ): Promise<{ typed: number; refused: Diagnosis[] }> {
20
- const { root, sources } = await sourcesUnder(appRoot)
21
- const read: { path: string; text: string }[] = []
22
- for (const source of sources)
23
- read.push({
24
- path: source.path,
25
- text: held.get(source.path) ?? source.text,
26
- })
27
- const refused: Diagnosis[] = []
28
- const parsed = parseSources(read, refused)
29
- const unparsed = refused.length
30
- await writeTypes(root, parsed, true, refused)
31
- return { typed: parsed.length - (refused.length - unparsed), refused }
32
- }
@@ -1,82 +0,0 @@
1
- // 34.24 — WHAT A `.abide` EXPORTS, AS A PLAIN `.ts` IMPORTING IT IS TYPED. TypeScript reads no
2
- // `.abide`, and a wildcard declaration can name a default alone, so the names a `<script module>` or
3
- // a `<script server>` exports were untyped to every `.ts` outside abide's own checker. Each file's
4
- // projection — its module and server blocks, exactly what that checker answers a `.abide` import
5
- // with (`TypeSession.standIn`) — is written to `.abide/types`, mirroring the tree, where the app's
6
- // `tsconfig` reaches it: `rootDirs` for a relative import, `paths` for `#src/*`.
7
- //
8
- // THE CHECKER IS NOT ASKED, because the API it answers over emits no declarations, and the
9
- // projection is a module TypeScript infers the same types from. `@ts-nocheck` because the blocks are
10
- // read as written — a reactive read by name is the checker's to answer (31.3) and `abide check`'s to
11
- // report, and reported here too it would be one finding twice, at a path nobody wrote.
12
-
13
- import { rmSync } from 'node:fs' // bun has no file removal of its own
14
- import { dirname, relative } from 'node:path' // bun exposes no path helpers of its own
15
- import { buildDirectory } from '#server/buildDirectory.ts'
16
- import { namesUnder } from '#server/namesUnder.ts'
17
- import { COMPONENT_MODULE } from './checkedShadow.ts'
18
- import { formatDiagnosis } from './formatDiagnosis.ts'
19
- import { projectShadow } from './projectShadow.ts'
20
- import { type ParsedSource, refusal } from './readSources.ts'
21
- import type { Diagnosis } from './TypeSession.ts'
22
-
23
- /**
24
- * Writes each source's types beside the build, and removes the ones whose `.abide` is gone.
25
- *
26
- * A file is written only where its text moved, so an editor watching the directory re-reads what
27
- * changed rather than everything. WITH `refused`, A SOURCE WHOSE PROJECTION IS REFUSED (34.3, …) is a
28
- * finding at its first line and keeps the types it last had, as `parseSources` treats one that does
29
- * not parse; without it the refusal is thrown, naming the file.
30
- */
31
- export async function writeTypes(
32
- root: string,
33
- sources: readonly Pick<ParsedSource, 'path' | 'file'>[],
34
- /** Whether `sources` is every file, so the types of one not among them are stale. */
35
- whole = true,
36
- refused?: Diagnosis[],
37
- ): Promise<void> {
38
- const above = dirname(root)
39
- // Mirrored below the directory above `root`, as the scaffold's `tsconfig` names it.
40
- const types = `${buildDirectory(root)}/types`
41
- const written = new Set<string>()
42
- const writes: Promise<unknown>[] = []
43
- for (const source of sources) {
44
- const path = `${types}/${relative(above, source.path)}.ts`
45
- written.add(path)
46
- let projected: string
47
- try {
48
- projected = projectShadow(source.file).text
49
- } catch (thrown) {
50
- const found = refusal(source.path, thrown)
51
- if (refused === undefined)
52
- throw new Error(formatDiagnosis(found, process.cwd()))
53
- refused.push(found)
54
- continue
55
- }
56
- // The checker's own stand-in for the default: a component taking anything (`COMPONENT_MODULE`).
57
- const text = `// @ts-nocheck\n${projected}\n${COMPONENT_MODULE}`
58
- if (WRITTEN.get(path) === text) continue
59
- writes.push(
60
- Bun.file(path)
61
- .text()
62
- .catch(() => undefined)
63
- .then((standing) => {
64
- WRITTEN.set(path, text)
65
- return standing === text ? undefined : Bun.write(path, text)
66
- }),
67
- )
68
- }
69
- await Promise.all(writes)
70
- if (!whole) return
71
- // Only below this root's own mirror: two apps beside one another share the directory above.
72
- const mirror = `${types}/${relative(above, root)}`
73
- for (const name of namesUnder(mirror))
74
- if (!written.has(`${mirror}/${name}`)) {
75
- rmSync(`${mirror}/${name}`)
76
- WRITTEN.delete(`${mirror}/${name}`)
77
- }
78
- }
79
-
80
- // What this process last left at each path, so a compile run again (`abide dev`, the language
81
- // server) reads back only the files whose text moved since.
82
- const WRITTEN = new Map<string, string>()
@@ -1,9 +0,0 @@
1
- // THE DESKTOP LAUNCHER'S FILES. Its page and the `launcher` options it exports sit at the app's root
2
- // beside the root layout, a name the framework finds rather than an address (23.14); the icon is a
3
- // public file, so it is wherever the address it answers at says (36.11).
4
-
5
- /** The launcher's page and options. 38.80, 38.82. */
6
- export const LAUNCHER_PAGE = 'launcher.abide'
7
-
8
- /** The addresses of the desktop app's icon, the `.icns` first. 38.79. */
9
- export const ICON_ADDRESSES = { icns: '/icon.icns', png: '/icon.png' } as const
File without changes
File without changes