@abide/abide 0.102.0 → 0.103.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 +10 -0
- package/bin/abide.ts +31 -28
- package/docs/REGISTRY.md +30 -26
- package/docs/RULEBOOK.md +98 -83
- package/docs/abide.md +771 -522
- package/package.json +2 -1
- package/scaffold/AGENTS.md +13 -6
- package/src/compiler/TypeSession.ts +110 -35
- package/src/compiler/appMap.ts +96 -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 +228 -70
- 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 +70 -45
- 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 +117 -51
- package/src/server/answerRequest.ts +9 -4
- package/src/server/answerRpc.ts +18 -16
- package/src/server/appTree.ts +83 -24
- 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/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/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 +13 -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/{ui → shared}/reportMenu.ts +7 -9
- package/src/shared/routePattern.ts +58 -20
- 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
48
|
import { 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,17 @@ 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, published } = appFiles(root)
|
|
307
354
|
const sources = await sourcesAt(root, abide)
|
|
308
|
-
|
|
355
|
+
// 16.2 — an app of handler files alone is still an app.
|
|
356
|
+
if (sources.length === 0 && handlers.length === 0)
|
|
309
357
|
return {
|
|
310
358
|
files: {},
|
|
311
359
|
tables: {},
|
|
360
|
+
handlers: {},
|
|
361
|
+
mounted: {},
|
|
312
362
|
routes: [],
|
|
313
363
|
mounts: [],
|
|
314
364
|
sources: 0,
|
|
@@ -321,9 +371,12 @@ export async function compileApp(
|
|
|
321
371
|
return withChecker(root, kept, async (session) => {
|
|
322
372
|
// A BUILD STOPS AT THE FIRST REFUSAL, so neither step is handed a list to report it in.
|
|
323
373
|
const own = parseSources(sources)
|
|
324
|
-
await writeTypes(root, own)
|
|
325
374
|
const resolve = resolverIn()
|
|
326
|
-
const
|
|
375
|
+
const {
|
|
376
|
+
sources: parsed,
|
|
377
|
+
handlers: handlerFiles,
|
|
378
|
+
mounted,
|
|
379
|
+
} = await withDependencies(root, own, handlers, resolve)
|
|
327
380
|
const { read, warnings } = await readSources(session, parsed)
|
|
328
381
|
const { ambient, unkeyed } = await session.lints(root, parsed)
|
|
329
382
|
// 11.37 — a build error, every read named, rather than a first one and a second run.
|
|
@@ -333,9 +386,16 @@ export async function compileApp(
|
|
|
333
386
|
lines.push(formatDiagnosis(read, process.cwd()))
|
|
334
387
|
throw new Error(lines.join('\n'))
|
|
335
388
|
}
|
|
336
|
-
const emitted = await emitApp(
|
|
389
|
+
const emitted = await emitApp(
|
|
390
|
+
root,
|
|
391
|
+
session,
|
|
392
|
+
read,
|
|
393
|
+
resolve,
|
|
394
|
+
handlerFiles,
|
|
395
|
+
)
|
|
337
396
|
return {
|
|
338
397
|
...emitted,
|
|
398
|
+
mounted,
|
|
339
399
|
published,
|
|
340
400
|
sources: sources.length,
|
|
341
401
|
// 11.30 — warned on, and the build goes on.
|
|
@@ -356,8 +416,15 @@ export async function emitApp(
|
|
|
356
416
|
sources: readonly ReadSource[],
|
|
357
417
|
/** The resolver the sources' dependencies were found with, whose answers it keeps. */
|
|
358
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,
|
|
359
423
|
): Promise<
|
|
360
|
-
Pick<
|
|
424
|
+
Pick<
|
|
425
|
+
CompiledApp,
|
|
426
|
+
'files' | 'tables' | 'handlers' | 'routes' | 'mounts' | 'warnings'
|
|
427
|
+
>
|
|
361
428
|
> {
|
|
362
429
|
const files: Record<string, CompiledFile> = {}
|
|
363
430
|
const tables: Record<string, string> = {}
|
|
@@ -380,10 +447,15 @@ export async function emitApp(
|
|
|
380
447
|
const route = routeOf(entry.position)
|
|
381
448
|
const held = entry.kind === 'layout' ? layoutAt : errorAt
|
|
382
449
|
const other = held.get(shapeOf(route))
|
|
383
|
-
if (other !== undefined)
|
|
384
|
-
|
|
385
|
-
|
|
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
|
+
),
|
|
386
456
|
)
|
|
457
|
+
continue
|
|
458
|
+
}
|
|
387
459
|
held.set(shapeOf(route), entry.relative)
|
|
388
460
|
if (entry.kind === 'layout' && entry.file.template.length > 0)
|
|
389
461
|
layouts.push({
|
|
@@ -395,8 +467,8 @@ export async function emitApp(
|
|
|
395
467
|
const errors: AppRoute[] = []
|
|
396
468
|
for (const entry of parsed) {
|
|
397
469
|
if (entry.kind !== 'page' && entry.kind !== 'error') continue
|
|
398
|
-
// 23.3, 35.14 — a
|
|
399
|
-
//
|
|
470
|
+
// 23.3, 35.14 — a page or an `error.abide` with nothing to paint is its route's hooks
|
|
471
|
+
// alone.
|
|
400
472
|
if (!paints(entry.file)) continue
|
|
401
473
|
;(entry.kind === 'page' ? routes : errors).push({
|
|
402
474
|
pattern: entry.route as string,
|
|
@@ -406,14 +478,22 @@ export async function emitApp(
|
|
|
406
478
|
}
|
|
407
479
|
|
|
408
480
|
// 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
|
-
|
|
481
|
+
// and the browser holds its wrapper in its place — a handler file's as that file, a private
|
|
482
|
+
// handler's in the server block's place in its own file's module. Written whether or not there
|
|
483
|
+
// is a page: a handler is answered over http to a caller that has none.
|
|
484
|
+
const { mounts, warnings } = await handlersUnder(
|
|
485
|
+
session,
|
|
486
|
+
parsed,
|
|
487
|
+
handlerFiles,
|
|
488
|
+
refuse,
|
|
489
|
+
)
|
|
490
|
+
refuseSharedRoutes(routes, mounts, refuse)
|
|
491
|
+
refuseStrayLauncher(parsed, handlerFiles, refuse)
|
|
414
492
|
const standIns = standInsOf(mounts)
|
|
415
|
-
// 37.1 — every module a server block runs in, for the boot to import before
|
|
493
|
+
// 37.1 — every module a server block or a handler file runs in, for the boot to import before
|
|
494
|
+
// `onStart`.
|
|
416
495
|
const servers: string[] = []
|
|
496
|
+
for (const file of handlerFiles) servers.push(file.relative)
|
|
417
497
|
let transitions = false
|
|
418
498
|
for (const entry of parsed) {
|
|
419
499
|
// 35.16 — of the files with an address, the root layout alone is outermost in every render.
|
|
@@ -421,10 +501,15 @@ export async function emitApp(
|
|
|
421
501
|
entry.file.document !== undefined &&
|
|
422
502
|
entry.kind !== 'component' &&
|
|
423
503
|
!(entry.kind === 'layout' && routeOf(entry.position) === '/')
|
|
424
|
-
)
|
|
425
|
-
|
|
426
|
-
|
|
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
|
+
),
|
|
427
510
|
)
|
|
511
|
+
continue
|
|
512
|
+
}
|
|
428
513
|
const emitted = emitSource(
|
|
429
514
|
entry.relative,
|
|
430
515
|
entry.file,
|
|
@@ -436,7 +521,7 @@ export async function emitApp(
|
|
|
436
521
|
)
|
|
437
522
|
if (entry.file.servers.length > 0) servers.push(entry.relative)
|
|
438
523
|
transitions ||= emitted.transitions
|
|
439
|
-
// 34.
|
|
524
|
+
// 34.26 — a dependency's module runs on the app's runtime, whatever the package carries.
|
|
440
525
|
const onAppRuntime =
|
|
441
526
|
placeOf(entry.relative) === 'dependency'
|
|
442
527
|
? (text: string) => onAbideOf(root, entry.path, text)
|
|
@@ -451,7 +536,7 @@ export async function emitApp(
|
|
|
451
536
|
}
|
|
452
537
|
}
|
|
453
538
|
// 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
|
|
539
|
+
// addresses exist is a fact about the file tree (a page is an address, 23.3),
|
|
455
540
|
// so a route that is not a file cannot be written and a file that is one cannot be
|
|
456
541
|
// forgotten. The loaders are dynamic imports, which is what keeps a destination's
|
|
457
542
|
// markup and bindings off the entry page's bundle until somebody navigates there.
|
|
@@ -462,6 +547,15 @@ export async function emitApp(
|
|
|
462
547
|
// page too — an app of handlers alone still answers — and the client's, with the entry
|
|
463
548
|
// that boots it, only where there is a page to boot.
|
|
464
549
|
tables[SERVER_TABLE] = serverTable(routes, errors, servers, mounts)
|
|
550
|
+
// 16.74 — what the browser holds of each handler file, and the route its hooks are filed at:
|
|
551
|
+
// read by the module's load, which runs before the server table is imported.
|
|
552
|
+
const handlers: Compilation['handlers'] = {}
|
|
553
|
+
for (const mount of mounts)
|
|
554
|
+
if (!mount.private)
|
|
555
|
+
handlers[mount.source] = {
|
|
556
|
+
scope: mount.scope,
|
|
557
|
+
client: standIns.get(mount.source) ?? '',
|
|
558
|
+
}
|
|
465
559
|
if (routes.length > 0) {
|
|
466
560
|
tables[ROUTES_TABLE] = clientTable(routes, transitions)
|
|
467
561
|
// The entry's SOURCE carries the name the bundle is written under — `entry-<hash>.js`
|
|
@@ -471,6 +565,7 @@ export async function emitApp(
|
|
|
471
565
|
return {
|
|
472
566
|
files,
|
|
473
567
|
tables,
|
|
568
|
+
handlers,
|
|
474
569
|
routes,
|
|
475
570
|
mounts,
|
|
476
571
|
warnings,
|
|
@@ -481,7 +576,7 @@ export async function emitApp(
|
|
|
481
576
|
* `text`, a module compiled from the dependency's `.abide` at `path`, with each of abide's own
|
|
482
577
|
* specifiers written as the path to the file the app's root resolves it to.
|
|
483
578
|
*
|
|
484
|
-
* 34.
|
|
579
|
+
* 34.26 — RELATIVE, so the compilation still serves once the app and its `node_modules` are moved
|
|
485
580
|
* together, and THE FILE the app's own code loads, so the two are one module instance. A path and
|
|
486
581
|
* not a specifier a plugin answers: bun's runtime hands a plugin no bare specifier that names no
|
|
487
582
|
* file, so a mark on `@abide/abide/server` was never read.
|
|
@@ -517,7 +612,11 @@ function placing(mappings: string | undefined): string | undefined {
|
|
|
517
612
|
* the module the file compiles to.
|
|
518
613
|
*/
|
|
519
614
|
function tableImport(source: string): string {
|
|
520
|
-
return JSON.stringify(
|
|
615
|
+
return JSON.stringify(
|
|
616
|
+
source.endsWith('.abide')
|
|
617
|
+
? `../${source}${COMPONENT_QUERY}`
|
|
618
|
+
: `../${source}`,
|
|
619
|
+
)
|
|
521
620
|
}
|
|
522
621
|
|
|
523
622
|
// A layout and the route it covers, parsed.
|
|
@@ -527,7 +626,7 @@ type AppLayout = { source: string; segments: Segment[] }
|
|
|
527
626
|
* The layouts a page renders inside, outermost first.
|
|
528
627
|
*
|
|
529
628
|
* 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/
|
|
629
|
+
* does: `src/layout.abide` wraps the page `src/media/index.abide` serves at `/media`, and so does
|
|
531
630
|
* `src/[section]/layout.abide`. So the test is a prefix of the route, kind by kind, and the nesting
|
|
532
631
|
* order is the route's depth.
|
|
533
632
|
*/
|
|
@@ -556,25 +655,66 @@ function shapeOf(route: string): string {
|
|
|
556
655
|
}
|
|
557
656
|
|
|
558
657
|
/**
|
|
559
|
-
*
|
|
658
|
+
* Refuses, naming both, where two files answer one method at one route — at the second.
|
|
560
659
|
*
|
|
561
660
|
* 19.1 — a page answers `GET` at its own, and a handler its method at its address.
|
|
562
661
|
*/
|
|
563
|
-
function refuseSharedRoutes(
|
|
662
|
+
function refuseSharedRoutes(
|
|
663
|
+
routes: AppRoute[],
|
|
664
|
+
mounts: Mount[],
|
|
665
|
+
refuse: Refuse,
|
|
666
|
+
): void {
|
|
564
667
|
const owners = new Map<string, string>()
|
|
565
668
|
const claim = (method: string, route: string, source: string): void => {
|
|
566
669
|
const key = `${method} ${shapeOf(route)}`
|
|
567
670
|
const owner = owners.get(key)
|
|
568
|
-
if (owner
|
|
569
|
-
|
|
570
|
-
|
|
671
|
+
if (owner === undefined) owners.set(key, source)
|
|
672
|
+
else
|
|
673
|
+
refuse(
|
|
674
|
+
source,
|
|
675
|
+
new Error(
|
|
676
|
+
`abide: ${owner} and ${source} both answer ${method} at ${route}. RULEBOOK 19.1.`,
|
|
677
|
+
),
|
|
571
678
|
)
|
|
572
|
-
owners.set(key, source)
|
|
573
679
|
}
|
|
574
680
|
for (const route of routes) claim('GET', route.pattern, route.source)
|
|
575
681
|
for (const mount of mounts) claim(mount.method, mount.address, mount.source)
|
|
576
682
|
}
|
|
577
683
|
|
|
684
|
+
/**
|
|
685
|
+
* Refuses, naming the file, where anything but the root's `launcher.abide` calls `onLauncher`.
|
|
686
|
+
*
|
|
687
|
+
* 38.107 — the launch loads that file alone, so a call anywhere else is never read.
|
|
688
|
+
*/
|
|
689
|
+
function refuseStrayLauncher(
|
|
690
|
+
sources: readonly { relative: string; position: string; file: AbideFile }[],
|
|
691
|
+
handlerFiles: readonly HandlerFile[],
|
|
692
|
+
refuse: Refuse,
|
|
693
|
+
): void {
|
|
694
|
+
const stray = (relative: string): void =>
|
|
695
|
+
refuse(
|
|
696
|
+
relative,
|
|
697
|
+
new Error(
|
|
698
|
+
`abide: ${relative} calls onLauncher, which is read from src/${LAUNCHER_PAGE} alone. Move the call there. RULEBOOK 38.107.`,
|
|
699
|
+
),
|
|
700
|
+
)
|
|
701
|
+
for (const source of sources)
|
|
702
|
+
if (
|
|
703
|
+
source.position !== LAUNCHER_PAGE &&
|
|
704
|
+
source.file.servers.some(callsLauncher)
|
|
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
|
+
|
|
578
718
|
/**
|
|
579
719
|
* Resolves a specifier from a directory as bun does, or `undefined` where it does not resolve.
|
|
580
720
|
*
|
|
@@ -714,7 +854,8 @@ function serverTable(
|
|
|
714
854
|
let handlers = ''
|
|
715
855
|
for (const mount of mounts) {
|
|
716
856
|
const fields = JSON.stringify(mount)
|
|
717
|
-
// The module the handler is declared in — the file
|
|
857
|
+
// The module the handler is declared in — the handler file, or the `.abide`'s server
|
|
858
|
+
// module, the route table's own.
|
|
718
859
|
handlers += ` { load: () => import(${tableImport(mount.source)}), ${fields.slice(1, -1)} },\n`
|
|
719
860
|
}
|
|
720
861
|
return `// Written by \`abide build\`. The addresses this app answers, and what paints each.
|
|
@@ -749,41 +890,58 @@ function clientModules(route: AppRoute): string {
|
|
|
749
890
|
}
|
|
750
891
|
|
|
751
892
|
/**
|
|
752
|
-
* The wrappers the browser
|
|
893
|
+
* The wrappers the browser holds in each handler's place, by source.
|
|
753
894
|
*
|
|
754
|
-
* 16.28
|
|
755
|
-
*
|
|
756
|
-
*
|
|
757
|
-
* handlers rather than the root's.
|
|
895
|
+
* 16.28 — WHAT A BROWSER HOLDS WHERE THE APP WROTE A PRIVATE HANDLER: the same name, the same call
|
|
896
|
+
* over the wire, bound in its `.abide`'s server block's place. 16.74 — a handler file's is its own
|
|
897
|
+
* module, its default, answered in the file's place.
|
|
758
898
|
*/
|
|
759
899
|
function standInsOf(mounts: Mount[]): Map<string, string> {
|
|
760
|
-
const lines = new Map<string, string>()
|
|
900
|
+
const lines = new Map<string, string[]>()
|
|
761
901
|
for (const mount of mounts) {
|
|
762
|
-
// 20.4 — withheld from the browser: no wrapper, so
|
|
902
|
+
// 20.4 — withheld from the browser: no wrapper, so a call of it there names nothing.
|
|
763
903
|
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)
|
|
904
|
+
const held = lines.get(mount.source) ?? []
|
|
905
|
+
held.push(
|
|
906
|
+
wireLine(
|
|
907
|
+
mount,
|
|
908
|
+
mount.private ? `const ${mount.name} =` : 'export default',
|
|
909
|
+
),
|
|
910
|
+
)
|
|
911
|
+
lines.set(mount.source, held)
|
|
775
912
|
}
|
|
776
913
|
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
|
-
)
|
|
914
|
+
for (const [source, held] of lines) standIns.set(source, wireModule(held))
|
|
784
915
|
return standIns
|
|
785
916
|
}
|
|
786
917
|
|
|
918
|
+
/**
|
|
919
|
+
* One handler's wrapper, bound as `binding` writes it.
|
|
920
|
+
*
|
|
921
|
+
* The address is written RELATIVE to the bundle's own directory and resolved against the chunk at
|
|
922
|
+
* run time (16.10), so an app mounted under a sub-path calls its own handlers rather than the
|
|
923
|
+
* root's.
|
|
924
|
+
*/
|
|
925
|
+
function wireLine(mount: Mount, binding: string): string {
|
|
926
|
+
const at = JSON.stringify(posix.relative(CLIENT_ADDRESS, mount.address))
|
|
927
|
+
// 18.4 — what the handler's value declares that the browser's copy behaves by.
|
|
928
|
+
// 11.29 — and the defaults its parameter writes, which the copy keys by.
|
|
929
|
+
const copy =
|
|
930
|
+
mount.defaults !== undefined
|
|
931
|
+
? `, ${JSON.stringify(mount.copy)}, ${JSON.stringify(mount.defaults)}`
|
|
932
|
+
: mount.copy === undefined
|
|
933
|
+
? ''
|
|
934
|
+
: `, ${JSON.stringify(mount.copy)}`
|
|
935
|
+
return `${binding} __wire(${JSON.stringify(mount.method)}, new URL(${at}, import.meta.url).pathname${copy})\n`
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
/** A module of wrappers: the import they share, then each line. */
|
|
939
|
+
function wireModule(lines: string[]): string {
|
|
940
|
+
return `// 16.28 — what a browser holds in place of a handler.
|
|
941
|
+
import { wire as __wire } from '${WIRE_SPECIFIER}'
|
|
942
|
+
${lines.join('')}`
|
|
943
|
+
}
|
|
944
|
+
|
|
787
945
|
/** Whether a page or an error page has anything to paint: a template, or a head. 23.3, 35.14. */
|
|
788
946
|
export function paints(file: AbideFile): boolean {
|
|
789
947
|
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({
|
|
@@ -49,7 +49,7 @@ import type { Diagnosis } from './TypeSession.ts'
|
|
|
49
49
|
/**
|
|
50
50
|
* Every value each module exports, with the abide method that declares it.
|
|
51
51
|
*
|
|
52
|
-
*
|
|
52
|
+
* 16.73 — EVERY VALUE EACH MODULE EXPORTS, and the method of the abide call that declares it, or
|
|
53
53
|
* `undefined` where it is not one. The checker's own export table, so a name exported apart from
|
|
54
54
|
* its declaration, under another name or from another module is the same export it is to an
|
|
55
55
|
* importer; and the callee's own symbol, so `GET` imported under another name or through a
|
|
@@ -75,7 +75,7 @@ export async function declaredExports(
|
|
|
75
75
|
const sourceFile = await project.program.getSourceFile(path)
|
|
76
76
|
if (sourceFile === undefined)
|
|
77
77
|
throw new Error(
|
|
78
|
-
`abide: ${path} is not inside the project ${projectName}, so what it exports cannot be read (
|
|
78
|
+
`abide: ${path} is not inside the project ${projectName}, so what it exports cannot be read (16.73). Give the app a tsconfig whose include reaches it.`,
|
|
79
79
|
)
|
|
80
80
|
const module = await checker.getSymbolAtLocation(sourceFile)
|
|
81
81
|
if (module === undefined) return []
|
|
@@ -198,7 +198,7 @@ async function declaringMethod(
|
|
|
198
198
|
: isExportAssignment(declaration)
|
|
199
199
|
? declaration.expression
|
|
200
200
|
: undefined
|
|
201
|
-
//
|
|
201
|
+
// 16.73 — A BINDING NAMING ANOTHER BINDING is the call that one was declared by: `export const
|
|
202
202
|
// named = drop` is `drop`'s handler under another name. Bounded, so a cycle the checker would
|
|
203
203
|
// refuse anyway cannot spin here.
|
|
204
204
|
for (
|
|
@@ -39,7 +39,7 @@ import {
|
|
|
39
39
|
type ValueRender,
|
|
40
40
|
} from './renderMarkup.ts'
|
|
41
41
|
import { SHARED_NAMES } from './SHARED_NAMES.ts'
|
|
42
|
-
import { RUNTIME_SPECIFIER, SHARED_SPECIFIER } from '
|
|
42
|
+
import { RUNTIME_SPECIFIER, SHARED_SPECIFIER } from '#shared/SPECIFIERS.ts'
|
|
43
43
|
import {
|
|
44
44
|
insideShare,
|
|
45
45
|
resumedShare,
|
|
@@ -267,8 +267,8 @@ export function emitClient(
|
|
|
267
267
|
const setup = splitImports(lowering.scripts())
|
|
268
268
|
if (!live && fills === '' && setup.body === '') {
|
|
269
269
|
const mount = carriesOwnMarkup ? 'export default () => {}\n' : ''
|
|
270
|
-
// 34.
|
|
271
|
-
//
|
|
270
|
+
// 34.2 — A `<script module>` RUNS IN THE BROWSER TOO, live markup or none: its module scope
|
|
271
|
+
// is the browser's as much as the server's.
|
|
272
272
|
if (moduleSetup.body === '' && moduleSetup.imports === '') return mount
|
|
273
273
|
return `${moduleImports(lowering, (name) => (SHARED_NAMES.has(name) ? SHARED_SPECIFIER : RUNTIME_SPECIFIER), [moduleSetup])}
|
|
274
274
|
${moduleSetup.body}
|