@abide/abide 0.104.0 → 0.105.1
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 +80 -173
- package/docs/REGISTRY.md +17 -17
- package/docs/RULEBOOK.md +50 -12
- package/docs/abide.md +9 -9
- package/package.json +1 -1
- package/src/compiler/Lowering.ts +15 -5
- package/src/compiler/Refusals.ts +57 -0
- package/src/compiler/TypeSession.ts +45 -86
- package/src/compiler/appLayout.ts +447 -0
- package/src/compiler/appMap.ts +21 -20
- package/src/compiler/checkApp.ts +61 -97
- package/src/compiler/checkedShadow.ts +11 -9
- package/src/compiler/classifyRead.ts +3 -6
- package/src/compiler/compileApp.ts +180 -512
- package/src/compiler/componentGraph.ts +2 -71
- package/src/compiler/declaredExports.ts +79 -60
- package/src/compiler/emitClient.ts +11 -13
- package/src/compiler/emitComponent.ts +21 -6
- package/src/compiler/emitSource.ts +20 -20
- package/src/compiler/handlersUnder.ts +46 -36
- package/src/compiler/hookCalls.ts +152 -0
- package/src/compiler/mountsOf.ts +31 -54
- package/src/compiler/readSources.ts +27 -42
- package/src/compiler/readsIn.ts +5 -16
- package/src/compiler/resolverIn.ts +77 -0
- package/src/server/abideModules.ts +92 -123
- package/src/server/answerRequest.ts +18 -4
- package/src/server/answerSocket.ts +136 -76
- package/src/server/answerValue.ts +28 -15
- package/src/server/appTree.ts +49 -71
- package/src/server/claudeApi.ts +2 -5
- package/src/server/clientBundle.ts +198 -0
- package/src/server/coerceArgs.ts +1 -1
- package/src/server/commands.ts +93 -57
- package/src/server/createApp.ts +90 -16
- package/src/server/deferredBuilds.ts +175 -0
- package/src/server/devWorker.ts +107 -319
- package/src/server/faceOf.ts +120 -0
- package/src/server/fenceServer.ts +10 -18
- package/src/server/index.ts +3 -0
- package/src/server/placement.ts +167 -0
- package/src/server/remote.ts +14 -42
- package/src/server/render.ts +25 -17
- package/src/server/renderComponent.ts +9 -5
- package/src/server/requestScope.ts +10 -4
- package/src/server/responseRelay.ts +389 -0
- package/src/server/runCompiled.ts +9 -12
- package/src/server/runDesktop.ts +6 -9
- package/src/server/serveBuild.ts +26 -73
- package/src/server/serveDev.ts +19 -176
- package/src/server/streamEvent.ts +29 -0
- package/src/server/withAbideSources.ts +2 -1
- package/src/shared/HandlerRoute.ts +68 -0
- package/src/shared/REFUSAL_KEY.ts +5 -0
- package/src/shared/answerOf.ts +105 -78
- package/src/shared/filledPath.ts +1 -9
- package/src/shared/framing.ts +127 -128
- package/src/shared/queryText.ts +15 -7
- package/src/shared/reactive/Cursor.ts +4 -2
- package/src/shared/reactive/Reactive.ts +285 -197
- package/src/shared/reactive/index.ts +11 -8
- package/src/shared/reactive/pairReactive.ts +1 -1
- package/src/shared/reactive/scoped.ts +4 -3
- package/src/shared/reactive/share.ts +11 -1
- package/src/shared/routePattern.ts +0 -123
- package/src/shared/rpc.ts +17 -24
- package/src/shared/rpcRequest.ts +18 -47
- package/src/shared/takeEarlyCall.ts +12 -37
- package/src/shared/url.ts +1 -1
- package/src/ui/Level.ts +276 -0
- package/src/ui/List.ts +17 -17
- package/src/ui/Router.ts +20 -87
- package/src/ui/applyHead.ts +92 -79
- package/src/ui/index.ts +4 -2
- package/src/ui/leave.ts +5 -0
- package/src/ui/reconcile.ts +23 -104
- package/src/ui/regions.ts +27 -2
- package/src/ui/rows.ts +123 -2
- package/src/ui/wire.ts +12 -27
- package/src/compiler/hooksCalled.ts +0 -35
- package/src/server/clientBuildOptions.ts +0 -54
- package/src/server/refuseAddressCollision.ts +0 -37
- package/src/shared/reactive/chunksOfStream.ts +0 -33
- package/src/ui/MOUNTING_LEVEL.ts +0 -18
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
import { relative as relativeTo } from 'node:path' // bun exposes no path helpers of its own
|
|
2
|
+
import type { Compilation } from '#server/abideModules.ts'
|
|
3
|
+
import { appFiles } from '#server/appTree.ts'
|
|
4
|
+
import { type Placement, placement, routeOf } from '#server/placement.ts'
|
|
5
|
+
import { LAUNCHER_PAGE } from '#shared/LAUNCHER_FILES.ts'
|
|
6
|
+
import {
|
|
7
|
+
coversRoute,
|
|
8
|
+
parseRoutePattern,
|
|
9
|
+
type Segment,
|
|
10
|
+
} from '#shared/routePattern.ts'
|
|
11
|
+
import { abideImportsOf, componentGraph } from './componentGraph.ts'
|
|
12
|
+
import type { HandlerFile } from './handlersUnder.ts'
|
|
13
|
+
import { hookCalls, hooksIn } from './hookCalls.ts'
|
|
14
|
+
import { mountsOf } from './mountsOf.ts'
|
|
15
|
+
import { type AbideFile, scriptBodies } from './parseAbide.ts'
|
|
16
|
+
import { Refusals } from './Refusals.ts'
|
|
17
|
+
import { type ParsedSource, parseSources } from './readSources.ts'
|
|
18
|
+
import { resolverIn } from './resolverIn.ts'
|
|
19
|
+
import { attributedImports } from './splitImports.ts'
|
|
20
|
+
import type { Diagnosis } from './TypeSession.ts'
|
|
21
|
+
|
|
22
|
+
// THE APP, LAID OUT ONCE PER COMPILE OR CHECK: every `.abide` and handler file it holds — its own, a
|
|
23
|
+
// dependency's it imports, a package's it mounts — each with where it sits and what that makes it
|
|
24
|
+
// (`placement`), the route table and the layouts around each route, and what the tree refuses. Every
|
|
25
|
+
// reader takes its answer from here rather than asking a path again, so the build's routes, the
|
|
26
|
+
// checker's `Routes`, `abide routes` and the browser's fence cannot disagree about one file.
|
|
27
|
+
|
|
28
|
+
/** One `.abide` in the app, parsed and placed. */
|
|
29
|
+
export type PlacedSource = ParsedSource &
|
|
30
|
+
Placement & {
|
|
31
|
+
/** 23.3, 35.14 — a template or a head: a page or an error page that does not is its hooks alone. */
|
|
32
|
+
paints: boolean
|
|
33
|
+
/** 32.17 — another file imports it, so it carries its own markup. */
|
|
34
|
+
invoked: boolean
|
|
35
|
+
/** 37.1 — each of abide's hooks its scripts call, in the order written (`hookCalls`). */
|
|
36
|
+
hooks: string[]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** One handler file in the app, read and placed. */
|
|
40
|
+
export type PlacedHandler = HandlerFile & {
|
|
41
|
+
/** 23.14 — the address its default answers at. */
|
|
42
|
+
route: string
|
|
43
|
+
/** 37.1 — the route its hooks are filed at. */
|
|
44
|
+
scope: string
|
|
45
|
+
text: string
|
|
46
|
+
hooks: string[]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** A page or an error page and the layouts it renders inside. */
|
|
50
|
+
export type AppRoute = {
|
|
51
|
+
/** The address, in the bracket spelling `routePattern` parses. */
|
|
52
|
+
pattern: string
|
|
53
|
+
/** The `.abide` this route paints, relative to the app root. */
|
|
54
|
+
source: string
|
|
55
|
+
/**
|
|
56
|
+
* 35.1 — the `layout.abide` files this page renders inside, outermost first.
|
|
57
|
+
*
|
|
58
|
+
* A fact about the file tree, like the address itself, so a layout cannot be forgotten and
|
|
59
|
+
* cannot be declared twice.
|
|
60
|
+
*/
|
|
61
|
+
layouts: string[]
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The app at a root, laid out (`appLayout`). */
|
|
65
|
+
export type AppLayout = {
|
|
66
|
+
/** Real, as bun resolves every import. */
|
|
67
|
+
root: string
|
|
68
|
+
/** Every `.abide` — the app's own first, in path order, then each one they reach. */
|
|
69
|
+
sources: PlacedSource[]
|
|
70
|
+
/** How many of `sources` are the app's own, which is what the commands report. */
|
|
71
|
+
own: number
|
|
72
|
+
/** The handler files, the app's own and each mounted one, at the position each answers by. */
|
|
73
|
+
handlers: PlacedHandler[]
|
|
74
|
+
/** 23.3 — the painting pages, each with the layouts around it. */
|
|
75
|
+
routes: AppRoute[]
|
|
76
|
+
/** 35.2 — the painting error pages, each with the layouts around it. */
|
|
77
|
+
errors: AppRoute[]
|
|
78
|
+
/** Every public file's source path to its address. 36.11. */
|
|
79
|
+
published: Map<string, string>
|
|
80
|
+
mounted: Compilation['mounted']
|
|
81
|
+
/** 30.26 — every address a link may name: a page's, and a public file's. */
|
|
82
|
+
addresses: string[]
|
|
83
|
+
/** What the tree refuses, each at its file; a build stops naming every one. */
|
|
84
|
+
refused: Diagnosis[]
|
|
85
|
+
/** The resolver every import was followed with, whose answers it keeps. */
|
|
86
|
+
resolve: (specifier: string, from: string) => string | undefined
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The app at `root` — real — laid out, every refusal collected at its file (`refused`).
|
|
91
|
+
*
|
|
92
|
+
* ONE WALK (`appFiles`), then each `.abide` parsed and followed through its component imports and its
|
|
93
|
+
* mounts (`withDependencies`), then each placed. A missing root is an app with nothing in it.
|
|
94
|
+
*/
|
|
95
|
+
export async function appLayout(root: string): Promise<AppLayout> {
|
|
96
|
+
// COLLECTED, whoever asks: a layout is kept across checks (`AppChecker`), and a build adopts
|
|
97
|
+
// what it found (`Refusals.adopt`).
|
|
98
|
+
const refusals = new Refusals(root, true)
|
|
99
|
+
const { abide, handlers, modules, published } = appFiles(root, refusals)
|
|
100
|
+
await refuseModuleHooks(root, modules, refusals)
|
|
101
|
+
const resolve = resolverIn()
|
|
102
|
+
const own = parseSources(
|
|
103
|
+
await textsAt(abide.map((path) => `${root}/${path}`)),
|
|
104
|
+
refusals,
|
|
105
|
+
)
|
|
106
|
+
const found = await withDependencies(root, own, handlers, resolve, refusals)
|
|
107
|
+
const invoked = componentGraph(found.sources, resolve).invoked
|
|
108
|
+
const sources: PlacedSource[] = []
|
|
109
|
+
for (const source of found.sources) {
|
|
110
|
+
const relative = relativeTo(root, source.path)
|
|
111
|
+
const placed = placement(
|
|
112
|
+
relative,
|
|
113
|
+
found.positions.get(source.path) ?? relative,
|
|
114
|
+
)
|
|
115
|
+
refusePlace(source, placed, resolve, refusals)
|
|
116
|
+
sources.push({
|
|
117
|
+
...source,
|
|
118
|
+
...placed,
|
|
119
|
+
paints: paints(source.file),
|
|
120
|
+
invoked: invoked.has(source.path),
|
|
121
|
+
hooks: hooksIn(scriptBodies(source.file)),
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
const texts = await textsAt(found.handlers.map((file) => file.path))
|
|
125
|
+
const placedHandlers: PlacedHandler[] = []
|
|
126
|
+
for (let at = 0; at < found.handlers.length; at += 1) {
|
|
127
|
+
const file = found.handlers[at] as HandlerFile
|
|
128
|
+
const { text } = texts[at] as { text: string }
|
|
129
|
+
placedHandlers.push({
|
|
130
|
+
...file,
|
|
131
|
+
route: routeOf(file.position),
|
|
132
|
+
scope: placement(file.relative, file.position).scope,
|
|
133
|
+
text,
|
|
134
|
+
hooks: hooksIn([text]),
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
refuseStrayLauncher(sources, placedHandlers, refusals)
|
|
138
|
+
const { routes, errors } = routesOf(sources, refusals)
|
|
139
|
+
const addresses = new Set<string>()
|
|
140
|
+
for (const route of routes) addresses.add(route.pattern)
|
|
141
|
+
for (const address of published.values()) addresses.add(address)
|
|
142
|
+
return {
|
|
143
|
+
root,
|
|
144
|
+
sources,
|
|
145
|
+
own: own.length,
|
|
146
|
+
handlers: placedHandlers,
|
|
147
|
+
routes,
|
|
148
|
+
errors,
|
|
149
|
+
published,
|
|
150
|
+
mounted: found.mounted,
|
|
151
|
+
addresses: [...addresses].sort(),
|
|
152
|
+
refused: refusals.found,
|
|
153
|
+
resolve,
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** The file at each of `paths`, read. The reads overlap. */
|
|
158
|
+
function textsAt(
|
|
159
|
+
paths: readonly string[],
|
|
160
|
+
): Promise<{ path: string; text: string }[]> {
|
|
161
|
+
return Promise.all(
|
|
162
|
+
paths.map(async (path) => ({
|
|
163
|
+
path,
|
|
164
|
+
text: await Bun.file(path).text(),
|
|
165
|
+
})),
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* `parsed` and every `.abide` outside them that one of them imports — a dependency's, or a linked
|
|
171
|
+
* package's — parsed, and every one those import in turn; and every `.abide` under a directory one
|
|
172
|
+
* of them mounts, at the position the mount gives it (23.19). With them, the app's own handler files
|
|
173
|
+
* — `own`, below `root`, as its walk found them (`appFiles`) — and every one a mounted directory
|
|
174
|
+
* holds, each at the position it answers by.
|
|
175
|
+
*
|
|
176
|
+
* 34.23 — COMPILED WITH THE APP, by the app's checker, so a component a package ships as source is
|
|
177
|
+
* one of the app's in every way but its address: it is never routed (`placeOf`) unless mounted.
|
|
178
|
+
*/
|
|
179
|
+
export async function withDependencies(
|
|
180
|
+
root: string,
|
|
181
|
+
parsed: readonly ParsedSource[],
|
|
182
|
+
own: readonly string[],
|
|
183
|
+
resolve: (specifier: string, from: string) => string | undefined,
|
|
184
|
+
refusals: Refusals,
|
|
185
|
+
): Promise<{
|
|
186
|
+
sources: ParsedSource[]
|
|
187
|
+
handlers: HandlerFile[]
|
|
188
|
+
mounted: Compilation['mounted']
|
|
189
|
+
/** 23.19 — each mounted `.abide`'s position, by its path; one at its own path is not here. */
|
|
190
|
+
positions: Map<string, string>
|
|
191
|
+
}> {
|
|
192
|
+
const all = [...parsed]
|
|
193
|
+
const handlers: HandlerFile[] = []
|
|
194
|
+
for (const relative of own)
|
|
195
|
+
handlers.push({
|
|
196
|
+
path: `${root}/${relative}`,
|
|
197
|
+
relative,
|
|
198
|
+
position: relative,
|
|
199
|
+
})
|
|
200
|
+
// The mounted handler files placed so far; the app's own is never mounted (23.20).
|
|
201
|
+
const handled = new Set<string>()
|
|
202
|
+
const mounted: Compilation['mounted'] = {}
|
|
203
|
+
const byPath = new Map<string, ParsedSource>()
|
|
204
|
+
for (const source of all) byPath.set(source.path, source)
|
|
205
|
+
const positions = new Map<string, string>()
|
|
206
|
+
// 23.20 — a directory mounts once, and by whom.
|
|
207
|
+
const mountedBy = new Map<string, string>()
|
|
208
|
+
const add = async (path: string): Promise<void> => {
|
|
209
|
+
let text: string
|
|
210
|
+
try {
|
|
211
|
+
text = await Bun.file(path).text()
|
|
212
|
+
} catch (thrown) {
|
|
213
|
+
// 38.65 — bun keeps a directory's listing for the process's life, so an import of a file
|
|
214
|
+
// deleted since still resolves; like a specifier that does not, it invokes nothing.
|
|
215
|
+
if ((thrown as { code?: unknown }).code === 'ENOENT') return
|
|
216
|
+
throw thrown
|
|
217
|
+
}
|
|
218
|
+
for (const one of parseSources([{ path, text }], refusals)) {
|
|
219
|
+
byPath.set(one.path, one)
|
|
220
|
+
all.push(one)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
for (let at = 0; at < all.length; at += 1) {
|
|
224
|
+
const source = all[at] as ParsedSource
|
|
225
|
+
const position =
|
|
226
|
+
positions.get(source.path) ?? relativeTo(root, source.path)
|
|
227
|
+
for (const mount of mountsOf(
|
|
228
|
+
root,
|
|
229
|
+
source.path,
|
|
230
|
+
position,
|
|
231
|
+
source.file,
|
|
232
|
+
refusals,
|
|
233
|
+
)) {
|
|
234
|
+
const other = mountedBy.get(mount.directory)
|
|
235
|
+
if (other !== undefined) {
|
|
236
|
+
refusals.at(
|
|
237
|
+
source.path,
|
|
238
|
+
new Error(
|
|
239
|
+
`abide: ${other} and ${position} both mount ${mount.specifier}, and a directory answers at one place. RULEBOOK 23.20.`,
|
|
240
|
+
),
|
|
241
|
+
)
|
|
242
|
+
continue
|
|
243
|
+
}
|
|
244
|
+
mountedBy.set(mount.directory, position)
|
|
245
|
+
mounted[relativeTo(root, mount.directory)] = mount.at
|
|
246
|
+
for (const placed of mount.handlers) {
|
|
247
|
+
// Two mounts can overlap, one directory inside the other; a file is placed once.
|
|
248
|
+
if (handled.has(placed.path)) continue
|
|
249
|
+
handled.add(placed.path)
|
|
250
|
+
handlers.push({
|
|
251
|
+
path: placed.path,
|
|
252
|
+
relative: relativeTo(root, placed.path),
|
|
253
|
+
position: placed.position,
|
|
254
|
+
})
|
|
255
|
+
}
|
|
256
|
+
for (const { path, position: placed } of mount.files) {
|
|
257
|
+
// A file a component import reached first takes its mount's position all the same.
|
|
258
|
+
positions.set(path, placed)
|
|
259
|
+
if (!byPath.has(path)) await add(path)
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
for (const target of abideImportsOf(
|
|
263
|
+
source.path,
|
|
264
|
+
source.file,
|
|
265
|
+
resolve,
|
|
266
|
+
).values()) {
|
|
267
|
+
if (!byPath.has(target)) await add(target)
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return { sources: all, handlers, mounted, positions }
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Whether a page or an error page has anything to paint: a template, or a head. 23.3, 35.14. */
|
|
274
|
+
export function paints(file: AbideFile): boolean {
|
|
275
|
+
return file.template.length > 0 || file.head.length > 0
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Refuses what a source's place forbids it.
|
|
280
|
+
*
|
|
281
|
+
* 34.20 — a server block under a `server` or `public` directory: neither holds a component that
|
|
282
|
+
* runs. 34.21 — a file imported as text that is not there, found at build rather than when its
|
|
283
|
+
* module first loads. 35.16 — of the files with an address, the root layout alone writes the
|
|
284
|
+
* document.
|
|
285
|
+
*/
|
|
286
|
+
function refusePlace(
|
|
287
|
+
source: ParsedSource,
|
|
288
|
+
placed: Placement,
|
|
289
|
+
resolve: (specifier: string, from: string) => string | undefined,
|
|
290
|
+
refusals: Refusals,
|
|
291
|
+
): void {
|
|
292
|
+
const { relative, place, kind } = placed
|
|
293
|
+
if (
|
|
294
|
+
source.file.servers.length > 0 &&
|
|
295
|
+
(place === 'server' || place === 'public')
|
|
296
|
+
)
|
|
297
|
+
refusals.at(
|
|
298
|
+
relative,
|
|
299
|
+
new Error(
|
|
300
|
+
`abide: ${relative} has a <script server>, and it is under a ${place} directory, where nothing has an address. Move the file out of it, or the code into a module under a server directory. RULEBOOK 34.20.`,
|
|
301
|
+
),
|
|
302
|
+
)
|
|
303
|
+
const from = source.path.slice(0, source.path.lastIndexOf('/'))
|
|
304
|
+
for (const script of scriptBodies(source.file))
|
|
305
|
+
for (const { specifier } of attributedImports(script, 'text'))
|
|
306
|
+
if (resolve(specifier, from) === undefined)
|
|
307
|
+
refusals.at(
|
|
308
|
+
relative,
|
|
309
|
+
new Error(
|
|
310
|
+
`abide: ${relative} imports ${specifier} as text, and there is no such file. RULEBOOK 34.21.`,
|
|
311
|
+
),
|
|
312
|
+
)
|
|
313
|
+
if (
|
|
314
|
+
source.file.document !== undefined &&
|
|
315
|
+
kind !== 'component' &&
|
|
316
|
+
!(kind === 'layout' && placed.route === '/')
|
|
317
|
+
)
|
|
318
|
+
refusals.at(
|
|
319
|
+
relative,
|
|
320
|
+
new Error(
|
|
321
|
+
`abide: ${relative} writes <html>, and of the pages, layouts and error pages only the root layout.abide writes the document. RULEBOOK 35.16.`,
|
|
322
|
+
),
|
|
323
|
+
)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Refuses, naming the module, every module under a `server` or a `lib` directory that calls a hook.
|
|
328
|
+
*
|
|
329
|
+
* 37.13 — a hook called in a module that is neither a `.abide` nor a handler file runs as the module
|
|
330
|
+
* is imported, before any file's route is set, and not at all where nothing imports it.
|
|
331
|
+
*/
|
|
332
|
+
async function refuseModuleHooks(
|
|
333
|
+
root: string,
|
|
334
|
+
modules: readonly string[],
|
|
335
|
+
refusals: Refusals,
|
|
336
|
+
): Promise<void> {
|
|
337
|
+
const texts = await textsAt(modules.map((module) => `${root}/${module}`))
|
|
338
|
+
for (let at = 0; at < modules.length; at += 1) {
|
|
339
|
+
const module = modules[at] as string
|
|
340
|
+
const hook = hookCalls((texts[at] as { text: string }).text)[0]?.hook
|
|
341
|
+
if (hook !== undefined)
|
|
342
|
+
refusals.at(
|
|
343
|
+
module,
|
|
344
|
+
new Error(
|
|
345
|
+
`abide: ${module} calls ${hook}, and it is neither a .abide nor a handler file, so the call has no route. Export what it is handed and call ${hook} in the page, layout or handler file it serves. RULEBOOK 37.13.`,
|
|
346
|
+
),
|
|
347
|
+
)
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Refuses, naming the file, where anything but the root's `launcher.abide` calls `onLauncher`.
|
|
353
|
+
*
|
|
354
|
+
* 38.107 — the launch loads that file alone, so a call anywhere else is never read.
|
|
355
|
+
*/
|
|
356
|
+
function refuseStrayLauncher(
|
|
357
|
+
sources: readonly PlacedSource[],
|
|
358
|
+
handlers: readonly PlacedHandler[],
|
|
359
|
+
refusals: Refusals,
|
|
360
|
+
): void {
|
|
361
|
+
const stray = (relative: string): void =>
|
|
362
|
+
refusals.at(
|
|
363
|
+
relative,
|
|
364
|
+
new Error(
|
|
365
|
+
`abide: ${relative} calls onLauncher, which is read from src/${LAUNCHER_PAGE} alone. Move the call there. RULEBOOK 38.107.`,
|
|
366
|
+
),
|
|
367
|
+
)
|
|
368
|
+
for (const source of sources)
|
|
369
|
+
if (
|
|
370
|
+
source.position !== LAUNCHER_PAGE &&
|
|
371
|
+
source.hooks.includes('onLauncher')
|
|
372
|
+
)
|
|
373
|
+
stray(source.relative)
|
|
374
|
+
for (const file of handlers)
|
|
375
|
+
if (file.hooks.includes('onLauncher')) stray(file.relative)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* The painting pages and error pages, each with the layouts around it, refusing two layouts or two
|
|
380
|
+
* error pages at one route.
|
|
381
|
+
*
|
|
382
|
+
* 35.1, 35.12 — every `layout.abide` by the route it covers, one to a route; 35.13 — and one with no
|
|
383
|
+
* template wraps nothing, being the route's hooks alone. 23.3, 35.14 — a page or an `error.abide`
|
|
384
|
+
* with nothing to paint is its route's hooks alone.
|
|
385
|
+
*/
|
|
386
|
+
function routesOf(
|
|
387
|
+
sources: readonly PlacedSource[],
|
|
388
|
+
refusals: Refusals,
|
|
389
|
+
): { routes: AppRoute[]; errors: AppRoute[] } {
|
|
390
|
+
const layouts: { source: string; segments: Segment[] }[] = []
|
|
391
|
+
const layoutAt = new Map<string, string>()
|
|
392
|
+
const errorAt = new Map<string, string>()
|
|
393
|
+
for (const source of sources) {
|
|
394
|
+
if (source.kind !== 'layout' && source.kind !== 'error') continue
|
|
395
|
+
const route = source.route as string
|
|
396
|
+
const held = source.kind === 'layout' ? layoutAt : errorAt
|
|
397
|
+
const other = held.get(shapeOf(route))
|
|
398
|
+
if (other !== undefined) {
|
|
399
|
+
refusals.at(
|
|
400
|
+
source.relative,
|
|
401
|
+
new Error(
|
|
402
|
+
`abide: ${other} and ${source.relative} are both the ${source.kind}.abide at ${route}. RULEBOOK 35.12.`,
|
|
403
|
+
),
|
|
404
|
+
)
|
|
405
|
+
continue
|
|
406
|
+
}
|
|
407
|
+
held.set(shapeOf(route), source.relative)
|
|
408
|
+
if (source.kind === 'layout' && source.file.template.length > 0)
|
|
409
|
+
layouts.push({
|
|
410
|
+
source: source.relative,
|
|
411
|
+
segments: parseRoutePattern(route),
|
|
412
|
+
})
|
|
413
|
+
}
|
|
414
|
+
// 35.1 — every `layout.abide` whose route lies ON THE WAY DOWN to the page's wraps it and no
|
|
415
|
+
// other does, so the test is a prefix of the route, kind by kind, and the nesting order is the
|
|
416
|
+
// route's depth: a shorter route is an outer one.
|
|
417
|
+
layouts.sort((one, two) => one.segments.length - two.segments.length)
|
|
418
|
+
const routes: AppRoute[] = []
|
|
419
|
+
const errors: AppRoute[] = []
|
|
420
|
+
for (const source of sources) {
|
|
421
|
+
if (source.kind !== 'page' && source.kind !== 'error') continue
|
|
422
|
+
if (!source.paints) continue
|
|
423
|
+
const pattern = source.route as string
|
|
424
|
+
const page = parseRoutePattern(pattern)
|
|
425
|
+
const around: string[] = []
|
|
426
|
+
for (const layout of layouts)
|
|
427
|
+
if (coversRoute(layout.segments, page)) around.push(layout.source)
|
|
428
|
+
;(source.kind === 'page' ? routes : errors).push({
|
|
429
|
+
pattern,
|
|
430
|
+
source: source.relative,
|
|
431
|
+
layouts: around,
|
|
432
|
+
})
|
|
433
|
+
}
|
|
434
|
+
return { routes, errors }
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* A route with its parameters' names erased: what two routes answering one address share.
|
|
439
|
+
*
|
|
440
|
+
* 19.1 — `/media/[id]` and `/media/[slug]` are one route.
|
|
441
|
+
*/
|
|
442
|
+
export function shapeOf(route: string): string {
|
|
443
|
+
let shape = ''
|
|
444
|
+
for (const segment of parseRoutePattern(route))
|
|
445
|
+
shape += `/${segment.kind === 'static' ? segment.name : `[${segment.kind}]`}`
|
|
446
|
+
return shape === '' ? '/' : shape
|
|
447
|
+
}
|
package/src/compiler/appMap.ts
CHANGED
|
@@ -2,15 +2,11 @@
|
|
|
2
2
|
// filed. What `abide routes` prints, off the build's own reading of the tree — the routes and the
|
|
3
3
|
// handler mounts `compileApp` answers with — so the map and what the server answers are one walk.
|
|
4
4
|
//
|
|
5
|
-
// THE HOOKS ARE READ, NOT RUN, as the handlers are: a call of one of abide's
|
|
6
|
-
//
|
|
5
|
+
// THE HOOKS ARE READ, NOT RUN, as the handlers are: a call of one of abide's hooks in a `.abide`'s
|
|
6
|
+
// scripts or a handler file — its own or a mounted package's — at the route 37.1 files it at, off the
|
|
7
|
+
// same layout the build compiled (`hookCalls`).
|
|
7
8
|
|
|
8
|
-
import {
|
|
9
|
-
import { appFiles } from '#server/appTree.ts'
|
|
10
|
-
import { scopeOf } from '#shared/routePattern.ts'
|
|
11
|
-
import { compileApp } from './compileApp.ts'
|
|
12
|
-
import { hooksCalled } from './hooksCalled.ts'
|
|
13
|
-
import { parseAbide } from './parseAbide.ts'
|
|
9
|
+
import { compileApp, openChecker } from './compileApp.ts'
|
|
14
10
|
|
|
15
11
|
/** One address the app answers, and the file answering it. */
|
|
16
12
|
type MappedRoute = {
|
|
@@ -27,7 +23,15 @@ type MappedHook = { hook: string; route: string; file: string }
|
|
|
27
23
|
export async function appMap(
|
|
28
24
|
root: string,
|
|
29
25
|
): Promise<{ routes: MappedRoute[]; hooks: MappedHook[] }> {
|
|
30
|
-
const
|
|
26
|
+
const checker = await openChecker(root)
|
|
27
|
+
let app: Awaited<ReturnType<typeof compileApp>>
|
|
28
|
+
let layout: Awaited<ReturnType<typeof checker.layout>>
|
|
29
|
+
try {
|
|
30
|
+
app = await compileApp(root, checker)
|
|
31
|
+
layout = await checker.layout()
|
|
32
|
+
} finally {
|
|
33
|
+
await checker.close()
|
|
34
|
+
}
|
|
31
35
|
const routes: MappedRoute[] = []
|
|
32
36
|
for (const route of app.routes)
|
|
33
37
|
routes.push({
|
|
@@ -53,17 +57,14 @@ export async function appMap(
|
|
|
53
57
|
: 1,
|
|
54
58
|
)
|
|
55
59
|
const hooks: MappedHook[] = []
|
|
56
|
-
|
|
57
|
-
for (const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
for (const file of handlers)
|
|
65
|
-
for (const hook of hooksCalled(readFileSync(`${root}/${file}`, 'utf8')))
|
|
66
|
-
hooks.push({ hook, route: scopeOf(file), file })
|
|
60
|
+
// A package's component the app imports, mounted nowhere, is the package's own to map.
|
|
61
|
+
for (const source of layout.sources)
|
|
62
|
+
if (source.place !== 'dependency')
|
|
63
|
+
for (const hook of source.hooks)
|
|
64
|
+
hooks.push({ hook, route: source.scope, file: source.relative })
|
|
65
|
+
for (const file of layout.handlers)
|
|
66
|
+
for (const hook of file.hooks)
|
|
67
|
+
hooks.push({ hook, route: file.scope, file: file.relative })
|
|
67
68
|
return { routes, hooks }
|
|
68
69
|
}
|
|
69
70
|
|