@abide/abide 0.102.0 → 0.103.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 (67) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/bin/abide.ts +31 -28
  3. package/docs/REGISTRY.md +30 -26
  4. package/docs/RULEBOOK.md +98 -83
  5. package/docs/abide.md +771 -522
  6. package/package.json +2 -1
  7. package/scaffold/AGENTS.md +13 -6
  8. package/src/compiler/TypeSession.ts +110 -35
  9. package/src/compiler/appMap.ts +96 -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 +228 -70
  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 +70 -45
  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 +117 -51
  33. package/src/server/answerRequest.ts +9 -4
  34. package/src/server/answerRpc.ts +18 -16
  35. package/src/server/appTree.ts +83 -24
  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/hooks.ts +17 -4
  42. package/src/server/index.ts +1 -0
  43. package/src/server/installSourceMaps.ts +6 -1
  44. package/src/server/launcherOptions.ts +12 -13
  45. package/src/server/launcherPage.ts +2 -2
  46. package/src/server/nativeMenu.ts +3 -3
  47. package/src/server/preload.ts +13 -0
  48. package/src/server/runDesktop.ts +11 -16
  49. package/src/server/serveDev.ts +8 -2
  50. package/src/server/surfaceOf.ts +2 -0
  51. package/src/server/withAbideSources.ts +1 -1
  52. package/src/shared/ADDRESSES.ts +3 -0
  53. package/src/shared/LAUNCHER_FILES.ts +9 -0
  54. package/src/shared/RESERVED_DIRECTORIES.ts +13 -4
  55. package/src/shared/index.ts +2 -1
  56. package/src/{ui → shared}/menuBridge.ts +1 -1
  57. package/src/shared/menuItem.ts +2 -2
  58. package/src/shared/onMenu.ts +58 -0
  59. package/src/{ui → shared}/reportMenu.ts +7 -9
  60. package/src/shared/routePattern.ts +58 -20
  61. package/src/shared/rpc.ts +1 -1
  62. package/src/ui/Router.ts +0 -6
  63. package/src/compiler/syncTypes.ts +0 -32
  64. package/src/compiler/writeTypes.ts +0 -82
  65. package/src/server/LAUNCHER_FILES.ts +0 -9
  66. /package/scaffold/src/{page.abide → index.abide} +0 -0
  67. /package/src/{compiler → shared}/SPECIFIERS.ts +0 -0
@@ -12,6 +12,7 @@ import { COMPONENT_QUERY } from '#server/MODULE_QUERIES.ts'
12
12
  import { ROUTES_TABLE, SERVER_TABLE } from '#server/TABLES.ts'
13
13
  import { VIRTUAL_DIRECTORY } from '#server/VIRTUAL_DIRECTORY.ts'
14
14
  import { CLIENT_ADDRESS } from '#shared/ADDRESSES.ts'
