@abide/abide 0.103.0 → 0.105.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 +29 -0
- package/bin/abide.ts +80 -173
- package/docs/REGISTRY.md +27 -27
- package/docs/RULEBOOK.md +68 -24
- package/docs/abide.md +31 -46
- package/package.json +1 -1
- package/src/compiler/Lowering.ts +15 -5
- package/src/compiler/Refusals.ts +57 -0
- package/src/compiler/TypeSession.ts +46 -89
- package/src/compiler/appLayout.ts +447 -0
- package/src/compiler/appMap.ts +21 -25
- package/src/compiler/checkApp.ts +61 -97
- package/src/compiler/checkedShadow.ts +12 -10
- package/src/compiler/classifyRead.ts +3 -6
- package/src/compiler/compileApp.ts +183 -478
- 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/projectShadow.ts +1 -1
- 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 +152 -146
- 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 +59 -73
- 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/generatedSurfaces.ts +12 -53
- 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/versionOf.ts +1 -2
- 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/RESERVED_DIRECTORIES.ts +6 -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 +69 -124
- package/src/shared/reactive/Reactive.ts +296 -162
- package/src/shared/reactive/callLoad.ts +4 -18
- 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 -110
- 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/reconcile.ts +4 -110
- 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 -57
- package/src/ui/MOUNTING_LEVEL.ts +0 -18
|
@@ -1,50 +1,31 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { dirname, posix
|
|
1
|
+
import { realpathSync } from 'node:fs' // bun exposes no sync realpath of its own
|
|
2
|
+
import { dirname, posix } from 'node:path' // bun exposes no path helpers of its own
|
|
3
3
|
import {
|
|
4
4
|
type Compilation,
|
|
5
5
|
type CompiledFile,
|
|
6
6
|
resolvedFile,
|
|
7
7
|
} from '#server/abideModules.ts'
|
|
8
8
|
import type { Mount } from '#server/answerRpc.ts'
|
|
9
|
-
import { abideUnder, appFiles } from '#server/appTree.ts'
|
|
10
9
|
import { CLIENT_ENTRY } from '#server/CLIENT_BUILD.ts'
|
|
11
10
|
import { COMPONENT_QUERY } from '#server/MODULE_QUERIES.ts'
|
|
12
11
|
import { ROUTES_TABLE, SERVER_TABLE } from '#server/TABLES.ts'
|
|
13
12
|
import { VIRTUAL_DIRECTORY } from '#server/VIRTUAL_DIRECTORY.ts'
|
|
14
13
|
import { CLIENT_ADDRESS } from '#shared/ADDRESSES.ts'
|
|
15
|
-
import {
|
|
14
|
+
import { ROUTING_SPECIFIER, WIRE_SPECIFIER } from '#shared/SPECIFIERS.ts'
|
|
16
15
|
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
} from '
|
|
23
|
-
import {
|
|
24
|
-
ROUTING_SPECIFIER,
|
|
25
|
-
SHARED_SPECIFIER,
|
|
26
|
-
WIRE_SPECIFIER,
|
|
27
|
-
} from '#shared/SPECIFIERS.ts'
|
|
28
|
-
import { abideImportsOf, appGraph } from './componentGraph.ts'
|
|
16
|
+
type AppLayout,
|
|
17
|
+
type AppRoute,
|
|
18
|
+
appLayout,
|
|
19
|
+
type PlacedSource,
|
|
20
|
+
shapeOf,
|
|
21
|
+
} from './appLayout.ts'
|
|
29
22
|
import { emitSource } from './emitSource.ts'
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
32
|
-
type HandlerFile,
|
|
33
|
-
handlersUnder,
|
|
34
|
-
type Refuse,
|
|
35
|
-
THROW,
|
|
36
|
-
} from './handlersUnder.ts'
|
|
37
|
-
import { hooksCalled } from './hooksCalled.ts'
|
|
38
|
-
import { mountsOf } from './mountsOf.ts'
|
|
23
|
+
import { handlersUnder } from './handlersUnder.ts'
|
|
24
|
+
import { isAbideSpecifier } from './hookCalls.ts'
|
|
39
25
|
import { nearest } from './nearest.ts'
|
|
40
|
-
import
|
|
41
|
-
import {
|
|
42
|
-
|
|
43
|
-
parseSources,
|
|
44
|
-
type ReadSource,
|
|
45
|
-
readSources,
|
|
46
|
-
refusal,
|
|
47
|
-
} from './readSources.ts'
|
|
26
|
+
import { projectShadow } from './projectShadow.ts'
|
|
27
|
+
import { Refusals } from './Refusals.ts'
|
|
28
|
+
import { type ReadSource, readSources } from './readSources.ts'
|
|
48
29
|
import { respecified } from './splitImports.ts'
|
|
49
30
|
import { type Diagnosis, TypeSession } from './TypeSession.ts'
|
|
50
31
|
import { writeDefaultProject } from './writeDefaultProject.ts'
|
|
@@ -55,20 +36,6 @@ import { writeDefaultProject } from './writeDefaultProject.ts'
|
|
|
55
36
|
// commands serving different markup from the same sources is exactly what one compile in two sinks
|
|
56
37
|
// makes impossible.
|
|
57
38
|
|
|
58
|
-
type AppRoute = {
|
|
59
|
-
/** The address, in the bracket spelling `routePattern` parses. */
|
|
60
|
-
pattern: string
|
|
61
|
-
/** The `.abide` this route paints, relative to the app root. */
|
|
62
|
-
source: string
|
|
63
|
-
/**
|
|
64
|
-
* 35.1 — the `layout.abide` files this page renders inside, outermost first.
|
|
65
|
-
*
|
|
66
|
-
* A fact about the file tree, like the address itself, so a layout cannot be forgotten and
|
|
67
|
-
* cannot be declared twice.
|
|
68
|
-
*/
|
|
69
|
-
layouts: string[]
|
|
70
|
-
}
|
|
71
|
-
|
|
72
39
|
/** An app's compiled output: every file and table (`Compilation`), its routes, and its warnings. */
|
|
73
40
|
export type CompiledApp = Compilation & {
|
|
74
41
|
routes: AppRoute[]
|
|
@@ -100,10 +67,10 @@ export type AppChecker = {
|
|
|
100
67
|
*/
|
|
101
68
|
saw: (path: string | undefined) => void
|
|
102
69
|
/**
|
|
103
|
-
* The app
|
|
104
|
-
*
|
|
70
|
+
* The app, laid out (`appLayout`) — kept until `saw` hears anything move, so a check asked again
|
|
71
|
+
* and again while an editor types re-reads nothing on disk.
|
|
105
72
|
*/
|
|
106
|
-
|
|
73
|
+
layout: () => Promise<AppLayout>
|
|
107
74
|
/**
|
|
108
75
|
* Whether the project it was opened on is still the app's; if so, flushes what `saw` held.
|
|
109
76
|
*
|
|
@@ -136,21 +103,21 @@ export async function openChecker(appRoot: string): Promise<AppChecker> {
|
|
|
136
103
|
session.root = root
|
|
137
104
|
const moved = new Set<string>()
|
|
138
105
|
let movedSomewhere = false
|
|
139
|
-
let
|
|
106
|
+
let layout: Promise<AppLayout> | undefined
|
|
140
107
|
return {
|
|
141
108
|
session,
|
|
142
109
|
root,
|
|
143
110
|
saw: (path) => {
|
|
144
|
-
|
|
111
|
+
layout = undefined
|
|
145
112
|
if (path === undefined) movedSomewhere = true
|
|
146
113
|
else moved.add(path)
|
|
147
114
|
},
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
115
|
+
layout: () => {
|
|
116
|
+
layout ??= appLayout(root).catch((thrown: unknown) => {
|
|
117
|
+
layout = undefined
|
|
151
118
|
throw thrown
|
|
152
119
|
})
|
|
153
|
-
return
|
|
120
|
+
return layout
|
|
154
121
|
},
|
|
155
122
|
current: () => {
|
|
156
123
|
if (projectFiles(root) !== opened) return false
|
|
@@ -183,7 +150,7 @@ function projectFiles(root: string): string {
|
|
|
183
150
|
* `kept` is one the caller holds across runs and closes itself; one opened here is closed whatever
|
|
184
151
|
* `run` did.
|
|
185
152
|
*/
|
|
186
|
-
|
|
153
|
+
async function withChecker<Result>(
|
|
187
154
|
root: string,
|
|
188
155
|
kept: AppChecker | undefined,
|
|
189
156
|
run: (session: TypeSession) => Promise<Result>,
|
|
@@ -196,135 +163,6 @@ export async function withChecker<Result>(
|
|
|
196
163
|
}
|
|
197
164
|
}
|
|
198
165
|
|
|
199
|
-
/**
|
|
200
|
-
* Every `.abide` of the app's own, read, in path order — and the root as bun resolves it.
|
|
201
|
-
*
|
|
202
|
-
* `Bun.resolveSync` answers a symlink-free path (`/tmp` is `/private/tmp` on macOS), so a root
|
|
203
|
-
* spelled through a link would have every import resolve to a file this walk never listed, and
|
|
204
|
-
* every component read as uninvoked. THE READS OVERLAP: nothing a source is read for depends on
|
|
205
|
-
* another, and this runs again on every `abide dev` save.
|
|
206
|
-
*/
|
|
207
|
-
export async function sourcesUnder(
|
|
208
|
-
appRoot: string,
|
|
209
|
-
): Promise<{ root: string; sources: { path: string; text: string }[] }> {
|
|
210
|
-
const root = realpathSync(appRoot)
|
|
211
|
-
return { root, sources: await sourcesAt(root, abideUnder(root)) }
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/** The `.abide` at each of `relatives`, below `root`, read. */
|
|
215
|
-
async function sourcesAt(
|
|
216
|
-
root: string,
|
|
217
|
-
relatives: readonly string[],
|
|
218
|
-
): Promise<{ path: string; text: string }[]> {
|
|
219
|
-
const paths: string[] = []
|
|
220
|
-
for (const path of relatives) paths.push(`${root}/${path}`)
|
|
221
|
-
const texts = await Promise.all(paths.map((path) => Bun.file(path).text()))
|
|
222
|
-
const sources: { path: string; text: string }[] = []
|
|
223
|
-
for (let at = 0; at < paths.length; at += 1)
|
|
224
|
-
sources.push({ path: paths[at] as string, text: texts[at] as string })
|
|
225
|
-
return sources
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* `parsed` and every `.abide` outside them that one of them imports — a dependency's, or a linked
|
|
230
|
-
* package's — parsed, and every one those import in turn; and every `.abide` under a directory one
|
|
231
|
-
* of them mounts, at the position the mount gives it (23.19). With them, the app's own handler files
|
|
232
|
-
* — `own`, below `root`, as its walk found them (`appFiles`) — and every one a mounted directory
|
|
233
|
-
* holds, each at the position it answers by.
|
|
234
|
-
*
|
|
235
|
-
* 34.23 — COMPILED WITH THE APP, by the app's checker, so a component a package ships as source is
|
|
236
|
-
* one of the app's in every way but its address: it is never routed (`placeOf`) unless mounted.
|
|
237
|
-
*/
|
|
238
|
-
export async function withDependencies(
|
|
239
|
-
root: string,
|
|
240
|
-
parsed: readonly ParsedSource[],
|
|
241
|
-
own: readonly string[],
|
|
242
|
-
resolve: (specifier: string, from: string) => string | undefined,
|
|
243
|
-
refused?: Diagnosis[],
|
|
244
|
-
): Promise<{
|
|
245
|
-
sources: ParsedSource[]
|
|
246
|
-
handlers: HandlerFile[]
|
|
247
|
-
mounted: Compilation['mounted']
|
|
248
|
-
}> {
|
|
249
|
-
const all = [...parsed]
|
|
250
|
-
const handlers: HandlerFile[] = []
|
|
251
|
-
for (const relative of own)
|
|
252
|
-
handlers.push({
|
|
253
|
-
path: `${root}/${relative}`,
|
|
254
|
-
relative,
|
|
255
|
-
position: relative,
|
|
256
|
-
})
|
|
257
|
-
// The mounted handler files placed so far; the app's own is never mounted (23.20).
|
|
258
|
-
const handled = new Set<string>()
|
|
259
|
-
const mounted: Compilation['mounted'] = {}
|
|
260
|
-
const byPath = new Map<string, ParsedSource>()
|
|
261
|
-
for (const source of all) byPath.set(source.path, source)
|
|
262
|
-
// 23.20 — a directory mounts once, and by whom.
|
|
263
|
-
const mountedBy = new Map<string, string>()
|
|
264
|
-
const refuse = (path: string, message: string): void => {
|
|
265
|
-
if (refused === undefined) throw new Error(message)
|
|
266
|
-
refused.push(refusal(path, new Error(message)))
|
|
267
|
-
}
|
|
268
|
-
const add = async (found: {
|
|
269
|
-
path: string
|
|
270
|
-
position?: string
|
|
271
|
-
}): Promise<void> => {
|
|
272
|
-
const text = await Bun.file(found.path).text()
|
|
273
|
-
for (const one of parseSources([{ ...found, text }], refused)) {
|
|
274
|
-
byPath.set(one.path, one)
|
|
275
|
-
all.push(one)
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
for (let at = 0; at < all.length; at += 1) {
|
|
279
|
-
const source = all[at] as ParsedSource
|
|
280
|
-
const position = source.position ?? relativeTo(root, source.path)
|
|
281
|
-
const mounts = mountsOf(root, source.path, position, source.file)
|
|
282
|
-
if (typeof mounts === 'string') {
|
|
283
|
-
refuse(source.path, mounts)
|
|
284
|
-
continue
|
|
285
|
-
}
|
|
286
|
-
for (const mount of mounts) {
|
|
287
|
-
const other = mountedBy.get(mount.directory)
|
|
288
|
-
if (other !== undefined) {
|
|
289
|
-
refuse(
|
|
290
|
-
source.path,
|
|
291
|
-
`abide: ${other} and ${position} both mount ${mount.specifier}, and a directory answers at one place. RULEBOOK 23.20.`,
|
|
292
|
-
)
|
|
293
|
-
continue
|
|
294
|
-
}
|
|
295
|
-
mountedBy.set(mount.directory, position)
|
|
296
|
-
mounted[relativeTo(root, mount.directory)] = mount.at
|
|
297
|
-
for (const placed of mount.handlers) {
|
|
298
|
-
// Two mounts can overlap, one directory inside the other; a file is placed once.
|
|
299
|
-
if (handled.has(placed.path)) continue
|
|
300
|
-
handled.add(placed.path)
|
|
301
|
-
handlers.push({
|
|
302
|
-
path: placed.path,
|
|
303
|
-
relative: relativeTo(root, placed.path),
|
|
304
|
-
position: placed.position,
|
|
305
|
-
})
|
|
306
|
-
}
|
|
307
|
-
for (const { path, position: placed } of mount.files) {
|
|
308
|
-
// A file a component import reached first takes its mount's position all the same.
|
|
309
|
-
const known = byPath.get(path)
|
|
310
|
-
if (known !== undefined) {
|
|
311
|
-
known.position = placed
|
|
312
|
-
continue
|
|
313
|
-
}
|
|
314
|
-
await add({ path, position: placed })
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
for (const target of abideImportsOf(
|
|
318
|
-
source.path,
|
|
319
|
-
source.file,
|
|
320
|
-
resolve,
|
|
321
|
-
).values()) {
|
|
322
|
-
if (!byPath.has(target)) await add({ path: target })
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
return { sources: all, handlers, mounted }
|
|
326
|
-
}
|
|
327
|
-
|
|
328
166
|
/**
|
|
329
167
|
* The `Compilation` alone out of `app`, which is what is written to disk and served: the rest of a
|
|
330
168
|
* compiled app is the commands' to read.
|
|
@@ -339,7 +177,8 @@ export function compilationOf(app: CompiledApp): Compilation {
|
|
|
339
177
|
}
|
|
340
178
|
|
|
341
179
|
/**
|
|
342
|
-
* Compiles every `.abide` source and handler of the app at `appRoot` (`Compilation`)
|
|
180
|
+
* Compiles every `.abide` source and handler of the app at `appRoot` (`Compilation`), stopping at
|
|
181
|
+
* the first thing it refuses.
|
|
343
182
|
*
|
|
344
183
|
* `kept` is a checker the caller holds across compiles and closes itself.
|
|
345
184
|
*/
|
|
@@ -347,185 +186,162 @@ export async function compileApp(
|
|
|
347
186
|
appRoot: string,
|
|
348
187
|
kept?: AppChecker,
|
|
349
188
|
): Promise<CompiledApp> {
|
|
189
|
+
return (await compiled(appRoot, kept, false)).app
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* The app at `appRoot` compiled — THE ONE PIPELINE `abide build` and `abide check` both run, the
|
|
194
|
+
* layout, the reads, the lints and the emit, differing only in what a refusal does (`Refusals`) and
|
|
195
|
+
* in what is checked beside: a build stops at the first refusal, and a check (`checking`) reports
|
|
196
|
+
* every one at its file with what the checker says of each file (38.3), and goes on checking
|
|
197
|
+
* whatever a refusal does not stop (38.33).
|
|
198
|
+
*/
|
|
199
|
+
export async function compiled(
|
|
200
|
+
appRoot: string,
|
|
201
|
+
kept: AppChecker | undefined,
|
|
202
|
+
checking: boolean,
|
|
203
|
+
): Promise<{ app: CompiledApp; findings: Diagnosis[] }> {
|
|
350
204
|
const root = realpathSync(appRoot)
|
|
351
|
-
|
|
352
|
-
//
|
|
353
|
-
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
205
|
+
const refusals = new Refusals(root, checking)
|
|
206
|
+
// ONE LAYOUT for the sources, the handler files and the public files, and what the tree refuses
|
|
207
|
+
// is met before the checker is asked anything.
|
|
208
|
+
const layout = await (kept?.layout() ?? appLayout(root))
|
|
209
|
+
refusals.adopt(layout.refused)
|
|
210
|
+
const app: CompiledApp = {
|
|
211
|
+
files: {},
|
|
212
|
+
tables: {},
|
|
213
|
+
handlers: {},
|
|
214
|
+
mounted: layout.mounted,
|
|
215
|
+
routes: [],
|
|
216
|
+
mounts: [],
|
|
217
|
+
sources: layout.own,
|
|
218
|
+
warnings: [],
|
|
219
|
+
published: layout.published,
|
|
220
|
+
}
|
|
221
|
+
// 16.2 — an app of handler files alone is still an app; one of neither has nothing to build, and
|
|
222
|
+
// a check still has its `.ts` to check.
|
|
223
|
+
if (
|
|
224
|
+
!checking &&
|
|
225
|
+
layout.sources.length === 0 &&
|
|
226
|
+
layout.handlers.length === 0
|
|
227
|
+
)
|
|
228
|
+
return { app, findings: [] }
|
|
368
229
|
|
|
369
|
-
// One session for the whole
|
|
230
|
+
// One session for the whole compile: opening it spawns the checker, and the cost is paid once
|
|
370
231
|
// rather than per file.
|
|
371
232
|
return withChecker(root, kept, async (session) => {
|
|
372
|
-
// A
|
|
373
|
-
|
|
374
|
-
const
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
if (ambient.length > 0) {
|
|
384
|
-
const lines: string[] = []
|
|
385
|
-
for (const read of ambient)
|
|
386
|
-
lines.push(formatDiagnosis(read, process.cwd()))
|
|
387
|
-
throw new Error(lines.join('\n'))
|
|
388
|
-
}
|
|
389
|
-
const emitted = await emitApp(
|
|
390
|
-
root,
|
|
233
|
+
// 38.33 — A FILE WHOSE PROJECTION IS REFUSED (34.3, 34.17) is refused at its file, found here
|
|
234
|
+
// because every later step projects it and would refuse it for the whole app.
|
|
235
|
+
const projected: PlacedSource[] = []
|
|
236
|
+
for (const source of layout.sources)
|
|
237
|
+
try {
|
|
238
|
+
projectShadow(source.file)
|
|
239
|
+
projected.push(source)
|
|
240
|
+
} catch (thrown) {
|
|
241
|
+
refusals.at(source.path, thrown)
|
|
242
|
+
}
|
|
243
|
+
const { read, warnings } = await readSources(
|
|
391
244
|
session,
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
handlerFiles,
|
|
245
|
+
projected,
|
|
246
|
+
refusals,
|
|
395
247
|
)
|
|
248
|
+
let found: Diagnosis[] = []
|
|
249
|
+
let emittable = read
|
|
250
|
+
if (checking) {
|
|
251
|
+
// THE APP'S OWN FILES ARE REPORTED ON: a dependency's is compiled with them (34.23), and
|
|
252
|
+
// what is wrong inside a package is the package's to report.
|
|
253
|
+
const reported: typeof read = []
|
|
254
|
+
for (const source of read)
|
|
255
|
+
if (!source.dependency) reported.push(source)
|
|
256
|
+
const diagnosed = await session.diagnose(reported, layout)
|
|
257
|
+
found = diagnosed.diagnoses
|
|
258
|
+
// A FILE WHOSE LOWERING REFUSED is `found` already, at its line, and is not emitted.
|
|
259
|
+
emittable = []
|
|
260
|
+
for (const source of read)
|
|
261
|
+
if (!diagnosed.refused.has(source.path)) emittable.push(source)
|
|
262
|
+
}
|
|
263
|
+
const { ambient, unkeyed } = await session.lints(root, projected)
|
|
264
|
+
// 11.37 — a build error, every read named.
|
|
265
|
+
refusals.adopt(ambient)
|
|
266
|
+
// What the emit throws with no one file to name is refused at the root; a refusal of a file
|
|
267
|
+
// is refused at it.
|
|
268
|
+
let emitWarnings: Diagnosis[] = []
|
|
269
|
+
try {
|
|
270
|
+
const whole = await emitApp(layout, session, emittable, refusals)
|
|
271
|
+
Object.assign(app, whole)
|
|
272
|
+
emitWarnings = whole.warnings
|
|
273
|
+
} catch (thrown) {
|
|
274
|
+
refusals.at(root, thrown)
|
|
275
|
+
}
|
|
276
|
+
// 11.30 — warned on, and the build goes on.
|
|
277
|
+
app.warnings = [...warnings, ...unkeyed, ...emitWarnings]
|
|
396
278
|
return {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
279
|
+
app,
|
|
280
|
+
findings: [
|
|
281
|
+
...refusals.found,
|
|
282
|
+
...unkeyed,
|
|
283
|
+
...warnings,
|
|
284
|
+
...emitWarnings,
|
|
285
|
+
...found,
|
|
286
|
+
],
|
|
403
287
|
}
|
|
404
288
|
})
|
|
405
289
|
}
|
|
406
290
|
|
|
407
|
-
/**
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
export async function emitApp(
|
|
414
|
-
root: string,
|
|
291
|
+
/** The emit half of `compiled`, over sources already laid out and read. */
|
|
292
|
+
async function emitApp(
|
|
293
|
+
layout: Pick<
|
|
294
|
+
AppLayout,
|
|
295
|
+
'root' | 'handlers' | 'routes' | 'errors' | 'published'
|
|
296
|
+
>,
|
|
415
297
|
session: TypeSession,
|
|
416
|
-
sources: readonly ReadSource[],
|
|
417
|
-
|
|
418
|
-
resolve: (specifier: string, from: string) => string | undefined,
|
|
419
|
-
/** The handler files, the app's own and each mounted one (`withDependencies`). */
|
|
420
|
-
handlerFiles: readonly HandlerFile[],
|
|
421
|
-
/** Told of each refusal at its file; the build's throws the first. */
|
|
422
|
-
refuse: Refuse = THROW,
|
|
298
|
+
sources: readonly ReadSource<PlacedSource>[],
|
|
299
|
+
refusals: Refusals,
|
|
423
300
|
): Promise<
|
|
424
301
|
Pick<
|
|
425
302
|
CompiledApp,
|
|
426
303
|
'files' | 'tables' | 'handlers' | 'routes' | 'mounts' | 'warnings'
|
|
427
304
|
>
|
|
428
305
|
> {
|
|
306
|
+
const { root, routes, errors } = layout
|
|
429
307
|
const files: Record<string, CompiledFile> = {}
|
|
430
308
|
const tables: Record<string, string> = {}
|
|
431
|
-
const routes: AppRoute[] = []
|
|
432
|
-
// 32.17 AND 34.14 — WHICH FILES ARE INVOKED AS COMPONENTS, and where each import lands. A
|
|
433
|
-
// caller's client module imports its child's, so a child with nothing to hydrate still
|
|
434
|
-
// needs one to exist (`emitClient`'s no-op), and an invoked file carries its own markup
|
|
435
|
-
// (`emitClient`). Built before the emit loop because a page may invoke a component the
|
|
436
|
-
// walk reaches later.
|
|
437
|
-
const graph = appGraph(root, sources, resolve)
|
|
438
|
-
const parsed = graph.nodes
|
|
439
|
-
|
|
440
|
-
// 35.1, 35.12 — every `layout.abide` by the route it covers, one to a route; 35.13 — and one
|
|
441
|
-
// with no template wraps nothing, being the route's hooks alone.
|
|
442
|
-
const layouts: AppLayout[] = []
|
|
443
|
-
const layoutAt = new Map<string, string>()
|
|
444
|
-
const errorAt = new Map<string, string>()
|
|
445
|
-
for (const entry of parsed) {
|
|
446
|
-
if (entry.kind !== 'layout' && entry.kind !== 'error') continue
|
|
447
|
-
const route = routeOf(entry.position)
|
|
448
|
-
const held = entry.kind === 'layout' ? layoutAt : errorAt
|
|
449
|
-
const other = held.get(shapeOf(route))
|
|
450
|
-
if (other !== undefined) {
|
|
451
|
-
refuse(
|
|
452
|
-
entry.relative,
|
|
453
|
-
new Error(
|
|
454
|
-
`abide: ${other} and ${entry.relative} are both the ${entry.kind}.abide at ${route}. RULEBOOK 35.12.`,
|
|
455
|
-
),
|
|
456
|
-
)
|
|
457
|
-
continue
|
|
458
|
-
}
|
|
459
|
-
held.set(shapeOf(route), entry.relative)
|
|
460
|
-
if (entry.kind === 'layout' && entry.file.template.length > 0)
|
|
461
|
-
layouts.push({
|
|
462
|
-
source: entry.relative,
|
|
463
|
-
segments: parseRoutePattern(route),
|
|
464
|
-
})
|
|
465
|
-
}
|
|
466
|
-
// 35.2 — every `error.abide`, with the route it answers for and the layouts around it.
|
|
467
|
-
const errors: AppRoute[] = []
|
|
468
|
-
for (const entry of parsed) {
|
|
469
|
-
if (entry.kind !== 'page' && entry.kind !== 'error') continue
|
|
470
|
-
// 23.3, 35.14 — a page or an `error.abide` with nothing to paint is its route's hooks
|
|
471
|
-
// alone.
|
|
472
|
-
if (!paints(entry.file)) continue
|
|
473
|
-
;(entry.kind === 'page' ? routes : errors).push({
|
|
474
|
-
pattern: entry.route as string,
|
|
475
|
-
source: entry.relative,
|
|
476
|
-
layouts: layoutsAround(entry.route as string, layouts),
|
|
477
|
-
})
|
|
478
|
-
}
|
|
479
|
-
|
|
480
309
|
// 16.2 AND 16.28 — THE HANDLERS, read before a module is emitted: the server mounts each one,
|
|
481
310
|
// and the browser holds its wrapper in its place — a handler file's as that file, a private
|
|
482
311
|
// handler's in the server block's place in its own file's module. Written whether or not there
|
|
483
312
|
// is a page: a handler is answered over http to a caller that has none.
|
|
484
313
|
const { mounts, warnings } = await handlersUnder(
|
|
485
314
|
session,
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
315
|
+
sources,
|
|
316
|
+
layout.handlers,
|
|
317
|
+
refusals,
|
|
489
318
|
)
|
|
490
|
-
refuseSharedRoutes(routes, mounts,
|
|
491
|
-
|
|
319
|
+
refuseSharedRoutes(routes, mounts, refusals)
|
|
320
|
+
refuseAddressCollision(routes, mounts, layout.published, refusals)
|
|
492
321
|
const standIns = standInsOf(mounts)
|
|
493
322
|
// 37.1 — every module a server block or a handler file runs in, for the boot to import before
|
|
494
323
|
// `onStart`.
|
|
495
324
|
const servers: string[] = []
|
|
496
|
-
for (const file of
|
|
325
|
+
for (const file of layout.handlers) servers.push(file.relative)
|
|
497
326
|
let transitions = false
|
|
498
|
-
for (const entry of
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
refuse(
|
|
506
|
-
entry.relative,
|
|
507
|
-
new Error(
|
|
508
|
-
`abide: ${entry.relative} writes <html>, and of the pages, layouts and error pages only the root layout.abide writes the document. RULEBOOK 35.16.`,
|
|
509
|
-
),
|
|
327
|
+
for (const entry of sources) {
|
|
328
|
+
let emitted: ReturnType<typeof emitSource>
|
|
329
|
+
try {
|
|
330
|
+
emitted = emitSource(
|
|
331
|
+
entry,
|
|
332
|
+
standIns.get(entry.relative),
|
|
333
|
+
entry.text,
|
|
510
334
|
)
|
|
335
|
+
} catch (thrown) {
|
|
336
|
+
refusals.at(entry.path, thrown)
|
|
511
337
|
continue
|
|
512
338
|
}
|
|
513
|
-
const emitted = emitSource(
|
|
514
|
-
entry.relative,
|
|
515
|
-
entry.file,
|
|
516
|
-
entry.answer,
|
|
517
|
-
graph.invoked.has(entry.path),
|
|
518
|
-
standIns.get(entry.relative),
|
|
519
|
-
entry.text,
|
|
520
|
-
entry.position,
|
|
521
|
-
)
|
|
522
339
|
if (entry.file.servers.length > 0) servers.push(entry.relative)
|
|
523
340
|
transitions ||= emitted.transitions
|
|
524
341
|
// 34.26 — a dependency's module runs on the app's runtime, whatever the package carries.
|
|
525
|
-
const onAppRuntime =
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
: (text: string) => text
|
|
342
|
+
const onAppRuntime = entry.dependency
|
|
343
|
+
? (text: string) => onAbideOf(root, entry.path, text)
|
|
344
|
+
: (text: string) => text
|
|
529
345
|
files[entry.relative] = {
|
|
530
346
|
server: onAppRuntime(emitted.server),
|
|
531
347
|
client: onAppRuntime(emitted.client),
|
|
@@ -541,11 +357,10 @@ export async function emitApp(
|
|
|
541
357
|
// forgotten. The loaders are dynamic imports, which is what keeps a destination's
|
|
542
358
|
// markup and bindings off the entry page's bundle until somebody navigates there.
|
|
543
359
|
//
|
|
544
|
-
// TWO TABLES OFF ONE
|
|
360
|
+
// TWO TABLES OFF ONE LAYOUT, each importing a file's `?component` face — the same addresses
|
|
545
361
|
// either way, which is what keeps the pair from being two walks that can disagree. The
|
|
546
|
-
// server's is written with no
|
|
547
|
-
//
|
|
548
|
-
// that boots it, only where there is a page to boot.
|
|
362
|
+
// server's is written with no page too — an app of handlers alone still answers — and the
|
|
363
|
+
// client's, with the entry that boots it, only where there is a page to boot.
|
|
549
364
|
tables[SERVER_TABLE] = serverTable(routes, errors, servers, mounts)
|
|
550
365
|
// 16.74 — what the browser holds of each handler file, and the route its hooks are filed at:
|
|
551
366
|
// read by the module's load, which runs before the server table is imported.
|
|
@@ -584,11 +399,7 @@ export async function emitApp(
|
|
|
584
399
|
function onAbideOf(root: string, path: string, text: string): string {
|
|
585
400
|
const from = dirname(realpathSync(path))
|
|
586
401
|
return respecified(text, (specifier) => {
|
|
587
|
-
if (
|
|
588
|
-
specifier !== SHARED_SPECIFIER &&
|
|
589
|
-
!specifier.startsWith(`${SHARED_SPECIFIER}/`)
|
|
590
|
-
)
|
|
591
|
-
return undefined
|
|
402
|
+
if (!isAbideSpecifier(specifier)) return undefined
|
|
592
403
|
let target: string
|
|
593
404
|
try {
|
|
594
405
|
target = resolvedFile(specifier, root)
|
|
@@ -619,41 +430,6 @@ function tableImport(source: string): string {
|
|
|
619
430
|
)
|
|
620
431
|
}
|
|
621
432
|
|
|
622
|
-
// A layout and the route it covers, parsed.
|
|
623
|
-
type AppLayout = { source: string; segments: Segment[] }
|
|
624
|
-
|
|
625
|
-
/**
|
|
626
|
-
* The layouts a page renders inside, outermost first.
|
|
627
|
-
*
|
|
628
|
-
* 35.1 — every `layout.abide` whose route lies ON THE WAY DOWN to the page's wraps it and no other
|
|
629
|
-
* does: `src/layout.abide` wraps the page `src/media/index.abide` serves at `/media`, and so does
|
|
630
|
-
* `src/[section]/layout.abide`. So the test is a prefix of the route, kind by kind, and the nesting
|
|
631
|
-
* order is the route's depth.
|
|
632
|
-
*/
|
|
633
|
-
function layoutsAround(route: string, layouts: AppLayout[]): string[] {
|
|
634
|
-
const page = parseRoutePattern(route)
|
|
635
|
-
const chain: AppLayout[] = []
|
|
636
|
-
for (const layout of layouts)
|
|
637
|
-
if (coversRoute(layout.segments, page)) chain.push(layout)
|
|
638
|
-
// A shorter route is an outer one: they are prefixes of each other by construction.
|
|
639
|
-
chain.sort((one, two) => one.segments.length - two.segments.length)
|
|
640
|
-
const sources: string[] = []
|
|
641
|
-
for (const layout of chain) sources.push(layout.source)
|
|
642
|
-
return sources
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
/**
|
|
646
|
-
* A route with its parameters' names erased: what two routes answering one address share.
|
|
647
|
-
*
|
|
648
|
-
* 19.1 — `/media/[id]` and `/media/[slug]` are one route.
|
|
649
|
-
*/
|
|
650
|
-
function shapeOf(route: string): string {
|
|
651
|
-
let shape = ''
|
|
652
|
-
for (const segment of parseRoutePattern(route))
|
|
653
|
-
shape += `/${segment.kind === 'static' ? segment.name : `[${segment.kind}]`}`
|
|
654
|
-
return shape === '' ? '/' : shape
|
|
655
|
-
}
|
|
656
|
-
|
|
657
433
|
/**
|
|
658
434
|
* Refuses, naming both, where two files answer one method at one route — at the second.
|
|
659
435
|
*
|
|
@@ -662,7 +438,7 @@ function shapeOf(route: string): string {
|
|
|
662
438
|
function refuseSharedRoutes(
|
|
663
439
|
routes: AppRoute[],
|
|
664
440
|
mounts: Mount[],
|
|
665
|
-
|
|
441
|
+
refusals: Refusals,
|
|
666
442
|
): void {
|
|
667
443
|
const owners = new Map<string, string>()
|
|
668
444
|
const claim = (method: string, route: string, source: string): void => {
|
|
@@ -670,7 +446,7 @@ function refuseSharedRoutes(
|
|
|
670
446
|
const owner = owners.get(key)
|
|
671
447
|
if (owner === undefined) owners.set(key, source)
|
|
672
448
|
else
|
|
673
|
-
|
|
449
|
+
refusals.at(
|
|
674
450
|
source,
|
|
675
451
|
new Error(
|
|
676
452
|
`abide: ${owner} and ${source} both answer ${method} at ${route}. RULEBOOK 19.1.`,
|
|
@@ -682,110 +458,44 @@ function refuseSharedRoutes(
|
|
|
682
458
|
}
|
|
683
459
|
|
|
684
460
|
/**
|
|
685
|
-
* Refuses, naming
|
|
461
|
+
* Refuses, naming both, where a public file claims a route's address or another public file's — at
|
|
462
|
+
* the public file.
|
|
686
463
|
*
|
|
687
|
-
*
|
|
464
|
+
* 36.13 — the walks that find routes and public files cannot see each other's collisions from
|
|
465
|
+
* inside either one; this is where they meet, and naming BOTH is the clause. Two `public`
|
|
466
|
+
* directories can claim one address too, since each mounts at the path above it (36.11). IN THE
|
|
467
|
+
* COMPILE, so the build, dev and check all ask it (38.33), and `abide start` trusts the build:
|
|
468
|
+
* `answerRequest` answers a public file AHEAD of any route on the ground that a collision cannot
|
|
469
|
+
* exist at run time, and 30.8 says dev resolves an address the way the built app does.
|
|
688
470
|
*/
|
|
689
|
-
function
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
471
|
+
function refuseAddressCollision(
|
|
472
|
+
routes: AppRoute[],
|
|
473
|
+
mounts: Mount[],
|
|
474
|
+
published: Map<string, string>,
|
|
475
|
+
refusals: Refusals,
|
|
693
476
|
): void {
|
|
694
|
-
const
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
)
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
stray(source.relative)
|
|
707
|
-
for (const file of handlerFiles)
|
|
708
|
-
if (callsLauncher(readFileSync(file.path, 'utf8'))) stray(file.relative)
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
function callsLauncher(source: string): boolean {
|
|
712
|
-
return (
|
|
713
|
-
source.includes('onLauncher') &&
|
|
714
|
-
hooksCalled(source).includes('onLauncher')
|
|
715
|
-
)
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
/**
|
|
719
|
-
* Resolves a specifier from a directory as bun does, or `undefined` where it does not resolve.
|
|
720
|
-
*
|
|
721
|
-
* A COMPONENT IMPORT RESOLVES THE WAY THE APP'S OWN IMPORTS DO — its import map, from the file
|
|
722
|
-
* that wrote it. One that does not resolve is left as written, and the bundle names it. THE
|
|
723
|
-
* ANSWER IS A REAL PATH, as every root handed to the compiler is.
|
|
724
|
-
*/
|
|
725
|
-
export function resolverIn(): (
|
|
726
|
-
specifier: string,
|
|
727
|
-
from: string,
|
|
728
|
-
) => string | undefined {
|
|
729
|
-
// ONE ANSWER PER SPECIFIER AND DIRECTORY for as long as the resolver lives: a compile resolves
|
|
730
|
-
// every import once for its dependencies and again for its graph, and hands both one resolver.
|
|
731
|
-
const answered = new Map<string, string | undefined>()
|
|
732
|
-
return (specifier, from) => {
|
|
733
|
-
const key = `${from}\0${specifier}`
|
|
734
|
-
if (answered.has(key)) return answered.get(key)
|
|
735
|
-
const found = resolveFrom(specifier, from)
|
|
736
|
-
answered.set(key, found)
|
|
737
|
-
return found
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
function resolveFrom(specifier: string, from: string): string | undefined {
|
|
742
|
-
try {
|
|
743
|
-
return resolvedFile(specifier, from)
|
|
744
|
-
} catch {
|
|
745
|
-
// 38.65 — BUN KEEPS A DIRECTORY'S LISTING FOR THE LIFE OF THE PROCESS, and looks a `#` specifier up
|
|
746
|
-
// in it with no second look — where a relative one that misses reads the directory again.
|
|
747
|
-
// `abide dev` compiles in one process for its whole session, so a component created since
|
|
748
|
-
// its directory was listed never resolved, and was read as uninvoked. The `imports` map is
|
|
749
|
-
// read here instead, and the file it names looked for on disk.
|
|
750
|
-
return specifier.startsWith('#')
|
|
751
|
-
? throughImports(specifier, from)
|
|
752
|
-
: undefined
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
/** A `#` specifier resolved through the nearest `package.json`'s `imports`, to a file that exists. */
|
|
757
|
-
function throughImports(specifier: string, from: string): string | undefined {
|
|
758
|
-
const manifest = nearest(from, 'package.json')
|
|
759
|
-
if (manifest === undefined) return undefined
|
|
760
|
-
const imports = (
|
|
761
|
-
JSON.parse(readFileSync(manifest, 'utf8')) as {
|
|
762
|
-
imports?: Record<string, unknown>
|
|
763
|
-
}
|
|
764
|
-
).imports
|
|
765
|
-
if (imports === undefined) return undefined
|
|
766
|
-
const base = dirname(manifest)
|
|
767
|
-
for (const [key, targets] of Object.entries(imports)) {
|
|
768
|
-
const star = key.indexOf('*')
|
|
769
|
-
let matched: string | undefined
|
|
770
|
-
if (star === -1) matched = key === specifier ? '' : undefined
|
|
771
|
-
else if (
|
|
772
|
-
specifier.startsWith(key.slice(0, star)) &&
|
|
773
|
-
specifier.endsWith(key.slice(star + 1)) &&
|
|
774
|
-
specifier.length >= key.length - 1
|
|
775
|
-
)
|
|
776
|
-
matched = specifier.slice(
|
|
777
|
-
star,
|
|
778
|
-
specifier.length - (key.length - star - 1),
|
|
477
|
+
const patterns = new Set<string>()
|
|
478
|
+
for (const route of routes) patterns.add(route.pattern)
|
|
479
|
+
for (const mount of mounts) patterns.add(mount.address)
|
|
480
|
+
const claimed = new Map<string, string>()
|
|
481
|
+
for (const [source, address] of published) {
|
|
482
|
+
const other = claimed.get(address)
|
|
483
|
+
if (patterns.has(address))
|
|
484
|
+
refusals.at(
|
|
485
|
+
source,
|
|
486
|
+
new Error(
|
|
487
|
+
`abide: ${source} and the route ${address} claim the same address. RULEBOOK 36.13.`,
|
|
488
|
+
),
|
|
779
489
|
)
|
|
780
|
-
if (
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
490
|
+
else if (other !== undefined)
|
|
491
|
+
refusals.at(
|
|
492
|
+
source,
|
|
493
|
+
new Error(
|
|
494
|
+
`abide: ${other} and ${source} are both answered at ${address}. RULEBOOK 36.13.`,
|
|
495
|
+
),
|
|
496
|
+
)
|
|
497
|
+
else claimed.set(address, source)
|
|
787
498
|
}
|
|
788
|
-
return undefined
|
|
789
499
|
}
|
|
790
500
|
|
|
791
501
|
/** The rows of either route table, one per route, `column` appending the fields only one carries. */
|
|
@@ -942,11 +652,6 @@ import { wire as __wire } from '${WIRE_SPECIFIER}'
|
|
|
942
652
|
${lines.join('')}`
|
|
943
653
|
}
|
|
944
654
|
|
|
945
|
-
/** Whether a page or an error page has anything to paint: a template, or a head. 23.3, 35.14. */
|
|
946
|
-
export function paints(file: AbideFile): boolean {
|
|
947
|
-
return file.template.length > 0 || file.head.length > 0
|
|
948
|
-
}
|
|
949
|
-
|
|
950
655
|
// THE ENTRY. An app does not write it: which module boots the client is the build's knowledge,
|
|
951
656
|
// exactly as the route table is, and a hand-written entry is one more file to keep in step with a
|
|
952
657
|
// layout that moves.
|