@abide/abide 0.117.2 → 0.119.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 (156) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/bin/abide.ts +74 -11
  3. package/docs/REGISTRY.md +18 -15
  4. package/docs/RULEBOOK.md +71 -10
  5. package/docs/abide.md +584 -274
  6. package/package.json +1 -1
  7. package/scaffold/AGENTS.md +1 -1
  8. package/scaffold/src/getGreeting.test.ts +2 -1
  9. package/scaffold/src/index.abide +5 -2
  10. package/scaffold/src/index.test.ts +2 -0
  11. package/scaffold/src/layout.abide +6 -6
  12. package/src/compiler/Lowering.ts +20 -2
  13. package/src/compiler/PositionTable.ts +1 -0
  14. package/src/compiler/Refusals.ts +1 -0
  15. package/src/compiler/TypeSession.ts +49 -13
  16. package/src/compiler/aliasedSymbol.ts +1 -1
  17. package/src/compiler/ambientReadsIn.ts +1 -0
  18. package/src/compiler/appLayout.ts +55 -3
  19. package/src/compiler/appMap.ts +15 -0
  20. package/src/compiler/argsDefaultsIn.ts +1 -0
  21. package/src/compiler/argsSchemaOf.ts +1 -0
  22. package/src/compiler/checkApp.ts +3 -1
  23. package/src/compiler/checkedShadow.ts +2 -1
  24. package/src/compiler/classifyRead.ts +1 -0
  25. package/src/compiler/classifyTemplate.ts +1 -0
  26. package/src/compiler/compileApp.ts +5 -3
  27. package/src/compiler/compiledEntry.ts +2 -1
  28. package/src/compiler/componentGraph.ts +1 -0
  29. package/src/compiler/declaredExports.ts +4 -2
  30. package/src/compiler/emitClient.ts +19 -14
  31. package/src/compiler/emitComponent.ts +1 -0
  32. package/src/compiler/emitSource.ts +2 -1
  33. package/src/compiler/formatAbide.ts +857 -0
  34. package/src/compiler/formatDiagnosis.ts +1 -0
  35. package/src/compiler/handlersUnder.ts +7 -3
  36. package/src/compiler/hookCalls.ts +1 -0
  37. package/src/compiler/lineOf.ts +17 -0
  38. package/src/compiler/matchersOf.ts +157 -0
  39. package/src/compiler/mountsOf.ts +5 -0
  40. package/src/compiler/paramFile.ts +125 -0
  41. package/src/compiler/parseAbide.ts +48 -28
  42. package/src/compiler/projectShadow.ts +11 -4
  43. package/src/compiler/reactiveOptions.ts +1 -0
  44. package/src/compiler/reactiveTypes.ts +2 -1
  45. package/src/compiler/readsIn.ts +3 -2
  46. package/src/compiler/renderMarkup.ts +3 -2
  47. package/src/compiler/resolverIn.ts +2 -0
  48. package/src/compiler/sameTemplate.ts +150 -0
  49. package/src/compiler/scaffold.ts +4 -11
  50. package/src/compiler/serveLanguage.ts +48 -21
  51. package/src/compiler/sharedModuleWrites.ts +2 -1
  52. package/src/compiler/sourceMapOf.ts +1 -0
  53. package/src/compiler/splitImports.ts +2 -0
  54. package/src/compiler/templateWarnings.ts +2 -2
  55. package/src/compiler/trackingWarnings.ts +1 -0
  56. package/src/compiler/writeDefaultProject.ts +5 -1
  57. package/src/server/BRIDGE_VERSION.ts +1 -1
  58. package/src/server/abideModules.ts +2 -0
  59. package/src/server/answerFailure.ts +1 -0
  60. package/src/server/answerRequest.ts +57 -19
  61. package/src/server/answerRpc.ts +6 -2
  62. package/src/server/answerSocket.ts +2 -1
  63. package/src/server/answerValue.ts +1 -0
  64. package/src/server/answerWorker.ts +1 -0
  65. package/src/server/appTree.ts +18 -9
  66. package/src/server/argsFor.ts +1 -0
  67. package/src/server/assistant.ts +2 -1
  68. package/src/server/banner.ts +2 -0
  69. package/src/server/bridgeRelay.ts +1 -0
  70. package/src/server/callerOf.ts +1 -0
  71. package/src/server/claudeApi.ts +1 -0
  72. package/src/server/claudeCode.ts +1 -0
  73. package/src/server/clientBundle.ts +1 -0
  74. package/src/server/coerceArgs.ts +1 -2
  75. package/src/server/commands.ts +9 -0
  76. package/src/server/compressAnswer.ts +2 -1
  77. package/src/server/config.ts +2 -0
  78. package/src/server/createApp.ts +4 -2
  79. package/src/server/crossingOrigin.ts +1 -0
  80. package/src/server/csp.ts +1 -0
  81. package/src/server/deferredBuilds.ts +1 -0
  82. package/src/server/derivedArgs.ts +1 -0
  83. package/src/server/devWorker.ts +2 -1
  84. package/src/server/faceOf.ts +2 -7
  85. package/src/server/generatedSurfaces.ts +79 -7
  86. package/src/server/hooks.ts +1 -0
  87. package/src/server/iconAccent.ts +1 -1
  88. package/src/server/installServer.ts +1 -0
  89. package/src/server/installSourceMaps.ts +1 -0
  90. package/src/server/launcherPage.ts +2 -2
  91. package/src/server/manifestOf.ts +1 -0
  92. package/src/server/muxUpgrade.ts +1 -0
  93. package/src/server/nativeMenu.ts +2 -0
  94. package/src/server/placement.ts +12 -2
  95. package/src/server/principal.ts +2 -1
  96. package/src/server/remote.ts +7 -2
  97. package/src/server/render.ts +3 -3
  98. package/src/server/renderComponent.ts +1 -0
  99. package/src/server/reportHealth.ts +1 -0
  100. package/src/server/requestScope.ts +3 -1
  101. package/src/server/runDesktop.ts +3 -2
  102. package/src/server/seedBuffer.ts +1 -0
  103. package/src/server/serveDev.ts +3 -2
  104. package/src/server/shell.ts +2 -0
  105. package/src/server/streamEvent.ts +1 -0
  106. package/src/server/surfaceOf.ts +6 -1
  107. package/src/server/testBuild.ts +1 -0
  108. package/src/server/testMount.ts +1 -0
  109. package/src/server/trace.ts +2 -0
  110. package/src/server/withAbideSources.ts +1 -0
  111. package/src/shared/Attributes.ts +12 -8
  112. package/src/shared/cookies.ts +1 -1
  113. package/src/shared/escapeText.ts +1 -1
  114. package/src/shared/filledPath.ts +16 -10
  115. package/src/shared/index.ts +1 -5
  116. package/src/shared/jsonSchema.ts +1 -3
  117. package/src/shared/matchRoute.ts +16 -4
  118. package/src/shared/menuBridge.ts +1 -2
  119. package/src/shared/outbox.ts +1 -1
  120. package/src/shared/reactive/Keyed.ts +2 -2
  121. package/src/shared/reactive/Reactive.ts +12 -16
  122. package/src/shared/reactive/index.ts +3 -4
  123. package/src/shared/reactive/isReactive.ts +1 -1
  124. package/src/shared/reactive/scoped.ts +2 -2
  125. package/src/shared/reactive/share.ts +1 -1
  126. package/src/shared/reactive/watch.ts +4 -4
  127. package/src/shared/refuse.ts +1 -1
  128. package/src/shared/routePattern.ts +66 -14
  129. package/src/shared/rpc.ts +8 -11
  130. package/src/shared/url.ts +24 -7
  131. package/src/shared/validateJson.ts +1 -1
  132. package/src/ui/ArmRegion.ts +1 -0
  133. package/src/ui/Level.ts +1 -0
  134. package/src/ui/List.ts +1 -0
  135. package/src/ui/RawRun.ts +1 -0
  136. package/src/ui/Router.ts +1 -0
  137. package/src/ui/TryRegion.ts +1 -0
  138. package/src/ui/adoptWalk.ts +2 -1
  139. package/src/ui/bindElement.ts +1 -0
  140. package/src/ui/bindFiles.ts +1 -0
  141. package/src/ui/bindGroup.ts +1 -0
  142. package/src/ui/bindProperty.ts +1 -0
  143. package/src/ui/bindSpread.ts +1 -0
  144. package/src/ui/index.ts +1 -6
  145. package/src/ui/outboxQueue.ts +1 -1
  146. package/src/ui/parseMarkup.ts +2 -1
  147. package/src/ui/reconcile.ts +2 -1
  148. package/src/ui/reconcileHead.ts +1 -0
  149. package/src/ui/regions.ts +2 -1
  150. package/src/ui/registerWorker.ts +1 -0
  151. package/src/ui/rows.ts +1 -0
  152. package/src/ui/setMarkup.ts +1 -0
  153. package/src/ui/setText.ts +1 -0
  154. package/src/ui/socketWire.ts +4 -3
  155. package/src/ui/wire.ts +2 -1
  156. /package/scaffold/src/{getGreeting.ts → getGreeting.handler.ts} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abide/abide",