15
+ import { LAUNCHER_PAGE } from '#shared/LAUNCHER_FILES.ts'
15
16
  import {
16
17
  coversRoute,
17
18
  parseRoutePattern,
@@ -19,10 +20,21 @@ import {
19
20
  routeOf,
20
21
  type Segment,
21
22
  } from '#shared/routePattern.ts'
23
+ import {
24
+ ROUTING_SPECIFIER,
25
+ SHARED_SPECIFIER,
26
+ WIRE_SPECIFIER,
27
+ } from '#shared/SPECIFIERS.ts'
22
28
  import { abideImportsOf, appGraph } from './componentGraph.ts'
23
29
  import { emitSource } from './emitSource.ts'
24
30
  import { formatDiagnosis } from './formatDiagnosis.ts'
25
- import { handlersUnder } from './handlersUnder.ts'
31
+ import {
32
+ type HandlerFile,
33
+ handlersUnder,
34
+ type Refuse,
35
+ THROW,
36
+ } from './handlersUnder.ts'
37
+ import { hooksCalled } from './hooksCalled.ts'
26
38
  import { mountsOf } from './mountsOf.ts'
27
39
  import { nearest } from './nearest.ts'
28
40
  import type { AbideFile } from './parseAbide.ts'
@@ -33,15 +45,9 @@ import {
33
45
  readSources,
34
46
  refusal,
35
47
  } from './readSources.ts'
36
- import {
37
- ROUTING_SPECIFIER,
38
- SHARED_SPECIFIER,
39
- WIRE_SPECIFIER,
40
- } from './SPECIFIERS.ts'
41
48
  import { respecified } from './splitImports.ts'
42
49
  import { type Diagnosis, TypeSession } from './TypeSession.ts'
43
50
  import { writeDefaultProject } from './writeDefaultProject.ts'
44
- import { writeTypes } from './writeTypes.ts'
45
51
 
46
52
  // AN APP'S `.abide` SOURCES, COMPILED — every file keyed by its path below the root, and loaded
47
53
  // there through its `?component` face (`abideModules`). Nothing here touches the output directory:
@@ -222,18 +228,35 @@ async function sourcesAt(
222
228
  /**
223
229
  * `parsed` and every `.abide` outside them that one of them imports — a dependency's, or a linked
224
230
  * package's — parsed, and every one those import in turn; and every `.abide` under a directory one
225
- * of them mounts, at the position the mount gives it (23.19).
231
+ * of them mounts, at the position the mount gives it (23.19). With them, the app's own handler files
232
+ * — `own`, below `root`, as its walk found them (`appFiles`) — and every one a mounted directory
233
+ * holds, each at the position it answers by.
226
234
  *
227
- * 34.27 — COMPILED WITH THE APP, by the app's checker, so a component a package ships as source is
235
+ * 34.23 — COMPILED WITH THE APP, by the app's checker, so a component a package ships as source is
228
236
  * one of the app's in every way but its address: it is never routed (`placeOf`) unless mounted.
229
237
  */
230
238
  export async function withDependencies(
231
239
  root: string,
232
240
  parsed: readonly ParsedSource[],
241
+ own: readonly string[],
233
242
  resolve: (specifier: string, from: string) => string | undefined,
234
243
  refused?: Diagnosis[],
235
- ): Promise<ParsedSource[]> {
244
+ ): Promise<{
245
+ sources: ParsedSource[]
246
+ handlers: HandlerFile[]
247
+ mounted: Compilation['mounted']
248
+ }> {
236
249
  const all = [...parsed]
250
+ const handlers: HandlerFile[] = []
251
+ for (const relative of own)
252
+ handlers.push({
253
+ path: `${root}/${relative}`,
254
+ relative,
255
+ position: relative,
256
+ })
257
+ // The mounted handler files placed so far; the app's own is never mounted (23.20).
258
+ const handled = new Set<string>()
259
+ const mounted: Compilation['mounted'] = {}
237
260
  const byPath = new Map<string, ParsedSource>()
238
261
  for (const source of all) byPath.set(source.path, source)
239
262
  // 23.20 — a directory mounts once, and by whom.
@@ -270,6 +293,17 @@ export async function withDependencies(
270
293
  continue
271
294
  }
272
295
  mountedBy.set(mount.directory, position)
296
+ mounted[relativeTo(root, mount.directory)] = mount.at
297
+ for (const placed of mount.handlers) {
298
+ // Two mounts can overlap, one directory inside the other; a file is placed once.
299
+ if (handled.has(placed.path)) continue
300
+ handled.add(placed.path)
301
+ handlers.push({
302
+ path: placed.path,
303
+ relative: relativeTo(root, placed.path),
304
+ position: placed.position,
305
+ })
306
+ }
273
307
  for (const { path, position: placed } of mount.files) {
274
308
  // A file a component import reached first takes its mount's position all the same.
275
309
  const known = byPath.get(path)
@@ -288,7 +322,20 @@ export async function withDependencies(
288
322
  if (!byPath.has(target)) await add({ path: target })
289
323
  }
290
324
  }
291
- return all
325
+ return { sources: all, handlers, mounted }
326
+ }
327
+
328
+ /**
329
+ * The `Compilation` alone out of `app`, which is what is written to disk and served: the rest of a
330
+ * compiled app is the commands' to read.
331
+ */
332
+ export function compilationOf(app: CompiledApp): Compilation {
333
+ return {
334
+ files: app.files,
335
+ handlers: app.handlers,
336
+ tables: app.tables,
337
+ mounted: app.mounted,
338
+ }
292
339
  }
293
340
 
294
341
  /**
@@ -301,14 +348,17 @@ export async function compileApp(
301
348
  kept?: AppChecker,
302
349
  ): Promise<CompiledApp> {
303
350
  const root = realpathSync(appRoot)
304
- // ONE WALK for the sources and the public files, refusing a tree 23.16-23.18 forbids before
305
- // anything is read.
306
- const { abide, published } = appFiles(root)
351
+ // ONE WALK for the sources, the handler files and the public files, refusing a tree 23.16-23.18
352
+ // forbids before anything is read.
353
+ const { abide, handlers, published } = appFiles(root)
307
354
  const sources = await sourcesAt(root, abide)
308
- if (sources.length === 0)
355
+ // 16.2 — an app of handler files alone is still an app.
356
+ if (sources.length === 0 && handlers.length === 0)
309
357
  return {
310
358
  files: {},
311
359
  tables: {},
360
+ handlers: {},
361
+ mounted: {},
312
362
  routes: [],
313
363
  mounts: [],
314
364
  sources: 0,
@@ -321,9 +371,12 @@ export async function compileApp(
321
371
  return withChecker(root, kept, async (session) => {
322
372
  // A BUILD STOPS AT THE FIRST REFUSAL, so neither step is handed a list to report it in.
323
373
  const own = parseSources(sources)
324
- await writeTypes(root, own)
325
374
  const resolve = resolverIn()
326
- const parsed = await withDependencies(root, own, resolve)
375
+ const {
376
+ sources: parsed,
377
+ handlers: handlerFiles,
378
+ mounted,
379
+ } = await withDependencies(root, own, handlers, resolve)
327
380
  const { read, warnings } = await readSources(session, parsed)
328
381
  const { ambient, unkeyed } = await session.lints(root, parsed)
329
382
  // 11.37 — a build error, every read named, rather than a first one and a second run.
@@ -333,9 +386,16 @@ export async function compileApp(
333
386
  lines.push(formatDiagnosis(read, process.cwd()))
334
387
  throw new Error(lines.join('\n'))
335
388
  }
336
- const emitted = await emitApp(root, session, read, resolve)
389
+ const emitted = await emitApp(
390
+ root,
391
+ session,
392
+ read,
393
+ resolve,
394
+ handlerFiles,
395
+ )
337
396
  return {
338
397
  ...emitted,
398
+ mounted,
339
399
  published,
340
400
  sources: sources.length,
341
401
  // 11.30 — warned on, and the build goes on.
@@ -356,8 +416,15 @@ export async function emitApp(
356
416
  sources: readonly ReadSource[],
357
417
  /** The resolver the sources' dependencies were found with, whose answers it keeps. */
358
418
  resolve: (specifier: string, from: string) => string | undefined,
419
+ /** The handler files, the app's own and each mounted one (`withDependencies`). */
420
+ handlerFiles: readonly HandlerFile[],
421
+ /** Told of each refusal at its file; the build's throws the first. */
422
+ refuse: Refuse = THROW,
359
423
  ): Promise<
360
- Pick<CompiledApp, 'files' | 'tables' | 'routes' | 'mounts' | 'warnings'>
424
+ Pick<
425
+ CompiledApp,
426
+ 'files' | 'tables' | 'handlers' | 'routes' | 'mounts' | 'warnings'
427
+ >
361
428
  > {
362
429
  const files: Record<string, CompiledFile> = {}
363
430
  const tables: Record<string, string> = {}
@@ -380,10 +447,15 @@ export async function emitApp(
380
447
  const route = routeOf(entry.position)
381
448
  const held = entry.kind === 'layout' ? layoutAt : errorAt
382
449
  const other = held.get(shapeOf(route))
383
- if (other !== undefined)
384
- throw new Error(
385
- `abide: ${other} and ${entry.relative} are both the ${entry.kind}.abide at ${route}. RULEBOOK 35.12.`,
450
+ if (other !== undefined) {
451
+ refuse(
452
+ entry.relative,
453
+ new Error(
454
+ `abide: ${other} and ${entry.relative} are both the ${entry.kind}.abide at ${route}. RULEBOOK 35.12.`,
455
+ ),
386
456
  )
457
+ continue
458
+ }
387
459
  held.set(shapeOf(route), entry.relative)
388
460
  if (entry.kind === 'layout' && entry.file.template.length > 0)
389
461
  layouts.push({
@@ -395,8 +467,8 @@ export async function emitApp(
395
467
  const errors: AppRoute[] = []
396
468
  for (const entry of parsed) {
397
469
  if (entry.kind !== 'page' && entry.kind !== 'error') continue
398
- // 23.3, 35.14 — a `page.abide` or an `error.abide` with nothing to paint is its route's
399
- // handlers and hooks alone.
470
+ // 23.3, 35.14 — a page or an `error.abide` with nothing to paint is its route's hooks
471
+ // alone.
400
472
  if (!paints(entry.file)) continue
401
473
  ;(entry.kind === 'page' ? routes : errors).push({
402
474
  pattern: entry.route as string,
@@ -406,14 +478,22 @@ export async function emitApp(
406
478
  }
407
479
 
408
480
  // 16.2 AND 16.28 — THE HANDLERS, read before a module is emitted: the server mounts each one,
409
- // and the browser's module of the file declaring it carries its wrapper in the server block's
410
- // place (34.23). Written whether or not there is a page: a handler is answered over http to a
411
- // caller that has none.
412
- const { mounts, warnings } = await handlersUnder(session, parsed)
413
- refuseSharedRoutes(routes, mounts)
481
+ // and the browser holds its wrapper in its place — a handler file's as that file, a private
482
+ // handler's in the server block's place in its own file's module. Written whether or not there
483
+ // is a page: a handler is answered over http to a caller that has none.
484
+ const { mounts, warnings } = await handlersUnder(
485
+ session,
486
+ parsed,
487
+ handlerFiles,
488
+ refuse,
489
+ )
490
+ refuseSharedRoutes(routes, mounts, refuse)
491
+ refuseStrayLauncher(parsed, handlerFiles, refuse)
414
492
  const standIns = standInsOf(mounts)
415
- // 37.1 — every module a server block runs in, for the boot to import before `onStart`.
493
+ // 37.1 — every module a server block or a handler file runs in, for the boot to import before
494
+ // `onStart`.
416
495
  const servers: string[] = []
496
+ for (const file of handlerFiles) servers.push(file.relative)
417
497
  let transitions = false
418
498
  for (const entry of parsed) {
419
499
  // 35.16 — of the files with an address, the root layout alone is outermost in every render.
@@ -421,10 +501,15 @@ export async function emitApp(
421
501
  entry.file.document !== undefined &&
422
502
  entry.kind !== 'component' &&
423
503
  !(entry.kind === 'layout' && routeOf(entry.position) === '/')
424
- )
425
- throw new Error(
426
- `abide: ${entry.relative} writes <html>, and of the pages, layouts and error pages only the root layout.abide writes the document. RULEBOOK 35.16.`,
504
+ ) {
505
+ refuse(
506
+ entry.relative,
507
+ new Error(
508
+ `abide: ${entry.relative} writes <html>, and of the pages, layouts and error pages only the root layout.abide writes the document. RULEBOOK 35.16.`,
509
+ ),
427
510
  )
511
+ continue
512
+ }
428
513
  const emitted = emitSource(
429
514
  entry.relative,
430
515
  entry.file,
@@ -436,7 +521,7 @@ export async function emitApp(
436
521
  )
437
522
  if (entry.file.servers.length > 0) servers.push(entry.relative)
438
523
  transitions ||= emitted.transitions
439
- // 34.30 — a dependency's module runs on the app's runtime, whatever the package carries.
524
+ // 34.26 — a dependency's module runs on the app's runtime, whatever the package carries.
440
525
  const onAppRuntime =
441
526
  placeOf(entry.relative) === 'dependency'
442
527
  ? (text: string) => onAbideOf(root, entry.path, text)
@@ -451,7 +536,7 @@ export async function emitApp(
451
536
  }
452
537
  }
453
538
  // THE ROUTE TABLE, as the build's own knowledge rather than a list an app maintains. Which
454
- // addresses exist is a fact about the file tree (a `page.abide` is an address, 23.3),
539
+ // addresses exist is a fact about the file tree (a page is an address, 23.3),
455
540
  // so a route that is not a file cannot be written and a file that is one cannot be
456
541
  // forgotten. The loaders are dynamic imports, which is what keeps a destination's
457
542
  // markup and bindings off the entry page's bundle until somebody navigates there.
@@ -462,6 +547,15 @@ export async function emitApp(
462
547
  // page too — an app of handlers alone still answers — and the client's, with the entry
463
548
  // that boots it, only where there is a page to boot.
464
549
  tables[SERVER_TABLE] = serverTable(routes, errors, servers, mounts)
550
+ // 16.74 — what the browser holds of each handler file, and the route its hooks are filed at:
551
+ // read by the module's load, which runs before the server table is imported.
552
+ const handlers: Compilation['handlers'] = {}
553
+ for (const mount of mounts)
554
+ if (!mount.private)
555
+ handlers[mount.source] = {
556
+ scope: mount.scope,
557
+ client: standIns.get(mount.source) ?? '',
558
+ }
465
559
  if (routes.length > 0) {
466
560
  tables[ROUTES_TABLE] = clientTable(routes, transitions)
467
561
  // The entry's SOURCE carries the name the bundle is written under — `entry-<hash>.js`
@@ -471,6 +565,7 @@ export async function emitApp(
471
565
  return {
472
566
  files,
473
567
  tables,
568
+ handlers,
474
569
  routes,
475
570
  mounts,
476
571
  warnings,
@@ -481,7 +576,7 @@ export async function emitApp(
481
576
  * `text`, a module compiled from the dependency's `.abide` at `path`, with each of abide's own
482
577
  * specifiers written as the path to the file the app's root resolves it to.
483
578
  *
484
- * 34.30 — RELATIVE, so the compilation still serves once the app and its `node_modules` are moved
579
+ * 34.26 — RELATIVE, so the compilation still serves once the app and its `node_modules` are moved
485
580
  * together, and THE FILE the app's own code loads, so the two are one module instance. A path and
486
581
  * not a specifier a plugin answers: bun's runtime hands a plugin no bare specifier that names no
487
582
  * file, so a mark on `@abide/abide/server` was never read.
@@ -517,7 +612,11 @@ function placing(mappings: string | undefined): string | undefined {
517
612
  * the module the file compiles to.
518
613
  */
519
614
  function tableImport(source: string): string {
520
- return JSON.stringify(`../${source}${COMPONENT_QUERY}`)
615
+ return JSON.stringify(
616
+ source.endsWith('.abide')
617
+ ? `../${source}${COMPONENT_QUERY}`
618
+ : `../${source}`,
619
+ )
521
620
  }
522
621
 
523
622
  // A layout and the route it covers, parsed.
@@ -527,7 +626,7 @@ type AppLayout = { source: string; segments: Segment[] }
527
626
  * The layouts a page renders inside, outermost first.
528
627
  *
529
628
  * 35.1 — every `layout.abide` whose route lies ON THE WAY DOWN to the page's wraps it and no other
530
- * does: `src/layout.abide` wraps the page `src/media/page.abide` serves at `/media`, and so does
629
+ * does: `src/layout.abide` wraps the page `src/media/index.abide` serves at `/media`, and so does
531
630
  * `src/[section]/layout.abide`. So the test is a prefix of the route, kind by kind, and the nesting
532
631
  * order is the route's depth.
533
632
  */
@@ -556,25 +655,66 @@ function shapeOf(route: string): string {
556
655
  }
557
656
 
558
657
  /**
559
- * Throws, naming both, where two files answer one method at one route.
658
+ * Refuses, naming both, where two files answer one method at one route — at the second.
560
659
  *
561
660
  * 19.1 — a page answers `GET` at its own, and a handler its method at its address.
562
661
  */
563
- function refuseSharedRoutes(routes: AppRoute[], mounts: Mount[]): void {
662
+ function refuseSharedRoutes(
663
+ routes: AppRoute[],
664
+ mounts: Mount[],
665
+ refuse: Refuse,
666
+ ): void {
564
667
  const owners = new Map<string, string>()
565
668
  const claim = (method: string, route: string, source: string): void => {
566
669
  const key = `${method} ${shapeOf(route)}`
567
670
  const owner = owners.get(key)
568
- if (owner !== undefined)
569
- throw new Error(
570
- `abide: ${owner} and ${source} both answer ${method} at ${route}. RULEBOOK 19.1.`,
671
+ if (owner === undefined) owners.set(key, source)
672
+ else
673
+ refuse(
674
+ source,
675
+ new Error(
676
+ `abide: ${owner} and ${source} both answer ${method} at ${route}. RULEBOOK 19.1.`,
677
+ ),
571
678
  )
572
- owners.set(key, source)
573
679
  }
574
680
  for (const route of routes) claim('GET', route.pattern, route.source)
575
681
  for (const mount of mounts) claim(mount.method, mount.address, mount.source)
576
682
  }
577
683
 
684
+ /**
685
+ * Refuses, naming the file, where anything but the root's `launcher.abide` calls `onLauncher`.
686
+ *
687
+ * 38.107 — the launch loads that file alone, so a call anywhere else is never read.
688
+ */
689
+ function refuseStrayLauncher(
690
+ sources: readonly { relative: string; position: string; file: AbideFile }[],
691
+ handlerFiles: readonly HandlerFile[],
692
+ refuse: Refuse,
693
+ ): void {
694
+ const stray = (relative: string): void =>
695
+ refuse(
696
+ relative,
697
+ new Error(
698
+ `abide: ${relative} calls onLauncher, which is read from src/${LAUNCHER_PAGE} alone. Move the call there. RULEBOOK 38.107.`,
699
+ ),
700
+ )
701
+ for (const source of sources)
702
+ if (
703
+ source.position !== LAUNCHER_PAGE &&
704
+ source.file.servers.some(callsLauncher)
705
+ )
706
+ stray(source.relative)
707
+ for (const file of handlerFiles)
708
+ if (callsLauncher(readFileSync(file.path, 'utf8'))) stray(file.relative)
709
+ }
710
+
711
+ function callsLauncher(source: string): boolean {
712
+ return (
713
+ source.includes('onLauncher') &&
714
+ hooksCalled(source).includes('onLauncher')
715
+ )
716
+ }
717
+
578
718
  /**
579
719
  * Resolves a specifier from a directory as bun does, or `undefined` where it does not resolve.
580
720
  *
@@ -714,7 +854,8 @@ function serverTable(
714
854
  let handlers = ''
715
855
  for (const mount of mounts) {
716
856
  const fields = JSON.stringify(mount)
717
- // The module the handler is declared in — the file's server module, the route table's own.
857
+ // The module the handler is declared in — the handler file, or the `.abide`'s server
858
+ // module, the route table's own.
718
859
  handlers += ` { load: () => import(${tableImport(mount.source)}), ${fields.slice(1, -1)} },\n`
719
860
  }
720
861
  return `// Written by \`abide build\`. The addresses this app answers, and what paints each.
@@ -749,41 +890,58 @@ function clientModules(route: AppRoute): string {
749
890
  }
750
891
 
751
892
  /**
752
- * The wrappers the browser's module of each file exports in its server block's place, by source.
893
+ * The wrappers the browser holds in each handler's place, by source.
753
894
  *
754
- * 16.28, 34.23 — WHAT A BROWSER IMPORTS WHERE THE APP WROTE A HANDLER: the same export name, the
755
- * same call over the wire. The address is written RELATIVE to the bundle's own directory and
756
- * resolved against the chunk at run time (16.10), so an app mounted under a sub-path calls its own
757
- * handlers rather than the root's.
895
+ * 16.28 — WHAT A BROWSER HOLDS WHERE THE APP WROTE A PRIVATE HANDLER: the same name, the same call
896
+ * over the wire, bound in its `.abide`'s server block's place. 16.74 — a handler file's is its own
897
+ * module, its default, answered in the file's place.
758
898
  */
759
899
  function standInsOf(mounts: Mount[]): Map<string, string> {
760
- const lines = new Map<string, string>()
900
+ const lines = new Map<string, string[]>()
761
901
  for (const mount of mounts) {
762
- // 20.4 — withheld from the browser: no wrapper, so an import of it names nothing there.
902
+ // 20.4 — withheld from the browser: no wrapper, so a call of it there names nothing.
763
903
  if (!mount.ui) continue
764
- const at = JSON.stringify(posix.relative(CLIENT_ADDRESS, mount.address))
765
- // 18.4 — what the handler's value declares that the browser's copy behaves by.
766
- // 11.29 — and the defaults its parameter writes, which the copy keys by.
767
- const copy =
768
- mount.defaults !== undefined
769
- ? `, ${JSON.stringify(mount.copy)}, ${JSON.stringify(mount.defaults)}`
770
- : mount.copy === undefined
771
- ? ''
772
- : `, ${JSON.stringify(mount.copy)}`
773
- const line = `export const ${mount.name} = __wire(${JSON.stringify(mount.method)}, new URL(${at}, import.meta.url).pathname${copy})\n`
774
- lines.set(mount.source, (lines.get(mount.source) ?? '') + line)
904
+ const held = lines.get(mount.source) ?? []
905
+ held.push(
906
+ wireLine(
907
+ mount,
908
+ mount.private ? `const ${mount.name} =` : 'export default',
909
+ ),
910
+ )
911
+ lines.set(mount.source, held)
775
912
  }
776
913
  const standIns = new Map<string, string>()
777
- for (const [source, body] of lines)
778
- standIns.set(
779
- source,
780
- `// 34.23 — what a browser holds in place of the server block.
781
- import { wire as __wire } from '${WIRE_SPECIFIER}'
782
- ${body}`,
783
- )
914
+ for (const [source, held] of lines) standIns.set(source, wireModule(held))
784
915
  return standIns
785
916
  }
786
917
 
918
+ /**
919
+ * One handler's wrapper, bound as `binding` writes it.
920
+ *
921
+ * The address is written RELATIVE to the bundle's own directory and resolved against the chunk at
922
+ * run time (16.10), so an app mounted under a sub-path calls its own handlers rather than the
923
+ * root's.
924
+ */
925
+ function wireLine(mount: Mount, binding: string): string {
926
+ const at = JSON.stringify(posix.relative(CLIENT_ADDRESS, mount.address))
927
+ // 18.4 — what the handler's value declares that the browser's copy behaves by.
928
+ // 11.29 — and the defaults its parameter writes, which the copy keys by.
929
+ const copy =
930
+ mount.defaults !== undefined
931
+ ? `, ${JSON.stringify(mount.copy)}, ${JSON.stringify(mount.defaults)}`
932
+ : mount.copy === undefined
933
+ ? ''
934
+ : `, ${JSON.stringify(mount.copy)}`
935
+ return `${binding} __wire(${JSON.stringify(mount.method)}, new URL(${at}, import.meta.url).pathname${copy})\n`
936
+ }
937
+
938
+ /** A module of wrappers: the import they share, then each line. */
939
+ function wireModule(lines: string[]): string {
940
+ return `// 16.28 — what a browser holds in place of a handler.
941
+ import { wire as __wire } from '${WIRE_SPECIFIER}'
942
+ ${lines.join('')}`
943
+ }
944
+
787
945
  /** Whether a page or an error page has anything to paint: a template, or a head. 23.3, 35.14. */
788
946
  export function paints(file: AbideFile): boolean {
789
947
  return file.template.length > 0 || file.head.length > 0
@@ -33,7 +33,14 @@ const PACKAGE = Bun.fileURLToPath(new URL('../../', import.meta.url))
33
33
  function abideEntries(): Map<string, string> {
34
34
  const specifiers = new Map<string, string>()
35
35
  for (const [entry, path] of Object.entries(abidePackage.exports)) {
36
- if (entry === './compiler' || !path.endsWith('.ts')) continue
36
+ // The compiler, and the preload a test run builds the app with (38.109), are no part of an
37
+ // app that is already built.
38
+ if (
39
+ entry === './compiler' ||
40
+ entry === './test' ||
41
+ !path.endsWith('.ts')
42
+ )
43
+ continue
37
44
  specifiers.set(
38
45
  entry === '.'
39
46
  ? abidePackage.name
@@ -59,13 +66,11 @@ function abideEntries(): Map<string, string> {
59
66
  export function compiledEntry(app: {
60
67
  /**
61
68
  * 38.70 — what `abide bundle` read of the launcher's files: `launch` the path of its
62
- * `launcher.abide`, and `custom` whether that file has markup of its own; `undefined` for
63
- * `abide compile`.
69
+ * `launcher.abide` where that file has markup of its own; `undefined` for `abide compile`.
64
70
  */
65
71
  desktop:
66
72
  | {
67
73
  launch: string | undefined
68
- custom: boolean
69
74
  document: string
70
75
  icon: string | undefined
71
76
  accent: string | undefined
@@ -107,7 +112,6 @@ import * as table from ${JSON.stringify(tablePath(app.root, SERVER_TABLE))}
107
112
  ? ''
108
113
  : `
109
114
  {
110
- custom: ${app.desktop.custom},
111
115
  launch: ${app.desktop.launch === undefined ? 'undefined' : `() => import(${JSON.stringify(app.desktop.launch)})`},
112
116
  document: ${JSON.stringify(app.desktop.document)},
113
117
  icon: ${JSON.stringify(app.desktop.icon)},
@@ -116,7 +120,7 @@ import * as table from ${JSON.stringify(tablePath(app.root, SERVER_TABLE))}
116
120
  return `// Written by \`abide compile\`. The whole app, as what one executable holds.
117
121
  ${imports}
118
122
  // 38.56 — a file declaring handlers as a script names it through the app's one alias:
119
- // \`#src/invoices/page.abide\`, to the module its server block compiled into.
123
+ // \`#src/invoices/getInvoice.ts\`, to the module it is.
120
124
  const declared = {}
121
125
  for (const row of table.handlers) declared[\`#src/\${row.source}\`] = row.load
122
126
 
@@ -89,26 +89,25 @@ export function appGraph<
89
89
  const nodes: AppNode<Source>[] = []
90
90
  for (const source of sources) {
91
91
  // Below the root for the app's own files, and above it — `../node_modules/…` — for a
92
- // dependency's (34.28).
92
+ // dependency's (34.24).
93
93
  const relative = relativeTo(root, source.path)
94
94
  const position = source.position ?? relative
95
- // 34.21 — a server block of the app's own under a `server` or `public` directory has no
96
- // address: refused here, before anything reads one off it. A dependency's has none of its own
97
- // (34.31); the app mounts what it declares where the app re-exports it.
95
+ // 34.20 — a server block of the app's own under a `server` or `public` directory, refused
96
+ // here before anything reads its handlers: neither directory holds a component that runs.
98
97
  const place = placeOf(position)
99
98
  if (
100
99
  source.file.servers.length > 0 &&
101
100
  (place === 'server' || place === 'public')
102
101
  )
103
102
  throw new Error(
104
- `abide: ${relative} has a <script server>, and it is under a ${place} directory, where nothing has an address. Move the file out of it, or the code into a module under a server directory. RULEBOOK 34.21.`,
103
+ `abide: ${relative} has a <script server>, and it is under a ${place} directory, where nothing has an address. Move the file out of it, or the code into a module under a server directory. RULEBOOK 34.20.`,
105
104
  )
106
- // 34.25 — a file imported as text is found at build, not when its module first loads.
105
+ // 34.21 — a file imported as text is found at build, not when its module first loads.
107
106
  for (const script of scriptBodies(source.file))
108
107
  for (const { specifier } of attributedImports(script, 'text'))
109
108
  if (resolve(specifier, dirname(source.path)) === undefined)
110
109
  throw new Error(
111
- `abide: ${relative} imports ${specifier} as text, and there is no such file. RULEBOOK 34.25.`,
110
+ `abide: ${relative} imports ${specifier} as text, and there is no such file. RULEBOOK 34.21.`,
112
111
  )
113
112
  const kind = fileKind(position)
114
113
  nodes.push({
@@ -49,7 +49,7 @@ import type { Diagnosis } from './TypeSession.ts'
49
49
  /**
50
50
  * Every value each module exports, with the abide method that declares it.
51
51
  *
52
- * 19.15 — EVERY VALUE EACH MODULE EXPORTS, and the method of the abide call that declares it, or
52
+ * 16.73 — EVERY VALUE EACH MODULE EXPORTS, and the method of the abide call that declares it, or
53
53
  * `undefined` where it is not one. The checker's own export table, so a name exported apart from
54
54
  * its declaration, under another name or from another module is the same export it is to an
55
55
  * importer; and the callee's own symbol, so `GET` imported under another name or through a
@@ -75,7 +75,7 @@ export async function declaredExports(
75
75
  const sourceFile = await project.program.getSourceFile(path)
76
76
  if (sourceFile === undefined)
77
77
  throw new Error(
78
- `abide: ${path} is not inside the project ${projectName}, so what it exports cannot be read (19.15). Give the app a tsconfig whose include reaches it.`,
78
+ `abide: ${path} is not inside the project ${projectName}, so what it exports cannot be read (16.73). Give the app a tsconfig whose include reaches it.`,
79
79
  )
80
80
  const module = await checker.getSymbolAtLocation(sourceFile)
81
81
  if (module === undefined) return []
@@ -198,7 +198,7 @@ async function declaringMethod(
198
198
  : isExportAssignment(declaration)
199
199
  ? declaration.expression
200
200
  : undefined
201
- // 19.15 — A BINDING NAMING ANOTHER BINDING is the call that one was declared by: `export const
201
+ // 16.73 — A BINDING NAMING ANOTHER BINDING is the call that one was declared by: `export const
202
202
  // named = drop` is `drop`'s handler under another name. Bounded, so a cycle the checker would
203
203
  // refuse anyway cannot spin here.
204
204
  for (
@@ -39,7 +39,7 @@ import {
39
39
  type ValueRender,
40
40
  } from './renderMarkup.ts'
41
41
  import { SHARED_NAMES } from './SHARED_NAMES.ts'
42
- import { RUNTIME_SPECIFIER, SHARED_SPECIFIER } from './SPECIFIERS.ts'
42
+ import { RUNTIME_SPECIFIER, SHARED_SPECIFIER } from '#shared/SPECIFIERS.ts'
43
43
  import {
44
44
  insideShare,
45
45
  resumedShare,
@@ -267,8 +267,8 @@ export function emitClient(
267
267
  const setup = splitImports(lowering.scripts())
268
268
  if (!live && fills === '' && setup.body === '') {
269
269
  const mount = carriesOwnMarkup ? 'export default () => {}\n' : ''
270
- // 34.17 — WHAT A `<script module>` EXPORTS IS THE BROWSER'S TOO, live markup or none: a
271
- // component nothing invokes is still a module something imports a name from.
270
+ // 34.2 — A `<script module>` RUNS IN THE BROWSER TOO, live markup or none: its module scope
271
+ // is the browser's as much as the server's.
272
272
  if (moduleSetup.body === '' && moduleSetup.imports === '') return mount
273
273
  return `${moduleImports(lowering, (name) => (SHARED_NAMES.has(name) ? SHARED_SPECIFIER : RUNTIME_SPECIFIER), [moduleSetup])}
274
274
  ${moduleSetup.body}