@abide/abide 0.101.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 +26 -0
- package/bin/abide.ts +226 -164
- package/docs/REGISTRY.md +730 -0
- package/docs/RULEBOOK.md +2768 -0
- package/docs/abide.md +13127 -0
- package/package.json +6 -3
- package/scaffold/AGENTS.md +38 -0
- package/scaffold/CLAUDE.md +1 -0
- package/scaffold/src/layout.abide +18 -0
- package/src/compiler/Lowering.ts +78 -9
- package/src/compiler/ShadowMap.ts +2 -2
- package/src/compiler/TypeSession.ts +341 -71
- package/src/compiler/appMap.ts +96 -0
- package/src/compiler/argsSchemaOf.ts +1 -1
- package/src/compiler/checkApp.ts +53 -8
- package/src/compiler/checkedShadow.ts +73 -6
- package/src/compiler/classifyRead.ts +6 -4
- package/src/compiler/classifyTemplate.ts +1 -1
- package/src/compiler/compileApp.ts +598 -200
- package/src/compiler/compiledEntry.ts +48 -40
- package/src/compiler/componentGraph.ts +57 -71
- package/src/compiler/declaredExports.ts +26 -9
- package/src/compiler/emitClient.ts +142 -72
- package/src/compiler/emitComponent.ts +41 -12
- package/src/compiler/emitSource.ts +102 -13
- package/src/compiler/handlersUnder.ts +164 -76
- package/src/compiler/hooksCalled.ts +35 -0
- package/src/compiler/index.ts +7 -0
- package/src/compiler/mountsOf.ts +150 -0
- package/src/compiler/nearest.ts +14 -0
- package/src/compiler/parseAbide.ts +157 -20
- package/src/compiler/projectShadow.ts +250 -19
- package/src/compiler/reactiveTypes.ts +20 -7
- package/src/compiler/readSources.ts +15 -2
- package/src/compiler/readsIn.ts +62 -8
- package/src/compiler/renderMarkup.ts +3 -3
- package/src/compiler/scaffold.ts +37 -15
- package/src/compiler/serveLanguage.ts +101 -34
- package/src/compiler/sharedModuleWrites.ts +2 -2
- package/src/compiler/sourceMapOf.ts +193 -0
- package/src/compiler/splitImports.ts +205 -83
- package/src/compiler/tokenize.ts +14 -0
- package/src/compiler/writeDefaultProject.ts +29 -2
- package/src/server/CLIENT_BUILD.ts +11 -13
- package/src/server/DEFAULT_PORT.ts +1 -1
- package/src/server/MODULE_QUERIES.ts +27 -0
- package/src/server/TABLES.ts +7 -0
- package/src/server/VIRTUAL_DIRECTORY.ts +4 -5
- package/src/server/abideModules.ts +457 -0
- package/src/server/answerFailure.ts +16 -6
- package/src/server/answerRequest.ts +241 -107
- package/src/server/answerRpc.ts +107 -33
- package/src/server/answerSocket.ts +65 -20
- package/src/server/answerValue.ts +22 -9
- package/src/server/appDirectories.ts +1 -1
- package/src/server/appPlugins.ts +1 -1
- package/src/server/appTree.ts +238 -0
- package/src/server/argsFor.ts +2 -2
- package/src/server/assistant.ts +2 -2
- package/src/server/bridgeRelay.ts +6 -8
- package/src/server/claudeCode.ts +0 -6
- package/src/server/clientBuildOptions.ts +8 -3
- package/src/server/coerceArgs.ts +1 -1
- package/src/server/commands.ts +38 -37
- package/src/server/componentModules.d.ts +2 -1
- package/src/server/compressAnswer.ts +4 -4
- package/src/server/config.ts +25 -29
- package/src/server/configFields.ts +31 -25
- package/src/server/createApp.ts +77 -79
- package/src/server/devWorker.ts +118 -181
- package/src/server/executableName.ts +4 -0
- package/src/server/fenceServer.ts +57 -32
- package/src/server/generatedSurfaces.ts +99 -66
- package/src/server/hooks.ts +137 -52
- package/src/server/iconAccent.ts +191 -0
- package/src/server/index.ts +12 -7
- package/src/server/installSourceMaps.ts +93 -0
- package/src/server/launcherOptions.ts +33 -31
- package/src/server/launcherPage.ts +107 -64
- package/src/server/manifestOf.ts +16 -13
- package/src/server/namesUnder.ts +2 -2
- package/src/server/nativeMenu.ts +6 -6
- package/src/server/negotiateEncoding.ts +1 -1
- package/src/server/preload.ts +13 -0
- package/src/server/principal.ts +10 -5
- package/src/server/realOrAsGiven.ts +10 -0
- package/src/server/refuseAddressCollision.ts +21 -14
- package/src/server/remote.ts +49 -52
- package/src/server/render.ts +65 -94
- package/src/server/renderComponent.ts +5 -0
- package/src/server/reportHealth.ts +2 -2
- package/src/server/requestScope.ts +12 -1
- package/src/server/runBuild.ts +24 -0
- package/src/server/runCompiled.ts +5 -5
- package/src/server/runDesktop.ts +48 -28
- package/src/server/serveBuild.ts +43 -52
- package/src/server/serveDev.ts +79 -37
- package/src/server/shell.ts +7 -22
- package/src/server/sourceMappings.ts +139 -0
- package/src/server/surfaceOf.ts +20 -3
- package/src/server/testBuild.ts +1 -19
- package/src/server/withAbideSources.ts +117 -0
- package/src/shared/ADDRESSES.ts +10 -18
- package/src/shared/APP_META.ts +7 -0
- package/src/shared/CopyOptions.ts +1 -1
- package/src/shared/DEBUG_META.ts +1 -1
- package/src/shared/LAUNCHER_FILES.ts +9 -0
- package/src/shared/RESERVED_DIRECTORIES.ts +24 -0
- package/src/shared/cookies.ts +2 -2
- package/src/shared/csp.ts +1 -1
- package/src/shared/escapeText.ts +1 -1
- package/src/shared/filledPath.ts +85 -0
- package/src/shared/framing.ts +79 -6
- package/src/shared/headKeyOf.ts +2 -0
- package/src/shared/index.ts +2 -1
- package/src/shared/log.ts +27 -13
- package/src/shared/matchRoute.ts +23 -4
- package/src/{ui → shared}/menuBridge.ts +2 -2
- package/src/shared/menuItem.ts +4 -4
- package/src/shared/onMenu.ts +58 -0
- package/src/shared/reactive/Keyed.ts +2 -2
- package/src/shared/reactive/NotLanded.ts +1 -1
- package/src/shared/reactive/Reactive.ts +43 -43
- package/src/shared/reactive/argsContract.ts +4 -4
- package/src/shared/reactive/canonicalKey.ts +4 -4
- package/src/shared/reactive/chunksOfStream.ts +6 -0
- package/src/shared/reactive/index.ts +1 -1
- package/src/shared/reactive/recordWrites.ts +103 -5
- package/src/shared/reactive/structural.ts +1 -1
- package/src/shared/reactive/tracking.ts +1 -1
- package/src/shared/reactive/watch.ts +3 -3
- package/src/shared/refuse.ts +1 -1
- package/src/{ui → shared}/reportMenu.ts +8 -10
- package/src/shared/routePattern.ts +122 -48
- package/src/shared/rpc.ts +37 -8
- package/src/shared/rpcRequest.ts +27 -3
- package/src/shared/url.ts +4 -44
- package/src/shared/writePath.ts +1 -1
- package/src/ui/LAYER_NAMES.ts +1 -1
- package/src/ui/List.ts +3 -3
- package/src/ui/MOUNTING_LEVEL.ts +18 -0
- package/src/ui/Mismatch.ts +1 -1
- package/src/ui/RawRun.ts +1 -1
- package/src/ui/Router.ts +37 -36
- package/src/ui/Scope.ts +1 -1
- package/src/ui/TryRegion.ts +2 -2
- package/src/ui/applyHead.ts +33 -0
- package/src/ui/index.ts +1 -1
- package/src/ui/moveNode.ts +1 -1
- package/src/ui/reconcile.ts +2 -2
- package/src/ui/routing.ts +2 -2
- package/src/ui/setAttribute.ts +1 -1
- package/src/ui/setText.ts +1 -1
- package/src/ui/socketWire.ts +9 -7
- package/src/ui/wire.ts +1 -1
- package/scaffold/src/server/app.ts +0 -6
- package/scaffold/src/shared/.gitkeep +0 -0
- package/scaffold/src/ui/app.html +0 -8
- package/src/server/componentImports.ts +0 -56
- /package/scaffold/src/{ui/pages/page.abide → index.abide} +0 -0
- /package/src/{compiler → shared}/SPECIFIERS.ts +0 -0
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
//
|
|
6
6
|
// ONLY ARRAYS AND PLAIN OBJECTS ARE WRAPPED. A `Map`, a `Set` or a class instance keeps internal
|
|
7
7
|
// slots a proxy cannot stand in for — `Map.prototype.set` refuses a proxy as its receiver — so one
|
|
8
|
-
// reached is marked written the moment it is read, path and all
|
|
9
|
-
//
|
|
8
|
+
// reached is marked written the moment it is read, path and all, and each object a `Map` or a `Set`
|
|
9
|
+
// hands out is marked as it leaves (`heldView`). That over-reports, and an over-report costs a row
|
|
10
|
+
// remade where an under-report shows a stale one.
|
|
10
11
|
//
|
|
11
12
|
// 18.11 — AND WHERE A SINK IS HANDED IN, each write is kept as data too, as JSON Patch (RFC 6902) at
|
|
12
13
|
// the JSON Pointer it was reached by: a member set is `add`, an index `replace` (or `add` one past
|
|
@@ -49,7 +50,15 @@ export function recordWrites<Value>(
|
|
|
49
50
|
sink?: { ops: PatchOp[]; lost: boolean },
|
|
50
51
|
): void {
|
|
51
52
|
if (!isWrappable(root)) {
|
|
52
|
-
|
|
53
|
+
// A root no proxy can stand in for is written as a whole: what the callback reads out of it
|
|
54
|
+
// reaches the rows that show it with no proxy to say so.
|
|
55
|
+
if (root === null || typeof root !== 'object') {
|
|
56
|
+
mutate(root)
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
PATCHES.epoch += 1
|
|
60
|
+
PATCHES.written.set(root, PATCHES.epoch)
|
|
61
|
+
mutate(heldView(root, PATCHES.epoch) as Value)
|
|
53
62
|
return
|
|
54
63
|
}
|
|
55
64
|
PATCHES.epoch += 1
|
|
@@ -217,9 +226,10 @@ export function recordWrites<Value>(
|
|
|
217
226
|
}
|
|
218
227
|
if (entry.proxy !== null) return entry.proxy
|
|
219
228
|
mark(value)
|
|
229
|
+
entry.view ??= heldView(value, epoch)
|
|
220
230
|
// Reached, and outside what a proxy can watch: whatever it does next is not kept.
|
|
221
231
|
if (sink !== undefined) sink.lost = true
|
|
222
|
-
return
|
|
232
|
+
return entry.view
|
|
223
233
|
},
|
|
224
234
|
set(held, property, value) {
|
|
225
235
|
mark(held)
|
|
@@ -363,7 +373,7 @@ function pointerOf(path: (string | number)[]): string {
|
|
|
363
373
|
/**
|
|
364
374
|
* The JSON Patch an array method makes, read off the array BEFORE it runs.
|
|
365
375
|
*
|
|
366
|
-
* A `splice`'s start and count are
|
|
376
|
+
* A `splice`'s start and count are normalized the way the method does, so the `remove`s and `add`s
|
|
367
377
|
* name the indices it touches.
|
|
368
378
|
*/
|
|
369
379
|
function opsOfCall(
|
|
@@ -421,10 +431,13 @@ class Reached {
|
|
|
421
431
|
* Written in this patch, directly or below — `PATCHES.written` holds this patch's epoch for it.
|
|
422
432
|
*/
|
|
423
433
|
marked: boolean
|
|
434
|
+
/** What one no proxy can stand in for is handed out as: `heldView` of it. */
|
|
435
|
+
view: object | undefined
|
|
424
436
|
constructor(proxy: object | null, parent: object | Parents | null) {
|
|
425
437
|
this.proxy = proxy
|
|
426
438
|
this.parent = parent
|
|
427
439
|
this.marked = false
|
|
440
|
+
this.view = undefined
|
|
428
441
|
}
|
|
429
442
|
}
|
|
430
443
|
|
|
@@ -463,6 +476,91 @@ class Parents {
|
|
|
463
476
|
}
|
|
464
477
|
}
|
|
465
478
|
|
|
479
|
+
/**
|
|
480
|
+
* `container` as the callback is handed it: a `Map` or a `Set` that marks written each object key,
|
|
481
|
+
* value or member it hands out, and anything else as it is.
|
|
482
|
+
*
|
|
483
|
+
* A row over `[...map.values()]` reads one of them, and a write the callback makes to it through
|
|
484
|
+
* `map.get(id)` reaches it with nothing on the way to record it — so each one taken out is taken as
|
|
485
|
+
* written. AS IT LEAVES rather than all of them up front, which would make a patch of one entry cost
|
|
486
|
+
* the whole container. One level down: a row reads its item.
|
|
487
|
+
*/
|
|
488
|
+
function heldView(container: object, epoch: number): object {
|
|
489
|
+
if (!(container instanceof Map) && !(container instanceof Set))
|
|
490
|
+
return container
|
|
491
|
+
return new Proxy(container, new HeldHandler(epoch))
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// `Map`'s and `Set`'s own methods run on the container itself, which is the receiver their internal
|
|
495
|
+
// slots answer to; the ones that hand an object out mark it first.
|
|
496
|
+
class HeldHandler {
|
|
497
|
+
epoch: number
|
|
498
|
+
constructor(epoch: number) {
|
|
499
|
+
this.epoch = epoch
|
|
500
|
+
}
|
|
501
|
+
get(
|
|
502
|
+
held: Map<unknown, unknown> | Set<unknown>,
|
|
503
|
+
property: string | symbol,
|
|
504
|
+
): unknown {
|
|
505
|
+
if (property === TARGET) return held
|
|
506
|
+
const value = Reflect.get(held, property, held)
|
|
507
|
+
if (typeof value !== 'function') return value
|
|
508
|
+
const epoch = this.epoch
|
|
509
|
+
if (property === 'get')
|
|
510
|
+
return (key: unknown) =>
|
|
511
|
+
markedOut((held as Map<unknown, unknown>).get(key), epoch)
|
|
512
|
+
if (property === 'forEach')
|
|
513
|
+
return (
|
|
514
|
+
each: (value: unknown, key: unknown, set: unknown) => void,
|
|
515
|
+
self?: unknown,
|
|
516
|
+
) => {
|
|
517
|
+
for (const [key, member] of held.entries())
|
|
518
|
+
each.call(
|
|
519
|
+
self,
|
|
520
|
+
markedOut(member, epoch),
|
|
521
|
+
markedOut(key, epoch),
|
|
522
|
+
held,
|
|
523
|
+
)
|
|
524
|
+
}
|
|
525
|
+
if (
|
|
526
|
+
property === 'values' ||
|
|
527
|
+
property === 'keys' ||
|
|
528
|
+
property === 'entries' ||
|
|
529
|
+
property === Symbol.iterator
|
|
530
|
+
)
|
|
531
|
+
// A Map's own iterator is its entries; a Set's is its values.
|
|
532
|
+
return () =>
|
|
533
|
+
markedEach(
|
|
534
|
+
(value as () => Iterator<unknown>).call(held),
|
|
535
|
+
property === 'entries' ||
|
|
536
|
+
(property === Symbol.iterator && held instanceof Map),
|
|
537
|
+
epoch,
|
|
538
|
+
)
|
|
539
|
+
return (value as (...args: unknown[]) => unknown).bind(held)
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function* markedEach(
|
|
544
|
+
each: Iterator<unknown>,
|
|
545
|
+
pairs: boolean,
|
|
546
|
+
epoch: number,
|
|
547
|
+
): Generator<unknown> {
|
|
548
|
+
for (let next = each.next(); next.done !== true; next = each.next()) {
|
|
549
|
+
if (pairs) {
|
|
550
|
+
const [key, value] = next.value as [unknown, unknown]
|
|
551
|
+
markedOut(key, epoch)
|
|
552
|
+
markedOut(value, epoch)
|
|
553
|
+
} else markedOut(next.value, epoch)
|
|
554
|
+
yield next.value
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function markedOut<Value>(value: Value, epoch: number): Value {
|
|
559
|
+
if (typeof value === 'object' && value !== null)
|
|
560
|
+
PATCHES.written.set(value, epoch)
|
|
561
|
+
return value
|
|
562
|
+
}
|
|
563
|
+
|
|
466
564
|
/** Whether a proxy can stand in for `value`: an array or a plain object. */
|
|
467
565
|
function isWrappable(value: unknown): value is object {
|
|
468
566
|
if (value === null || typeof value !== 'object') return false
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The by-value comparator an app passes as `identity`: deep equality over arrays and plain objects.
|
|
3
3
|
*
|
|
4
4
|
* Not the default: the default is the reference, and this is what a value whose producer
|
|
5
|
-
* RE-
|
|
5
|
+
* RE-MATERIALIZES declares — `reactive(() => getInvoice({ id }), { identity: structural })`. A
|
|
6
6
|
* fresh-but-equal payload waking every reader is why it exists at all; what it stops being is the
|
|
7
7
|
* price every other value pays.
|
|
8
8
|
*
|
|
@@ -255,7 +255,7 @@ export function dropProbeReader(source: Tracked, reader: Subscriber): void {
|
|
|
255
255
|
* sweep below takes it out.
|
|
256
256
|
*
|
|
257
257
|
* SWEPT ONCE HALF THE LIST IS DEAD, which keeps a walk to at most twice the live readers and the
|
|
258
|
-
* sweep
|
|
258
|
+
* sweep amortized O(1) a drop — and keeps the invariant every `length` test leans on: a list with
|
|
259
259
|
* entries has a LIVE one, since a list that is all dead is at least half dead. HALF, NOT MORE THAN
|
|
260
260
|
* HALF: a keyed replace ends on exactly half, n old rows dead behind n new ones, and a sweep that
|
|
261
261
|
* waited for one more drop kept the old rows — and through each one's `notify`, its scope and its
|
|
@@ -203,7 +203,7 @@ export function watchOver(
|
|
|
203
203
|
// UNTRACKED, AND THE CLEAR IS WHAT MAKES IT SO. The sources are given rather
|
|
204
204
|
// than discovered, but the effect still RUNS wherever it was reached from — a
|
|
205
205
|
// `s.watch` opened inside a `watch` had its reads attributed to the OUTER
|
|
206
|
-
// reader, which then
|
|
206
|
+
// reader, which then reran on a value it never read. A mirror is the same
|
|
207
207
|
// shape one turn up: it runs inside the mark walk, and 4.7 has the `transform`
|
|
208
208
|
// it calls there running untracked.
|
|
209
209
|
const previous = TRACKING.reader
|
|
@@ -219,7 +219,7 @@ export function watchOver(
|
|
|
219
219
|
// subscribed to, so its landing is what runs this again.
|
|
220
220
|
if (thrown instanceof NotLanded) return
|
|
221
221
|
// 12.8: one that throws once usually throws every run, and a watch
|
|
222
|
-
//
|
|
222
|
+
// rerunning into the same throw is a loop with a log line per iteration.
|
|
223
223
|
reader.alive = false
|
|
224
224
|
detach(reader)
|
|
225
225
|
effectThrew(thrown)
|
|
@@ -242,7 +242,7 @@ export function watchOver(
|
|
|
242
242
|
}
|
|
243
243
|
|
|
244
244
|
/**
|
|
245
|
-
* The bare form: runs `effect` as a tracked context,
|
|
245
|
+
* The bare form: runs `effect` as a tracked context, rerunning when anything it read changes.
|
|
246
246
|
*
|
|
247
247
|
* UNOWNED — the caller holds the stop. `watch` is this handed to its owners (`owned`); a template
|
|
248
248
|
* binding's `Scope` and a `{#try}`'s retry hold their own, and call this direct.
|
package/src/shared/refuse.ts
CHANGED
|
@@ -15,7 +15,7 @@ import type { JsonValue } from './JsonValue.ts'
|
|
|
15
15
|
import { namedLogger } from './log.ts'
|
|
16
16
|
import { type Found, runSchema, type Schema } from './Schema.ts'
|
|
17
17
|
|
|
18
|
-
/** The `abide:refuse` logger (28.
|
|
18
|
+
/** The `abide:refuse` logger (28.8) — 15.13 and 15.15's namespace. */
|
|
19
19
|
const REFUSE_LOG = namedLogger('abide:refuse')
|
|
20
20
|
|
|
21
21
|
/**
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
// 38.
|
|
2
|
-
// the window has opened `abideMenu` to the page; anywhere else
|
|
1
|
+
// 38.88 — A ROUTE'S MENUS, TOLD TO THE WINDOW IT IS SHOWN IN. Loaded only inside a desktop app, where
|
|
2
|
+
// the window has opened `abideMenu` to the page; anywhere else `onMenu` never imports it.
|
|
3
3
|
|
|
4
|
-
import { MenuEntry, type MenuItem } from '#shared/menuItem.ts'
|
|
5
|
-
import { navigate } from '#shared/navigate.ts'
|
|
6
4
|
import { menuBridge } from './menuBridge.ts'
|
|
5
|
+
import { MenuEntry, type MenuItem } from './menuItem.ts'
|
|
6
|
+
import { navigate } from './navigate.ts'
|
|
7
7
|
|
|
8
8
|
/** A menu item as the window builds it: an item the page answers carries the `id` it is told by. */
|
|
9
9
|
type Told =
|
|
@@ -20,18 +20,16 @@ type Told =
|
|
|
20
20
|
let numbered: Exclude<MenuItem, 'separator'>[] = []
|
|
21
21
|
let listening = false
|
|
22
22
|
|
|
23
|
-
/** Tells the window
|
|
24
|
-
export function reportMenu(
|
|
23
|
+
/** Tells the window `menus`, the layouts' outermost first and the page's last. */
|
|
24
|
+
export function reportMenu(menus: readonly MenuItem[]): void {
|
|
25
25
|
const bridge = menuBridge()
|
|
26
26
|
if (bridge === undefined) return
|
|
27
27
|
numbered = []
|
|
28
28
|
const told: Told[] = []
|
|
29
|
-
for (const
|
|
30
|
-
if (Array.isArray(level.menu))
|
|
31
|
-
for (const menu of level.menu as MenuItem[]) told.push(tell(menu))
|
|
29
|
+
for (const menu of menus) told.push(tell(menu))
|
|
32
30
|
if (!listening) {
|
|
33
31
|
listening = true
|
|
34
|
-
// 38.
|
|
32
|
+
// 38.90, 38.91 — the window names the item picked; the page does what it means.
|
|
35
33
|
addEventListener('abide:menu', (event) => {
|
|
36
34
|
const item = numbered[(event as CustomEvent<number>).detail]
|
|
37
35
|
if (item === undefined) return
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// A ROUTE LITERAL, PARSED. One grammar for two things that would otherwise drift: the
|
|
2
|
-
// page is filed
|
|
1
|
+
// A ROUTE LITERAL, PARSED. One grammar for two things that would otherwise drift: the file a
|
|
2
|
+
// page is filed as (`invoices/[id].abide`) and the literal an author writes
|
|
3
3
|
// (`'/invoices/[id]'`). The bracket spelling is the file system's, so the address is read off the
|
|
4
4
|
// tree rather than declared beside it and the two cannot disagree.
|
|
5
5
|
//
|
|
@@ -7,8 +7,11 @@
|
|
|
7
7
|
// required takes a value, optional may be left out and omits its segment with it, and rest takes
|
|
8
8
|
// an array as well as a scalar.
|
|
9
9
|
//
|
|
10
|
-
// A LEAF:
|
|
11
|
-
// a constant crossing a seam lives where nothing pulls a graph in behind it.
|
|
10
|
+
// A LEAF: nothing imported but another leaf, because the compiler, the client router and a server
|
|
11
|
+
// router all read it and a constant crossing a seam lives where nothing pulls a graph in behind it.
|
|
12
|
+
|
|
13
|
+
import { LAUNCHER_PAGE } from './LAUNCHER_FILES.ts'
|
|
14
|
+
import { RESERVED_DIRECTORIES } from './RESERVED_DIRECTORIES.ts'
|
|
12
15
|
|
|
13
16
|
type SegmentKind = 'static' | 'required' | 'optional' | 'rest'
|
|
14
17
|
|
|
@@ -40,57 +43,111 @@ export function parseRoutePattern(pattern: string): Segment[] {
|
|
|
40
43
|
return out
|
|
41
44
|
}
|
|
42
45
|
|
|
43
|
-
const ROUTES = 'ui/pages/'
|
|
44
|
-
|
|
45
46
|
/** Whether a source is a page, a layout, an error page or a component. */
|
|
46
47
|
export type FileKind = 'page' | 'layout' | 'error' | 'component'
|
|
47
48
|
|
|
48
49
|
/**
|
|
49
50
|
* Whether the `.abide` at `path` is a page, a layout, an error page or a component.
|
|
50
51
|
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
52
|
+
* 23.21 — WHERE IT SITS, THEN ITS NAME. Every `.abide` directly in an address directory is a page
|
|
53
|
+
* but a `layout.abide`, an `error.abide` and the root's `launcher.abide`; one under a fence is a
|
|
54
|
+
* component whatever it is called.
|
|
55
|
+
* One test for every reader: the build's routes, the checker's `Routes` and the `route.params` a
|
|
56
|
+
* page is typed by.
|
|
56
57
|
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* to the app or the project, or absolute.
|
|
58
|
+
* `path` is RELATIVE TO THE APP'S ROOT: its directories are its route, so an absolute path would
|
|
59
|
+
* read the machine's directories as the app's.
|
|
60
60
|
*/
|
|
61
61
|
export function fileKind(path: string): FileKind {
|
|
62
|
-
if (
|
|
63
|
-
|
|
62
|
+
if (placeOf(path) !== 'address' || path === LAUNCHER_PAGE)
|
|
63
|
+
return 'component'
|
|
64
64
|
const name = path.slice(path.lastIndexOf('/') + 1)
|
|
65
|
-
if (name === 'page.abide') return 'page'
|
|
66
65
|
if (name === 'layout.abide') return 'layout'
|
|
67
|
-
|
|
66
|
+
if (name === 'error.abide') return 'error'
|
|
67
|
+
return 'page'
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
/** Where a file sits in an app, which is what it is to every reader (`placeOf`). */
|
|
71
|
+
export type Place =
|
|
72
|
+
| 'address'
|
|
73
|
+
| 'server'
|
|
74
|
+
| 'public'
|
|
75
|
+
| 'lib'
|
|
76
|
+
| 'tests'
|
|
77
|
+
| 'dependency'
|
|
78
|
+
| 'skipped'
|
|
79
|
+
|
|
70
80
|
/**
|
|
71
|
-
*
|
|
81
|
+
* Where the file at `path`, relative to the app's root, sits — THE ONE ANSWER every walk, fence and
|
|
82
|
+
* emit reads, so no two of them disagree about a path.
|
|
72
83
|
*
|
|
73
|
-
* `
|
|
74
|
-
*
|
|
84
|
+
* - `dependency` — 34.24: a package's, outside the root or under `node_modules`, whose directories
|
|
85
|
+
* are its package's layout and none of the app's;
|
|
86
|
+
* - `skipped` — under a dot directory, which no walk of the app enters;
|
|
87
|
+
* - `server`, `public`, `lib` or `tests` — 23.15: under that fence, the nearest to the root deciding
|
|
88
|
+
* (23.16 refuses one inside another);
|
|
89
|
+
* - `address` — 23.14: everywhere else, its directories the segments of its route.
|
|
90
|
+
*/
|
|
91
|
+
export function placeOf(path: string): Place {
|
|
92
|
+
if (path.startsWith('../') || path.startsWith('/')) return 'dependency'
|
|
93
|
+
const parts = path.split('/')
|
|
94
|
+
// The last part is the file itself, which is never a directory.
|
|
95
|
+
for (let index = 0; index < parts.length - 1; index += 1) {
|
|
96
|
+
const part = parts[index] as string
|
|
97
|
+
if (part === 'node_modules') return 'dependency'
|
|
98
|
+
if (part.startsWith('.')) return 'skipped'
|
|
99
|
+
// Each fence's place is its name.
|
|
100
|
+
if (RESERVED_DIRECTORIES.has(part)) return part as Place
|
|
101
|
+
}
|
|
102
|
+
return 'address'
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Whether a walk of the app passes over the entry `name`: a dot file or directory, or `node_modules`. */
|
|
106
|
+
export function skipped(name: string): boolean {
|
|
107
|
+
return name.startsWith('.') || name === 'node_modules'
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The route a file answers at — its path below the root without its extension.
|
|
75
112
|
*
|
|
76
|
-
* 23.
|
|
77
|
-
* a
|
|
78
|
-
*
|
|
113
|
+
* 23.14 — `media/[id].abide` and `media/getGrid.ts` are `/media/[id]` and `/media/getGrid`; an
|
|
114
|
+
* `index.abide`, a `layout.abide` and an `error.abide` are their directory's. `path` is relative to
|
|
115
|
+
* the app's root, as `fileKind`'s is.
|
|
79
116
|
*/
|
|
80
|
-
export function routeOf(
|
|
81
|
-
const
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
return `/${parts.join('/')}`
|
|
117
|
+
export function routeOf(path: string): string {
|
|
118
|
+
const parts = path.split('/')
|
|
119
|
+
const file = parts.pop() as string
|
|
120
|
+
// The extension alone: `[...rest].abide` carries dots of its own.
|
|
121
|
+
if (!DIRECTORY_FILES.has(file)) parts.push(file.replace(EXTENSION, ''))
|
|
122
|
+
return `/${parts.filter((part) => part !== '').join('/')}`
|
|
87
123
|
}
|
|
88
124
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
125
|
+
const EXTENSION = /\.(?:abide|ts)$/
|
|
126
|
+
|
|
127
|
+
/** 23.14, 23.18 — the names whose route is their directory's, and so a route's wherever they stand. */
|
|
128
|
+
export const DIRECTORY_FILES = new Set([
|
|
129
|
+
'index.abide',
|
|
130
|
+
'layout.abide',
|
|
131
|
+
'error.abide',
|
|
132
|
+
])
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The route the hooks and private handlers of the file at `path` are filed at.
|
|
136
|
+
*
|
|
137
|
+
* 37.1 — an address's own route; under a `lib` or a `tests` directory, the route of the directory
|
|
138
|
+
* holding it, so a component an admin page renders runs under the admin route's rungs; under a
|
|
139
|
+
* `server` or a `public` directory, and for a dependency's file, the root's.
|
|
140
|
+
*/
|
|
141
|
+
export function scopeOf(path: string): string {
|
|
142
|
+
// 38.82 — the launcher answers at no route, and files what it calls at the root's.
|
|
143
|
+
if (path === LAUNCHER_PAGE) return '/'
|
|
144
|
+
const place = placeOf(path)
|
|
145
|
+
if (place === 'address') return routeOf(path)
|
|
146
|
+
if (place !== 'lib' && place !== 'tests') return '/'
|
|
147
|
+
const parts = path.split('/')
|
|
148
|
+
// The fence `placeOf` named is the first one, and its name is its place.
|
|
149
|
+
const fence = parts.indexOf(place)
|
|
150
|
+
return `/${parts.slice(0, fence).join('/')}`
|
|
94
151
|
}
|
|
95
152
|
|
|
96
153
|
/**
|
|
@@ -107,27 +164,44 @@ function routesAt(path: string): number {
|
|
|
107
164
|
* themselves, and `/x` ranked BELOW `/[[a]]/[[b]]` for the path `/x`: a static segment beaten by
|
|
108
165
|
* two optionals, which is 30.9 exactly inverted. Both were live on the scalar this replaced.
|
|
109
166
|
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
167
|
+
* WHERE ONE PATTERN ENDS, ITS END RANKS AS A SEGMENT: below a static or a required one and above an
|
|
168
|
+
* optional or a rest. `/a` admits one address and `/a/[[b]]` admits two, so the shorter is the
|
|
169
|
+
* narrower claim; but `/[[lang]]` and `/[[lang]]/about` both match `/about`, the longer by a static
|
|
170
|
+
* segment where the shorter spends `about` on an optional, so there the longer is.
|
|
114
171
|
*/
|
|
115
172
|
export function compareRoutes(a: Segment[], b: Segment[]): number {
|
|
116
|
-
const
|
|
117
|
-
for (let at = 0; at <
|
|
118
|
-
const kind =
|
|
119
|
-
specificity(b[at] as Segment) - specificity(a[at] as Segment)
|
|
173
|
+
const longest = a.length > b.length ? a.length : b.length
|
|
174
|
+
for (let at = 0; at < longest; at += 1) {
|
|
175
|
+
const kind = specificity(b[at]) - specificity(a[at])
|
|
120
176
|
if (kind !== 0) return kind
|
|
121
177
|
}
|
|
122
|
-
return
|
|
178
|
+
return 0
|
|
123
179
|
}
|
|
124
180
|
|
|
125
|
-
function specificity(segment: Segment): number {
|
|
126
|
-
if (segment
|
|
127
|
-
if (segment.kind === '
|
|
181
|
+
function specificity(segment: Segment | undefined): number {
|
|
182
|
+
if (segment === undefined) return 2
|
|
183
|
+
if (segment.kind === 'static') return 4
|
|
184
|
+
if (segment.kind === 'required') return 3
|
|
128
185
|
return segment.kind === 'optional' ? 1 : 0
|
|
129
186
|
}
|
|
130
187
|
|
|
188
|
+
/**
|
|
189
|
+
* Whether `outer` is the front of `route`: a static segment by name, a parameter by kind.
|
|
190
|
+
*
|
|
191
|
+
* 35.1, 37.2 — what a layout wraps and what a route's hooks run over: the routes at or below its
|
|
192
|
+
* own, a parameter's name aside, so `/media/[id]` covers `/media/[slug]/edit`.
|
|
193
|
+
*/
|
|
194
|
+
export function coversRoute(outer: Segment[], route: Segment[]): boolean {
|
|
195
|
+
if (outer.length > route.length) return false
|
|
196
|
+
for (let at = 0; at < outer.length; at += 1) {
|
|
197
|
+
const one = outer[at] as Segment
|
|
198
|
+
const two = route[at] as Segment
|
|
199
|
+
if (one.kind !== two.kind) return false
|
|
200
|
+
if (one.kind === 'static' && one.name !== two.name) return false
|
|
201
|
+
}
|
|
202
|
+
return true
|
|
203
|
+
}
|
|
204
|
+
|
|
131
205
|
/**
|
|
132
206
|
* A route table compiled for matching: each row with its pattern parsed, most specific first.
|
|
133
207
|
*
|
package/src/shared/rpc.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// A HANDLER IS A `reactive` OR BECOMES ONE (16.1), so an `Rpc` is not a second kind of value: the
|
|
7
7
|
// call hands back the handler's own `Reactive` (16.3), and every probe, trigger and failure on it is
|
|
8
8
|
// the reactive core's — the `Rpc`'s own `busy`, `ready`, `refresh` and `invalidate` are the
|
|
9
|
-
// handler's, passed through. What this file adds is the
|
|
9
|
+
// handler's, passed through. What this file adds is the normalizing — a plain function or a value becomes the
|
|
10
10
|
// `reactive` it would have been (16.6) — and the method.
|
|
11
11
|
//
|
|
12
12
|
// A HANDLER DECLARING `middleware` IS REACHED THROUGH ITS RUNGS (16.11), in process as over the wire:
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
// handler's own.
|
|
15
15
|
|
|
16
16
|
import { type Failed, isFailedNamed } from './Failed.ts'
|
|
17
|
+
import { filledSegments } from './filledPath.ts'
|
|
17
18
|
import { GLOBAL_GETS } from './GLOBAL_GETS.ts'
|
|
18
19
|
import { compose, type Middleware } from './Middleware.ts'
|
|
19
20
|
import { principalRotated } from './principalRotated.ts'
|
|
@@ -23,7 +24,7 @@ import {
|
|
|
23
24
|
type Gated,
|
|
24
25
|
gateOf,
|
|
25
26
|
keyOf,
|
|
26
|
-
|
|
27
|
+
normalized,
|
|
27
28
|
} from './reactive/argsContract.ts'
|
|
28
29
|
import { callLoad } from './reactive/callLoad.ts'
|
|
29
30
|
import {
|
|
@@ -48,7 +49,8 @@ import { MUTATION } from './reactive/MUTATION.ts'
|
|
|
48
49
|
import { SELECTION_MEMBERS } from './reactive/SELECTION_MEMBERS.ts'
|
|
49
50
|
import { isForwarder } from './reactive/scoped.ts'
|
|
50
51
|
import { checkArgs } from './refuse.ts'
|
|
51
|
-
import {
|
|
52
|
+
import { parseRoutePattern, type Segment } from './routePattern.ts'
|
|
53
|
+
import { pathRefusal, rpcRequest } from './rpcRequest.ts'
|
|
52
54
|
import type { Schema } from './Schema.ts'
|
|
53
55
|
|
|
54
56
|
export type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
|
|
@@ -131,7 +133,7 @@ export type RpcContext<Args = unknown> = { request: Request; args: () => Args }
|
|
|
131
133
|
* The context key holding the args as sent.
|
|
132
134
|
*
|
|
133
135
|
* 11.26 — THE ARGS AS SENT, carried on the context under a key no rung spells, so the reach at the
|
|
134
|
-
* onion's
|
|
136
|
+
* onion's center keys the value by them rather than by what the gate made of them.
|
|
135
137
|
*/
|
|
136
138
|
const SENT: unique symbol = Symbol('abide.sent')
|
|
137
139
|
|
|
@@ -331,10 +333,10 @@ export function declare(method: Method): Declare {
|
|
|
331
333
|
args: argsOnce(gateOf(contract), args),
|
|
332
334
|
[SENT]: args,
|
|
333
335
|
}) as RpcContext
|
|
334
|
-
// 16.32 BEHIND RUNGS — A STREAM'S CHUNKS ARE FORWARDED, not collected. The
|
|
336
|
+
// 16.32 BEHIND RUNGS — A STREAM'S CHUNKS ARE FORWARDED, not collected. The center of the
|
|
335
337
|
// onion is an async function, and a `Reactive` is thenable (1.4), so handing a streaming value
|
|
336
338
|
// out through it would wait out the whole run and answer the accumulation once it closed. So
|
|
337
|
-
// the
|
|
339
|
+
// the center waits only until the value settles or turns out to stream (9.6), and hands a
|
|
338
340
|
// stream on as its chunks — a generator, which is not thenable. The value the call reaches
|
|
339
341
|
// resolves to it and is a stream of its own from then (9.6), so a second call joining it
|
|
340
342
|
// (1.8) reads the same accumulation. A refusal a rung answers with is the value's refusal.
|
|
@@ -369,8 +371,35 @@ export function declare(method: Method): Declare {
|
|
|
369
371
|
// this handler and these args is then answered from what this one reached, rather than by
|
|
370
372
|
// running the handler again. In a browser nothing fills the slot, so the report is one load
|
|
371
373
|
// of a `null`.
|
|
374
|
+
//
|
|
375
|
+
// 15.8 — AND REFUSED AHEAD OF THE HANDLER where the args leave a parameter of its route no
|
|
376
|
+
// spelling (30.3, 30.19), as the wire refuses them: a render and the browser's call then
|
|
377
|
+
// reach the same refusal, and there is no request for the render to report. The route is
|
|
378
|
+
// parsed once per address, which is stamped after the declaration (16.10).
|
|
379
|
+
let routed: string | undefined
|
|
380
|
+
let routeSegments: Segment[] | null = null
|
|
372
381
|
const call = (given?: unknown, options?: CallOptions): unknown => {
|
|
373
|
-
const args =
|
|
382
|
+
const args = normalized(contract, given)
|
|
383
|
+
const address = rpc.address
|
|
384
|
+
if (address !== routed) {
|
|
385
|
+
routed = address
|
|
386
|
+
routeSegments =
|
|
387
|
+
address?.includes('[') === true
|
|
388
|
+
? parseRoutePattern(address)
|
|
389
|
+
: null
|
|
390
|
+
}
|
|
391
|
+
if (routeSegments !== null) {
|
|
392
|
+
try {
|
|
393
|
+
filledSegments(
|
|
394
|
+
routeSegments,
|
|
395
|
+
(args ?? {}) as Record<string, unknown>,
|
|
396
|
+
routed as string,
|
|
397
|
+
pathRefusal,
|
|
398
|
+
)
|
|
399
|
+
} catch (refused) {
|
|
400
|
+
return reactive(() => refused)
|
|
401
|
+
}
|
|
402
|
+
}
|
|
374
403
|
const reached = callLoad(
|
|
375
404
|
reachThrough,
|
|
376
405
|
args,
|
|
@@ -472,7 +501,7 @@ export const DELETE: Declare = declare('DELETE')
|
|
|
472
501
|
function addressOf(rpc: { address: string | undefined }): string {
|
|
473
502
|
if (rpc.address === undefined)
|
|
474
503
|
throw new Error(
|
|
475
|
-
|
|
504
|
+
"abide: this handler has no address yet. A handler is given one where it is mounted: a handler file's default (16.2), or a `const` in a <script server> (16.70).",
|
|
476
505
|
)
|
|
477
506
|
return rpc.address
|
|
478
507
|
}
|
package/src/shared/rpcRequest.ts
CHANGED
|
@@ -4,9 +4,12 @@
|
|
|
4
4
|
// that agreement: method, URL and body, which is what makes two requests the same one.
|
|
5
5
|
//
|
|
6
6
|
// A `GET` or `DELETE` carries its args in the URL, in `queryText`'s spelling, and a mutation in its
|
|
7
|
-
// body; 19.7 is where text becomes the declared type again.
|
|
7
|
+
// body; 19.7 is where text becomes the declared type again. 16.9 — whatever the method, a parameter
|
|
8
|
+
// of the handler's route rides in the path, and the server puts it back among the args (16.65).
|
|
8
9
|
|
|
10
|
+
import { filledPath, parametersOf } from './filledPath.ts'
|
|
9
11
|
import { queryText } from './queryText.ts'
|
|
12
|
+
import { validationError } from './refuse.ts'
|
|
10
13
|
import type { Method } from './rpc.ts'
|
|
11
14
|
|
|
12
15
|
export type RpcRequest = {
|
|
@@ -32,10 +35,26 @@ export function argsInURL(method: Method): boolean {
|
|
|
32
35
|
export function rpcRequest(
|
|
33
36
|
method: Method,
|
|
34
37
|
address: string,
|
|
35
|
-
|
|
38
|
+
given: Record<string, unknown> | undefined,
|
|
36
39
|
): RpcRequest {
|
|
40
|
+
let args = given
|
|
37
41
|
let url = address
|
|
38
42
|
let body: string | undefined
|
|
43
|
+
// A route with parameters: they are written into the path and leave the args, so the query or
|
|
44
|
+
// body never carries one twice (16.66). 15.8 — one no address carries (30.3, 30.19) is refused as
|
|
45
|
+
// args a gate refused, so every caller answers it as the wire answers a 422.
|
|
46
|
+
if (address.includes('[')) {
|
|
47
|
+
const params = given ?? {}
|
|
48
|
+
url = filledPath(address, params, address, pathRefusal)
|
|
49
|
+
const inPath = parametersOf(address)
|
|
50
|
+
let rest: Record<string, unknown> | undefined
|
|
51
|
+
for (const name in params) {
|
|
52
|
+
if (inPath.has(name)) continue
|
|
53
|
+
rest ??= {}
|
|
54
|
+
rest[name] = params[name]
|
|
55
|
+
}
|
|
56
|
+
args = rest
|
|
57
|
+
}
|
|
39
58
|
if (argsInURL(method)) {
|
|
40
59
|
if (args !== undefined) {
|
|
41
60
|
let text = queryText(args)
|
|
@@ -47,7 +66,7 @@ export function rpcRequest(
|
|
|
47
66
|
const bare = bareName(name)
|
|
48
67
|
text = text === '' ? bare : `${text}&${bare}`
|
|
49
68
|
}
|
|
50
|
-
if (text !== '') url = `${
|
|
69
|
+
if (text !== '') url = `${url}?${text}`
|
|
51
70
|
}
|
|
52
71
|
} else if (args !== undefined) body = JSON.stringify(args)
|
|
53
72
|
// 18.14 — A COPY IS ITS OWN CACHE and keeps its own `max-age`. What it asks the server is asked
|
|
@@ -67,6 +86,11 @@ export function rpcRequest(
|
|
|
67
86
|
return { method, url, body, init, key: `${method} ${url} ${body ?? ''}` }
|
|
68
87
|
}
|
|
69
88
|
|
|
89
|
+
/** 15.8 — a parameter no address carries, refused as args a gate refused. */
|
|
90
|
+
export function pathRefusal(name: string, message: string): unknown {
|
|
91
|
+
return validationError<Record<string, unknown>>({ [name]: [message] })
|
|
92
|
+
}
|
|
93
|
+
|
|
70
94
|
/** A query member's name in the encoding `URLSearchParams` gives it, without its `=`. */
|
|
71
95
|
export function bareName(name: string): string {
|
|
72
96
|
return new URLSearchParams([[name, '']]).toString().slice(0, -1)
|