3
- "version": "0.117.2",
3
+ "version": "0.119.0",
4
4
  "type": "module",
5
5
  "description": "Isomorphic type-safe framework for reactive async interfaces for humans and machines",
6
6
  "license": "MIT",
@@ -36,7 +36,7 @@ before writing code, rather than guessing from Svelte, Vue or React. It installs
36
36
  - A file's path below `src/` is its address. `src/invoices/index.abide` answers `/invoices`,
37
37
  `src/invoices/[id].abide` answers `/invoices/123`, and `layout.abide` wraps the pages below it;
38
38
  the outermost one writes `<html>`. Run `bun run routes` to see every address and the file behind it.
39
- - A `.ts` beside the pages is a handler: `src/invoices/getInvoice.ts` holds
39
+ - A `<name>.handler.ts` beside the pages is a handler: `src/invoices/getInvoice.handler.ts` holds
40
40
  `export default GET(…)` and answers `/invoices/getInvoice`. It is the app's public API, for pages,
41
41
  scripts, the CLI and MCP alike.
42
42
  - A `.abide` cannot export. A `<script server>` in one holds that file's private handlers
@@ -1,5 +1,6 @@
1
1
  import { expect, test } from 'bun:test'
2
- import getGreeting from './getGreeting.ts'
2
+
3
+ import getGreeting from './getGreeting.handler.ts'
3
4
 
