@abide/abide 0.102.0 → 0.104.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 +23 -0
- package/bin/abide.ts +31 -28
- package/docs/REGISTRY.md +40 -36
- package/docs/RULEBOOK.md +113 -92
- package/docs/abide.md +788 -554
- package/package.json +2 -1
- package/scaffold/AGENTS.md +13 -6
- package/src/compiler/TypeSession.ts +108 -35
- package/src/compiler/appMap.ts +91 -0
- package/src/compiler/argsSchemaOf.ts +1 -1
- package/src/compiler/checkApp.ts +35 -18
- package/src/compiler/checkedShadow.ts +40 -7
- package/src/compiler/compileApp.ts +271 -76
- package/src/compiler/compiledEntry.ts +10 -6
- package/src/compiler/componentGraph.ts +6 -7
- package/src/compiler/declaredExports.ts +3 -3
- package/src/compiler/emitClient.ts +3 -3
- package/src/compiler/emitComponent.ts +9 -9
- package/src/compiler/emitSource.ts +5 -9
- package/src/compiler/handlersUnder.ts +114 -73
- package/src/compiler/hooksCalled.ts +35 -0
- package/src/compiler/mountsOf.ts +30 -18
- package/src/compiler/parseAbide.ts +12 -12
- package/src/compiler/projectShadow.ts +71 -46
- package/src/compiler/readsIn.ts +2 -2
- package/src/compiler/scaffold.ts +24 -21
- package/src/compiler/serveLanguage.ts +0 -24
- package/src/compiler/sharedModuleWrites.ts +2 -2
- package/src/compiler/splitImports.ts +4 -4
- package/src/compiler/writeDefaultProject.ts +28 -1
- package/src/server/MODULE_QUERIES.ts +12 -3
- package/src/server/abideModules.ts +156 -53
- package/src/server/answerRequest.ts +9 -4
- package/src/server/answerRpc.ts +18 -16
- package/src/server/appTree.ts +92 -25
- package/src/server/commands.ts +5 -4
- package/src/server/componentModules.d.ts +1 -2
- package/src/server/createApp.ts +23 -11
- package/src/server/devWorker.ts +5 -1
- package/src/server/fenceServer.ts +36 -13
- package/src/server/generatedSurfaces.ts +12 -53
- package/src/server/hooks.ts +17 -4
- package/src/server/index.ts +1 -0
- package/src/server/installSourceMaps.ts +6 -1
- package/src/server/launcherOptions.ts +12 -13
- package/src/server/launcherPage.ts +2 -2
- package/src/server/nativeMenu.ts +3 -3
- package/src/server/preload.ts +13 -0
- package/src/server/runDesktop.ts +11 -16
- package/src/server/serveDev.ts +8 -2
- package/src/server/surfaceOf.ts +2 -0
- package/src/server/versionOf.ts +1 -2
- package/src/server/withAbideSources.ts +1 -1
- package/src/shared/ADDRESSES.ts +3 -0
- package/src/shared/LAUNCHER_FILES.ts +9 -0
- package/src/shared/RESERVED_DIRECTORIES.ts +19 -4
- package/src/shared/index.ts +2 -1
- package/src/{ui → shared}/menuBridge.ts +1 -1
- package/src/shared/menuItem.ts +2 -2
- package/src/shared/onMenu.ts +58 -0
- package/src/shared/reactive/Cursor.ts +66 -123
- package/src/shared/reactive/Reactive.ts +74 -28
- package/src/shared/reactive/callLoad.ts +4 -18
- package/src/shared/reactive/chunksOfStream.ts +10 -34
- package/src/{ui → shared}/reportMenu.ts +7 -9
- package/src/shared/routePattern.ts +74 -23
- package/src/shared/rpc.ts +1 -1
- package/src/ui/Router.ts +0 -6
- package/src/compiler/syncTypes.ts +0 -32
- package/src/compiler/writeTypes.ts +0 -82
- package/src/server/LAUNCHER_FILES.ts +0 -9
- /package/scaffold/src/{page.abide → index.abide} +0 -0
- /package/src/{compiler → shared}/SPECIFIERS.ts +0 -0
|
@@ -12,6 +12,7 @@ import { COMPONENT_QUERY } from '#server/MODULE_QUERIES.ts'
|
|
|
12
12
|
import { ROUTES_TABLE, SERVER_TABLE } from '#server/TABLES.ts'
|
|
13
13
|
import { VIRTUAL_DIRECTORY } from '#server/VIRTUAL_DIRECTORY.ts'
|
|
14
14
|
import { CLIENT_ADDRESS } from '#shared/ADDRESSES.ts'
|
|
15
|
+
import { LAUNCHER_PAGE } from '#shared/LAUNCHER_FILES.ts'
|
|
15
16
|
import {
|
|
16
17
|
coversRoute,
|
|
17
18
|
parseRoutePattern,
|
|
@@ -19,10 +20,21 @@ import {
|
|
|
19
20
|
routeOf,
|
|
20
21
|
type Segment,
|
|
21
22
|
} from '#shared/routePattern.ts'
|
|
23
|
+
import {
|
|
24
|
+
ROUTING_SPECIFIER,
|
|
25
|
+
SHARED_SPECIFIER,
|
|
26
|
+
WIRE_SPECIFIER,
|
|
27
|
+
} from '#shared/SPECIFIERS.ts'
|
|
22
28
|
import { abideImportsOf, appGraph } from './componentGraph.ts'
|
|
23
29
|
import { emitSource } from './emitSource.ts'
|
|
24
30
|
import { formatDiagnosis } from './formatDiagnosis.ts'
|
|
25
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
type HandlerFile,
|
|
33
|
+
handlersUnder,
|
|
34
|
+
type Refuse,
|
|
35
|
+
THROW,
|
|
36
|
+
} from './handlersUnder.ts'
|
|
37
|
+
import { hooksCalled } from './hooksCalled.ts'
|
|
26
38
|
import { mountsOf } from './mountsOf.ts'
|
|
27
39
|
import { nearest } from './nearest.ts'
|
|
28
40
|
import type { AbideFile } from './parseAbide.ts'
|
|
@@ -33,15 +45,9 @@ import {
|
|
|
33
45
|
readSources,
|
|
34
46
|
refusal,
|
|
35
47
|
} from './readSources.ts'
|
|
36
|
-
import {
|
|
37
|
-
ROUTING_SPECIFIER,
|
|
38
|
-
SHARED_SPECIFIER,
|
|
39
|
-
WIRE_SPECIFIER,
|
|
40
|
-
} from './SPECIFIERS.ts'
|
|
41
|
-
import { respecified } from './splitImports.ts'
|
|
48
|
+
import { importStatements, respecified } from './splitImports.ts'
|
|
42
49
|
import { type Diagnosis, TypeSession } from './TypeSession.ts'
|
|
43
50
|
import { writeDefaultProject } from './writeDefaultProject.ts'
|
|
44
|
-
import { writeTypes } from './writeTypes.ts'
|
|
45
51
|
|
|
46
52
|
// AN APP'S `.abide` SOURCES, COMPILED — every file keyed by its path below the root, and loaded
|
|
47
53
|
// there through its `?component` face (`abideModules`). Nothing here touches the output directory:
|
|
@@ -222,18 +228,35 @@ async function sourcesAt(
|
|
|
222
228
|
/**
|
|
223
229
|
* `parsed` and every `.abide` outside them that one of them imports — a dependency's, or a linked
|
|
224
230
|
* package's — parsed, and every one those import in turn; and every `.abide` under a directory one
|
|
225
|
-
* of them mounts, at the position the mount gives it (23.19).
|
|
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.
|
|
226
234
|
*
|
|
227
|
-
* 34.
|
|
235
|
+
* 34.23 — COMPILED WITH THE APP, by the app's checker, so a component a package ships as source is
|
|
228
236
|
* one of the app's in every way but its address: it is never routed (`placeOf`) unless mounted.
|
|
229
237
|
*/
|
|
230
238
|
export async function withDependencies(
|
|
231
239
|
root: string,
|
|
232
240
|
parsed: readonly ParsedSource[],
|
|
241
|
+
own: readonly string[],
|
|
233
242
|
resolve: (specifier: string, from: string) => string | undefined,
|
|
234
243
|
refused?: Diagnosis[],
|
|
235
|
-
): Promise<
|
|
244
|
+
): Promise<{
|
|
245
|
+
sources: ParsedSource[]
|
|
246
|
+
handlers: HandlerFile[]
|
|
247
|
+
mounted: Compilation['mounted']
|
|
248
|
+
}> {
|
|
236
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'] = {}
|
|
237
260
|
const byPath = new Map<string, ParsedSource>()
|
|
238
261
|
for (const source of all) byPath.set(source.path, source)
|
|
239
262
|
// 23.20 — a directory mounts once, and by whom.
|
|
@@ -270,6 +293,17 @@ export async function withDependencies(
|
|
|
270
293
|
continue
|
|
271
294
|
}
|
|
272
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
|
+
}
|
|
273
307
|
for (const { path, position: placed } of mount.files) {
|
|
274
308
|
// A file a component import reached first takes its mount's position all the same.
|
|
275
309
|
const known = byPath.get(path)
|
|
@@ -288,7 +322,20 @@ export async function withDependencies(
|
|
|
288
322
|
if (!byPath.has(target)) await add({ path: target })
|
|
289
323
|
}
|
|
290
324
|
}
|
|
291
|
-
return all
|
|
325
|
+
return { sources: all, handlers, mounted }
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* The `Compilation` alone out of `app`, which is what is written to disk and served: the rest of a
|
|
330
|
+
* compiled app is the commands' to read.
|
|
331
|
+
*/
|
|
332
|
+
export function compilationOf(app: CompiledApp): Compilation {
|
|
333
|
+
return {
|
|
334
|
+
files: app.files,
|
|
335
|
+
handlers: app.handlers,
|
|
336
|
+
tables: app.tables,
|
|
337
|
+
mounted: app.mounted,
|
|
338
|
+
}
|
|
292
339
|
}
|
|
293
340
|
|
|
294
341
|
/**
|
|
@@ -301,14 +348,18 @@ export async function compileApp(
|
|
|
301
348
|
kept?: AppChecker,
|
|
302
349
|
): Promise<CompiledApp> {
|
|
303
350
|
const root = realpathSync(appRoot)
|
|
304
|
-
// ONE WALK for the sources and the public files, refusing a tree 23.16-23.18
|
|
305
|
-
// anything is read.
|
|
306
|
-
const { abide, published } = appFiles(root)
|
|
351
|
+
// ONE WALK for the sources, the handler files and the public files, refusing a tree 23.16-23.18
|
|
352
|
+
// forbids before anything is read.
|
|
353
|
+
const { abide, handlers, modules, published } = appFiles(root)
|
|
354
|
+
refuseModuleHooks(root, modules)
|
|
307
355
|
const sources = await sourcesAt(root, abide)
|
|
308
|
-
|
|
356
|
+
// 16.2 — an app of handler files alone is still an app.
|
|
357
|
+
if (sources.length === 0 && handlers.length === 0)
|
|
309
358
|
return {
|
|
310
359
|
files: {},
|
|
311
360
|
tables: {},
|
|
361
|
+
handlers: {},
|
|
362
|
+
mounted: {},
|
|
312
363
|
routes: [],
|
|
313
364
|
mounts: [],
|
|
314
365
|
sources: 0,
|
|
@@ -321,9 +372,12 @@ export async function compileApp(
|
|
|
321
372
|
return withChecker(root, kept, async (session) => {
|
|
322
373
|
// A BUILD STOPS AT THE FIRST REFUSAL, so neither step is handed a list to report it in.
|
|
323
374
|
const own = parseSources(sources)
|
|
324
|
-
await writeTypes(root, own)
|
|
325
375
|
const resolve = resolverIn()
|
|
326
|
-
const
|
|
376
|
+
const {
|
|
377
|
+
sources: parsed,
|
|
378
|
+
handlers: handlerFiles,
|
|
379
|
+
mounted,
|
|
380
|
+
} = await withDependencies(root, own, handlers, resolve)
|
|
327
381
|
const { read, warnings } = await readSources(session, parsed)
|
|
328
382
|
const { ambient, unkeyed } = await session.lints(root, parsed)
|
|
329
383
|
// 11.37 — a build error, every read named, rather than a first one and a second run.
|
|
@@ -333,9 +387,16 @@ export async function compileApp(
|
|
|
333
387
|
lines.push(formatDiagnosis(read, process.cwd()))
|
|
334
388
|
throw new Error(lines.join('\n'))
|
|
335
389
|
}
|
|
336
|
-
const emitted = await emitApp(
|
|
390
|
+
const emitted = await emitApp(
|
|
391
|
+
root,
|
|
392
|
+
session,
|
|
393
|
+
read,
|
|
394
|
+
resolve,
|
|
395
|
+
handlerFiles,
|
|
396
|
+
)
|
|
337
397
|
return {
|
|
338
398
|
...emitted,
|
|
399
|
+
mounted,
|
|
339
400
|
published,
|
|
340
401
|
sources: sources.length,
|
|
341
402
|
// 11.30 — warned on, and the build goes on.
|
|
@@ -356,8 +417,15 @@ export async function emitApp(
|
|
|
356
417
|
sources: readonly ReadSource[],
|
|
357
418
|
/** The resolver the sources' dependencies were found with, whose answers it keeps. */
|
|
358
419
|
resolve: (specifier: string, from: string) => string | undefined,
|
|
420
|
+
/** The handler files, the app's own and each mounted one (`withDependencies`). */
|
|
421
|
+
handlerFiles: readonly HandlerFile[],
|
|
422
|
+
/** Told of each refusal at its file; the build's throws the first. */
|
|
423
|
+
refuse: Refuse = THROW,
|
|
359
424
|
): Promise<
|
|
360
|
-
Pick<
|
|
425
|
+
Pick<
|
|
426
|
+
CompiledApp,
|
|
427
|
+
'files' | 'tables' | 'handlers' | 'routes' | 'mounts' | 'warnings'
|
|
428
|
+
>
|
|
361
429
|
> {
|
|
362
430
|
const files: Record<string, CompiledFile> = {}
|
|
363
431
|
const tables: Record<string, string> = {}
|
|
@@ -380,10 +448,15 @@ export async function emitApp(
|
|
|
380
448
|
const route = routeOf(entry.position)
|
|
381
449
|
const held = entry.kind === 'layout' ? layoutAt : errorAt
|
|
382
450
|
const other = held.get(shapeOf(route))
|
|
383
|
-
if (other !== undefined)
|
|
384
|
-
|
|
385
|
-
|
|
451
|
+
if (other !== undefined) {
|
|
452
|
+
refuse(
|
|
453
|
+
entry.relative,
|
|
454
|
+
new Error(
|
|
455
|
+
`abide: ${other} and ${entry.relative} are both the ${entry.kind}.abide at ${route}. RULEBOOK 35.12.`,
|
|
456
|
+
),
|
|
386
457
|
)
|
|
458
|
+
continue
|
|
459
|
+
}
|
|
387
460
|
held.set(shapeOf(route), entry.relative)
|
|
388
461
|
if (entry.kind === 'layout' && entry.file.template.length > 0)
|
|
389
462
|
layouts.push({
|
|
@@ -395,8 +468,8 @@ export async function emitApp(
|
|
|
395
468
|
const errors: AppRoute[] = []
|
|
396
469
|
for (const entry of parsed) {
|
|
397
470
|
if (entry.kind !== 'page' && entry.kind !== 'error') continue
|
|
398
|
-
// 23.3, 35.14 — a
|
|
399
|
-
//
|
|
471
|
+
// 23.3, 35.14 — a page or an `error.abide` with nothing to paint is its route's hooks
|
|
472
|
+
// alone.
|
|
400
473
|
if (!paints(entry.file)) continue
|
|
401
474
|
;(entry.kind === 'page' ? routes : errors).push({
|
|
402
475
|
pattern: entry.route as string,
|
|
@@ -406,14 +479,22 @@ export async function emitApp(
|
|
|
406
479
|
}
|
|
407
480
|
|
|
408
481
|
// 16.2 AND 16.28 — THE HANDLERS, read before a module is emitted: the server mounts each one,
|
|
409
|
-
// and the browser
|
|
410
|
-
//
|
|
411
|
-
// caller that has none.
|
|
412
|
-
const { mounts, warnings } = await handlersUnder(
|
|
413
|
-
|
|
482
|
+
// and the browser holds its wrapper in its place — a handler file's as that file, a private
|
|
483
|
+
// handler's in the server block's place in its own file's module. Written whether or not there
|
|
484
|
+
// is a page: a handler is answered over http to a caller that has none.
|
|
485
|
+
const { mounts, warnings } = await handlersUnder(
|
|
486
|
+
session,
|
|
487
|
+
parsed,
|
|
488
|
+
handlerFiles,
|
|
489
|
+
refuse,
|
|
490
|
+
)
|
|
491
|
+
refuseSharedRoutes(routes, mounts, refuse)
|
|
492
|
+
refuseStrayLauncher(parsed, handlerFiles, refuse)
|
|
414
493
|
const standIns = standInsOf(mounts)
|
|
415
|
-
// 37.1 — every module a server block runs in, for the boot to import before
|
|
494
|
+
// 37.1 — every module a server block or a handler file runs in, for the boot to import before
|
|
495
|
+
// `onStart`.
|
|
416
496
|
const servers: string[] = []
|
|
497
|
+
for (const file of handlerFiles) servers.push(file.relative)
|
|
417
498
|
let transitions = false
|
|
418
499
|
for (const entry of parsed) {
|
|
419
500
|
// 35.16 — of the files with an address, the root layout alone is outermost in every render.
|
|
@@ -421,10 +502,15 @@ export async function emitApp(
|
|
|
421
502
|
entry.file.document !== undefined &&
|
|
422
503
|
entry.kind !== 'component' &&
|
|
423
504
|
!(entry.kind === 'layout' && routeOf(entry.position) === '/')
|
|
424
|
-
)
|
|
425
|
-
|
|
426
|
-
|
|
505
|
+
) {
|
|
506
|
+
refuse(
|
|
507
|
+
entry.relative,
|
|
508
|
+
new Error(
|
|
509
|
+
`abide: ${entry.relative} writes <html>, and of the pages, layouts and error pages only the root layout.abide writes the document. RULEBOOK 35.16.`,
|
|
510
|
+
),
|
|
427
511
|
)
|
|
512
|
+
continue
|
|
513
|
+
}
|
|
428
514
|
const emitted = emitSource(
|
|
429
515
|
entry.relative,
|
|
430
516
|
entry.file,
|
|
@@ -436,7 +522,7 @@ export async function emitApp(
|
|
|
436
522
|
)
|
|
437
523
|
if (entry.file.servers.length > 0) servers.push(entry.relative)
|
|
438
524
|
transitions ||= emitted.transitions
|
|
439
|
-
// 34.
|
|
525
|
+
// 34.26 — a dependency's module runs on the app's runtime, whatever the package carries.
|
|
440
526
|
const onAppRuntime =
|
|
441
527
|
placeOf(entry.relative) === 'dependency'
|
|
442
528
|
? (text: string) => onAbideOf(root, entry.path, text)
|
|
@@ -451,7 +537,7 @@ export async function emitApp(
|
|
|
451
537
|
}
|
|
452
538
|
}
|
|
453
539
|
// THE ROUTE TABLE, as the build's own knowledge rather than a list an app maintains. Which
|
|
454
|
-
// addresses exist is a fact about the file tree (a
|
|
540
|
+
// addresses exist is a fact about the file tree (a page is an address, 23.3),
|
|
455
541
|
// so a route that is not a file cannot be written and a file that is one cannot be
|
|
456
542
|
// forgotten. The loaders are dynamic imports, which is what keeps a destination's
|
|
457
543
|
// markup and bindings off the entry page's bundle until somebody navigates there.
|
|
@@ -462,6 +548,15 @@ export async function emitApp(
|
|
|
462
548
|
// page too — an app of handlers alone still answers — and the client's, with the entry
|
|
463
549
|
// that boots it, only where there is a page to boot.
|
|
464
550
|
tables[SERVER_TABLE] = serverTable(routes, errors, servers, mounts)
|
|
551
|
+
// 16.74 — what the browser holds of each handler file, and the route its hooks are filed at:
|
|
552
|
+
// read by the module's load, which runs before the server table is imported.
|
|
553
|
+
const handlers: Compilation['handlers'] = {}
|
|
554
|
+
for (const mount of mounts)
|
|
555
|
+
if (!mount.private)
|
|
556
|
+
handlers[mount.source] = {
|
|
557
|
+
scope: mount.scope,
|
|
558
|
+
client: standIns.get(mount.source) ?? '',
|
|
559
|
+
}
|
|
465
560
|
if (routes.length > 0) {
|
|
466
561
|
tables[ROUTES_TABLE] = clientTable(routes, transitions)
|
|
467
562
|
// The entry's SOURCE carries the name the bundle is written under — `entry-<hash>.js`
|
|
@@ -471,6 +566,7 @@ export async function emitApp(
|
|
|
471
566
|
return {
|
|
472
567
|
files,
|
|
473
568
|
tables,
|
|
569
|
+
handlers,
|
|
474
570
|
routes,
|
|
475
571
|
mounts,
|
|
476
572
|
warnings,
|
|
@@ -481,7 +577,7 @@ export async function emitApp(
|
|
|
481
577
|
* `text`, a module compiled from the dependency's `.abide` at `path`, with each of abide's own
|
|
482
578
|
* specifiers written as the path to the file the app's root resolves it to.
|
|
483
579
|
*
|
|
484
|
-
* 34.
|
|
580
|
+
* 34.26 — RELATIVE, so the compilation still serves once the app and its `node_modules` are moved
|
|
485
581
|
* together, and THE FILE the app's own code loads, so the two are one module instance. A path and
|
|
486
582
|
* not a specifier a plugin answers: bun's runtime hands a plugin no bare specifier that names no
|
|
487
583
|
* file, so a mark on `@abide/abide/server` was never read.
|
|
@@ -489,11 +585,7 @@ export async function emitApp(
|
|
|
489
585
|
function onAbideOf(root: string, path: string, text: string): string {
|
|
490
586
|
const from = dirname(realpathSync(path))
|
|
491
587
|
return respecified(text, (specifier) => {
|
|
492
|
-
if (
|
|
493
|
-
specifier !== SHARED_SPECIFIER &&
|
|
494
|
-
!specifier.startsWith(`${SHARED_SPECIFIER}/`)
|
|
495
|
-
)
|
|
496
|
-
return undefined
|
|
588
|
+
if (!isAbideSpecifier(specifier)) return undefined
|
|
497
589
|
let target: string
|
|
498
590
|
try {
|
|
499
591
|
target = resolvedFile(specifier, root)
|
|
@@ -517,7 +609,11 @@ function placing(mappings: string | undefined): string | undefined {
|
|
|
517
609
|
* the module the file compiles to.
|
|
518
610
|
*/
|
|
519
611
|
function tableImport(source: string): string {
|
|
520
|
-
return JSON.stringify(
|
|
612
|
+
return JSON.stringify(
|
|
613
|
+
source.endsWith('.abide')
|
|
614
|
+
? `../${source}${COMPONENT_QUERY}`
|
|
615
|
+
: `../${source}`,
|
|
616
|
+
)
|
|
521
617
|
}
|
|
522
618
|
|
|
523
619
|
// A layout and the route it covers, parsed.
|
|
@@ -527,7 +623,7 @@ type AppLayout = { source: string; segments: Segment[] }
|
|
|
527
623
|
* The layouts a page renders inside, outermost first.
|
|
528
624
|
*
|
|
529
625
|
* 35.1 — every `layout.abide` whose route lies ON THE WAY DOWN to the page's wraps it and no other
|
|
530
|
-
* does: `src/layout.abide` wraps the page `src/media/
|
|
626
|
+
* does: `src/layout.abide` wraps the page `src/media/index.abide` serves at `/media`, and so does
|
|
531
627
|
* `src/[section]/layout.abide`. So the test is a prefix of the route, kind by kind, and the nesting
|
|
532
628
|
* order is the route's depth.
|
|
533
629
|
*/
|
|
@@ -556,25 +652,106 @@ function shapeOf(route: string): string {
|
|
|
556
652
|
}
|
|
557
653
|
|
|
558
654
|
/**
|
|
559
|
-
*
|
|
655
|
+
* Refuses, naming both, where two files answer one method at one route — at the second.
|
|
560
656
|
*
|
|
561
657
|
* 19.1 — a page answers `GET` at its own, and a handler its method at its address.
|
|
562
658
|
*/
|
|
563
|
-
function refuseSharedRoutes(
|
|
659
|
+
function refuseSharedRoutes(
|
|
660
|
+
routes: AppRoute[],
|
|
661
|
+
mounts: Mount[],
|
|
662
|
+
refuse: Refuse,
|
|
663
|
+
): void {
|
|
564
664
|
const owners = new Map<string, string>()
|
|
565
665
|
const claim = (method: string, route: string, source: string): void => {
|
|
566
666
|
const key = `${method} ${shapeOf(route)}`
|
|
567
667
|
const owner = owners.get(key)
|
|
568
|
-
if (owner
|
|
569
|
-
|
|
570
|
-
|
|
668
|
+
if (owner === undefined) owners.set(key, source)
|
|
669
|
+
else
|
|
670
|
+
refuse(
|
|
671
|
+
source,
|
|
672
|
+
new Error(
|
|
673
|
+
`abide: ${owner} and ${source} both answer ${method} at ${route}. RULEBOOK 19.1.`,
|
|
674
|
+
),
|
|
571
675
|
)
|
|
572
|
-
owners.set(key, source)
|
|
573
676
|
}
|
|
574
677
|
for (const route of routes) claim('GET', route.pattern, route.source)
|
|
575
678
|
for (const mount of mounts) claim(mount.method, mount.address, mount.source)
|
|
576
679
|
}
|
|
577
680
|
|
|
681
|
+
/**
|
|
682
|
+
* Refuses, naming the file, where anything but the root's `launcher.abide` calls `onLauncher`.
|
|
683
|
+
*
|
|
684
|
+
* 38.107 — the launch loads that file alone, so a call anywhere else is never read.
|
|
685
|
+
*/
|
|
686
|
+
function refuseStrayLauncher(
|
|
687
|
+
sources: readonly { relative: string; position: string; file: AbideFile }[],
|
|
688
|
+
handlerFiles: readonly HandlerFile[],
|
|
689
|
+
refuse: Refuse,
|
|
690
|
+
): void {
|
|
691
|
+
const stray = (relative: string): void =>
|
|
692
|
+
refuse(
|
|
693
|
+
relative,
|
|
694
|
+
new Error(
|
|
695
|
+
`abide: ${relative} calls onLauncher, which is read from src/${LAUNCHER_PAGE} alone. Move the call there. RULEBOOK 38.107.`,
|
|
696
|
+
),
|
|
697
|
+
)
|
|
698
|
+
for (const source of sources)
|
|
699
|
+
if (
|
|
700
|
+
source.position !== LAUNCHER_PAGE &&
|
|
701
|
+
source.file.servers.some(callsLauncher)
|
|
702
|
+
)
|
|
703
|
+
stray(source.relative)
|
|
704
|
+
for (const file of handlerFiles)
|
|
705
|
+
if (callsLauncher(readFileSync(file.path, 'utf8'))) stray(file.relative)
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* 37.13 — a hook called in a module that is neither a `.abide` nor a handler file runs as the module
|
|
710
|
+
* is imported, before any file's route is set, and not at all where nothing imports it. A hook is a
|
|
711
|
+
* name the module imports from abide, or a member of a namespace it does, so `emitter.onError(…)` in
|
|
712
|
+
* a module that imports abide for `reactive` is no hook.
|
|
713
|
+
*/
|
|
714
|
+
export function refuseModuleHooks(
|
|
715
|
+
root: string,
|
|
716
|
+
modules: readonly string[],
|
|
717
|
+
refuse: Refuse = THROW,
|
|
718
|
+
): void {
|
|
719
|
+
for (const module of modules) {
|
|
720
|
+
const source = readFileSync(`${root}/${module}`, 'utf8')
|
|
721
|
+
if (!source.includes(SHARED_SPECIFIER)) continue
|
|
722
|
+
const imported = new Set<string>()
|
|
723
|
+
for (const statement of importStatements(source))
|
|
724
|
+
if (isAbideSpecifier(statement.specifier))
|
|
725
|
+
for (const binding of statement.bindings)
|
|
726
|
+
if (!binding.typeOnly) imported.add(binding.imported)
|
|
727
|
+
const hook = hooksCalled(source).find(
|
|
728
|
+
(name) => imported.has(name) || imported.has('*'),
|
|
729
|
+
)
|
|
730
|
+
if (hook !== undefined)
|
|
731
|
+
refuse(
|
|
732
|
+
module,
|
|
733
|
+
new Error(
|
|
734
|
+
`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.`,
|
|
735
|
+
),
|
|
736
|
+
)
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/** `@abide/abide` or one of its subpaths. */
|
|
741
|
+
function isAbideSpecifier(specifier: string): boolean {
|
|
742
|
+
return (
|
|
743
|
+
specifier === SHARED_SPECIFIER ||
|
|
744
|
+
specifier.startsWith(`${SHARED_SPECIFIER}/`)
|
|
745
|
+
)
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function callsLauncher(source: string): boolean {
|
|
749
|
+
return (
|
|
750
|
+
source.includes('onLauncher') &&
|
|
751
|
+
hooksCalled(source).includes('onLauncher')
|
|
752
|
+
)
|
|
753
|
+
}
|
|
754
|
+
|
|
578
755
|
/**
|
|
579
756
|
* Resolves a specifier from a directory as bun does, or `undefined` where it does not resolve.
|
|
580
757
|
*
|
|
@@ -714,7 +891,8 @@ function serverTable(
|
|
|
714
891
|
let handlers = ''
|
|
715
892
|
for (const mount of mounts) {
|
|
716
893
|
const fields = JSON.stringify(mount)
|
|
717
|
-
// The module the handler is declared in — the file
|
|
894
|
+
// The module the handler is declared in — the handler file, or the `.abide`'s server
|
|
895
|
+
// module, the route table's own.
|
|
718
896
|
handlers += ` { load: () => import(${tableImport(mount.source)}), ${fields.slice(1, -1)} },\n`
|
|
719
897
|
}
|
|
720
898
|
return `// Written by \`abide build\`. The addresses this app answers, and what paints each.
|
|
@@ -749,41 +927,58 @@ function clientModules(route: AppRoute): string {
|
|
|
749
927
|
}
|
|
750
928
|
|
|
751
929
|
/**
|
|
752
|
-
* The wrappers the browser
|
|
930
|
+
* The wrappers the browser holds in each handler's place, by source.
|
|
753
931
|
*
|
|
754
|
-
* 16.28
|
|
755
|
-
*
|
|
756
|
-
*
|
|
757
|
-
* handlers rather than the root's.
|
|
932
|
+
* 16.28 — WHAT A BROWSER HOLDS WHERE THE APP WROTE A PRIVATE HANDLER: the same name, the same call
|
|
933
|
+
* over the wire, bound in its `.abide`'s server block's place. 16.74 — a handler file's is its own
|
|
934
|
+
* module, its default, answered in the file's place.
|
|
758
935
|
*/
|
|
759
936
|
function standInsOf(mounts: Mount[]): Map<string, string> {
|
|
760
|
-
const lines = new Map<string, string>()
|
|
937
|
+
const lines = new Map<string, string[]>()
|
|
761
938
|
for (const mount of mounts) {
|
|
762
|
-
// 20.4 — withheld from the browser: no wrapper, so
|
|
939
|
+
// 20.4 — withheld from the browser: no wrapper, so a call of it there names nothing.
|
|
763
940
|
if (!mount.ui) continue
|
|
764
|
-
const
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
: `, ${JSON.stringify(mount.copy)}`
|
|
773
|
-
const line = `export const ${mount.name} = __wire(${JSON.stringify(mount.method)}, new URL(${at}, import.meta.url).pathname${copy})\n`
|
|
774
|
-
lines.set(mount.source, (lines.get(mount.source) ?? '') + line)
|
|
941
|
+
const held = lines.get(mount.source) ?? []
|
|
942
|
+
held.push(
|
|
943
|
+
wireLine(
|
|
944
|
+
mount,
|
|
945
|
+
mount.private ? `const ${mount.name} =` : 'export default',
|
|
946
|
+
),
|
|
947
|
+
)
|
|
948
|
+
lines.set(mount.source, held)
|
|
775
949
|
}
|
|
776
950
|
const standIns = new Map<string, string>()
|
|
777
|
-
for (const [source,
|
|
778
|
-
standIns.set(
|
|
779
|
-
source,
|
|
780
|
-
`// 34.23 — what a browser holds in place of the server block.
|
|
781
|
-
import { wire as __wire } from '${WIRE_SPECIFIER}'
|
|
782
|
-
${body}`,
|
|
783
|
-
)
|
|
951
|
+
for (const [source, held] of lines) standIns.set(source, wireModule(held))
|
|
784
952
|
return standIns
|
|
785
953
|
}
|
|
786
954
|
|
|
955
|
+
/**
|
|
956
|
+
* One handler's wrapper, bound as `binding` writes it.
|
|
957
|
+
*
|
|
958
|
+
* The address is written RELATIVE to the bundle's own directory and resolved against the chunk at
|
|
959
|
+
* run time (16.10), so an app mounted under a sub-path calls its own handlers rather than the
|
|
960
|
+
* root's.
|
|
961
|
+
*/
|
|
962
|
+
function wireLine(mount: Mount, binding: string): string {
|
|
963
|
+
const at = JSON.stringify(posix.relative(CLIENT_ADDRESS, mount.address))
|
|
964
|
+
// 18.4 — what the handler's value declares that the browser's copy behaves by.
|
|
965
|
+
// 11.29 — and the defaults its parameter writes, which the copy keys by.
|
|
966
|
+
const copy =
|
|
967
|
+
mount.defaults !== undefined
|
|
968
|
+
? `, ${JSON.stringify(mount.copy)}, ${JSON.stringify(mount.defaults)}`
|
|
969
|
+
: mount.copy === undefined
|
|
970
|
+
? ''
|
|
971
|
+
: `, ${JSON.stringify(mount.copy)}`
|
|
972
|
+
return `${binding} __wire(${JSON.stringify(mount.method)}, new URL(${at}, import.meta.url).pathname${copy})\n`
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
/** A module of wrappers: the import they share, then each line. */
|
|
976
|
+
function wireModule(lines: string[]): string {
|
|
977
|
+
return `// 16.28 — what a browser holds in place of a handler.
|
|
978
|
+
import { wire as __wire } from '${WIRE_SPECIFIER}'
|
|
979
|
+
${lines.join('')}`
|
|
980
|
+
}
|
|
981
|
+
|
|
787
982
|
/** Whether a page or an error page has anything to paint: a template, or a head. 23.3, 35.14. */
|
|
788
983
|
export function paints(file: AbideFile): boolean {
|
|
789
984
|
return file.template.length > 0 || file.head.length > 0
|
|
@@ -33,7 +33,14 @@ const PACKAGE = Bun.fileURLToPath(new URL('../../', import.meta.url))
|
|
|
33
33
|
function abideEntries(): Map<string, string> {
|
|
34
34
|
const specifiers = new Map<string, string>()
|
|
35
35
|
for (const [entry, path] of Object.entries(abidePackage.exports)) {
|
|
36
|
-
|
|
36
|
+
// The compiler, and the preload a test run builds the app with (38.109), are no part of an
|
|
37
|
+
// app that is already built.
|
|
38
|
+
if (
|
|
39
|
+
entry === './compiler' ||
|
|
40
|
+
entry === './test' ||
|
|
41
|
+
!path.endsWith('.ts')
|
|
42
|
+
)
|
|
43
|
+
continue
|
|
37
44
|
specifiers.set(
|
|
38
45
|
entry === '.'
|
|
39
46
|
? abidePackage.name
|
|
@@ -59,13 +66,11 @@ function abideEntries(): Map<string, string> {
|
|
|
59
66
|
export function compiledEntry(app: {
|
|
60
67
|
/**
|
|
61
68
|
* 38.70 — what `abide bundle` read of the launcher's files: `launch` the path of its
|
|
62
|
-
* `launcher.abide
|
|
63
|
-
* `abide compile`.
|
|
69
|
+
* `launcher.abide` where that file has markup of its own; `undefined` for `abide compile`.
|
|
64
70
|
*/
|
|
65
71
|
desktop:
|
|
66
72
|
| {
|
|
67
73
|
launch: string | undefined
|
|
68
|
-
custom: boolean
|
|
69
74
|
document: string
|
|
70
75
|
icon: string | undefined
|
|
71
76
|
accent: string | undefined
|
|
@@ -107,7 +112,6 @@ import * as table from ${JSON.stringify(tablePath(app.root, SERVER_TABLE))}
|
|
|
107
112
|
? ''
|
|
108
113
|
: `
|
|
109
114
|
{
|
|
110
|
-
custom: ${app.desktop.custom},
|
|
111
115
|
launch: ${app.desktop.launch === undefined ? 'undefined' : `() => import(${JSON.stringify(app.desktop.launch)})`},
|
|
112
116
|
document: ${JSON.stringify(app.desktop.document)},
|
|
113
117
|
icon: ${JSON.stringify(app.desktop.icon)},
|
|
@@ -116,7 +120,7 @@ import * as table from ${JSON.stringify(tablePath(app.root, SERVER_TABLE))}
|
|
|
116
120
|
return `// Written by \`abide compile\`. The whole app, as what one executable holds.
|
|
117
121
|
${imports}
|
|
118
122
|
// 38.56 — a file declaring handlers as a script names it through the app's one alias:
|
|
119
|
-
// \`#src/invoices/
|
|
123
|
+
// \`#src/invoices/getInvoice.ts\`, to the module it is.
|
|
120
124
|
const declared = {}
|
|
121
125
|
for (const row of table.handlers) declared[\`#src/\${row.source}\`] = row.load
|
|
122
126
|
|
|
@@ -89,26 +89,25 @@ export function appGraph<
|
|
|
89
89
|
const nodes: AppNode<Source>[] = []
|
|
90
90
|
for (const source of sources) {
|
|
91
91
|
// Below the root for the app's own files, and above it — `../node_modules/…` — for a
|
|
92
|
-
// dependency's (34.
|
|
92
|
+
// dependency's (34.24).
|
|
93
93
|
const relative = relativeTo(root, source.path)
|
|
94
94
|
const position = source.position ?? relative
|
|
95
|
-
// 34.
|
|
96
|
-
//
|
|
97
|
-
// (34.31); the app mounts what it declares where the app re-exports it.
|
|
95
|
+
// 34.20 — a server block of the app's own under a `server` or `public` directory, refused
|
|
96
|
+
// here before anything reads its handlers: neither directory holds a component that runs.
|
|
98
97
|
const place = placeOf(position)
|
|
99
98
|
if (
|
|
100
99
|
source.file.servers.length > 0 &&
|
|
101
100
|
(place === 'server' || place === 'public')
|
|
102
101
|
)
|
|
103
102
|
throw new Error(
|
|
104
|
-
`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.
|
|
103
|
+
`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.`,
|
|
105
104
|
)
|
|
106
|
-
// 34.
|
|
105
|
+
// 34.21 — a file imported as text is found at build, not when its module first loads.
|
|
107
106
|
for (const script of scriptBodies(source.file))
|
|
108
107
|
for (const { specifier } of attributedImports(script, 'text'))
|
|
109
108
|
if (resolve(specifier, dirname(source.path)) === undefined)
|
|
110
109
|
throw new Error(
|
|
111
|
-
`abide: ${relative} imports ${specifier} as text, and there is no such file. RULEBOOK 34.
|
|
110
|
+
`abide: ${relative} imports ${specifier} as text, and there is no such file. RULEBOOK 34.21.`,
|
|
112
111
|
)
|
|
113
112
|
const kind = fileKind(position)
|
|
114
113
|
nodes.push({
|