@abide/abide 0.117.1 → 0.118.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abide/abide",
3
- "version": "0.117.1",
3
+ "version": "0.118.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,5 @@
1
1
  import { expect, test } from 'bun:test'
2
- import getGreeting from './getGreeting.ts'
2
+ import getGreeting from './getGreeting.handler.ts'
3
3
 
4
4
  test('the handler answers with the name it is given', async () => {
5
5
  expect(await getGreeting({ name: 'Ada' })).toBe('Hello, Ada')
@@ -1,6 +1,6 @@
1
1
  <script>
2
2
  import { reactive } from '@abide/abide'
3
- import getGreeting from '#src/getGreeting.ts'
3
+ import getGreeting from '#src/getGreeting.handler.ts'
4
4
 
5
5
  let count = reactive(0)
6
6
  const greeting = reactive(() => getGreeting({ name: 'world' }))
@@ -167,7 +167,7 @@ function copiedFrom(
167
167
  * markup, or a read the lowering rewrote. Runs are ascending by where they stand, not by where they
168
168
  * came from, so this is a scan.
169
169
  */
170
- function mapForward(
170
+ export function mapForward(
171
171
  origins: readonly Origin[],
172
172
  position: number,
173
173
  ): number | undefined {
@@ -21,9 +21,10 @@ import {
21
21
  type Type,
22
22
  } from 'typescript/unstable/async'
23
23
  import { placeOf } from '#server/placement.ts'
24
+ import { parseRoutePattern } from '#shared/routePattern.ts'
24
25
  import { aliasedSymbol } from './aliasedSymbol.ts'
25
26
  import { AMBIENT_SOURCES, ambientReadsIn } from './ambientReadsIn.ts'
26
- import type { AppLayout, PlacedSource } from './appLayout.ts'
27
+ import type { AppLayout, AppRoute, PlacedSource } from './appLayout.ts'
27
28
  import { argsDefaultsIn } from './argsDefaultsIn.ts'
28
29
  import { type CheckerAnswer, UNCHECKED } from './CheckerAnswer.ts'
29
30
  import {
@@ -38,10 +39,15 @@ import { CHECKED_VALUES } from './Lowering.ts'
38
39
  import { diagnosisAt, lineOf } from './lineOf.ts'
39
40
  import type { AbideFile } from './parseAbide.ts'
40
41
  import { projectShadow, type Span } from './projectShadow.ts'
41
- import { abideIn } from './reactiveTypes.ts'
42
+ import { type Abide, abideIn, isReactiveType } from './reactiveTypes.ts'
42
43
  import type { ParsedSource, ReadSource } from './readSources.ts'
43
44
  import { readsIn, type ShadowSpans } from './readsIn.ts'
44
- import { CHECKED_SUFFIX, SHADOW_SUFFIX, ShadowMap } from './ShadowMap.ts'
45
+ import {
46
+ CHECKED_SUFFIX,
47
+ mapForward,
48
+ SHADOW_SUFFIX,
49
+ ShadowMap,
50
+ } from './ShadowMap.ts'
45
51
  import { SOURCE_SUFFIX } from './splitImports.ts'
46
52
  import { type TemplateContext, templateContext } from './templateContext.ts'
47
53
  import { templateWarnings } from './templateWarnings.ts'
@@ -711,56 +717,144 @@ export class TypeSession {
711
717
  if (queries.every((query) => query === undefined))
712
718
  return { answers, warnings }
713
719
 
714
- const project = await this.currentProject(created, changed)
720
+ let project = await this.currentProject(created, changed)
715
721
  if (project === undefined) return { answers, warnings }
716
- const checker = project.checker
717
- const abide = await abideIn(project)
718
-
719
- await Promise.all(
720
- queries.map(async (query, at) => {
721
- if (query === undefined) return
722
- const source = sources[at] as (typeof sources)[number]
723
- const sourceFile = await project.program.getSourceFile(
722
+ // Per source, its compiler warnings, the bindings the projection could not type
723
+ // (`bindingsOfReads`), and those the lowered program proved a `Reactive`.
724
+ const warned: Diagnosis[][] = sources.map(() => [])
725
+ const suspects: number[][] = sources.map(() => [])
726
+ const held: Set<number>[] = sources.map(() => new Set())
727
+ const answer = async (
728
+ asked: Project,
729
+ abide: Abide,
730
+ at: number,
731
+ ): Promise<void> => {
732
+ const query = queries[at]
733
+ if (query === undefined) return
734
+ const source = sources[at] as (typeof sources)[number]
735
+ const sourceFile = await asked.program.getSourceFile(
736
+ query.map.shadowPath,
737
+ )
738
+ // The shadow has to be INSIDE the opened project or the checker never reads it.
739
+ // Refusing loudly rather than answering nothing: no reads is a file whose every
740
+ // name lowers as written, which renders `[object Object]` where a value was due —
741
+ // a wrong page, with nothing about it saying so.
742
+ if (sourceFile === undefined)
743
+ throw new Error(
744
+ `abide: ${source.path} is not inside the project ${this.tsconfig}, so no name in it can be typed (RULEBOOK 31.3). Give the app a tsconfig whose include reaches it.`,
745
+ )
746
+ const read = await readsIn(
747
+ asked.checker,
748
+ abide,
749
+ sourceFile,
750
+ query.map.shadowPath,
751
+ query.spans,
752
+ source.file,
753
+ held[at] as Set<number>,
754
+ )
755
+ answers[at] = read.answer
756
+ suspects[at] = read.suspects
757
+ const placed: Diagnosis[] = []
758
+ for (const warning of read.warnings) {
759
+ const offset = query.map.toSource(
724
760
  query.map.shadowPath,
761
+ warning.at,
762
+ )
763
+ if (
764
+ source.text === undefined ||
765
+ !isIgnored(source.text, offset, warning.message)
725
766
  )
726
- // The shadow has to be INSIDE the opened project or the checker never reads it.
727
- // Refusing loudly rather than answering nothing: no reads is a file whose every
728
- // name lowers as written, which renders `[object Object]` where a value was due —
729
- // a wrong page, with nothing about it saying so.
730
- if (sourceFile === undefined)
731
- throw new Error(
732
- `abide: ${source.path} is not inside the project ${this.tsconfig}, so no name in it can be typed (RULEBOOK 31.3). Give the app a tsconfig whose include reaches it.`,
767
+ placed.push(
768
+ diagnosisAt(
769
+ source.path,
770
+ source.text,
771
+ offset,
772
+ 'warning',
773
+ warning.message,
774
+ ),
733
775
  )
734
- const read = await readsIn(
735
- checker,
736
- abide,
737
- sourceFile,
738
- query.map.shadowPath,
739
- query.spans,
776
+ }
777
+ warned[at] = placed
778
+ }
779
+ const every = queries.map((_, at) => at)
780
+ const first = project
781
+ const firstAbide = await abideIn(first)
782
+ await Promise.all(every.map((at) => answer(first, firstAbide, at)))
783
+ // 31.1 — A BINDING THE PROJECTION TYPED FROM A `Reactive` IS ASKED OF THE PROGRAM IT RUNS
784
+ // AS: each file holding one is lowered with what it has answered so far, and a binding that
785
+ // is a `Reactive` there is one in the projection too. A binding typed from one that just
786
+ // turned out to be a `Reactive` is asked again, until a round turns none. Nothing is asked,
787
+ // and no second snapshot taken, where no file holds one (`bindingsOfReads`).
788
+ for (;;) {
789
+ const asking = every.filter(
790
+ (at) => (suspects[at] as number[]).length > 0,
791
+ )
792
+ if (asking.length === 0) break
793
+ const loweredCreated: string[] = []
794
+ const loweredChanged: string[] = []
795
+ const lowered = asking.map((at) => {
796
+ const source = sources[at] as (typeof sources)[number]
797
+ const query = queries[at] as { map: ShadowMap }
798
+ const shadow = checkedShadow(
740
799
  source.file,
800
+ answers[at] as CheckerAnswer,
741
801
  )
742
- answers[at] = read.answer
743
- for (const warning of read.warnings) {
744
- const offset = query.map.toSource(
745
- query.map.shadowPath,
746
- warning.at,
747
- )
748
- if (
749
- source.text === undefined ||
750
- !isIgnored(source.text, offset, warning.message)
751
- )
752
- warnings.push(
753
- diagnosisAt(
754
- source.path,
755
- source.text,
756
- offset,
757
- 'warning',
758
- warning.message,
759
- ),
760
- )
802
+ this.register(
803
+ query.map.checkedPath,
804
+ shadow.text,
805
+ loweredCreated,
806
+ loweredChanged,
807
+ )
808
+ this.openOutside(
809
+ source.path,
810
+ query.map.checkedPath,
811
+ loweredChanged,
812
+ )
813
+ return {
814
+ at,
815
+ path: query.map.checkedPath,
816
+ origins: shadow.origins,
761
817
  }
762
- }),
763
- )
818
+ })
819
+ project = await this.currentProject(loweredCreated, loweredChanged)
820
+ if (project === undefined) return { answers, warnings }
821
+ const asked = project
822
+ const abide = await abideIn(asked)
823
+ const turned = new Set<number>()
824
+ await Promise.all(
825
+ lowered.map(async ({ at, path, origins }) => {
826
+ const sourceFile = await asked.program.getSourceFile(path)
827
+ if (sourceFile === undefined) return
828
+ for (const start of suspects[at] as number[]) {
829
+ const position = mapForward(origins, start)
830
+ if (position === undefined) continue
831
+ const symbol = await asked.checker.getSymbolAtLocation(
832
+ getTouchingPropertyName(sourceFile, position),
833
+ )
834
+ const type =
835
+ symbol === undefined
836
+ ? undefined
837
+ : await asked.checker.getTypeOfSymbol(symbol)
838
+ if (
839
+ type !== undefined &&
840
+ (await isReactiveType(
841
+ asked.checker,
842
+ abide,
843
+ type,
844
+ false,
845
+ ))
846
+ ) {
847
+ ;(held[at] as Set<number>).add(start)
848
+ turned.add(at)
849
+ }
850
+ }
851
+ }),
852
+ )
853
+ if (turned.size === 0) break
854
+ await Promise.all([...turned].map((at) => answer(asked, abide, at)))
855
+ for (const at of asking) if (!turned.has(at)) suspects[at] = []
856
+ }
857
+ for (const placed of warned) warnings.push(...placed)
764
858
  return { answers, warnings }
765
859
  }
766
860
 
@@ -881,7 +975,7 @@ export class TypeSession {
881
975
  Partial<Pick<PlacedSource, 'kind' | 'route'>> & {
882
976
  quiet?: boolean
883
977
  })[],
884
- app: Pick<AppLayout, 'root' | 'addresses' | 'handlers'>,
978
+ app: Pick<AppLayout, 'root' | 'addresses' | 'handlers' | 'routes'>,
885
979
  scripts = true,
886
980
  ): Promise<{ diagnoses: Diagnosis[]; refused: Set<string> }> {
887
981
  using _held = this.hold()
@@ -940,7 +1034,7 @@ export class TypeSession {
940
1034
  // module augments one.
941
1035
  this.register(
942
1036
  `${this.canonical(within)}/${ROUTES_DECLARATION}`,
943
- routesDeclaration(app.addresses),
1037
+ routesDeclaration(app.addresses, app.routes),
944
1038
  created,
945
1039
  changed,
946
1040
  )
@@ -1477,13 +1571,45 @@ function isFile(path: string): boolean {
1477
1571
  const ROUTES_DECLARATION = 'abide-routes.d.ts'
1478
1572
  const URL_SOURCE = Bun.fileURLToPath(new URL('../shared/url', import.meta.url))
1479
1573
 
1480
- /** 30.26 — each of `addresses` as a key of abide's `Routes`. */
1481
- function routesDeclaration(addresses: readonly string[]): string {
1574
+ /**
1575
+ * 30.26 — each of `addresses` as a key of abide's `Routes`, and 23.9, 30.3 — a page route's params
1576
+ * narrowed where its matcher is an alternation of literals, which is the one matcher a type can say.
1577
+ */
1578
+ function routesDeclaration(
1579
+ addresses: readonly string[],
1580
+ routes: readonly AppRoute[],
1581
+ ): string {
1582
+ const narrowed = new Map<string, string>()
1583
+ for (const route of routes) {
1584
+ let members = ''
1585
+ for (const segment of parseRoutePattern(route.pattern)) {
1586
+ if (segment.kind !== 'required' && segment.kind !== 'optional')
1587
+ continue
1588
+ const literals = literalsOf(
1589
+ segment.matcher === undefined
1590
+ ? undefined
1591
+ : route.matchers[segment.matcher],
1592
+ )
1593
+ if (literals === undefined) continue
1594
+ members += ` ${JSON.stringify(segment.name)}${segment.kind === 'optional' ? '?' : ''}: ${literals};`
1595
+ }
1596
+ if (members !== '') narrowed.set(route.pattern, `{${members} }`)
1597
+ }
1482
1598
  let keys = ''
1483
1599
  for (const route of addresses)
1484
- keys += ` ${JSON.stringify(route)}: true\n`
1600
+ keys += ` ${JSON.stringify(route)}: ${narrowed.get(route) ?? 'true'}\n`
1485
1601
  return `export {}\ndeclare module ${JSON.stringify(URL_SOURCE)} {\n interface Routes {\n${keys} }\n}\n`
1486
1602
  }
1603
+
1604
+ /** The union of the literals an anchored matcher source alternates between, or `undefined`. */
1605
+ function literalsOf(source: string | undefined): string | undefined {
1606
+ const body = source?.match(/^\^\(\?!\\\.\\\.\?\$\)\(\?:(.*)\)\$$/)?.[1]
1607
+ if (body === undefined || !/^[\w~-]+(?:\|[\w~-]+)*$/.test(body)) return undefined
1608
+ return body
1609
+ .split('|')
1610
+ .map((literal) => JSON.stringify(literal))
1611
+ .join(' | ')
1612
+ }
1487
1613
  const AMBIENT_DECLARATIONS = `declare module '*.css' {}
1488
1614
  declare module '*.md' {
1489
1615
  const text: string
@@ -16,6 +16,7 @@ import {
16
16
  import { abideImportsOf, componentGraph } from './componentGraph.ts'
17
17
  import type { HandlerFile } from './handlersUnder.ts'
18
18
  import { hookCalls, hooksIn } from './hookCalls.ts'
19
+ import { type MountedTree, matchersOf } from './matchersOf.ts'
19
20
  import { mountsOf } from './mountsOf.ts'
20
21
  import { type AbideFile, scriptBodies } from './parseAbide.ts'
21
22
  import { Refusals } from './Refusals.ts'
@@ -49,6 +50,8 @@ export type PlacedHandler = HandlerFile & {
49
50
  scope: string
50
51
  text: string
51
52
  hooks: string[]
53
+ /** 23.26 — each matcher its route names, to the anchored source the route tables carry. */
54
+ matchers: Record<string, string>
52
55
  }
53
56
 
54
57
  /** A page or an error page and the layouts it renders inside. */
@@ -64,6 +67,8 @@ export type AppRoute = {
64
67
  * cannot be declared twice.
65
68
  */
66
69
  layouts: string[]
70
+ /** 23.26 — each matcher its pattern names, to the anchored source the route tables carry. */
71
+ matchers: Record<string, string>
67
72
  }
68
73
 
69
74
  /** The app at a root, laid out (`appLayout`). */
@@ -105,7 +110,10 @@ export async function appLayout(root: string): Promise<AppLayout> {
105
110
  // COLLECTED, whoever asks: a layout is kept across checks (`AppChecker`), and a build adopts
106
111
  // what it found (`Refusals.adopt`).
107
112
  const refusals = new Refusals(root, true)
108
- const { abide, handlers, modules, published } = appFiles(root, refusals)
113
+ const { abide, handlers, params, modules, published } = appFiles(
114
+ root,
115
+ refusals,
116
+ )
109
117
  await refuseModuleHooks(root, modules, refusals)
110
118
  const resolve = resolverIn()
111
119
  const unparsed = new Refusals(root, true)
@@ -149,10 +157,43 @@ export async function appLayout(root: string): Promise<AppLayout> {
149
157
  scope: placement(file.relative, file.position).scope,
150
158
  text,
151
159
  hooks: hooksIn([text]),
160
+ matchers: {},
152
161
  })
153
162
  }
154
163
  refuseStrayLauncher(sources, placedHandlers, refusals)
155
164
  const { routes, errors } = routesOf(sources, refusals)
165
+ // 23.26 — every routed file's matchers, a layout's among them for 23.27's sake though no table
166
+ // carries a layout's own.
167
+ const routed: {
168
+ path: string
169
+ relative: string
170
+ position: string
171
+ route: string
172
+ }[] = []
173
+ const relativeToPath = new Map<string, string>()
174
+ for (const source of sources) {
175
+ if (source.route === undefined) continue
176
+ relativeToPath.set(source.relative, source.path)
177
+ routed.push({
178
+ path: source.path,
179
+ relative: source.relative,
180
+ position: source.position,
181
+ route: source.route,
182
+ })
183
+ }
184
+ for (const handler of placedHandlers) routed.push(handler)
185
+ const matchers = await matchersOf(
186
+ root,
187
+ routed,
188
+ params.map((path) => `${root}/${path}`),
189
+ found.trees,
190
+ refusals,
191
+ )
192
+ for (const handler of placedHandlers)
193
+ handler.matchers = matchers.get(handler.path) ?? {}
194
+ for (const route of [...routes, ...errors])
195
+ route.matchers =
196
+ matchers.get(relativeToPath.get(route.source) ?? '') ?? {}
156
197
  const addresses = new Set<string>()
157
198
  for (const route of routes) addresses.add(route.pattern)
158
199
  for (const address of published.values()) addresses.add(address)
@@ -215,6 +256,8 @@ export async function withDependencies(
215
256
  handlers: HandlerFile[]
216
257
  /** 23.19 — each mounted `.abide`'s position, by its path; one at its own path is not here. */
217
258
  positions: Map<string, string>
259
+ /** 23.26 — each mounted directory, where it mounts, and the param files under it. */
260
+ trees: MountedTree[]
218
261
  }> {
219
262
  const all = [...parsed]
220
263
  const handlers: HandlerFile[] = []
@@ -231,6 +274,7 @@ export async function withDependencies(
231
274
  const positions = new Map<string, string>()
232
275
  // 23.20 — a directory mounts once, and by whom.
233
276
  const mountedBy = new Map<string, string>()
277
+ const trees: MountedTree[] = []
234
278
  const add = async (path: string): Promise<void> => {
235
279
  let text: string
236
280
  try {
@@ -268,6 +312,11 @@ export async function withDependencies(
268
312
  continue
269
313
  }
270
314
  mountedBy.set(mount.directory, position)
315
+ trees.push({
316
+ directory: mount.directory,
317
+ at: mount.at,
318
+ params: mount.params,
319
+ })
271
320
  for (const placed of mount.handlers) {
272
321
  // Two mounts can overlap, one directory inside the other; a file is placed once.
273
322
  if (handled.has(placed.path)) continue
@@ -292,7 +341,7 @@ export async function withDependencies(
292
341
  if (!byPath.has(target)) await add(target)
293
342
  }
294
343
  }
295
- return { sources: all, handlers, positions }
344
+ return { sources: all, handlers, positions, trees }
296
345
  }
297
346
 
298
347
  /** Whether a page or an error page has anything to paint: a template, or a head. 23.3, 35.14. */
@@ -450,6 +499,7 @@ function routesOf(
450
499
  pattern,
451
500
  source: source.relative,
452
501
  layouts: around,
502
+ matchers: {},
453
503
  })
454
504
  }
455
505
  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))
@@ -514,7 +514,7 @@ function tableOf(
514
514
  (layout) => `() => import(${tableImport(layout)})`,
515
515
  )
516
516
  rows.push(
517
- ` { pattern: ${JSON.stringify(route.pattern)}, load: () => import(${tableImport(route.source)}), layouts: [${wrapping.join(', ')}]${column(route)} },`,
517
+ ` { pattern: ${JSON.stringify(route.pattern)}, load: () => import(${tableImport(route.source)}), layouts: [${wrapping.join(', ')}], matchers: ${JSON.stringify(route.matchers)}${column(route)} },`,
518
518
  )
519
519
  }
520
520
  return rows.join('\n')
@@ -120,7 +120,7 @@ import * as table from ${JSON.stringify(tablePath(app.root, SERVER_TABLE))}
120
120
  return `// Written by \`abide compile\`. The whole app, as what one executable holds.
121
121
  ${imports}
122
122
  // 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.
123
+ // \`#src/invoices/getInvoice.handler.ts\`, to the module it is.
124
124
  const declared = {}
125
125
  for (const row of table.handlers) declared[\`#src/\${row.source}\`] = row.load
126
126
 
@@ -45,7 +45,7 @@ export async function handlersUnder(
45
45
  sources: readonly Source[],
46
46
  files: readonly Pick<
47
47
  PlacedHandler,
48
- 'path' | 'relative' | 'route' | 'scope'
48
+ 'path' | 'relative' | 'route' | 'scope' | 'matchers'
49
49
  >[],
50
50
  refusals: Refusals,
51
51
  ): Promise<{ mounts: Mount[]; warnings: Diagnosis[] }> {
@@ -84,6 +84,7 @@ export async function handlersUnder(
84
84
  relative,
85
85
  route: address,
86
86
  scope,
87
+ matchers,
87
88
  } = files[at] as (typeof files)[number]
88
89
  const exported = declared[at]
89
90
  if (exported === undefined) continue
@@ -99,7 +100,7 @@ export async function handlersUnder(
99
100
  )
100
101
  if (handler?.method === undefined)
101
102
  throw new Error(
102
- `abide: ${relative} is a handler file — a .ts directly in an address directory — and exports no handler as its default. Export one (\`export default GET(…)\`), or move the file into a lib directory. RULEBOOK 16.73, 23.22.`,
103
+ `abide: ${relative} is a handler file — a <name>.handler.ts directly in an address directory — and exports no handler as its default. Export one (\`export default GET(…)\`), or move the file into a lib directory. RULEBOOK 16.73, 23.22.`,
103
104
  )
104
105
  const framework = frameworkRouteRefusal(address, relative)
105
106
  if (framework !== undefined) throw framework
@@ -119,6 +120,7 @@ export async function handlersUnder(
119
120
  name: address.slice(address.lastIndexOf('/') + 1),
120
121
  export: 'default',
121
122
  scope,
123
+ matchers,
122
124
  private: false,
123
125
  ui: handler.ui,
124
126
  cli: handler.cli,
@@ -150,6 +152,7 @@ export async function handlersUnder(
150
152
  name,
151
153
  export: each.name,
152
154
  scope,
155
+ matchers: {},
153
156
  private: true,
154
157
  ui: each.ui,
155
158
  cli: false,