4
5
  test('the handler answers with the name it is given', async () => {
5
6
  expect(await getGreeting({ name: 'Ada' })).toBe('Hello, Ada')
@@ -1,6 +1,7 @@
1
1
  <script>
2
2
  import { reactive } from '@abide/abide'
3
- import getGreeting from '#src/getGreeting.ts'
3
+
4
+ import getGreeting from '#src/getGreeting.handler.ts'
4
5
 
5
6
  let count = reactive(0)
6
7
  const greeting = reactive(() => getGreeting({ name: 'world' }))
@@ -11,5 +12,7 @@ const greeting = reactive(() => getGreeting({ name: 'world' }))
11
12
  <main>
12
13
  <h1>{greeting}</h1>
13
14
  <p>Clicked {count} times.</p>
14
- <button onclick={() => { count = count + 1 }}>Click</button>
15
+ <button onclick={() => {
16
+ count = count + 1
17
+ }}>Click</button>
15
18
  </main>
@@ -1,6 +1,8 @@
1
1
  import { expect, test } from 'bun:test'
2
+
2
3
  import { flushSync } from '@abide/abide'
3
4
  import { measure, mount } from '@abide/harness/abide'
5
+
4
6
  import Index from './index.abide'
5
7
 
6
8
  test('a click rewrites the count and builds nothing', async () => {
@@ -8,11 +8,11 @@ onStart(async (start) => {
8
8
  </script>
9
9
 
10
10
  <html lang="en">
11
- <head>
12
- <meta charset="utf-8" />
13
- <meta name="viewport" content="width=device-width, initial-scale=1" />
14
- </head>
11
+ <head>
12
+ <meta charset="utf-8" />
13
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
14
+ </head>
15
15
  <body>
16
- <slot/>
17
- </body>
16
+ <slot />
17
+ </body>
18
18
  </html>
@@ -1,15 +1,17 @@
1
1
  import { SyntaxKind } from 'typescript/unstable/ast'
2
+
2
3
  import type { CheckerAnswer } from './CheckerAnswer.ts'
3
4
  import type { Read } from './classifyRead.ts'
4
5
  import { holdsSlot } from './classifyTemplate.ts'
5
6
  import type { Finding } from './Finding.ts'
6
- import type { AbideFile } from './parseAbide.ts'
7
+ import { type AbideFile, scriptBodies } from './parseAbide.ts'
7
8
  import {
8
9
  type Origin,
9
10
  type Projection,
10
11
  projectShadow,
11
12
  type Span,
12
13
  } from './projectShadow.ts'
14
+ import { importedNames } from './splitImports.ts'
13
15
  import type { InlineComponent, Setup, TemplateNode } from './TemplateNode.ts'
14
16
  import { nesting, type Token, tokenize } from './tokenize.ts'
15
17
 
@@ -82,6 +84,11 @@ export class Lowering {
82
84
  * rather than called, whose forwarding arrow takes an untyped rest.
83
85
  */
84
86
  readonly checking: boolean
87
+ /**
88
+ * Whether a script imports abide's own `raw`, which a checked lowering then leaves a template's
89
+ * `raw(x)` calling — the same function, and the import read where the author reads it.
90
+ */
91
+ private readonly importsRaw: boolean
85
92
  /**
86
93
  * The names a checked lowering read, each with whether it was read through a props accessor.
87
94
  *
@@ -133,6 +140,7 @@ export class Lowering {
133
140
  this.answer = answer
134
141
  this.holdsSlot = holdsSlot(file.template)
135
142
  this.checking = checking
143
+ this.importsRaw = importsAbideRaw(file)
136
144
  this.chunks = chunks
137
145
  this.routed = routed
138
146
  }
@@ -804,7 +812,9 @@ export class Lowering {
804
812
  // template are the same object. The read exists only where a template expression held
805
813
  // the call (`expressionFacts`), so a `<script>`'s `raw` stays the file's.
806
814
  case 'raw':
807
- return this.use('raw')
815
+ return this.checking && this.importsRaw
816
+ ? undefined
817
+ : this.use('raw')
808
818
  // 27.13 — the call bound to a name, for the checker to read the defaults' type off.
809
819
  case 'config':
810
820
  return this.checking
@@ -897,6 +907,14 @@ export class Lowering {
897
907
  }
898
908
  }
899
909
 
910
+ function importsAbideRaw(file: AbideFile): boolean {
911
+ for (const body of scriptBodies(file))
912
+ for (const { name, specifier } of importedNames(body))
913
+ if (name === 'raw' && specifier.startsWith('@abide/abide'))
914
+ return true
915
+ return false
916
+ }
917
+
900
918
  function isRawCallee(read: Read): boolean {
901
919
  return read.shape === 'raw'
902
920
  }
@@ -1,4 +1,5 @@
1
1
  import type { Node } from 'typescript/unstable/ast'
2
+
2
3
  import type { Span } from './projectShadow.ts'
3
4
 
4
5
  /**
@@ -1,4 +1,5 @@
1
1
  import { resolve } from 'node:path' // bun exposes no path helpers of its own
2
+
2
3
  import { formatDiagnosis } from './formatDiagnosis.ts'
3
4
  import { diagnosisAt } from './lineOf.ts'
4
5
  import { PositionedRefusal } from './PositionedRefusal.ts'
@@ -1,5 +1,6 @@
1
1
  import { type Dirent, readdirSync, realpathSync, statSync } from 'node:fs' // Bun exposes no sync directory listing or realpath of its own
2
2
  import { basename, dirname, join, resolve } from 'node:path' // Bun exposes no path helpers of its own
3
+
3
4
  import {
4
5
  getTouchingPropertyName,
5
6
  type Node,
@@ -8,7 +9,6 @@ import {
8
9
  } from 'typescript/unstable/ast'
9
10
  import {
10
11
  API,
11
- type Symbol as CheckedSymbol,
12
12
  type Checker,
13
13
  type CompletionEntry,
14
14
  type CompletionInfo,
@@ -17,30 +17,34 @@ import {
17
17
  type Project,
18
18
  SignatureKind,
19
19
  type Snapshot,
20
+ type Symbol as CheckedSymbol,
20
21
  SymbolFlags,
21
22
  type Type,
22
23
  } from 'typescript/unstable/async'
24
+
23
25
  import { placeOf } from '#server/placement.ts'
26
+ import { parseRoutePattern } from '#shared/routePattern.ts'
27
+
24
28
  import { aliasedSymbol } from './aliasedSymbol.ts'
25
29
  import { AMBIENT_SOURCES, ambientReadsIn } from './ambientReadsIn.ts'
26
- import type { AppLayout, PlacedSource } from './appLayout.ts'
30
+ import type { AppLayout, AppRoute, PlacedSource } from './appLayout.ts'
27
31
  import { argsDefaultsIn } from './argsDefaultsIn.ts'
28
- import { type CheckerAnswer, UNCHECKED } from './CheckerAnswer.ts'
29
32
  import {
33
+ checkedShadow,
30
34
  COMPONENT_MODULE,
31
35
  COMPONENT_STAND_IN,
32
- checkedShadow,
33
36
  moduleConfigShadow,
34
37
  } from './checkedShadow.ts'
38
+ import { type CheckerAnswer, UNCHECKED } from './CheckerAnswer.ts'
35
39
  import type { Finding } from './Finding.ts'
36
40
  import { isIgnored } from './isIgnored.ts'
37
- import { CHECKED_VALUES } from './Lowering.ts'
38
41
  import { diagnosisAt, lineOf } from './lineOf.ts'
42
+ import { CHECKED_VALUES } from './Lowering.ts'
39
43
  import type { AbideFile } from './parseAbide.ts'
40
44
  import { projectShadow, type Span } from './projectShadow.ts'
41
45
  import { type Abide, abideIn, isReactiveType } from './reactiveTypes.ts'
42
- import type { ParsedSource, ReadSource } from './readSources.ts'
43
46
  import { readsIn, type ShadowSpans } from './readsIn.ts'
47
+ import type { ParsedSource, ReadSource } from './readSources.ts'
44
48
  import {
45
49
  CHECKED_SUFFIX,
46
50
  mapForward,
@@ -974,7 +978,7 @@ export class TypeSession {
974
978
  Partial<Pick<PlacedSource, 'kind' | 'route'>> & {
975
979
  quiet?: boolean
976
980
  })[],
977
- app: Pick<AppLayout, 'root' | 'addresses' | 'handlers'>,
981
+ app: Pick<AppLayout, 'root' | 'addresses' | 'handlers' | 'routes'>,
978
982
  scripts = true,
979
983
  ): Promise<{ diagnoses: Diagnosis[]; refused: Set<string> }> {
980
984
  using _held = this.hold()
@@ -1033,7 +1037,7 @@ export class TypeSession {
1033
1037
  // module augments one.
1034
1038
  this.register(
1035
1039
  `${this.canonical(within)}/${ROUTES_DECLARATION}`,
1036
- routesDeclaration(app.addresses),
1040
+ routesDeclaration(app.addresses, app.routes),
1037
1041
  created,
1038
1042
  changed,
1039
1043
  )
@@ -1570,13 +1574,46 @@ function isFile(path: string): boolean {
1570
1574
  const ROUTES_DECLARATION = 'abide-routes.d.ts'
1571
1575
  const URL_SOURCE = Bun.fileURLToPath(new URL('../shared/url', import.meta.url))
1572
1576
 
1573
- /** 30.26 — each of `addresses` as a key of abide's `Routes`. */
1574
- function routesDeclaration(addresses: readonly string[]): string {
1577
+ /**
1578
+ * 30.26 — each of `addresses` as a key of abide's `Routes`, and 23.9, 30.3 — a page route's params
1579
+ * narrowed where its matcher is an alternation of literals, which is the one matcher a type can say.
1580
+ */
1581
+ function routesDeclaration(
1582
+ addresses: readonly string[],
1583
+ routes: readonly AppRoute[],
1584
+ ): string {
1585
+ const narrowed = new Map<string, string>()
1586
+ for (const route of routes) {
1587
+ let members = ''
1588
+ for (const segment of parseRoutePattern(route.pattern)) {
1589
+ if (segment.kind !== 'required' && segment.kind !== 'optional')
1590
+ continue
1591
+ const literals = literalsOf(
1592
+ segment.matcher === undefined
1593
+ ? undefined
1594
+ : route.matchers[segment.matcher],
1595
+ )
1596
+ if (literals === undefined) continue
1597
+ members += ` ${JSON.stringify(segment.name)}${segment.kind === 'optional' ? '?' : ''}: ${literals};`
1598
+ }
1599
+ if (members !== '') narrowed.set(route.pattern, `{${members} }`)
1600
+ }
1575
1601
  let keys = ''
1576
1602
  for (const route of addresses)
1577
- keys += ` ${JSON.stringify(route)}: true\n`
1603
+ keys += ` ${JSON.stringify(route)}: ${narrowed.get(route) ?? 'true'}\n`
1578
1604
  return `export {}\ndeclare module ${JSON.stringify(URL_SOURCE)} {\n interface Routes {\n${keys} }\n}\n`
1579
1605
  }
1606
+
1607
+ /** The union of the literals an anchored matcher source alternates between, or `undefined`. */
1608
+ function literalsOf(source: string | undefined): string | undefined {
1609
+ const body = source?.match(/^\^\(\?!\\\.\\\.\?\$\)\(\?:(.*)\)\$$/)?.[1]
1610
+ if (body === undefined || !/^[\w~-]+(?:\|[\w~-]+)*$/.test(body))
1611
+ return undefined
1612
+ return body
1613
+ .split('|')
1614
+ .map((literal) => JSON.stringify(literal))
1615
+ .join(' | ')
1616
+ }
1580
1617
  const AMBIENT_DECLARATIONS = `declare module '*.css' {}
1581
1618
  declare module '*.md' {
1582
1619
  const text: string
@@ -1769,8 +1806,7 @@ const CATEGORIES = ['warning', 'error', 'suggestion', 'message'] as const
1769
1806
  function flatten(diagnostic: {
1770
1807
  text: string
1771
1808
  messageChain?:
1772
- | readonly { text: string; messageChain?: unknown }[]
1773
- | undefined
1809
+ readonly { text: string; messageChain?: unknown }[] | undefined
1774
1810
  }): string {
1775
1811
  let message = diagnostic.text
1776
1812
  const chain = diagnostic.messageChain ?? []
@@ -1,6 +1,6 @@
1
1
  import {
2
- type Symbol as CheckedSymbol,
3
2
  type Checker,
3
+ type Symbol as CheckedSymbol,
4
4
  SymbolFlags,
5
5
  } from 'typescript/unstable/async'
6
6
 
@@ -10,6 +10,7 @@ import {
10
10
  isPropertyAssignment,
11
11
  } from 'typescript/unstable/ast/is'
12
12
  import type { Checker } from 'typescript/unstable/async'
13
+
13
14
  import { aliasedSymbol } from './aliasedSymbol.ts'
14
15
  import {
15
16
  isWrittenInPlace,
@@ -1,4 +1,5 @@
1
1
  import { relative as relativeTo } from 'node:path' // bun exposes no path helpers of its own
2
+
2
3
  import { appFiles } from '#server/appTree.ts'
3
4
  import { type Placement, placement, routeOf } from '#server/placement.ts'
4
5
  import {
@@ -13,13 +14,15 @@ import {
13
14
  parseRoutePattern,
14
15
  type Segment,
15
16
  } from '#shared/routePattern.ts'
17
+
16
18
  import { abideImportsOf, componentGraph } from './componentGraph.ts'
17
19
  import type { HandlerFile } from './handlersUnder.ts'
18
20
  import { hookCalls, hooksIn } from './hookCalls.ts'
21
+ import { matchersOf, type MountedTree } from './matchersOf.ts'
19
22
  import { mountsOf } from './mountsOf.ts'
20
23
  import { type AbideFile, scriptBodies } from './parseAbide.ts'
21
- import { Refusals } from './Refusals.ts'
22
24
  import { type ParsedSource, parseSources } from './readSources.ts'
25
+ import { Refusals } from './Refusals.ts'
23
26
  import { resolverIn } from './resolverIn.ts'
24
27
  import { attributedImports } from './splitImports.ts'
25
28
  import type { Diagnosis } from './TypeSession.ts'
@@ -49,6 +52,8 @@ export type PlacedHandler = HandlerFile & {
49
52
  scope: string
50
53
  text: string
51
54
  hooks: string[]
55
+ /** 23.26 — each matcher its route names, to the anchored source the route tables carry. */
56
+ matchers: Record<string, string>
52
57
  }
53
58
 
54
59
  /** A page or an error page and the layouts it renders inside. */
@@ -64,6 +69,8 @@ export type AppRoute = {
64
69
  * cannot be declared twice.
65
70
  */
66
71
  layouts: string[]
72
+ /** 23.26 — each matcher its pattern names, to the anchored source the route tables carry. */
73
+ matchers: Record<string, string>
67
74
  }
68
75
 
69
76
  /** The app at a root, laid out (`appLayout`). */
@@ -105,7 +112,10 @@ export async function appLayout(root: string): Promise<AppLayout> {
105
112
  // COLLECTED, whoever asks: a layout is kept across checks (`AppChecker`), and a build adopts
106
113
  // what it found (`Refusals.adopt`).
107
114
  const refusals = new Refusals(root, true)
108
- const { abide, handlers, modules, published } = appFiles(root, refusals)
115
+ const { abide, handlers, params, modules, published } = appFiles(
116
+ root,
117
+ refusals,
118
+ )
109
119
  await refuseModuleHooks(root, modules, refusals)
110
120
  const resolve = resolverIn()
111
121
  const unparsed = new Refusals(root, true)
@@ -149,10 +159,43 @@ export async function appLayout(root: string): Promise<AppLayout> {
149
159
  scope: placement(file.relative, file.position).scope,
150
160
  text,
151
161
  hooks: hooksIn([text]),
162
+ matchers: {},
152
163
  })
153
164
  }
154
165
  refuseStrayLauncher(sources, placedHandlers, refusals)
155
166
  const { routes, errors } = routesOf(sources, refusals)
167
+ // 23.26 — every routed file's matchers, a layout's among them for 23.27's sake though no table
168
+ // carries a layout's own.
169
+ const routed: {
170
+ path: string
171
+ relative: string
172
+ position: string
173
+ route: string
174
+ }[] = []
175
+ const relativeToPath = new Map<string, string>()
176
+ for (const source of sources) {
177
+ if (source.route === undefined) continue
178
+ relativeToPath.set(source.relative, source.path)
179
+ routed.push({
180
+ path: source.path,
181
+ relative: source.relative,
182
+ position: source.position,
183
+ route: source.route,
184
+ })
185
+ }
186
+ for (const handler of placedHandlers) routed.push(handler)
187
+ const matchers = await matchersOf(
188
+ root,
189
+ routed,
190
+ params.map((path) => `${root}/${path}`),
191
+ found.trees,
192
+ refusals,
193
+ )
194
+ for (const handler of placedHandlers)
195
+ handler.matchers = matchers.get(handler.path) ?? {}
196
+ for (const route of [...routes, ...errors])
197
+ route.matchers =
198
+ matchers.get(relativeToPath.get(route.source) ?? '') ?? {}
156
199
  const addresses = new Set<string>()
157
200
  for (const route of routes) addresses.add(route.pattern)
158
201
  for (const address of published.values()) addresses.add(address)
@@ -215,6 +258,8 @@ export async function withDependencies(
215
258
  handlers: HandlerFile[]
216
259
  /** 23.19 — each mounted `.abide`'s position, by its path; one at its own path is not here. */
217
260
  positions: Map<string, string>
261
+ /** 23.26 — each mounted directory, where it mounts, and the param files under it. */
262
+ trees: MountedTree[]
218
263
  }> {
219
264
  const all = [...parsed]
220
265
  const handlers: HandlerFile[] = []
@@ -231,6 +276,7 @@ export async function withDependencies(
231
276
  const positions = new Map<string, string>()
232
277
  // 23.20 — a directory mounts once, and by whom.
233
278
  const mountedBy = new Map<string, string>()
279
+ const trees: MountedTree[] = []
234
280
  const add = async (path: string): Promise<void> => {
235
281
  let text: string
236
282
  try {
@@ -268,6 +314,11 @@ export async function withDependencies(
268
314
  continue
269
315
  }
270
316
  mountedBy.set(mount.directory, position)
317
+ trees.push({
318
+ directory: mount.directory,
319
+ at: mount.at,
320
+ params: mount.params,
321
+ })
271
322
  for (const placed of mount.handlers) {
272
323
  // Two mounts can overlap, one directory inside the other; a file is placed once.
273
324
  if (handled.has(placed.path)) continue
@@ -292,7 +343,7 @@ export async function withDependencies(
292
343
  if (!byPath.has(target)) await add(target)
293
344
  }
294
345
  }
295
- return { sources: all, handlers, positions }
346
+ return { sources: all, handlers, positions, trees }
296
347
  }
297
348
 
298
349
  /** Whether a page or an error page has anything to paint: a template, or a head. 23.3, 35.14. */
@@ -450,6 +501,7 @@ function routesOf(
450
501
  pattern,
451
502
  source: source.relative,
452
503
  layouts: around,
504
+ matchers: {},
453
505
  })
454
506
  }
455
507
  return { routes, errors }
@@ -14,6 +14,8 @@ type MappedRoute = {
14
14
  address: string
15
15
  file: string
16
16
  kind: 'page' | 'handler' | 'private'
17
+ /** 23.26 — each matcher its address names, to the anchored pattern it takes a part by. */
18
+ matchers: Record<string, string>
17
19
  }
18
20
 
19
21
  /** One hook call, the route it is filed at, and the file making it. */
@@ -39,6 +41,7 @@ export async function appMap(
39
41
  address: route.pattern,
40
42
  file: route.source,
41
43
  kind: 'page',
44
+ matchers: route.matchers,
42
45
  })
43
46
  for (const mount of app.mounts)
44
47
  routes.push({
@@ -46,6 +49,7 @@ export async function appMap(
46
49
  address: mount.address,
47
50
  file: mount.source,
48
51
  kind: mount.private ? 'private' : 'handler',
52
+ matchers: mount.matchers,
49
53
  })
50
54
  routes.sort((one, two) =>
51
55
  one.address === two.address
@@ -79,6 +83,17 @@ export function mapText(map: Awaited<ReturnType<typeof appMap>>): string {
79
83
  lines.push(
80
84
  `${route.method.padEnd(methods)} ${route.address.padEnd(addresses)} ${route.file}${route.kind === 'private' ? ' (private)' : ''}`,
81
85
  )
86
+ // 23.26 — each matcher once, whichever routes name it: what a part must match to be taken.
87
+ const matchers = new Map<string, string>()
88
+ for (const route of map.routes)
89
+ for (const name in route.matchers)
90
+ matchers.set(name, route.matchers[name] as string)
91
+ if (matchers.size > 0) {
92
+ lines.push('', 'matchers')
93
+ const names = width([...matchers.keys()])
94
+ for (const [name, pattern] of matchers)
95
+ lines.push(` ${name.padEnd(names)} /${pattern}/`)
96
+ }
82
97
  if (map.hooks.length > 0) {
83
98
  lines.push('', 'hooks')
84
99
  const hooks = width(map.hooks.map((hook) => hook.hook))
@@ -16,6 +16,7 @@ import {
16
16
  isStringLiteral,
17
17
  } from 'typescript/unstable/ast/is'
18
18
  import { type Checker, SymbolFlags, type Type } from 'typescript/unstable/async'
19
+
19
20
  import type { Finding } from './Finding.ts'
20
21
  import { optionNamed, reactiveOptions } from './reactiveOptions.ts'
21
22
 
@@ -18,6 +18,7 @@ import {
18
18
  type TypeReference,
19
19
  type UnionOrIntersectionType,
20
20
  } from 'typescript/unstable/async'
21
+
21
22
  import type { JsonValue } from '#shared/JsonValue.ts'
22
23
 
23
24
  type Schema = { [keyword: string]: JsonValue }
@@ -1,12 +1,14 @@
1
1
  import { relative } from 'node:path' // bun exposes no path helpers of its own
2
+
2
3
  import { type Placement, placement } from '#server/placement.ts'
4
+
3
5
  import type { PlacedSource } from './appLayout.ts'
4
6
  import { type AppChecker, compiled } from './compileApp.ts'
5
7
  import { abideImportsOf } from './componentGraph.ts'
6
8
  import { hooksIn } from './hookCalls.ts'
7
9
  import { parseAbide, scriptBodies } from './parseAbide.ts'
8
- import { Refusals } from './Refusals.ts'
9
10
  import { parseSources, readSources } from './readSources.ts'
11
+ import { Refusals } from './Refusals.ts'
10
12
  import type { Diagnosis } from './TypeSession.ts'
11
13
 
12
14
  /**
@@ -1,4 +1,5 @@
1
1
  import { SERVER_SPECIFIER } from '#shared/SPECIFIERS.ts'
2
+
2
3
  import type { CheckerAnswer } from './CheckerAnswer.ts'
3
4
  import type { Finding } from './Finding.ts'
4
5
  import { hookCalls } from './hookCalls.ts'
@@ -10,8 +11,8 @@ import {
10
11
  Lowering,
11
12
  type Routed,
12
13
  } from './Lowering.ts'
13
- import { holdsReactive } from './PositionTable.ts'
14
14
  import type { AbideFile } from './parseAbide.ts'
15
+ import { holdsReactive } from './PositionTable.ts'
15
16
  import { NO_PROPS, type Origin, type Span } from './projectShadow.ts'
16
17
  import { CHECKED_SUFFIX } from './ShadowMap.ts'
17
18
  import { abideImports } from './splitImports.ts'
@@ -17,6 +17,7 @@ import {
17
17
  isPropertyAccessExpression,
18
18
  isShorthandPropertyAssignment,
19
19
  } from 'typescript/unstable/ast/is'
20
+
20
21
  import type { Span } from './projectShadow.ts'
21
22
  import { REACTIVE_MEMBERS } from './REACTIVE_MEMBERS.ts'
22
23
 
@@ -3,6 +3,7 @@ import { escapeAttribute, escapeQuote } from '#shared/escapeText.ts'
3
3
  import { isEventName } from '#shared/isEventName.ts'
4
4
  import { safeUrl } from '#shared/safeUrl.ts'
5
5
  import { URL_ATTRIBUTES } from '#shared/URL_ATTRIBUTES.ts'
6
+
6
7
  import { decodeCharacterReferences } from './decodeCharacterReferences.ts'
7
8
  import {
8
9
  type Attribute,
@@ -1,5 +1,6 @@
1
1
  import { realpathSync } from 'node:fs' // bun exposes no sync realpath of its own
2
2
  import { dirname, posix } from 'node:path' // bun exposes no path helpers of its own
3
+
3
4
  import {
4
5
  type Compilation,
5
6
  type CompiledFile,
@@ -13,10 +14,11 @@ import { VIRTUAL_DIRECTORY } from '#server/VIRTUAL_DIRECTORY.ts'
13
14
  import { CLIENT_ADDRESS } from '#shared/ADDRESSES.ts'
14
15
  import type { JsonValue } from '#shared/JsonValue.ts'
15
16
  import { ROUTING_SPECIFIER, WIRE_SPECIFIER } from '#shared/SPECIFIERS.ts'
17
+
16
18
  import {
17
19
  type AppLayout,
18
- type AppRoute,
19
20
  appLayout,
21
+ type AppRoute,
20
22
  type PlacedSource,
21
23
  shapeOf,
22
24
  } from './appLayout.ts'
@@ -25,8 +27,8 @@ import { handlersUnder } from './handlersUnder.ts'
25
27
  import { isAbideSpecifier } from './hookCalls.ts'
26
28
  import { nearest } from './nearest.ts'
27
29
  import { projectShadow } from './projectShadow.ts'
28
- import { Refusals } from './Refusals.ts'
29
30
  import { type ReadSource, readSources } from './readSources.ts'
31
+ import { Refusals } from './Refusals.ts'
30
32
  import { respecified } from './splitImports.ts'
31
33
  import { type Diagnosis, TypeSession } from './TypeSession.ts'
32
34
  import { writeDefaultProject } from './writeDefaultProject.ts'
@@ -514,7 +516,7 @@ function tableOf(
514
516
  (layout) => `() => import(${tableImport(layout)})`,
515
517
  )
516
518
  rows.push(
517
- ` { pattern: ${JSON.stringify(route.pattern)}, load: () => import(${tableImport(route.source)}), layouts: [${wrapping.join(', ')}]${column(route)} },`,
519
+ ` { pattern: ${JSON.stringify(route.pattern)}, load: () => import(${tableImport(route.source)}), layouts: [${wrapping.join(', ')}], matchers: ${JSON.stringify(route.matchers)}${column(route)} },`,
518
520
  )
519
521
  }
520
522
  return rows.join('\n')
@@ -2,6 +2,7 @@ import { tablePath } from '#server/abideModules.ts'
2
2
  import type { AppPackage } from '#server/config.ts'
3
3
  import type { SourceMaps } from '#server/installSourceMaps.ts'
4
4
  import { SERVER_TABLE } from '#server/TABLES.ts'
5
+
5
6
  import abidePackage from '../../package.json' with { type: 'json' }
6
7
 
7
8
  // What the executable starts from: `abide compile`'s, and `abide bundle`'s window over it (38.70).
@@ -120,7 +121,7 @@ import * as table from ${JSON.stringify(tablePath(app.root, SERVER_TABLE))}
120
121
  return `// Written by \`abide compile\`. The whole app, as what one executable holds.
121
122
  ${imports}
122
123
  // 38.56 — a file declaring handlers as a script names it through the app's one alias:
123
- // \`#src/invoices/getInvoice.ts\`, to the module it is.
124
+ // \`#src/invoices/getInvoice.handler.ts\`, to the module it is.
124
125
  const declared = {}
125
126
  for (const row of table.handlers) declared[\`#src/\${row.source}\`] = row.load
126
127
 
@@ -1,4 +1,5 @@
1
1
  import { dirname } from 'node:path' // bun exposes no path helpers of its own
2
+
2
3
  import { type AbideFile, scriptBodies } from './parseAbide.ts'
3
4
  import { abideImports } from './splitImports.ts'
4
5
 
@@ -14,18 +14,19 @@ import {
14
14
  isVariableDeclaration,
15
15
  } from 'typescript/unstable/ast/is'
16
16
  import {
17
- type Symbol as CheckedSymbol,
18
17
  type Checker,
19
18
  ObjectFlags,
20
19
  type ObjectType,
21
20
  type Project,
22
21
  SignatureKind,
22
+ type Symbol as CheckedSymbol,
23
23
  SymbolFlags,
24
24
  type Type,
25
25
  TypeFlags,
26
26
  type TypeReference,
27
27
  type UnionOrIntersectionType,
28
28
  } from 'typescript/unstable/async'
29
+
29
30
  import type { Mount } from '#server/answerRpc.ts'
30
31
  import type { CopyOptions } from '#shared/CopyOptions.ts'
31
32
  import {
@@ -33,6 +34,7 @@ import {
33
34
  defaultsInSource,
34
35
  } from '#shared/reactive/parameterDefaults.ts'
35
36
  import type { Method } from '#shared/rpc.ts'
37
+
36
38
  import { aliasedSymbol } from './aliasedSymbol.ts'
37
39
  import { defaultedMembers, parameterPattern } from './argsDefaultsIn.ts'
38
40
  import {
@@ -42,7 +44,6 @@ import {
42
44
  type Walk,
43
45
  } from './argsSchemaOf.ts'
44
46
  import { diagnosisAt } from './lineOf.ts'
45
- import type { Refusals } from './Refusals.ts'
46
47
  import { isReactiveCall, METHODS, optionNamed } from './reactiveOptions.ts'
47
48
  import {
48
49
  abideIn,
@@ -50,6 +51,7 @@ import {
50
51
  isReactiveMember,
51
52
  valueTypeOf,
52
53
  } from './reactiveTypes.ts'
54
+ import type { Refusals } from './Refusals.ts'
53
55
  import type { Diagnosis } from './TypeSession.ts'
54
56
 
55
57
  /**