@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.
- package/CHANGELOG.md +22 -0
- package/bin/abide.ts +74 -11
- package/docs/REGISTRY.md +18 -15
- package/docs/RULEBOOK.md +71 -10
- package/docs/abide.md +584 -274
- package/package.json +1 -1
- package/scaffold/AGENTS.md +1 -1
- package/scaffold/src/getGreeting.test.ts +2 -1
- package/scaffold/src/index.abide +5 -2
- package/scaffold/src/index.test.ts +2 -0
- package/scaffold/src/layout.abide +6 -6
- package/src/compiler/Lowering.ts +20 -2
- package/src/compiler/PositionTable.ts +1 -0
- package/src/compiler/Refusals.ts +1 -0
- package/src/compiler/TypeSession.ts +49 -13
- package/src/compiler/aliasedSymbol.ts +1 -1
- package/src/compiler/ambientReadsIn.ts +1 -0
- package/src/compiler/appLayout.ts +55 -3
- package/src/compiler/appMap.ts +15 -0
- package/src/compiler/argsDefaultsIn.ts +1 -0
- package/src/compiler/argsSchemaOf.ts +1 -0
- package/src/compiler/checkApp.ts +3 -1
- package/src/compiler/checkedShadow.ts +2 -1
- package/src/compiler/classifyRead.ts +1 -0
- package/src/compiler/classifyTemplate.ts +1 -0
- package/src/compiler/compileApp.ts +5 -3
- package/src/compiler/compiledEntry.ts +2 -1
- package/src/compiler/componentGraph.ts +1 -0
- package/src/compiler/declaredExports.ts +4 -2
- package/src/compiler/emitClient.ts +19 -14
- package/src/compiler/emitComponent.ts +1 -0
- package/src/compiler/emitSource.ts +2 -1
- package/src/compiler/formatAbide.ts +857 -0
- package/src/compiler/formatDiagnosis.ts +1 -0
- package/src/compiler/handlersUnder.ts +7 -3
- package/src/compiler/hookCalls.ts +1 -0
- package/src/compiler/lineOf.ts +17 -0
- package/src/compiler/matchersOf.ts +157 -0
- package/src/compiler/mountsOf.ts +5 -0
- package/src/compiler/paramFile.ts +125 -0
- package/src/compiler/parseAbide.ts +48 -28
- package/src/compiler/projectShadow.ts +11 -4
- package/src/compiler/reactiveOptions.ts +1 -0
- package/src/compiler/reactiveTypes.ts +2 -1
- package/src/compiler/readsIn.ts +3 -2
- package/src/compiler/renderMarkup.ts +3 -2
- package/src/compiler/resolverIn.ts +2 -0
- package/src/compiler/sameTemplate.ts +150 -0
- package/src/compiler/scaffold.ts +4 -11
- package/src/compiler/serveLanguage.ts +48 -21
- package/src/compiler/sharedModuleWrites.ts +2 -1
- package/src/compiler/sourceMapOf.ts +1 -0
- package/src/compiler/splitImports.ts +2 -0
- package/src/compiler/templateWarnings.ts +2 -2
- package/src/compiler/trackingWarnings.ts +1 -0
- package/src/compiler/writeDefaultProject.ts +5 -1
- package/src/server/BRIDGE_VERSION.ts +1 -1
- package/src/server/abideModules.ts +2 -0
- package/src/server/answerFailure.ts +1 -0
- package/src/server/answerRequest.ts +57 -19
- package/src/server/answerRpc.ts +6 -2
- package/src/server/answerSocket.ts +2 -1
- package/src/server/answerValue.ts +1 -0
- package/src/server/answerWorker.ts +1 -0
- package/src/server/appTree.ts +18 -9
- package/src/server/argsFor.ts +1 -0
- package/src/server/assistant.ts +2 -1
- package/src/server/banner.ts +2 -0
- package/src/server/bridgeRelay.ts +1 -0
- package/src/server/callerOf.ts +1 -0
- package/src/server/claudeApi.ts +1 -0
- package/src/server/claudeCode.ts +1 -0
- package/src/server/clientBundle.ts +1 -0
- package/src/server/coerceArgs.ts +1 -2
- package/src/server/commands.ts +9 -0
- package/src/server/compressAnswer.ts +2 -1
- package/src/server/config.ts +2 -0
- package/src/server/createApp.ts +4 -2
- package/src/server/crossingOrigin.ts +1 -0
- package/src/server/csp.ts +1 -0
- package/src/server/deferredBuilds.ts +1 -0
- package/src/server/derivedArgs.ts +1 -0
- package/src/server/devWorker.ts +2 -1
- package/src/server/faceOf.ts +2 -7
- package/src/server/generatedSurfaces.ts +79 -7
- package/src/server/hooks.ts +1 -0
- package/src/server/iconAccent.ts +1 -1
- package/src/server/installServer.ts +1 -0
- package/src/server/installSourceMaps.ts +1 -0
- package/src/server/launcherPage.ts +2 -2
- package/src/server/manifestOf.ts +1 -0
- package/src/server/muxUpgrade.ts +1 -0
- package/src/server/nativeMenu.ts +2 -0
- package/src/server/placement.ts +12 -2
- package/src/server/principal.ts +2 -1
- package/src/server/remote.ts +7 -2
- package/src/server/render.ts +3 -3
- package/src/server/renderComponent.ts +1 -0
- package/src/server/reportHealth.ts +1 -0
- package/src/server/requestScope.ts +3 -1
- package/src/server/runDesktop.ts +3 -2
- package/src/server/seedBuffer.ts +1 -0
- package/src/server/serveDev.ts +3 -2
- package/src/server/shell.ts +2 -0
- package/src/server/streamEvent.ts +1 -0
- package/src/server/surfaceOf.ts +6 -1
- package/src/server/testBuild.ts +1 -0
- package/src/server/testMount.ts +1 -0
- package/src/server/trace.ts +2 -0
- package/src/server/withAbideSources.ts +1 -0
- package/src/shared/Attributes.ts +12 -8
- package/src/shared/cookies.ts +1 -1
- package/src/shared/escapeText.ts +1 -1
- package/src/shared/filledPath.ts +16 -10
- package/src/shared/index.ts +1 -5
- package/src/shared/jsonSchema.ts +1 -3
- package/src/shared/matchRoute.ts +16 -4
- package/src/shared/menuBridge.ts +1 -2
- package/src/shared/outbox.ts +1 -1
- package/src/shared/reactive/Keyed.ts +2 -2
- package/src/shared/reactive/Reactive.ts +12 -16
- package/src/shared/reactive/index.ts +3 -4
- package/src/shared/reactive/isReactive.ts +1 -1
- package/src/shared/reactive/scoped.ts +2 -2
- package/src/shared/reactive/share.ts +1 -1
- package/src/shared/reactive/watch.ts +4 -4
- package/src/shared/refuse.ts +1 -1
- package/src/shared/routePattern.ts +66 -14
- package/src/shared/rpc.ts +8 -11
- package/src/shared/url.ts +24 -7
- package/src/shared/validateJson.ts +1 -1
- package/src/ui/ArmRegion.ts +1 -0
- package/src/ui/Level.ts +1 -0
- package/src/ui/List.ts +1 -0
- package/src/ui/RawRun.ts +1 -0
- package/src/ui/Router.ts +1 -0
- package/src/ui/TryRegion.ts +1 -0
- package/src/ui/adoptWalk.ts +2 -1
- package/src/ui/bindElement.ts +1 -0
- package/src/ui/bindFiles.ts +1 -0
- package/src/ui/bindGroup.ts +1 -0
- package/src/ui/bindProperty.ts +1 -0
- package/src/ui/bindSpread.ts +1 -0
- package/src/ui/index.ts +1 -6
- package/src/ui/outboxQueue.ts +1 -1
- package/src/ui/parseMarkup.ts +2 -1
- package/src/ui/reconcile.ts +2 -1
- package/src/ui/reconcileHead.ts +1 -0
- package/src/ui/regions.ts +2 -1
- package/src/ui/registerWorker.ts +1 -0
- package/src/ui/rows.ts +1 -0
- package/src/ui/setMarkup.ts +1 -0
- package/src/ui/setText.ts +1 -0
- package/src/ui/socketWire.ts +4 -3
- package/src/ui/wire.ts +2 -1
- /package/scaffold/src/{getGreeting.ts → getGreeting.handler.ts} +0 -0
|
@@ -11,12 +11,13 @@
|
|
|
11
11
|
|
|
12
12
|
import type { Mount } from '#server/answerRpc.ts'
|
|
13
13
|
import { frameworkRouteRefusal } from '#server/appTree.ts'
|
|
14
|
+
import { PRIVATE_EXPORT } from '#server/PRIVATE_EXPORT.ts'
|
|
14
15
|
import { surfaceName } from '#server/surfaceOf.ts'
|
|
15
16
|
import { PRIVATE_HANDLERS_ADDRESS } from '#shared/ADDRESSES.ts'
|
|
16
17
|
import { parseRoutePattern } from '#shared/routePattern.ts'
|
|
18
|
+
|
|
17
19
|
import type { PlacedHandler, PlacedSource } from './appLayout.ts'
|
|
18
20
|
import { declaredExports } from './declaredExports.ts'
|
|
19
|
-
import { PRIVATE_EXPORT } from '#server/PRIVATE_EXPORT.ts'
|
|
20
21
|
import type { Refusals } from './Refusals.ts'
|
|
21
22
|
import type { Diagnosis, TypeSession } from './TypeSession.ts'
|
|
22
23
|
|
|
@@ -45,7 +46,7 @@ export async function handlersUnder(
|
|
|
45
46
|
sources: readonly Source[],
|
|
46
47
|
files: readonly Pick<
|
|
47
48
|
PlacedHandler,
|
|
48
|
-
'path' | 'relative' | 'route' | 'scope'
|
|
49
|
+
'path' | 'relative' | 'route' | 'scope' | 'matchers'
|
|
49
50
|
>[],
|
|
50
51
|
refusals: Refusals,
|
|
51
52
|
): Promise<{ mounts: Mount[]; warnings: Diagnosis[] }> {
|
|
@@ -84,6 +85,7 @@ export async function handlersUnder(
|
|
|
84
85
|
relative,
|
|
85
86
|
route: address,
|
|
86
87
|
scope,
|
|
88
|
+
matchers,
|
|
87
89
|
} = files[at] as (typeof files)[number]
|
|
88
90
|
const exported = declared[at]
|
|
89
91
|
if (exported === undefined) continue
|
|
@@ -99,7 +101,7 @@ export async function handlersUnder(
|
|
|
99
101
|
)
|
|
100
102
|
if (handler?.method === undefined)
|
|
101
103
|
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.`,
|
|
104
|
+
`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
105
|
)
|
|
104
106
|
const framework = frameworkRouteRefusal(address, relative)
|
|
105
107
|
if (framework !== undefined) throw framework
|
|
@@ -119,6 +121,7 @@ export async function handlersUnder(
|
|
|
119
121
|
name: address.slice(address.lastIndexOf('/') + 1),
|
|
120
122
|
export: 'default',
|
|
121
123
|
scope,
|
|
124
|
+
matchers,
|
|
122
125
|
private: false,
|
|
123
126
|
ui: handler.ui,
|
|
124
127
|
cli: handler.cli,
|
|
@@ -150,6 +153,7 @@ export async function handlersUnder(
|
|
|
150
153
|
name,
|
|
151
154
|
export: each.name,
|
|
152
155
|
scope,
|
|
156
|
+
matchers: {},
|
|
153
157
|
private: true,
|
|
154
158
|
ui: each.ui,
|
|
155
159
|
cli: false,
|
package/src/compiler/lineOf.ts
CHANGED
|
@@ -20,6 +20,23 @@ export function lineOf(
|
|
|
20
20
|
return { line, column: end - lineStart + 1 }
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* The protocol's `{ line, character }`, from 0, as an offset into `text`. A character is a UTF-16
|
|
25
|
+
* code unit in the protocol and in a JavaScript string alike.
|
|
26
|
+
*/
|
|
27
|
+
export function offsetOf(
|
|
28
|
+
text: string,
|
|
29
|
+
position: { line: number; character: number },
|
|
30
|
+
): number {
|
|
31
|
+
let at = 0
|
|
32
|
+
for (let line = 0; line < position.line; line += 1) {
|
|
33
|
+
const end = text.indexOf('\n', at)
|
|
34
|
+
if (end === -1) return text.length
|
|
35
|
+
at = end + 1
|
|
36
|
+
}
|
|
37
|
+
return at + position.character
|
|
38
|
+
}
|
|
39
|
+
|
|
23
40
|
/** abide's own finding at `offset` into `text`, or at line 0 where the text was not handed in. */
|
|
24
41
|
export function diagnosisAt(
|
|
25
42
|
path: string,
|
|
@@ -0,0 +1,157 @@
|
|
|
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
|
+
|
|
13
|
+
import { matcherOf } from './paramFile.ts'
|
|
14
|
+
import type { Refusals } from './Refusals.ts'
|
|
15
|
+
|
|
16
|
+
/** A directory a file mounts (23.19), where it mounts, and the param files under it, by path. */
|
|
17
|
+
export type MountedTree = { directory: string; at: string; params: string[] }
|
|
18
|
+
|
|
19
|
+
/** A file with a route: its path, the name refusals give it, and the position it answers by. */
|
|
20
|
+
type Routed = {
|
|
21
|
+
path: string
|
|
22
|
+
relative: string
|
|
23
|
+
position: string
|
|
24
|
+
route: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Each routed file's matchers, by its path: every name its route's segments give, to the anchored
|
|
29
|
+
* source of the param file it resolves to — empty where its route names none.
|
|
30
|
+
*
|
|
31
|
+
* `params` is every param file of the app's own, by path. Refusals land at the file the fault is in:
|
|
32
|
+
* a param file that declares no matcher at itself, a name that resolves to none at the segment's file.
|
|
33
|
+
*/
|
|
34
|
+
export async function matchersOf(
|
|
35
|
+
root: string,
|
|
36
|
+
routed: readonly Routed[],
|
|
37
|
+
params: readonly string[],
|
|
38
|
+
trees: readonly MountedTree[],
|
|
39
|
+
refusals: Refusals,
|
|
40
|
+
): Promise<Map<string, Record<string, string>>> {
|
|
41
|
+
// Every param file's matcher, read once whichever routes name it; `undefined` where 23.24 or
|
|
42
|
+
// 23.25 refused the file, so a segment naming it refuses nothing more.
|
|
43
|
+
const sources = new Map<string, string | undefined>()
|
|
44
|
+
const all = [...params]
|
|
45
|
+
for (const tree of trees) all.push(...tree.params)
|
|
46
|
+
for (const path of all) {
|
|
47
|
+
if (sources.has(path)) continue
|
|
48
|
+
const matcher = matcherOf(
|
|
49
|
+
labelOf(root, path),
|
|
50
|
+
await Bun.file(path).text(),
|
|
51
|
+
)
|
|
52
|
+
if (matcher instanceof Error) {
|
|
53
|
+
refusals.at(path, matcher)
|
|
54
|
+
sources.set(path, undefined)
|
|
55
|
+
} else sources.set(path, matcher)
|
|
56
|
+
}
|
|
57
|
+
const byPath = new Map<string, Record<string, string>>()
|
|
58
|
+
// 23.27 — what each parameter position resolved to, and the first file that said so.
|
|
59
|
+
const atPosition = new Map<string, { found: string; file: string }>()
|
|
60
|
+
for (const file of routed) {
|
|
61
|
+
const matchers: Record<string, string> = {}
|
|
62
|
+
const tree = treeOf(file.path, trees)
|
|
63
|
+
const positionParts = file.position.split('/')
|
|
64
|
+
let shape = ''
|
|
65
|
+
let at = 0
|
|
66
|
+
for (const segment of parseRoutePattern(file.route)) {
|
|
67
|
+
shape += `/${segment.kind === 'static' ? segment.name : `[${segment.kind}]`}`
|
|
68
|
+
const index = at
|
|
69
|
+
at += 1
|
|
70
|
+
if (segment.kind === 'static') continue
|
|
71
|
+
const found =
|
|
72
|
+
segment.matcher === undefined
|
|
73
|
+
? ''
|
|
74
|
+
: nearestParam(
|
|
75
|
+
root,
|
|
76
|
+
tree,
|
|
77
|
+
positionParts.slice(0, index),
|
|
78
|
+
segment.matcher,
|
|
79
|
+
sources,
|
|
80
|
+
)
|
|
81
|
+
if (found === undefined) {
|
|
82
|
+
refusals.at(
|
|
83
|
+
file.path,
|
|
84
|
+
new Error(
|
|
85
|
+
`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.`,
|
|
86
|
+
),
|
|
87
|
+
)
|
|
88
|
+
continue
|
|
89
|
+
}
|
|
90
|
+
const other = atPosition.get(shape)
|
|
91
|
+
if (other === undefined)
|
|
92
|
+
atPosition.set(shape, { found, file: file.relative })
|
|
93
|
+
else if (other.found !== found)
|
|
94
|
+
refusals.at(
|
|
95
|
+
file.path,
|
|
96
|
+
new Error(
|
|
97
|
+
`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.`,
|
|
98
|
+
),
|
|
99
|
+
)
|
|
100
|
+
const source = found === '' ? undefined : sources.get(found)
|
|
101
|
+
if (segment.matcher !== undefined && source !== undefined)
|
|
102
|
+
matchers[segment.matcher] = source
|
|
103
|
+
}
|
|
104
|
+
byPath.set(file.path, matchers)
|
|
105
|
+
}
|
|
106
|
+
return byPath
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The param file `NAME.param.ts` a segment at `holding` names, nearest first, or `undefined`.
|
|
111
|
+
*
|
|
112
|
+
* `holding` is the position of the directory holding the segment, and the walk stops at the root
|
|
113
|
+
* the segment is filed under: inside a mounted directory, that directory; a segment of the position
|
|
114
|
+
* the mount itself takes is the mounting app's.
|
|
115
|
+
*/
|
|
116
|
+
function nearestParam(
|
|
117
|
+
root: string,
|
|
118
|
+
tree: MountedTree | undefined,
|
|
119
|
+
holding: string[],
|
|
120
|
+
name: string,
|
|
121
|
+
sources: Map<string, string | undefined>,
|
|
122
|
+
): string | undefined {
|
|
123
|
+
const atParts = tree === undefined ? 0 : tree.at.split('/').length - 1
|
|
124
|
+
const inside = tree !== undefined && holding.length >= atParts
|
|
125
|
+
const base = inside ? tree.directory : root
|
|
126
|
+
const below = inside ? holding.slice(atParts) : holding
|
|
127
|
+
for (let depth = below.length; depth >= 0; depth -= 1) {
|
|
128
|
+
const directory = [base, ...below.slice(0, depth)].join('/')
|
|
129
|
+
const candidate = `${directory}/${name}.param.ts`
|
|
130
|
+
if (sources.has(candidate)) return candidate
|
|
131
|
+
}
|
|
132
|
+
return undefined
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** The mounted directory `path` lies in, the deepest where two nest, or `undefined` for the app's own. */
|
|
136
|
+
function treeOf(
|
|
137
|
+
path: string,
|
|
138
|
+
trees: readonly MountedTree[],
|
|
139
|
+
): MountedTree | undefined {
|
|
140
|
+
let deepest: MountedTree | undefined
|
|
141
|
+
for (const tree of trees)
|
|
142
|
+
if (
|
|
143
|
+
path.startsWith(`${tree.directory}/`) &&
|
|
144
|
+
(deepest === undefined ||
|
|
145
|
+
tree.directory.length > deepest.directory.length)
|
|
146
|
+
)
|
|
147
|
+
deepest = tree
|
|
148
|
+
return deepest
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function labelOf(root: string, path: string): string {
|
|
152
|
+
return path.startsWith(`${root}/`) ? path.slice(root.length + 1) : path
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function describe(root: string, found: string): string {
|
|
156
|
+
return found === '' ? 'no matcher' : labelOf(root, found)
|
|
157
|
+
}
|
package/src/compiler/mountsOf.ts
CHANGED
|
@@ -10,8 +10,10 @@
|
|
|
10
10
|
|
|
11
11
|
import { realpathSync, statSync } from 'node:fs' // bun exposes no sync stat or realpath of its own
|
|
12
12
|
import { dirname, relative, resolve } from 'node:path' // bun exposes no path helpers of its own
|
|
13
|
+
|
|
13
14
|
import { appFiles } from '#server/appTree.ts'
|
|
14
15
|
import { placeOf } from '#server/placement.ts'
|
|
16
|
+
|
|
15
17
|
import { nearest } from './nearest.ts'
|
|
16
18
|
import { type AbideFile, scriptBodies } from './parseAbide.ts'
|
|
17
19
|
import { Refusals } from './Refusals.ts'
|
|
@@ -31,6 +33,8 @@ type DirectoryMount = {
|
|
|
31
33
|
at: string
|
|
32
34
|
files: Placed[]
|
|
33
35
|
handlers: Placed[]
|
|
36
|
+
/** 23.26 — the param files under it, by path: what its own segments' matchers resolve in. */
|
|
37
|
+
params: string[]
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
/**
|
|
@@ -85,6 +89,7 @@ export function mountsOf(
|
|
|
85
89
|
at,
|
|
86
90
|
files: found.abide.map(placed),
|
|
87
91
|
handlers: found.handlers.map(placed),
|
|
92
|
+
params: found.params.map((below) => `${directory}/${below}`),
|
|
88
93
|
})
|
|
89
94
|
}
|
|
90
95
|
return mounts
|
|
@@ -0,0 +1,125 @@
|
|
|
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
|
+
|
|
13
|
+
import { tokenize } from './tokenize.ts'
|
|
14
|
+
|
|
15
|
+
/** The name a param file declares: its file name before `.param.ts`. */
|
|
16
|
+
export function paramNameOf(path: string): string {
|
|
17
|
+
return path.slice(path.lastIndexOf('/') + 1, -'.param.ts'.length)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The anchored source of the matcher the param file at `relative` declares, or why it declares none.
|
|
22
|
+
*
|
|
23
|
+
* Anchored as `^(?!\.\.?$)(?:…)$`: the whole part, percent-decoded, and never a dot-segment.
|
|
24
|
+
*/
|
|
25
|
+
export function matcherOf(relative: string, text: string): string | Error {
|
|
26
|
+
const name = paramNameOf(relative)
|
|
27
|
+
const tokens = tokenize(text)
|
|
28
|
+
const kinds: SyntaxKind[] = []
|
|
29
|
+
for (const token of tokens)
|
|
30
|
+
if (token.kind !== SyntaxKind.EndOfFile) kinds.push(token.kind)
|
|
31
|
+
const declared =
|
|
32
|
+
kinds.length >= 5 &&
|
|
33
|
+
kinds.length <= 6 &&
|
|
34
|
+
kinds[0] === SyntaxKind.ExportKeyword &&
|
|
35
|
+
kinds[1] === SyntaxKind.ConstKeyword &&
|
|
36
|
+
kinds[2] === SyntaxKind.Identifier &&
|
|
37
|
+
kinds[3] === SyntaxKind.EqualsToken &&
|
|
38
|
+
kinds[4] === SyntaxKind.RegularExpressionLiteral &&
|
|
39
|
+
(kinds.length === 5 || kinds[5] === SyntaxKind.SemicolonToken)
|
|
40
|
+
if (!declared)
|
|
41
|
+
return new Error(
|
|
42
|
+
`abide: ${relative} must hold one \`export const ${name} = /…/\` and nothing else, an import included. RULEBOOK 23.24.`,
|
|
43
|
+
)
|
|
44
|
+
const exported = (tokens[2] as { text: string }).text
|
|
45
|
+
if (exported !== name)
|
|
46
|
+
return new Error(
|
|
47
|
+
`abide: ${relative} exports ${exported}, and a param file exports the name it is filed under, ${name}. RULEBOOK 23.24.`,
|
|
48
|
+
)
|
|
49
|
+
const literal = (tokens[4] as { text: string }).text
|
|
50
|
+
const close = literal.lastIndexOf('/')
|
|
51
|
+
if (close !== literal.length - 1)
|
|
52
|
+
return new Error(
|
|
53
|
+
`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.`,
|
|
54
|
+
)
|
|
55
|
+
const body = literal.slice(1, close)
|
|
56
|
+
const runaway = runawayIn(body)
|
|
57
|
+
if (runaway !== undefined)
|
|
58
|
+
return new Error(
|
|
59
|
+
`abide: ${relative} carries ${runaway}, which can take time exponential in a part's length on an address made to. RULEBOOK 23.25.`,
|
|
60
|
+
)
|
|
61
|
+
return `^(?!\\.\\.?$)(?:${body})$`
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 23.25 — what in a regex's source can backtrack without bound, or `undefined`.
|
|
66
|
+
*
|
|
67
|
+
* A repetition with no upper bound (`*`, `+`, `{n,}`) of a group that itself holds one, and a
|
|
68
|
+
* backreference. Read character by character rather than parsed: a class and an escape are skipped
|
|
69
|
+
* as single atoms, which is all the shape the two questions need.
|
|
70
|
+
*/
|
|
71
|
+
export function runawayIn(body: string): string | undefined {
|
|
72
|
+
// One entry per open group: whether it holds an unbounded repetition.
|
|
73
|
+
const holds: boolean[] = [false]
|
|
74
|
+
// Whether the atom just read is a group holding one.
|
|
75
|
+
let groupHolding = false
|
|
76
|
+
for (let at = 0; at < body.length; at += 1) {
|
|
77
|
+
const char = body[at] as string
|
|
78
|
+
if (char === '\\') {
|
|
79
|
+
const next = body[at + 1] ?? ''
|
|
80
|
+
if ((next >= '1' && next <= '9') || next === 'k')
|
|
81
|
+
return 'a backreference'
|
|
82
|
+
at += 1
|
|
83
|
+
groupHolding = false
|
|
84
|
+
continue
|
|
85
|
+
}
|
|
86
|
+
if (char === '[') {
|
|
87
|
+
at += 1
|
|
88
|
+
while (at < body.length && body[at] !== ']')
|
|
89
|
+
at += body[at] === '\\' ? 2 : 1
|
|
90
|
+
groupHolding = false
|
|
91
|
+
continue
|
|
92
|
+
}
|
|
93
|
+
if (char === '(') {
|
|
94
|
+
holds.push(false)
|
|
95
|
+
groupHolding = false
|
|
96
|
+
continue
|
|
97
|
+
}
|
|
98
|
+
if (char === ')') {
|
|
99
|
+
const inner = holds.pop() ?? false
|
|
100
|
+
if (inner) holds[holds.length - 1] = true
|
|
101
|
+
groupHolding = inner
|
|
102
|
+
continue
|
|
103
|
+
}
|
|
104
|
+
const unbounded =
|
|
105
|
+
char === '*' ||
|
|
106
|
+
char === '+' ||
|
|
107
|
+
(char === '{' && /^\{\d+,\}/.test(body.slice(at)))
|
|
108
|
+
if (unbounded) {
|
|
109
|
+
if (groupHolding) return 'a repetition of a group that repeats'
|
|
110
|
+
holds[holds.length - 1] = true
|
|
111
|
+
groupHolding = false
|
|
112
|
+
continue
|
|
113
|
+
}
|
|
114
|
+
// A bounded `{n}` or `{n,m}` repeats a group a fixed number of times, and keeps what it
|
|
115
|
+
// repeats as the atom a following quantifier would apply to.
|
|
116
|
+
if (char === '{') {
|
|
117
|
+
const end = body.indexOf('}', at)
|
|
118
|
+
if (end !== -1) at = end
|
|
119
|
+
continue
|
|
120
|
+
}
|
|
121
|
+
if (char === '?') continue
|
|
122
|
+
groupHolding = false
|
|
123
|
+
}
|
|
124
|
+
return undefined
|
|
125
|
+
}
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { SyntaxKind } from 'typescript/unstable/ast'
|
|
2
|
+
|
|
2
3
|
import { URL_ATTRIBUTES } from '#shared/URL_ATTRIBUTES.ts'
|
|
4
|
+
|
|
3
5
|
import { isComponentTag, isSlot, propKeyOf } from './classifyTemplate.ts'
|
|
4
6
|
import { decodeCharacterReferences } from './decodeCharacterReferences.ts'
|
|
5
7
|
import { isNamedAlone } from './isNamedAlone.ts'
|
|
6
8
|
import { lineOf } from './lineOf.ts'
|
|
7
9
|
import { PositionedRefusal } from './PositionedRefusal.ts'
|
|
8
|
-
import { TEMPLATE_BLOCKS } from './TEMPLATE_BLOCKS.ts'
|
|
9
10
|
import { importedNames } from './splitImports.ts'
|
|
11
|
+
import { TEMPLATE_BLOCKS } from './TEMPLATE_BLOCKS.ts'
|
|
10
12
|
import {
|
|
11
13
|
type Attribute,
|
|
12
|
-
type Branch,
|
|
13
14
|
bodiesOf,
|
|
15
|
+
type Branch,
|
|
14
16
|
type ElementNode,
|
|
15
17
|
type ForBlock,
|
|
16
18
|
type InlineComponent,
|
|
@@ -106,6 +108,8 @@ type Origins = {
|
|
|
106
108
|
names: Map<object, number>
|
|
107
109
|
/** 38.116 — per element closed by a tag of its own, where that close tag's name starts. */
|
|
108
110
|
closes: Map<object, number>
|
|
111
|
+
/** Where each block body's `<script>` setup starts — 32.48, 14.8. */
|
|
112
|
+
setups: number[]
|
|
109
113
|
}
|
|
110
114
|
|
|
111
115
|
/**
|
|
@@ -216,6 +220,7 @@ export function parseAbide(source: string): AbideFile {
|
|
|
216
220
|
bindings: new Map(),
|
|
217
221
|
names: new Map(),
|
|
218
222
|
closes: new Map(),
|
|
223
|
+
setups: [],
|
|
219
224
|
}
|
|
220
225
|
const root: TemplateNode[] = []
|
|
221
226
|
// The open elements, innermost last. `root` is the floor and is never popped.
|
|
@@ -365,8 +370,7 @@ export function parseAbide(source: string): AbideFile {
|
|
|
365
370
|
if (block === styles && blocks.length > 0) {
|
|
366
371
|
const owner = bodyOf(
|
|
367
372
|
blocks[blocks.length - 1] as
|
|
368
|
-
|
|
|
369
|
-
| InlineComponent,
|
|
373
|
+
TemplateNode | InlineComponent,
|
|
370
374
|
)
|
|
371
375
|
owner.style =
|
|
372
376
|
owner.style === null
|
|
@@ -377,8 +381,7 @@ export function parseAbide(source: string): AbideFile {
|
|
|
377
381
|
}
|
|
378
382
|
if (block === scripts && blocks.length > 0) {
|
|
379
383
|
const holder = blocks[blocks.length - 1] as
|
|
380
|
-
|
|
|
381
|
-
| InlineComponent
|
|
384
|
+
TemplateNode | InlineComponent
|
|
382
385
|
const owner = bodyOf(holder)
|
|
383
386
|
if (owner.setup !== null)
|
|
384
387
|
throw new Error(
|
|
@@ -388,6 +391,7 @@ export function parseAbide(source: string): AbideFile {
|
|
|
388
391
|
)
|
|
389
392
|
owner.setup = { text: raw.body }
|
|
390
393
|
origins.holders.set(owner.setup, tag.end)
|
|
394
|
+
origins.setups.push(tag.end)
|
|
391
395
|
at = raw.end
|
|
392
396
|
continue
|
|
393
397
|
}
|
|
@@ -1072,7 +1076,7 @@ function rawBody(
|
|
|
1072
1076
|
* already `trim()`s for this, which is one question with two answers until now — so the end is
|
|
1073
1077
|
* handed back rather than recomputed from the tag's length.
|
|
1074
1078
|
*/
|
|
1075
|
-
function closeTagAt(
|
|
1079
|
+
export function closeTagAt(
|
|
1076
1080
|
source: string,
|
|
1077
1081
|
tag: string,
|
|
1078
1082
|
from: number,
|
|
@@ -1113,6 +1117,9 @@ function pushText(
|
|
|
1113
1117
|
return next
|
|
1114
1118
|
}
|
|
1115
1119
|
|
|
1120
|
+
/** The ` if` after `{:else` — a whole word, so `{:elseif}` and `{:else ifValid}` are not one. */
|
|
1121
|
+
export const ELSE_IF = /^\s+if(?=[\s(])/
|
|
1122
|
+
|
|
1116
1123
|
/**
|
|
1117
1124
|
* Where the condition on a `{:else …}` arm starts in `inner`, or its end for the bare `{:else}`
|
|
1118
1125
|
* that always takes.
|
|
@@ -1124,7 +1131,7 @@ function pushText(
|
|
|
1124
1131
|
function elseSubjectAt(inner: string): number {
|
|
1125
1132
|
const rest = inner.slice(':else'.length)
|
|
1126
1133
|
if (rest.trim() === '') return inner.length
|
|
1127
|
-
const keyword =
|
|
1134
|
+
const keyword = ELSE_IF.exec(rest)
|
|
1128
1135
|
if (keyword === null || rest.slice(keyword[0].length).trim() === '')
|
|
1129
1136
|
throw new Error(
|
|
1130
1137
|
`abide: {${inner}} is not a control block. Write {:else} or {:else if <condition>}.`,
|
|
@@ -1147,28 +1154,18 @@ function trimmed(
|
|
|
1147
1154
|
}
|
|
1148
1155
|
|
|
1149
1156
|
/**
|
|
1150
|
-
*
|
|
1151
|
-
*
|
|
1152
|
-
* `item, index of list by key` — `index` and `by` are optional, and everything after `of` up to
|
|
1153
|
-
* a top-level `by` is the subject. Split on the KEYWORDS rather than on commas, because the
|
|
1154
|
-
* subject may hold both.
|
|
1157
|
+
* The `of`, the `,` and the `by` that divide a `{#for}` header — each the first at depth zero.
|
|
1155
1158
|
*
|
|
1156
|
-
* THE
|
|
1157
|
-
*
|
|
1158
|
-
*
|
|
1159
|
-
* names.filter(n => n !== "run by me")}` split on the ` by ` inside the string literal and the
|
|
1160
|
-
* subject ended mid-quote. One scanner for all three rather than one fix for the comma.
|
|
1159
|
+
* THE HEADER IS TOKENS, read by the scanner a slot is: an `of`, a `,` or a `by` inside a string,
|
|
1160
|
+
* a call or an object pattern is the expression's own, and only one at depth zero divides the
|
|
1161
|
+
* header. `by` is a word rather than a keyword, so a member named `by` is not one.
|
|
1161
1162
|
*/
|
|
1162
|
-
function
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
const
|
|
1168
|
-
// THE HEADER IS TOKENS, read by the scanner a slot is: an `of`, a `,` or a `by` inside a string,
|
|
1169
|
-
// a call or an object pattern is the expression's own, and only one at depth zero divides the
|
|
1170
|
-
// header. `by` is a word rather than a keyword, so a member named `by` is not one.
|
|
1171
|
-
const tokens = tokenize(source)
|
|
1163
|
+
export function forSeparators(header: string): {
|
|
1164
|
+
of: Token | undefined
|
|
1165
|
+
comma: Token | undefined
|
|
1166
|
+
by: Token | undefined
|
|
1167
|
+
} {
|
|
1168
|
+
const tokens = tokenize(header)
|
|
1172
1169
|
let depth = 0
|
|
1173
1170
|
let of: Token | undefined
|
|
1174
1171
|
let comma: Token | undefined
|
|
@@ -1191,6 +1188,29 @@ function forBlock(
|
|
|
1191
1188
|
)
|
|
1192
1189
|
by = token
|
|
1193
1190
|
}
|
|
1191
|
+
return { of, comma, by }
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
/**
|
|
1195
|
+
* Parses a `{#for}` header into its item, index, subject and key.
|
|
1196
|
+
*
|
|
1197
|
+
* `item, index of list by key` — `index` and `by` are optional, and everything after `of` up to
|
|
1198
|
+
* a top-level `by` is the subject. Split on the KEYWORDS rather than on commas, because the
|
|
1199
|
+
* subject may hold both.
|
|
1200
|
+
*
|
|
1201
|
+
* THE SEPARATOR IS THE ONE AT DEPTH ZERO AND OUTSIDE A STRING — all three of them, and each was
|
|
1202
|
+
* wrong its own way. `{#for { id, name } of rows}` split on the pattern's own comma and the item
|
|
1203
|
+
* became `{ id`, which the emitters write straight into a parameter list. `{#for x of
|
|
1204
|
+
* names.filter(n => n !== "run by me")}` split on the ` by ` inside the string literal and the
|
|
1205
|
+
* subject ended mid-quote. One scanner for all three rather than one fix for the comma.
|
|
1206
|
+
*/
|
|
1207
|
+
function forBlock(
|
|
1208
|
+
file: string,
|
|
1209
|
+
header: { text: string; at: number },
|
|
1210
|
+
origins: Origins,
|
|
1211
|
+
): ForBlock {
|
|
1212
|
+
const source = header.text
|
|
1213
|
+
const { of, comma, by } = forSeparators(source)
|
|
1194
1214
|
// A `{#for}` with nothing to iterate rendered as a block over the subject '' — zero rows,
|
|
1195
1215
|
// forever, with nothing saying why (32.22 is an ABSENT subject, not a missing one).
|
|
1196
1216
|
if (of === undefined)
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { SyntaxKind } from 'typescript/unstable/ast'
|
|
2
|
+
|
|
2
3
|
import { PRIVATE_EXPORT } from '#server/PRIVATE_EXPORT.ts'
|
|
3
4
|
import { isEventName } from '#shared/isEventName.ts'
|
|
5
|
+
|
|
4
6
|
import { isComponentTag, propKeyOf } from './classifyTemplate.ts'
|
|
5
7
|
import { isNamedAlone } from './isNamedAlone.ts'
|
|
8
|
+
import type { AbideFile } from './parseAbide.ts'
|
|
6
9
|
import {
|
|
7
10
|
type Position,
|
|
8
11
|
type PositionKind,
|
|
9
12
|
PositionTable,
|
|
10
13
|
} from './PositionTable.ts'
|
|
11
|
-
import type { AbideFile } from './parseAbide.ts'
|
|
12
14
|
import {
|
|
13
15
|
attributedImports,
|
|
14
16
|
IMPORT_ATTRIBUTES,
|
|
@@ -501,12 +503,17 @@ function project(file: AbideFile): Projection {
|
|
|
501
503
|
body(node.children, node.components)
|
|
502
504
|
continue
|
|
503
505
|
}
|
|
504
|
-
// 32.16 — the catch arm binds the failure, and the checker has to see the name.
|
|
506
|
+
// 32.16 — the catch arm binds the failure, and the checker has to see the name. Read once
|
|
507
|
+
// with `void`, as a `catch` binding is never reported unused. An arm that names none binds
|
|
508
|
+
// nothing.
|
|
505
509
|
if (node.block === 'try') {
|
|
506
510
|
const [tried, caught] = node.branches as [Branch, Branch]
|
|
507
511
|
text += '{\n'
|
|
508
512
|
inBody(tried)
|
|
509
|
-
text +=
|
|
513
|
+
text +=
|
|
514
|
+
node.error === ''
|
|
515
|
+
? '}\n{\n'
|
|
516
|
+
: `}\n{ const ${node.error}: any = undefined; void ${node.error}\n`
|
|
510
517
|
inBody(caught)
|
|
511
518
|
text += '}\n'
|
|
512
519
|
continue
|
|
@@ -763,7 +770,7 @@ function refuseExport(source: string): void {
|
|
|
763
770
|
if (moved !== 0) depth += moved
|
|
764
771
|
else if (depth === 0 && text === EXPORT && previous !== '.')
|
|
765
772
|
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.',
|
|
773
|
+
'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
774
|
)
|
|
768
775
|
previous = text
|
|
769
776
|
}
|
|
@@ -4,18 +4,19 @@ import {
|
|
|
4
4
|
isVariableDeclaration,
|
|
5
5
|
} from 'typescript/unstable/ast/is'
|
|
6
6
|
import {
|
|
7
|
-
type Symbol as CheckedSymbol,
|
|
8
7
|
type Checker,
|
|
9
8
|
ObjectFlags,
|
|
10
9
|
type ObjectType,
|
|
11
10
|
type Project,
|
|
12
11
|
SignatureKind,
|
|
12
|
+
type Symbol as CheckedSymbol,
|
|
13
13
|
type Type,
|
|
14
14
|
TypeFlags,
|
|
15
15
|
type TypeParameter,
|
|
16
16
|
type TypeReference,
|
|
17
17
|
type UnionOrIntersectionType,
|
|
18
18
|
} from 'typescript/unstable/async'
|
|
19
|
+
|
|
19
20
|
import { aliasedSymbol } from './aliasedSymbol.ts'
|
|
20
21
|
|
|
21
22
|
// WHAT THE CHECKER SAYS A NAME HOLDS, asked by both of the session's jobs over one snapshot: the
|
package/src/compiler/readsIn.ts
CHANGED
|
@@ -26,13 +26,14 @@ import {
|
|
|
26
26
|
isVariableStatement,
|
|
27
27
|
} from 'typescript/unstable/ast/is'
|
|
28
28
|
import {
|
|
29
|
-
type Symbol as CheckedSymbol,
|
|
30
29
|
type Checker,
|
|
31
30
|
SignatureKind,
|
|
31
|
+
type Symbol as CheckedSymbol,
|
|
32
32
|
SymbolFlags,
|
|
33
33
|
type Type,
|
|
34
34
|
TypeFlags,
|
|
35
35
|
} from 'typescript/unstable/async'
|
|
36
|
+
|
|
36
37
|
import { aliasedSymbol } from './aliasedSymbol.ts'
|
|
37
38
|
import { CheckerAnswer } from './CheckerAnswer.ts'
|
|
38
39
|
import {
|
|
@@ -45,13 +46,13 @@ import {
|
|
|
45
46
|
TRANSPARENT,
|
|
46
47
|
} from './classifyRead.ts'
|
|
47
48
|
import type { Finding } from './Finding.ts'
|
|
49
|
+
import type { AbideFile } from './parseAbide.ts'
|
|
48
50
|
import {
|
|
49
51
|
holdsReactive,
|
|
50
52
|
type Position,
|
|
51
53
|
type PositionKind,
|
|
52
54
|
type PositionTable,
|
|
53
55
|
} from './PositionTable.ts'
|
|
54
|
-
import type { AbideFile } from './parseAbide.ts'
|
|
55
56
|
import { INLINE_PROPS, type Span } from './projectShadow.ts'
|
|
56
57
|
import { REACTIVE_MEMBERS } from './REACTIVE_MEMBERS.ts'
|
|
57
58
|
import { optionNamed, reactiveOptions } from './reactiveOptions.ts'
|