@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/CHANGELOG.md +18 -0
- package/docs/REGISTRY.md +16 -14
- package/docs/RULEBOOK.md +49 -10
- package/docs/abide.md +246 -156
- package/package.json +1 -1
- package/scaffold/AGENTS.md +1 -1
- package/scaffold/src/getGreeting.test.ts +1 -1
- package/scaffold/src/index.abide +1 -1
- package/src/compiler/ShadowMap.ts +1 -1
- package/src/compiler/TypeSession.ts +177 -51
- package/src/compiler/appLayout.ts +52 -2
- package/src/compiler/appMap.ts +15 -0
- package/src/compiler/compileApp.ts +1 -1
- package/src/compiler/compiledEntry.ts +1 -1
- package/src/compiler/handlersUnder.ts +5 -2
- package/src/compiler/matchersOf.ts +147 -0
- package/src/compiler/mountsOf.ts +3 -0
- package/src/compiler/paramFile.ts +124 -0
- package/src/compiler/projectShadow.ts +1 -1
- package/src/compiler/readsIn.ts +90 -2
- package/src/compiler/serveLanguage.ts +2 -2
- package/src/server/answerRequest.ts +42 -5
- package/src/server/answerRpc.ts +3 -0
- package/src/server/appTree.ts +15 -8
- package/src/server/generatedSurfaces.ts +75 -7
- package/src/server/placement.ts +12 -2
- package/src/server/remote.ts +6 -2
- package/src/server/surfaceOf.ts +5 -1
- package/src/shared/filledPath.ts +16 -10
- package/src/shared/matchRoute.ts +16 -4
- package/src/shared/reactive/Reactive.ts +4 -0
- package/src/shared/routePattern.ts +66 -14
- package/src/shared/url.ts +24 -7
- /package/scaffold/src/{getGreeting.ts → getGreeting.handler.ts} +0 -0
package/package.json
CHANGED
package/scaffold/AGENTS.md
CHANGED
|
@@ -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
|
|
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')
|
package/scaffold/src/index.abide
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
720
|
+
let project = await this.currentProject(created, changed)
|
|
715
721
|
if (project === undefined) return { answers, warnings }
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
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
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
767
|
+
placed.push(
|
|
768
|
+
diagnosisAt(
|
|
769
|
+
source.path,
|
|
770
|
+
source.text,
|
|
771
|
+
offset,
|
|
772
|
+
'warning',
|
|
773
|
+
warning.message,
|
|
774
|
+
),
|
|
733
775
|
)
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
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
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
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
|
-
/**
|
|
1481
|
-
|
|
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(
|
|
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 }
|
package/src/compiler/appMap.ts
CHANGED
|
@@ -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,
|