@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
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// 23.26, 23.27 — WHICH MATCHER EACH SEGMENT NAMES, resolved once at build and carried into the route
|
|
2
|
+
// tables as text, so nothing at run time looks a name up. A segment's `=NAME` is the param file
|
|
3
|
+
// `NAME.param.ts` in the directory holding that segment or the nearest one above it, below the root
|
|
4
|
+
// the file is filed under: the app's root for the app's own, and a mounted directory for the files
|
|
5
|
+
// under it (23.19), so a host's matcher never decides a mounted package's segment.
|
|
6
|
+
//
|
|
7
|
+
// ONE MATCHER PER POSITION across every file filed there (23.27): a layout, a page and a handler
|
|
8
|
+
// directory at `/holdings/[symbol=TICKER]` share the segment, and a page that took `BRK.B` under a
|
|
9
|
+
// layout whose segment refused it would be wrapped by a layout no address of its own reaches.
|
|
10
|
+
|
|
11
|
+
import { parseRoutePattern } from '#shared/routePattern.ts'
|
|
12
|
+
import { matcherOf } from './paramFile.ts'
|
|
13
|
+
import type { Refusals } from './Refusals.ts'
|
|
14
|
+
|
|
15
|
+
/** A directory a file mounts (23.19), where it mounts, and the param files under it, by path. */
|
|
16
|
+
export type MountedTree = { directory: string; at: string; params: string[] }
|
|
17
|
+
|
|
18
|
+
/** A file with a route: its path, the name refusals give it, and the position it answers by. */
|
|
19
|
+
type Routed = { path: string; relative: string; position: string; route: string }
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Each routed file's matchers, by its path: every name its route's segments give, to the anchored
|
|
23
|
+
* source of the param file it resolves to — empty where its route names none.
|
|
24
|
+
*
|
|
25
|
+
* `params` is every param file of the app's own, by path. Refusals land at the file the fault is in:
|
|
26
|
+
* a param file that declares no matcher at itself, a name that resolves to none at the segment's file.
|
|
27
|
+
*/
|
|
28
|
+
export async function matchersOf(
|
|
29
|
+
root: string,
|
|
30
|
+
routed: readonly Routed[],
|
|
31
|
+
params: readonly string[],
|
|
32
|
+
trees: readonly MountedTree[],
|
|
33
|
+
refusals: Refusals,
|
|
34
|
+
): Promise<Map<string, Record<string, string>>> {
|
|
35
|
+
// Every param file's matcher, read once whichever routes name it; `undefined` where 23.24 or
|
|
36
|
+
// 23.25 refused the file, so a segment naming it refuses nothing more.
|
|
37
|
+
const sources = new Map<string, string | undefined>()
|
|
38
|
+
const all = [...params]
|
|
39
|
+
for (const tree of trees) all.push(...tree.params)
|
|
40
|
+
for (const path of all) {
|
|
41
|
+
if (sources.has(path)) continue
|
|
42
|
+
const matcher = matcherOf(labelOf(root, path), await Bun.file(path).text())
|
|
43
|
+
if (matcher instanceof Error) {
|
|
44
|
+
refusals.at(path, matcher)
|
|
45
|
+
sources.set(path, undefined)
|
|
46
|
+
} else sources.set(path, matcher)
|
|
47
|
+
}
|
|
48
|
+
const byPath = new Map<string, Record<string, string>>()
|
|
49
|
+
// 23.27 — what each parameter position resolved to, and the first file that said so.
|
|
50
|
+
const atPosition = new Map<string, { found: string; file: string }>()
|
|
51
|
+
for (const file of routed) {
|
|
52
|
+
const matchers: Record<string, string> = {}
|
|
53
|
+
const tree = treeOf(file.path, trees)
|
|
54
|
+
const positionParts = file.position.split('/')
|
|
55
|
+
let shape = ''
|
|
56
|
+
let at = 0
|
|
57
|
+
for (const segment of parseRoutePattern(file.route)) {
|
|
58
|
+
shape += `/${segment.kind === 'static' ? segment.name : `[${segment.kind}]`}`
|
|
59
|
+
const index = at
|
|
60
|
+
at += 1
|
|
61
|
+
if (segment.kind === 'static') continue
|
|
62
|
+
const found =
|
|
63
|
+
segment.matcher === undefined
|
|
64
|
+
? ''
|
|
65
|
+
: nearestParam(
|
|
66
|
+
root,
|
|
67
|
+
tree,
|
|
68
|
+
positionParts.slice(0, index),
|
|
69
|
+
segment.matcher,
|
|
70
|
+
sources,
|
|
71
|
+
)
|
|
72
|
+
if (found === undefined) {
|
|
73
|
+
refusals.at(
|
|
74
|
+
file.path,
|
|
75
|
+
new Error(
|
|
76
|
+
`abide: ${file.relative} names the matcher ${segment.matcher}, and no ${segment.matcher}.param.ts is in the directory holding that segment or any directory above it. RULEBOOK 23.26.`,
|
|
77
|
+
),
|
|
78
|
+
)
|
|
79
|
+
continue
|
|
80
|
+
}
|
|
81
|
+
const other = atPosition.get(shape)
|
|
82
|
+
if (other === undefined) atPosition.set(shape, { found, file: file.relative })
|
|
83
|
+
else if (other.found !== found)
|
|
84
|
+
refusals.at(
|
|
85
|
+
file.path,
|
|
86
|
+
new Error(
|
|
87
|
+
`abide: ${other.file} and ${file.relative} decide the parameter at ${shape} by ${describe(root, other.found)} and by ${describe(root, found)}, and one position takes one matcher. RULEBOOK 23.27.`,
|
|
88
|
+
),
|
|
89
|
+
)
|
|
90
|
+
const source = found === '' ? undefined : sources.get(found)
|
|
91
|
+
if (segment.matcher !== undefined && source !== undefined)
|
|
92
|
+
matchers[segment.matcher] = source
|
|
93
|
+
}
|
|
94
|
+
byPath.set(file.path, matchers)
|
|
95
|
+
}
|
|
96
|
+
return byPath
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The param file `NAME.param.ts` a segment at `holding` names, nearest first, or `undefined`.
|
|
101
|
+
*
|
|
102
|
+
* `holding` is the position of the directory holding the segment, and the walk stops at the root
|
|
103
|
+
* the segment is filed under: inside a mounted directory, that directory; a segment of the position
|
|
104
|
+
* the mount itself takes is the mounting app's.
|
|
105
|
+
*/
|
|
106
|
+
function nearestParam(
|
|
107
|
+
root: string,
|
|
108
|
+
tree: MountedTree | undefined,
|
|
109
|
+
holding: string[],
|
|
110
|
+
name: string,
|
|
111
|
+
sources: Map<string, string | undefined>,
|
|
112
|
+
): string | undefined {
|
|
113
|
+
const atParts = tree === undefined ? 0 : tree.at.split('/').length - 1
|
|
114
|
+
const inside = tree !== undefined && holding.length >= atParts
|
|
115
|
+
const base = inside ? tree.directory : root
|
|
116
|
+
const below = inside ? holding.slice(atParts) : holding
|
|
117
|
+
for (let depth = below.length; depth >= 0; depth -= 1) {
|
|
118
|
+
const directory = [base, ...below.slice(0, depth)].join('/')
|
|
119
|
+
const candidate = `${directory}/${name}.param.ts`
|
|
120
|
+
if (sources.has(candidate)) return candidate
|
|
121
|
+
}
|
|
122
|
+
return undefined
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** The mounted directory `path` lies in, the deepest where two nest, or `undefined` for the app's own. */
|
|
126
|
+
function treeOf(
|
|
127
|
+
path: string,
|
|
128
|
+
trees: readonly MountedTree[],
|
|
129
|
+
): MountedTree | undefined {
|
|
130
|
+
let deepest: MountedTree | undefined
|
|
131
|
+
for (const tree of trees)
|
|
132
|
+
if (
|
|
133
|
+
path.startsWith(`${tree.directory}/`) &&
|
|
134
|
+
(deepest === undefined ||
|
|
135
|
+
tree.directory.length > deepest.directory.length)
|
|
136
|
+
)
|
|
137
|
+
deepest = tree
|
|
138
|
+
return deepest
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function labelOf(root: string, path: string): string {
|
|
142
|
+
return path.startsWith(`${root}/`) ? path.slice(root.length + 1) : path
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function describe(root: string, found: string): string {
|
|
146
|
+
return found === '' ? 'no matcher' : labelOf(root, found)
|
|
147
|
+
}
|
package/src/compiler/mountsOf.ts
CHANGED
|
@@ -31,6 +31,8 @@ type DirectoryMount = {
|
|
|
31
31
|
at: string
|
|
32
32
|
files: Placed[]
|
|
33
33
|
handlers: Placed[]
|
|
34
|
+
/** 23.26 — the param files under it, by path: what its own segments' matchers resolve in. */
|
|
35
|
+
params: string[]
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
/**
|
|
@@ -85,6 +87,7 @@ export function mountsOf(
|
|
|
85
87
|
at,
|
|
86
88
|
files: found.abide.map(placed),
|
|
87
89
|
handlers: found.handlers.map(placed),
|
|
90
|
+
params: found.params.map((below) => `${directory}/${below}`),
|
|
88
91
|
})
|
|
89
92
|
}
|
|
90
93
|
return mounts
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// 23.24, 23.25 — A PARAM FILE, READ AND NEVER RUN. `<NAME>.param.ts` says what a segment naming
|
|
2
|
+
// `NAME` accepts, and the build carries that into both route tables as a regex literal, so the
|
|
3
|
+
// file is read off its tokens: one `export const NAME = /…/`, nothing else. A file that does
|
|
4
|
+
// anything more could only be honoured by running it, and nothing about building a route table is
|
|
5
|
+
// a reason to run app code.
|
|
6
|
+
//
|
|
7
|
+
// 30.36 — THE DOT-SEGMENT FLOOR IS WRITTEN INTO THE SOURCE rather than tested beside it, so the walk
|
|
8
|
+
// holds one test per part whatever the segment names: a matcher can lift the period default (30.34)
|
|
9
|
+
// and still never admit `.` or `..`, which the URL parser collapses into another route.
|
|
10
|
+
|
|
11
|
+
import { SyntaxKind } from 'typescript/unstable/ast'
|
|
12
|
+
import { tokenize } from './tokenize.ts'
|
|
13
|
+
|
|
14
|
+
/** The name a param file declares: its file name before `.param.ts`. */
|
|
15
|
+
export function paramNameOf(path: string): string {
|
|
16
|
+
return path.slice(path.lastIndexOf('/') + 1, -'.param.ts'.length)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The anchored source of the matcher the param file at `relative` declares, or why it declares none.
|
|
21
|
+
*
|
|
22
|
+
* Anchored as `^(?!\.\.?$)(?:…)$`: the whole part, percent-decoded, and never a dot-segment.
|
|
23
|
+
*/
|
|
24
|
+
export function matcherOf(relative: string, text: string): string | Error {
|
|
25
|
+
const name = paramNameOf(relative)
|
|
26
|
+
const tokens = tokenize(text)
|
|
27
|
+
const kinds: SyntaxKind[] = []
|
|
28
|
+
for (const token of tokens)
|
|
29
|
+
if (token.kind !== SyntaxKind.EndOfFile) kinds.push(token.kind)
|
|
30
|
+
const declared =
|
|
31
|
+
kinds.length >= 5 &&
|
|
32
|
+
kinds.length <= 6 &&
|
|
33
|
+
kinds[0] === SyntaxKind.ExportKeyword &&
|
|
34
|
+
kinds[1] === SyntaxKind.ConstKeyword &&
|
|
35
|
+
kinds[2] === SyntaxKind.Identifier &&
|
|
36
|
+
kinds[3] === SyntaxKind.EqualsToken &&
|
|
37
|
+
kinds[4] === SyntaxKind.RegularExpressionLiteral &&
|
|
38
|
+
(kinds.length === 5 || kinds[5] === SyntaxKind.SemicolonToken)
|
|
39
|
+
if (!declared)
|
|
40
|
+
return new Error(
|
|
41
|
+
`abide: ${relative} must hold one \`export const ${name} = /…/\` and nothing else, an import included. RULEBOOK 23.24.`,
|
|
42
|
+
)
|
|
43
|
+
const exported = (tokens[2] as { text: string }).text
|
|
44
|
+
if (exported !== name)
|
|
45
|
+
return new Error(
|
|
46
|
+
`abide: ${relative} exports ${exported}, and a param file exports the name it is filed under, ${name}. RULEBOOK 23.24.`,
|
|
47
|
+
)
|
|
48
|
+
const literal = (tokens[4] as { text: string }).text
|
|
49
|
+
const close = literal.lastIndexOf('/')
|
|
50
|
+
if (close !== literal.length - 1)
|
|
51
|
+
return new Error(
|
|
52
|
+
`abide: ${relative} carries the flag ${literal.slice(close + 1)}, and a matcher carries none: a route, OpenAPI and a JSON Schema's \`pattern\` would each read it differently. RULEBOOK 23.24.`,
|
|
53
|
+
)
|
|
54
|
+
const body = literal.slice(1, close)
|
|
55
|
+
const runaway = runawayIn(body)
|
|
56
|
+
if (runaway !== undefined)
|
|
57
|
+
return new Error(
|
|
58
|
+
`abide: ${relative} carries ${runaway}, which can take time exponential in a part's length on an address made to. RULEBOOK 23.25.`,
|
|
59
|
+
)
|
|
60
|
+
return `^(?!\\.\\.?$)(?:${body})$`
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 23.25 — what in a regex's source can backtrack without bound, or `undefined`.
|
|
65
|
+
*
|
|
66
|
+
* A repetition with no upper bound (`*`, `+`, `{n,}`) of a group that itself holds one, and a
|
|
67
|
+
* backreference. Read character by character rather than parsed: a class and an escape are skipped
|
|
68
|
+
* as single atoms, which is all the shape the two questions need.
|
|
69
|
+
*/
|
|
70
|
+
export function runawayIn(body: string): string | undefined {
|
|
71
|
+
// One entry per open group: whether it holds an unbounded repetition.
|
|
72
|
+
const holds: boolean[] = [false]
|
|
73
|
+
// Whether the atom just read is a group holding one.
|
|
74
|
+
let groupHolding = false
|
|
75
|
+
for (let at = 0; at < body.length; at += 1) {
|
|
76
|
+
const char = body[at] as string
|
|
77
|
+
if (char === '\\') {
|
|
78
|
+
const next = body[at + 1] ?? ''
|
|
79
|
+
if ((next >= '1' && next <= '9') || next === 'k')
|
|
80
|
+
return 'a backreference'
|
|
81
|
+
at += 1
|
|
82
|
+
groupHolding = false
|
|
83
|
+
continue
|
|
84
|
+
}
|
|
85
|
+
if (char === '[') {
|
|
86
|
+
at += 1
|
|
87
|
+
while (at < body.length && body[at] !== ']')
|
|
88
|
+
at += body[at] === '\\' ? 2 : 1
|
|
89
|
+
groupHolding = false
|
|
90
|
+
continue
|
|
91
|
+
}
|
|
92
|
+
if (char === '(') {
|
|
93
|
+
holds.push(false)
|
|
94
|
+
groupHolding = false
|
|
95
|
+
continue
|
|
96
|
+
}
|
|
97
|
+
if (char === ')') {
|
|
98
|
+
const inner = holds.pop() ?? false
|
|
99
|
+
if (inner) holds[holds.length - 1] = true
|
|
100
|
+
groupHolding = inner
|
|
101
|
+
continue
|
|
102
|
+
}
|
|
103
|
+
const unbounded =
|
|
104
|
+
char === '*' ||
|
|
105
|
+
char === '+' ||
|
|
106
|
+
(char === '{' && /^\{\d+,\}/.test(body.slice(at)))
|
|
107
|
+
if (unbounded) {
|
|
108
|
+
if (groupHolding) return 'a repetition of a group that repeats'
|
|
109
|
+
holds[holds.length - 1] = true
|
|
110
|
+
groupHolding = false
|
|
111
|
+
continue
|
|
112
|
+
}
|
|
113
|
+
// A bounded `{n}` or `{n,m}` repeats a group a fixed number of times, and keeps what it
|
|
114
|
+
// repeats as the atom a following quantifier would apply to.
|
|
115
|
+
if (char === '{') {
|
|
116
|
+
const end = body.indexOf('}', at)
|
|
117
|
+
if (end !== -1) at = end
|
|
118
|
+
continue
|
|
119
|
+
}
|
|
120
|
+
if (char === '?') continue
|
|
121
|
+
groupHolding = false
|
|
122
|
+
}
|
|
123
|
+
return undefined
|
|
124
|
+
}
|
|
@@ -763,7 +763,7 @@ function refuseExport(source: string): void {
|
|
|
763
763
|
if (moved !== 0) depth += moved
|
|
764
764
|
else if (depth === 0 && text === EXPORT && previous !== '.')
|
|
765
765
|
throw new Error(
|
|
766
|
-
'abide: a .abide cannot export — its component is its one export. A handler another file calls is a handler file (a .ts beside the page, `export default GET(…)`); a value files share is a module under a lib directory. RULEBOOK 34.17.',
|
|
766
|
+
'abide: a .abide cannot export — its component is its one export. A handler another file calls is a handler file (a <name>.handler.ts beside the page, `export default GET(…)`); a value files share is a module under a lib directory. RULEBOOK 34.17.',
|
|
767
767
|
)
|
|
768
768
|
previous = text
|
|
769
769
|
}
|
package/src/compiler/readsIn.ts
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
import {
|
|
29
29
|
type Symbol as CheckedSymbol,
|
|
30
30
|
type Checker,
|
|
31
|
+
SignatureKind,
|
|
31
32
|
SymbolFlags,
|
|
32
33
|
type Type,
|
|
33
34
|
TypeFlags,
|
|
@@ -129,7 +130,9 @@ export async function readsIn(
|
|
|
129
130
|
shadowPath: string,
|
|
130
131
|
spans: ShadowSpans,
|
|
131
132
|
file: AbideFile,
|
|
132
|
-
|
|
133
|
+
/** Where each name the lowered program proved a `Reactive` starts (`bindingsOfReads`). */
|
|
134
|
+
held: ReadonlySet<number>,
|
|
135
|
+
): Promise<{ answer: CheckerAnswer; warnings: Finding[]; suspects: number[] }> {
|
|
133
136
|
// A destructuring pattern binds names this does not see, and that is 31.12's case.
|
|
134
137
|
const names: Node[] = []
|
|
135
138
|
const spelled = new Set<string>()
|
|
@@ -210,6 +213,12 @@ export async function readsIn(
|
|
|
210
213
|
),
|
|
211
214
|
)
|
|
212
215
|
await dropValueHolders(checker, names, reactive)
|
|
216
|
+
for (let at = 0; at < names.length; at += 1)
|
|
217
|
+
if (
|
|
218
|
+
reactive[at] === undefined &&
|
|
219
|
+
held.has((names[at] as Node).getStart())
|
|
220
|
+
)
|
|
221
|
+
reactive[at] = symbols[at]
|
|
213
222
|
// 32.32 — A VALUE WHOSE `reactive(…)` DECLARES `by` keys a `{#for}` over it,
|
|
214
223
|
// wherever the value is declared.
|
|
215
224
|
const byValue = await Promise.all(
|
|
@@ -296,7 +305,86 @@ export async function readsIn(
|
|
|
296
305
|
spans.scripts,
|
|
297
306
|
))
|
|
298
307
|
warnings.push(warning)
|
|
299
|
-
|
|
308
|
+
const answer = new CheckerAnswer(reads, statics)
|
|
309
|
+
return {
|
|
310
|
+
answer,
|
|
311
|
+
warnings,
|
|
312
|
+
suspects: await bindingsOfReads(
|
|
313
|
+
checker,
|
|
314
|
+
abide,
|
|
315
|
+
names,
|
|
316
|
+
reactive,
|
|
317
|
+
answer,
|
|
318
|
+
),
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Where each binding starts whose type the projection cannot vouch for, for the lowered program to
|
|
324
|
+
* answer (`TypeSession.classify`).
|
|
325
|
+
*
|
|
326
|
+
* 31.1 — A BINDING TYPED FROM A READ IS TYPED FROM THE `Reactive` IN THE PROJECTION, and the read is
|
|
327
|
+
* of its value. `reactive(route.url.pathname)` asks a `Reactive<URL>` for a `pathname` it does not
|
|
328
|
+
* declare; the argument is the error type, which `reactive`'s overloads resolve to a `Keyed`, and
|
|
329
|
+
* the binding then lowered as no `Reactive` at all — `bookId = 'y'` rebinding the name and nothing
|
|
330
|
+
* waking. Lowered, the argument is `route.url().pathname` and the binding a `Reactive<string>`.
|
|
331
|
+
*
|
|
332
|
+
* ONLY A CALL THAT CAN HAND BACK A `Reactive` IS ASKED: an un-annotated binding, not one already,
|
|
333
|
+
* initialized by a call holding a read, to a callee one of whose signatures returns a `Reactive`.
|
|
334
|
+
* Anything else is a value however its reads are typed, and asking would cost every page that
|
|
335
|
+
* computes from one a second snapshot.
|
|
336
|
+
*/
|
|
337
|
+
async function bindingsOfReads(
|
|
338
|
+
checker: Checker,
|
|
339
|
+
abide: Abide,
|
|
340
|
+
names: readonly Node[],
|
|
341
|
+
reactive: readonly (CheckedSymbol | undefined)[],
|
|
342
|
+
answer: CheckerAnswer,
|
|
343
|
+
): Promise<number[]> {
|
|
344
|
+
const suspects: number[] = []
|
|
345
|
+
const asked: Promise<void>[] = []
|
|
346
|
+
for (let at = 0; at < names.length; at += 1) {
|
|
347
|
+
const name = names[at] as Node
|
|
348
|
+
const declaration = name.parent
|
|
349
|
+
if (
|
|
350
|
+
reactive[at] !== undefined ||
|
|
351
|
+
!isVariableDeclaration(declaration) ||
|
|
352
|
+
declaration.type !== undefined ||
|
|
353
|
+
declaration.initializer === undefined
|
|
354
|
+
)
|
|
355
|
+
continue
|
|
356
|
+
let call: Node = declaration.initializer
|
|
357
|
+
while (TRANSPARENT.has(call.kind))
|
|
358
|
+
call = (call as unknown as { expression: Node }).expression
|
|
359
|
+
if (
|
|
360
|
+
!isCallExpression(call) ||
|
|
361
|
+
!answer.holdsRead({ start: call.getStart(), end: call.end })
|
|
362
|
+
)
|
|
363
|
+
continue
|
|
364
|
+
const callee = call.expression
|
|
365
|
+
asked.push(
|
|
366
|
+
(async () => {
|
|
367
|
+
const type = await checker.getTypeAtLocation(callee)
|
|
368
|
+
if (type === undefined) return
|
|
369
|
+
for (const signature of await checker.getSignaturesOfType(
|
|
370
|
+
type,
|
|
371
|
+
SignatureKind.Call,
|
|
372
|
+
)) {
|
|
373
|
+
const returned =
|
|
374
|
+
await checker.getReturnTypeOfSignature(signature)
|
|
375
|
+
if (
|
|
376
|
+
returned !== undefined &&
|
|
377
|
+
(await isReactiveType(checker, abide, returned, false))
|
|
378
|
+
) {
|
|
379
|
+
suspects.push(name.getStart())
|
|
380
|
+
return
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
})(),
|
|
384
|
+
)
|
|
385
|
+
}
|
|
386
|
+
await Promise.all(asked)
|
|
387
|
+
return suspects
|
|
300
388
|
}
|
|
301
389
|
|
|
302
390
|
/**
|
|
@@ -129,8 +129,8 @@ export async function serveLanguage(
|
|
|
129
129
|
documents.get(Bun.pathToFileURL(spelled(path)).href)?.text
|
|
130
130
|
|
|
131
131
|
// An app's checker, opened on first use and kept for the session — and TOLD WHAT MOVED ON DISK,
|
|
132
|
-
// or it answers every check from the text it read when it opened: an export added to a
|
|
133
|
-
//
|
|
132
|
+
// or it answers every check from the text it read when it opened: an export added to a `lib`
|
|
133
|
+
// module the page imports is "no exported member" until the editor restarts this. A project whose
|
|
134
134
|
// tsconfig or package.json moved is opened again.
|
|
135
135
|
const checkerOf = async (app: string): Promise<AppChecker> => {
|
|
136
136
|
let checker = checkers.get(app)
|
|
@@ -100,6 +100,8 @@ type ServerRoute = {
|
|
|
100
100
|
// 38.28 — the modules a browser loads at this address, the page's then its layouts', as keys
|
|
101
101
|
// into the manifest `manifestOf` reads.
|
|
102
102
|
clientModules: string[]
|
|
103
|
+
// 23.26 — each matcher its pattern names, to the anchored source the build resolved it to.
|
|
104
|
+
matchers: Record<string, string>
|
|
103
105
|
}
|
|
104
106
|
|
|
105
107
|
// 35.2 — an error page, and the directory it answers for.
|
|
@@ -122,6 +124,7 @@ type Ranked = RankedError & {
|
|
|
122
124
|
*/
|
|
123
125
|
type RouteRow = {
|
|
124
126
|
pattern: string
|
|
127
|
+
matchers: Record<string, string>
|
|
125
128
|
segments: Segment[]
|
|
126
129
|
page: Ranked | undefined
|
|
127
130
|
handlers: Map<Method, Mounted>
|
|
@@ -178,6 +181,16 @@ export type AppSources = {
|
|
|
178
181
|
// 41.29 — the seed sink of a render no page hydrates: every call is left to run when it is made.
|
|
179
182
|
const SEEDS_NOTHING: PageRender['seed'] = () => false
|
|
180
183
|
|
|
184
|
+
// 35.19 — what fetch calls a navigation request's destinations, and a script's `fetch`.
|
|
185
|
+
const PAGE_DESTINATIONS = new Set([
|
|
186
|
+
'document',
|
|
187
|
+
'embed',
|
|
188
|
+
'frame',
|
|
189
|
+
'iframe',
|
|
190
|
+
'object',
|
|
191
|
+
'empty',
|
|
192
|
+
])
|
|
193
|
+
|
|
181
194
|
/** The `Response` to one request: a handler, a surface of abide's own, a file, or a page. */
|
|
182
195
|
export async function answerRequest(
|
|
183
196
|
app: AppSources,
|
|
@@ -331,6 +344,17 @@ async function answerMatched(
|
|
|
331
344
|
headers: { 'cache-control': 'no-store' },
|
|
332
345
|
})
|
|
333
346
|
|
|
347
|
+
// 35.19 — A REQUEST THAT CANNOT DISPLAY A PAGE, such as the `/favicon.ico` a browser asks every
|
|
348
|
+
// site for where a `[param]` page matches it: refused before setup, so no handler runs for it
|
|
349
|
+
// and no caller is minted. Bare, because an error page would set up too and nothing shows it;
|
|
350
|
+
// `no-store`, because the address answers a navigation with the page.
|
|
351
|
+
const destination = request.headers.get('sec-fetch-dest')
|
|
352
|
+
if (destination !== null && !PAGE_DESTINATIONS.has(destination))
|
|
353
|
+
return new Response(null, {
|
|
354
|
+
status: 404,
|
|
355
|
+
headers: { 'cache-control': 'no-store' },
|
|
356
|
+
})
|
|
357
|
+
|
|
334
358
|
// 33.4 — a prop is an ACCESSOR, which is the contract `renderComponent` renders a child under
|
|
335
359
|
// and the one the client router mounts a page under; the params are the record `firstMatch`
|
|
336
360
|
// hands the client for this same address.
|
|
@@ -534,16 +558,22 @@ export async function appSources(parts: SourceParts): Promise<AppSources> {
|
|
|
534
558
|
pattern: route.pattern,
|
|
535
559
|
load: route.load,
|
|
536
560
|
layouts: route.layouts,
|
|
561
|
+
matchers: route.matchers,
|
|
537
562
|
document: documentOf(route.clientModules),
|
|
538
563
|
})
|
|
539
564
|
// 19.1 — the build refused two files at one route and method, so a route is one row however
|
|
540
|
-
// many methods answer at it.
|
|
565
|
+
// many methods answer at it — and 23.27 one matcher per position, so whichever file opens the
|
|
566
|
+
// row brings what every file at it would.
|
|
541
567
|
const rows = new Map<string, Omit<RouteRow, 'segments'>>()
|
|
542
|
-
const rowAt = (
|
|
568
|
+
const rowAt = (
|
|
569
|
+
pattern: string,
|
|
570
|
+
matchers: Record<string, string>,
|
|
571
|
+
): Omit<RouteRow, 'segments'> => {
|
|
543
572
|
let row = rows.get(pattern)
|
|
544
573
|
if (row === undefined) {
|
|
545
574
|
row = {
|
|
546
575
|
pattern,
|
|
576
|
+
matchers,
|
|
547
577
|
page: undefined,
|
|
548
578
|
handlers: new Map(),
|
|
549
579
|
answers: new Map(),
|
|
@@ -552,12 +582,19 @@ export async function appSources(parts: SourceParts): Promise<AppSources> {
|
|
|
552
582
|
}
|
|
553
583
|
return row
|
|
554
584
|
}
|
|
555
|
-
for (const page of rankRoutes(pages))
|
|
585
|
+
for (const page of rankRoutes(pages))
|
|
586
|
+
rowAt(page.pattern, page.matchers).page = page
|
|
556
587
|
for (const handler of mounted.handlers.values())
|
|
557
|
-
rowAt(handler.address).handlers.set(
|
|
588
|
+
rowAt(handler.address, handler.matchers).handlers.set(
|
|
589
|
+
handler.method,
|
|
590
|
+
handler,
|
|
591
|
+
)
|
|
558
592
|
const errors: RankedError[] = []
|
|
559
593
|
for (const error of parts.table.errors)
|
|
560
|
-
errors.push({
|
|
594
|
+
errors.push({
|
|
595
|
+
...error,
|
|
596
|
+
segments: parseRoutePattern(error.pattern, error.matchers),
|
|
597
|
+
})
|
|
561
598
|
errors.sort((a, b) => b.segments.length - a.segments.length)
|
|
562
599
|
const sources: AppSources = {
|
|
563
600
|
routes: rankRoutes([...rows.values()]),
|
package/src/server/answerRpc.ts
CHANGED
|
@@ -72,6 +72,8 @@ export type Mount = {
|
|
|
72
72
|
export: string
|
|
73
73
|
/** The route its hooks are filed at, which its rungs are read over (37.1, 16.72). */
|
|
74
74
|
scope: string
|
|
75
|
+
/** 23.26 — each matcher its address names, to the anchored source its row is ranked with. */
|
|
76
|
+
matchers: Record<string, string>
|
|
75
77
|
/** Whether it is a private handler, listed nowhere (16.71). */
|
|
76
78
|
private: boolean
|
|
77
79
|
/** Whether a browser is handed a wrapper for it. 20.4. */
|
|
@@ -154,6 +156,7 @@ export function mountedOwn(
|
|
|
154
156
|
name,
|
|
155
157
|
export: name,
|
|
156
158
|
scope: '/',
|
|
159
|
+
matchers: {},
|
|
157
160
|
private: false,
|
|
158
161
|
ui: false,
|
|
159
162
|
cli: false,
|
package/src/server/appTree.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// 23.14 — WHERE AN APP'S ADDRESSES ARE, read off the tree in one walk. A file directly in a directory
|
|
2
2
|
// below the root is an address — a page, a layout, an error page or a handler file — and nothing
|
|
3
|
-
// else is there but its tests (23.22); a fence is never one (23.15): `public` holds files answered as
|
|
3
|
+
// else is there but its tests and the param files its segments name (23.22, 23.24); a fence is never one (23.15): `public` holds files answered as
|
|
4
4
|
// authored at the path of the directories above it, and `lib` holds the rest, a server file among it
|
|
5
5
|
// (23.23). So listing a directory lists its routes.
|
|
6
6
|
//
|
|
@@ -22,6 +22,8 @@ import {
|
|
|
22
22
|
import {
|
|
23
23
|
DIRECTORY_FILES,
|
|
24
24
|
isDeclarationFile,
|
|
25
|
+
isHandlerFile,
|
|
26
|
+
isParamFile,
|
|
25
27
|
isServerFile,
|
|
26
28
|
isBenchFile,
|
|
27
29
|
isTestFile,
|
|
@@ -36,6 +38,8 @@ type AppFiles = {
|
|
|
36
38
|
abide: string[]
|
|
37
39
|
/** Every handler file, in path order. */
|
|
38
40
|
handlers: string[]
|
|
41
|
+
/** 23.24 — every param file directly in an address directory, in path order. */
|
|
42
|
+
params: string[]
|
|
39
43
|
/** Every module under a `lib` directory, in path order — read for 37.13. */
|
|
40
44
|
modules: string[]
|
|
41
45
|
/** Every file under a `public` directory, to the address it is answered at below the mount. */
|
|
@@ -52,6 +56,7 @@ export function appFiles(root: string, refusals: Refusals): AppFiles {
|
|
|
52
56
|
const files: AppFiles = {
|
|
53
57
|
abide: [],
|
|
54
58
|
handlers: [],
|
|
59
|
+
params: [],
|
|
55
60
|
modules: [],
|
|
56
61
|
published: new Map(),
|
|
57
62
|
}
|
|
@@ -61,6 +66,7 @@ export function appFiles(root: string, refusals: Refusals): AppFiles {
|
|
|
61
66
|
walk(root, '', undefined, files, refusals, packaged)
|
|
62
67
|
files.abide.sort()
|
|
63
68
|
files.handlers.sort()
|
|
69
|
+
files.params.sort()
|
|
64
70
|
files.modules.sort()
|
|
65
71
|
return files
|
|
66
72
|
}
|
|
@@ -126,13 +132,17 @@ function walk(
|
|
|
126
132
|
isDeclarationFile(path)
|
|
127
133
|
)
|
|
128
134
|
continue
|
|
129
|
-
if (
|
|
135
|
+
if (isParamFile(path)) {
|
|
136
|
+
files.params.push(path)
|
|
137
|
+
continue
|
|
138
|
+
}
|
|
139
|
+
if (!abide && !isHandlerFile(path)) {
|
|
130
140
|
if (packaged && relative === '' && !path.endsWith('.ts'))
|
|
131
141
|
continue
|
|
132
142
|
refusals.at(
|
|
133
143
|
path,
|
|
134
144
|
new Error(
|
|
135
|
-
`abide: ${path} is directly in an address directory, which holds pages, layouts, error pages, handler files, test files, bench files and declaration files alone. Move it into a lib directory, beside it or in any directory above it. RULEBOOK 23.22.`,
|
|
145
|
+
`abide: ${path} is directly in an address directory, which holds pages, layouts, error pages, handler files, param files, test files, bench files and declaration files alone. ${path.endsWith('.ts') ? 'Name a handler file <name>.handler.ts, or move anything else' : 'Move it'} into a lib directory, beside it or in any directory above it. RULEBOOK 23.22.`,
|
|
136
146
|
),
|
|
137
147
|
)
|
|
138
148
|
continue
|
|
@@ -211,8 +221,7 @@ export function holdsApp(root: string): boolean {
|
|
|
211
221
|
return !appSourcesUnder(root, '').next().done
|
|
212
222
|
}
|
|
213
223
|
|
|
214
|
-
// Every `.abide` under `directory`, and every handler file
|
|
215
|
-
// no server file (23.23).
|
|
224
|
+
// Every `.abide` under `directory`, and every handler file whose place is an address.
|
|
216
225
|
function* appSourcesUnder(
|
|
217
226
|
directory: string,
|
|
218
227
|
relative: string,
|
|
@@ -230,9 +239,7 @@ function* appSourcesUnder(
|
|
|
230
239
|
yield* appSourcesUnder(`${directory}/${entry.name}`, `${path}/`)
|
|
231
240
|
else if (
|
|
232
241
|
path.endsWith('.abide') ||
|
|
233
|
-
(
|
|
234
|
-
!isServerFile(path) &&
|
|
235
|
-
placeOf(path) === 'address')
|
|
242
|
+
(isHandlerFile(path) && placeOf(path) === 'address')
|
|
236
243
|
)
|
|
237
244
|
yield path
|
|
238
245
|
}
|