@abide/abide 0.123.2 → 0.123.3
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 +27 -0
- package/bin/abide.ts +36 -65
- package/package.json +1 -1
- package/src/compiler/Diagnostics.ts +198 -0
- package/src/compiler/KeptChecker.ts +97 -0
- package/src/compiler/Lowering.ts +1 -1
- package/src/compiler/PositionedRefusal.ts +1 -1
- package/src/compiler/TypeSession.ts +62 -85
- package/src/compiler/appLayout.ts +28 -20
- package/src/compiler/argsDefaultsIn.ts +1 -1
- package/src/compiler/checkApp.ts +64 -68
- package/src/compiler/checkedShadow.ts +1 -1
- package/src/compiler/compileApp.ts +101 -70
- package/src/compiler/declaredExports.ts +2 -4
- package/src/compiler/emitClient.ts +1 -1
- package/src/compiler/formatAbide.ts +2 -2
- package/src/compiler/handlersUnder.ts +17 -29
- package/src/compiler/lineOf.ts +0 -19
- package/src/compiler/matchersOf.ts +3 -3
- package/src/compiler/mountsOf.ts +3 -3
- package/src/compiler/readSources.ts +16 -18
- package/src/compiler/readsIn.ts +1 -1
- package/src/compiler/serveLanguage.ts +23 -57
- package/src/compiler/sharedModuleWrites.ts +1 -1
- package/src/compiler/templateWarnings.ts +1 -2
- package/src/compiler/trackingWarnings.ts +1 -1
- package/src/server/CLIENT_BUILD.ts +4 -32
- package/src/server/RESUME_MILLISECONDS.ts +2 -0
- package/src/server/answerRequest.ts +11 -300
- package/src/server/answerRpc.ts +12 -54
- package/src/server/answerSocket.ts +8 -35
- package/src/server/appBuild.ts +377 -0
- package/src/server/appPlugins.ts +15 -17
- package/src/server/appTree.ts +6 -5
- package/src/server/argsContract.ts +209 -0
- package/src/server/bridgeRelay.ts +153 -65
- package/src/server/claudeCode.ts +30 -127
- package/src/server/config.ts +6 -7
- package/src/server/createApp.ts +30 -64
- package/src/server/devWorker.ts +11 -15
- package/src/server/handlerFileMount.ts +40 -0
- package/src/server/index.ts +2 -2
- package/src/server/packageDirectory.ts +14 -0
- package/src/server/preload.ts +3 -2
- package/src/server/principal.ts +4 -3
- package/src/server/remote.ts +1 -1
- package/src/server/runCompiled.ts +1 -1
- package/src/server/runDesktop.ts +1 -1
- package/src/server/seedBuffer.ts +1 -1
- package/src/server/serveDev.ts +24 -58
- package/src/server/servedApp.ts +390 -0
- package/src/server/signInChanged.ts +20 -0
- package/src/server/surfaceOf.ts +2 -1
- package/src/server/versionOf.ts +6 -15
- package/src/shared/applyPatch.ts +1 -1
- package/src/shared/muxFrames.ts +1 -1
- package/src/shared/reactive/Keyed.ts +1 -1
- package/src/shared/reactive/Reactive.ts +21 -8
- package/src/shared/reactive/argsContract.ts +131 -4
- package/src/shared/reactive/index.ts +1 -0
- package/src/shared/reactive/patchLog.ts +79 -0
- package/src/shared/reactive/patchMarks.ts +34 -0
- package/src/shared/reactive/recordWrites.ts +11 -14
- package/src/shared/reactive/share.ts +4 -3
- package/src/shared/rpc.ts +12 -19
- package/src/ui/ArmRegion.ts +10 -4
- package/src/ui/Level.ts +11 -20
- package/src/ui/LevelChain.ts +272 -0
- package/src/ui/Router.ts +54 -245
- package/src/ui/Scope.ts +17 -3
- package/src/ui/TryRegion.ts +22 -23
- package/src/ui/bindSpread.ts +2 -4
- package/src/ui/listenForRoutes.ts +3 -10
- package/src/ui/outboxQueue.ts +21 -3
- package/src/ui/reconcile.ts +4 -6
- package/src/ui/routing.ts +2 -1
- package/src/ui/rows.ts +2 -2
- package/src/ui/setAttribute.ts +12 -3
- package/src/ui/socketWire.ts +26 -11
- package/src/compiler/Finding.ts +0 -2
- package/src/compiler/Refusals.ts +0 -65
- package/src/compiler/formatDiagnosis.ts +0 -20
- package/src/server/appDirectories.ts +0 -9
- package/src/server/argsFor.ts +0 -39
- package/src/server/buildDirectory.ts +0 -10
- package/src/server/coerceArgs.ts +0 -109
- package/src/server/derivedArgs.ts +0 -26
- package/src/server/runBuild.ts +0 -24
- package/src/server/serveBuild.ts +0 -144
- package/src/server/testBuild.ts +0 -220
- package/src/shared/reactive/FOLLOWED.ts +0 -8
- package/src/shared/reactive/PATCHES.ts +0 -11
- package/src/shared/reactive/PATCH_SINKS.ts +0 -26
- package/src/shared/reactive/canonicalKey.ts +0 -95
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# @abide/abide
|
|
2
2
|
|
|
3
|
+
## 0.123.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- abfff21: Router's doc names the LevelChain that stops a page's scope ([`00c3d2d`](https://github.com/briancray/abide/commit/00c3d2d8038c673e5a067b01402b0d270fa485b8))
|
|
8
|
+
- abfff21: a spread writes through setAttribute's compare-then-write (writeAttribute) ([`017a6ff`](https://github.com/briancray/abide/commit/017a6ff26a1e9ba460973b63b2c8ffc5f9e2f863))
|
|
9
|
+
- abfff21: the routed mount argument comment names LevelChain.mountFrom ([`0942e6e`](https://github.com/briancray/abide/commit/0942e6e9d6545dd24d41579fd5ec28e9478892d9))
|
|
10
|
+
- abfff21: a patch-log release run twice leaves a log a later follower opened ([`206d8df`](https://github.com/briancray/abide/commit/206d8dfde626592eb5ca885567f30afeed6b0b6e))
|
|
11
|
+
- abfff21: one patch log for followed values, and patch marks behind functions ([`3cdda11`](https://github.com/briancray/abide/commit/3cdda118d9a8ed4cbd32834c8cd7823c26f0b5fb))
|
|
12
|
+
- abfff21: one args contract for every way a handler is called ([`4e2dcab`](https://github.com/briancray/abide/commit/4e2dcab0bddaf555404fc22687e2efd41d28b500))
|
|
13
|
+
- abfff21: the {#try} ambient is Scope's private, set only through underBoundary; TryRegion swaps through ArmRegion's fills/armOf ([`54e6a91`](https://github.com/briancray/abide/commit/54e6a9158bd9fe167578479a3ce4669d13797457))
|
|
14
|
+
- abfff21: a file the layout has not seen is checked as painting only where it paints ([`587597f`](https://github.com/briancray/abide/commit/587597f23cdeb331816049ff0670b99eebcb1cd8))
|
|
15
|
+
- abfff21: one diagnostics sink owns the finding type, its placement, abide-ignore, order and printing for build, dev, check, format and the language server ([`6d11f57`](https://github.com/briancray/abide/commit/6d11f57adc226b45bd0ddf1b4a305ee6d7a7a000))
|
|
16
|
+
- abfff21: socketWire and outboxQueue each reset in one call (resetSocketWire, resetOutbox); tests stop reaching into Mux internals and draining the outbox by timing ([`721774f`](https://github.com/briancray/abide/commit/721774f93df6040e20015f03755879c4cf534a38))
|
|
17
|
+
- abfff21: a mounted handler is held to the schema its row derived, so a bundled executable coerces and validates its args (RULEBOOK 19.9) ([`76c92a5`](https://github.com/briancray/abide/commit/76c92a51f0b3f49ee95e95d8088c320d7a896341))
|
|
18
|
+
- abfff21: an editor shows the findings abide check has for a file — lints, handler warnings and the emit's refusals included (RULEBOOK 38.11) ([`9824535`](https://github.com/briancray/abide/commit/9824535e191b0f9748edaafadc0bc8ecc08f75d0))
|
|
19
|
+
- abfff21: simplify pass — one placedSource, packageDirectory in appTree, derivedArgs probes once, manifest read beside the table, PatchLog followers always a set, pairingStatus yields the code alone, KeptChecker's heard takes nothing ([`9a6e663`](https://github.com/briancray/abide/commit/9a6e66361364b841cb53362ace080eb1e7d0372f))
|
|
20
|
+
- abfff21: ServerRoute and ServerError stay private to answerRequest ([`9c51de6`](https://github.com/briancray/abide/commit/9c51de674db98e4c2793fa8f496ebe54924c087f))
|
|
21
|
+
- abfff21: the build on disk is one module, and booting from a compilation is one ordering ([`a3a5349`](https://github.com/briancray/abide/commit/a3a5349baa9c55c9a931f00d635a57743ddf1cdc))
|
|
22
|
+
|
|
23
|
+
An app with no `src/` — its root is its package — now keeps its default `.abide` build and its `bun test` build cache inside the package rather than in the directory above it, as RULEBOOK 38.27 has a build sit beside its app; an app with a `src/` is unchanged.
|
|
24
|
+
- abfff21: a LevelChain owns the routed levels and their head bracketing; Router takes a popped history step through one entry and keeps its step bookkeeping private ([`a4d9641`](https://github.com/briancray/abide/commit/a4d9641105f9d1f9958b9bb6fcf90323c07cd6a8))
|
|
25
|
+
- abfff21: a kept checker reopens a stale project, drops one that threw and owns its watchers, for dev and the language server alike ([`c733d7e`](https://github.com/briancray/abide/commit/c733d7ea7b648a02ed366ffe172aa3b996c7e0de))
|
|
26
|
+
- abfff21: a handler file's mount row is built once, for the compiler and the in-memory test app ([`c7db045`](https://github.com/briancray/abide/commit/c7db0459e89ba4ea74918f29d7823c87a9e01d47))
|
|
27
|
+
- abfff21: format the try and wire changes ([`c85913a`](https://github.com/briancray/abide/commit/c85913a0a36bc8e828885b795ba31340057a22ff))
|
|
28
|
+
- abfff21: the bridge relay writes its own frames and hears sign-ins rather than being told ([`ed69e87`](https://github.com/briancray/abide/commit/ed69e87be6e92f9980cb4f9b66edc59f56f7a88f))
|
|
29
|
+
|
|
3
30
|
## 0.123.2
|
|
4
31
|
|
|
5
32
|
### Patch Changes
|
package/bin/abide.ts
CHANGED
|
@@ -16,8 +16,8 @@ import {
|
|
|
16
16
|
type CompiledApp,
|
|
17
17
|
} from '#compiler/compileApp.ts'
|
|
18
18
|
import { compiledEntry } from '#compiler/compiledEntry.ts'
|
|
19
|
+
import { type Diagnosis, printDiagnoses } from '#compiler/Diagnostics.ts'
|
|
19
20
|
import { formatAbide } from '#compiler/formatAbide.ts'
|
|
20
|
-
import { formatDiagnosis } from '#compiler/formatDiagnosis.ts'
|
|
21
21
|
import { parseAbide } from '#compiler/parseAbide.ts'
|
|
22
22
|
import { scaffold } from '#compiler/scaffold.ts'
|
|
23
23
|
import { serveLanguage } from '#compiler/serveLanguage.ts'
|
|
@@ -26,15 +26,17 @@ import {
|
|
|
26
26
|
type Compilation,
|
|
27
27
|
serverMapsOf,
|
|
28
28
|
} from '#server/abideModules.ts'
|
|
29
|
-
import { banner } from '#server/banner.ts'
|
|
30
|
-
import { buildDirectory } from '#server/buildDirectory.ts'
|
|
31
29
|
import {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
30
|
+
buildDirectory,
|
|
31
|
+
type BuildFiles,
|
|
32
|
+
buildFiles,
|
|
33
|
+
builtFiles,
|
|
34
|
+
markBuild,
|
|
35
|
+
unmarkedBuild,
|
|
36
|
+
writeWhole,
|
|
37
|
+
} from '#server/appBuild.ts'
|
|
38
|
+
import { banner } from '#server/banner.ts'
|
|
39
|
+
import { ENCODING_SUFFIX } from '#server/CLIENT_BUILD.ts'
|
|
38
40
|
import { type ClientBundle, clientBundle } from '#server/clientBundle.ts'
|
|
39
41
|
import { type Given, type Host, runCommands } from '#server/commands.ts'
|
|
40
42
|
import { type AppPackage, packageUnder } from '#server/config.ts'
|
|
@@ -45,7 +47,6 @@ import { namesUnder } from '#server/namesUnder.ts'
|
|
|
45
47
|
import { skipped } from '#server/placement.ts'
|
|
46
48
|
import { DEFAULT_DOCUMENT, intoHead } from '#server/render.ts'
|
|
47
49
|
import { runtimeRefusal } from '#server/runtimeRefusal.ts'
|
|
48
|
-
import { pathsUnder, serveBuild } from '#server/serveBuild.ts'
|
|
49
50
|
import { serveDev } from '#server/serveDev.ts'
|
|
50
51
|
import { escapeText } from '#shared/escapeText.ts'
|
|
51
52
|
import { ICON_ADDRESSES, LAUNCHER_PAGE } from '#shared/LAUNCHER_FILES.ts'
|
|
@@ -72,12 +73,8 @@ const DISK: Host = {
|
|
|
72
73
|
// 38.100 — built in this process rather than a child, where nobody built it, and said on
|
|
73
74
|
// stderr: the command's stdout is its own — `openapi`'s document, `mcp`'s messages.
|
|
74
75
|
app: ({ values }) => {
|
|
75
|
-
const handle = appOnDisk(directoriesOf(values),
|
|
76
|
-
(
|
|
77
|
-
writeBuild({ root, out }, console.error),
|
|
78
|
-
))
|
|
79
|
-
? out
|
|
80
|
-
: undefined,
|
|
76
|
+
const handle = appOnDisk(directoriesOf(values), (root, out) =>
|
|
77
|
+
writeBuild({ root, out }, console.error),
|
|
81
78
|
)
|
|
82
79
|
return RUNTIME_REFUSAL === undefined
|
|
83
80
|
? handle
|
|
@@ -116,15 +113,12 @@ async function writeBuild(
|
|
|
116
113
|
process.env.NODE_ENV ??= 'production'
|
|
117
114
|
// 38.66 — what this build may remove is only what was written before it began.
|
|
118
115
|
const began = Date.now()
|
|
119
|
-
// 38.100 — a build that fails leaves no build
|
|
120
|
-
|
|
121
|
-
const marker = `${out}/${COMPILATION}`
|
|
122
|
-
await rm(marker, { force: true })
|
|
116
|
+
// 38.100 — before anything can fail, so a build that fails leaves no build.
|
|
117
|
+
const files = await unmarkedBuild(out)
|
|
123
118
|
// `--out` is a real flag rather than a constant because two test files each building into
|
|
124
119
|
// one `.abide/` is the contention CLAUDE.md's "checks" section already records once.
|
|
125
120
|
const app = await compileApp(root)
|
|
126
|
-
|
|
127
|
-
console.warn(formatDiagnosis(warning, process.cwd()))
|
|
121
|
+
printDiagnoses(app.warnings, console.warn)
|
|
128
122
|
if (
|
|
129
123
|
Object.keys(app.files).length === 0 &&
|
|
130
124
|
Object.keys(app.handlers).length === 0
|
|
@@ -138,14 +132,13 @@ async function writeBuild(
|
|
|
138
132
|
// 36.11 — the directories above `public` and the path below it ARE the address.
|
|
139
133
|
// `src/public/favicon.ico` answers at `/favicon.ico`, and nothing declares that anywhere.
|
|
140
134
|
await writeClient(
|
|
141
|
-
|
|
135
|
+
files,
|
|
142
136
|
await clientBundle(root, compilation, app.published, 'build'),
|
|
143
137
|
began,
|
|
144
138
|
)
|
|
145
139
|
// An app's static files are its own whether or not it has a page yet.
|
|
146
|
-
const copied = await copyPublic(root,
|
|
147
|
-
|
|
148
|
-
await writeWhole(marker, JSON.stringify(compilation))
|
|
140
|
+
const copied = await copyPublic(root, files, app.published, began)
|
|
141
|
+
await markBuild(files, compilation)
|
|
149
142
|
say(
|
|
150
143
|
`abide build: ${app.sources} source(s), ${copied} public file(s) -> ${out}`,
|
|
151
144
|
)
|
|
@@ -299,9 +292,7 @@ async function writtenEntry(
|
|
|
299
292
|
root,
|
|
300
293
|
sourceMaps: serverMapsOf(app, root),
|
|
301
294
|
package: packageUnder(root),
|
|
302
|
-
|
|
303
|
-
client: pathsUnder(`${out}/${CLIENT_DIRECTORY}`),
|
|
304
|
-
published: pathsUnder(`${out}/${PUBLIC_DIRECTORY}`),
|
|
295
|
+
...(await builtFiles(out)),
|
|
305
296
|
}),
|
|
306
297
|
)
|
|
307
298
|
return { entry, compilation: app }
|
|
@@ -443,7 +434,7 @@ async function writeIcon(root: string, resources: string): Promise<boolean> {
|
|
|
443
434
|
|
|
444
435
|
/** 38.79 — the public file the app's icon is drawn from: `/icon.icns`, else `/icon.png`. */
|
|
445
436
|
async function publishedIcon(root: string): Promise<string | undefined> {
|
|
446
|
-
const published =
|
|
437
|
+
const { published } = buildFiles(buildDirectory(root))
|
|
447
438
|
for (const address of [ICON_ADDRESSES.icns, ICON_ADDRESSES.png])
|
|
448
439
|
if (await Bun.file(`${published}${address}`).exists())
|
|
449
440
|
return `${published}${address}`
|
|
@@ -517,7 +508,7 @@ ${body}</dict>
|
|
|
517
508
|
*/
|
|
518
509
|
async function copyPublic(
|
|
519
510
|
root: string,
|
|
520
|
-
|
|
511
|
+
files: BuildFiles,
|
|
521
512
|
published: Map<string, string>,
|
|
522
513
|
began: number,
|
|
523
514
|
): Promise<number> {
|
|
@@ -528,35 +519,35 @@ async function copyPublic(
|
|
|
528
519
|
produced.add(address.slice(1))
|
|
529
520
|
copies.push(
|
|
530
521
|
writeWhole(
|
|
531
|
-
`${
|
|
522
|
+
`${files.published}${address}`,
|
|
532
523
|
Bun.file(`${root}/${source}`),
|
|
533
524
|
),
|
|
534
525
|
)
|
|
535
526
|
}
|
|
536
527
|
await Promise.all(copies)
|
|
537
528
|
// 38.66 — a public file the app no longer has is no longer answered.
|
|
538
|
-
await pruneStale(
|
|
529
|
+
await pruneStale(files.published, produced, began)
|
|
539
530
|
return published.size
|
|
540
531
|
}
|
|
541
532
|
|
|
542
533
|
/**
|
|
543
|
-
* Writes the client bundle (`clientBundle`) into
|
|
534
|
+
* Writes the client bundle (`clientBundle`) into the build, with its manifest and precompressed copies.
|
|
544
535
|
*
|
|
545
536
|
* 38.5 — the manifest beside the bundle, and every file in each encoding (`precompressed`). AN APP
|
|
546
537
|
* OF HANDLERS ALONE is written no client, and its manifest anyway, so `abide start` reads one shape
|
|
547
538
|
* whatever the app holds.
|
|
548
539
|
*/
|
|
549
540
|
async function writeClient(
|
|
550
|
-
|
|
541
|
+
files: BuildFiles,
|
|
551
542
|
bundle: ClientBundle,
|
|
552
543
|
began: number,
|
|
553
544
|
): Promise<void> {
|
|
554
|
-
const client =
|
|
545
|
+
const { client } = files
|
|
555
546
|
// 38.66 — STAGED, THEN MOVED IN FILE BY FILE. A second build into the same `out` — or `abide
|
|
556
547
|
// start` serving it — read a chunk half written, and emptying `client/` first took the one it
|
|
557
548
|
// was serving away. Every name but the manifest's is a hash of what it holds, so two builds of
|
|
558
549
|
// one tree move in the same bytes under the same names.
|
|
559
|
-
const staging = `${
|
|
550
|
+
const staging = `${dirname(client)}/.staging-${crypto.randomUUID()}`
|
|
560
551
|
try {
|
|
561
552
|
const written: Promise<void>[] = []
|
|
562
553
|
for (const [name, { bytes }] of bundle.files)
|
|
@@ -574,7 +565,7 @@ async function writeClient(
|
|
|
574
565
|
}
|
|
575
566
|
await Promise.all(moves)
|
|
576
567
|
await pruneStale(client, produced, began)
|
|
577
|
-
await writeWhole(
|
|
568
|
+
await writeWhole(files.manifest, JSON.stringify(bundle.manifest))
|
|
578
569
|
} finally {
|
|
579
570
|
await rm(staging, { recursive: true, force: true })
|
|
580
571
|
}
|
|
@@ -642,17 +633,6 @@ async function pruneStale(
|
|
|
642
633
|
await Promise.all(stale)
|
|
643
634
|
}
|
|
644
635
|
|
|
645
|
-
/**
|
|
646
|
-
* Writes `path` whole: to a name of its own beside it, then renamed over it.
|
|
647
|
-
*
|
|
648
|
-
* 38.66 — a reader, or a second build, finds the last complete file there and never a partial one.
|
|
649
|
-
*/
|
|
650
|
-
async function writeWhole(path: string, content: string | Blob): Promise<void> {
|
|
651
|
-
const partial = `${path}.${crypto.randomUUID()}.partial`
|
|
652
|
-
await Bun.write(partial, content)
|
|
653
|
-
await rename(partial, path)
|
|
654
|
-
}
|
|
655
|
-
|
|
656
636
|
/**
|
|
657
637
|
* Wraps a command body with the app's absolute root and build directory, or fails with no app.
|
|
658
638
|
*
|
|
@@ -741,24 +721,18 @@ async function check(
|
|
|
741
721
|
{ positionals }: Given,
|
|
742
722
|
): Promise<number> {
|
|
743
723
|
const within = namedDirectories(positionals)
|
|
744
|
-
const found =
|
|
745
|
-
|
|
746
|
-
let warnings = 0
|
|
747
|
-
for (const diagnosis of found) {
|
|
724
|
+
const found: Diagnosis[] = []
|
|
725
|
+
for (const diagnosis of await checkApp(root))
|
|
748
726
|
if (
|
|
749
|
-
within.length
|
|
750
|
-
|
|
727
|
+
within.length === 0 ||
|
|
728
|
+
within.some(
|
|
751
729
|
(directory) =>
|
|
752
730
|
diagnosis.path === directory ||
|
|
753
731
|
diagnosis.path.startsWith(`${directory}/`),
|
|
754
732
|
)
|
|
755
733
|
)
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
else if (diagnosis.category === 'warning') warnings += 1
|
|
759
|
-
else continue
|
|
760
|
-
console.log(formatDiagnosis(diagnosis, process.cwd()))
|
|
761
|
-
}
|
|
734
|
+
found.push(diagnosis)
|
|
735
|
+
const { errors, warnings } = printDiagnoses(found, console.log)
|
|
762
736
|
console.log(
|
|
763
737
|
`abide check: ${errors} error${errors === 1 ? '' : 's'}, ${warnings} warning${warnings === 1 ? '' : 's'}`,
|
|
764
738
|
)
|
|
@@ -802,10 +776,7 @@ async function format(within: string[], { values }: Given): Promise<number> {
|
|
|
802
776
|
next += 1
|
|
803
777
|
const text = await Bun.file(path).text()
|
|
804
778
|
const formatted = await formatAbide(path, text)
|
|
805
|
-
|
|
806
|
-
errors += 1
|
|
807
|
-
console.log(formatDiagnosis(finding, process.cwd()))
|
|
808
|
-
}
|
|
779
|
+
errors += printDiagnoses(formatted.findings, console.log).errors
|
|
809
780
|
if (formatted.text === text) continue
|
|
810
781
|
changed += 1
|
|
811
782
|
if (values.check)
|
package/package.json
CHANGED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { relative, resolve } from 'node:path' // bun exposes no path helpers of its own
|
|
2
|
+
|
|
3
|
+
import { isIgnored } from './isIgnored.ts'
|
|
4
|
+
import { lineOf } from './lineOf.ts'
|
|
5
|
+
import { PositionedRefusal } from './PositionedRefusal.ts'
|
|
6
|
+
|
|
7
|
+
// EVERYTHING THE COMPILER HAS TO SAY, IN ONE PLACE: what a finding is, where it lands on the line an
|
|
8
|
+
// author wrote, which an author silenced, the order the lot is reported in, and the one line each is
|
|
9
|
+
// printed as. `abide build`, `abide dev`, `abide check`, `abide format` and `abide lsp` all read
|
|
10
|
+
// findings off this, so none of them decides for itself what a refusal does or how a warning reads.
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* One thing the checker said, placed in the file the author wrote.
|
|
14
|
+
*
|
|
15
|
+
* 38.3 — line and column from 1.
|
|
16
|
+
*/
|
|
17
|
+
export type Diagnosis = {
|
|
18
|
+
path: string
|
|
19
|
+
line: number
|
|
20
|
+
column: number
|
|
21
|
+
code: number
|
|
22
|
+
category: 'error' | 'warning' | 'suggestion' | 'message'
|
|
23
|
+
message: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** A warning or a refusal at a projection offset. */
|
|
27
|
+
export type Finding = { at: number; message: string }
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Where everything a compile finds goes, each at the file it was found in.
|
|
31
|
+
*
|
|
32
|
+
* TWO MODES AND ONE CALL for a refusal: a build stops at one, so a throwing sink throws it; `abide
|
|
33
|
+
* check` reports every one at its file and goes on (38.33), so a collecting sink keeps each as a
|
|
34
|
+
* finding at the file's first line. A warning is kept either way — it does not stop a build — and so
|
|
35
|
+
* is what the checker says (`checked`). Every stage takes this and nothing else, so no stage decides
|
|
36
|
+
* for itself what a refusal does, and none spells a path its own way: a file is named relative to
|
|
37
|
+
* `root` or absolutely, and kept absolute.
|
|
38
|
+
*/
|
|
39
|
+
export class Diagnostics {
|
|
40
|
+
/** Every refusal kept, in the order met; empty in a throwing sink. */
|
|
41
|
+
readonly refused: Diagnosis[] = []
|
|
42
|
+
/** Every warning the compiler raised, in the order met (14, 32.14, 11.30, 18.15). */
|
|
43
|
+
readonly warnings: Diagnosis[] = []
|
|
44
|
+
/** What the checker said of each file (38.3). */
|
|
45
|
+
readonly checked: Diagnosis[] = []
|
|
46
|
+
/** What a file below the root is named relative to. */
|
|
47
|
+
readonly root: string
|
|
48
|
+
/** Whether a refusal is kept and the compile goes on, rather than thrown. */
|
|
49
|
+
readonly collecting: boolean
|
|
50
|
+
|
|
51
|
+
constructor(root: string, collecting: boolean) {
|
|
52
|
+
this.root = root
|
|
53
|
+
this.collecting = collecting
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Every finding, in the order reported: the refusals, the warnings, then the checker's. */
|
|
57
|
+
get findings(): Diagnosis[] {
|
|
58
|
+
return [...this.refused, ...this.warnings, ...this.checked]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Refuses at `file`, below the root or absolute: `thrown` is thrown, or kept. Handed the file's
|
|
63
|
+
* `text`, a `PositionedRefusal` is placed on its line and column, and thrown as that line.
|
|
64
|
+
*/
|
|
65
|
+
at(file: string, thrown: unknown, text?: string): void {
|
|
66
|
+
const positioned =
|
|
67
|
+
text !== undefined && thrown instanceof PositionedRefusal
|
|
68
|
+
if (!this.collecting && !positioned) throw thrown
|
|
69
|
+
const diagnosis = diagnosisAt(
|
|
70
|
+
resolve(this.root, file),
|
|
71
|
+
positioned ? text : undefined,
|
|
72
|
+
positioned ? thrown.offset : 0,
|
|
73
|
+
'error',
|
|
74
|
+
thrown instanceof Error ? thrown.message : String(thrown),
|
|
75
|
+
)
|
|
76
|
+
if (!this.collecting)
|
|
77
|
+
throw new Error(formatDiagnosis(diagnosis, process.cwd()))
|
|
78
|
+
this.refused.push(diagnosis)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* What another stage kept, as this one takes it: every one kept, or every one thrown as one
|
|
83
|
+
* error, each at its file — a build that stops names all it found rather than a first one and a
|
|
84
|
+
* second run (11.37).
|
|
85
|
+
*/
|
|
86
|
+
adopt(found: readonly Diagnosis[]): void {
|
|
87
|
+
if (found.length === 0) return
|
|
88
|
+
if (this.collecting) {
|
|
89
|
+
this.refused.push(...found)
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
const lines: string[] = []
|
|
93
|
+
for (const one of found) lines.push(formatDiagnosis(one, process.cwd()))
|
|
94
|
+
throw new Error(lines.join('\n'))
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Keeps `found` as warnings: the build goes on. */
|
|
98
|
+
warn(found: readonly Diagnosis[]): void {
|
|
99
|
+
this.warnings.push(...found)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Keeps what the checker said. */
|
|
103
|
+
check(found: readonly Diagnosis[]): void {
|
|
104
|
+
this.checked.push(...found)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** abide's own finding at `offset` into `text`, or at line 0 where the text was not handed in. */
|
|
109
|
+
export function diagnosisAt(
|
|
110
|
+
path: string,
|
|
111
|
+
text: string | undefined,
|
|
112
|
+
offset: number,
|
|
113
|
+
category: Diagnosis['category'],
|
|
114
|
+
message: string,
|
|
115
|
+
): Diagnosis {
|
|
116
|
+
return {
|
|
117
|
+
path,
|
|
118
|
+
...(text === undefined ? { line: 0, column: 0 } : lineOf(text, offset)),
|
|
119
|
+
code: 0,
|
|
120
|
+
category,
|
|
121
|
+
message,
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Each of `findings`, at an offset `place` maps into the file at `path` the author wrote, as a
|
|
127
|
+
* finding on its line — or on line 0 where the text was not handed in.
|
|
128
|
+
*
|
|
129
|
+
* 14.19 — `reads`: they are warnings on reactive reads, and one the line above silences with
|
|
130
|
+
* `abide-ignore` and its clause is left out. No other finding is silenced so.
|
|
131
|
+
*/
|
|
132
|
+
export function placedFindings(
|
|
133
|
+
path: string,
|
|
134
|
+
text: string | undefined,
|
|
135
|
+
findings: readonly Finding[],
|
|
136
|
+
place: (at: number) => number,
|
|
137
|
+
category: Diagnosis['category'],
|
|
138
|
+
reads = false,
|
|
139
|
+
): Diagnosis[] {
|
|
140
|
+
const placed: Diagnosis[] = []
|
|
141
|
+
for (const finding of findings) {
|
|
142
|
+
const offset = place(finding.at)
|
|
143
|
+
if (
|
|
144
|
+
reads &&
|
|
145
|
+
text !== undefined &&
|
|
146
|
+
isIgnored(text, offset, finding.message)
|
|
147
|
+
)
|
|
148
|
+
continue
|
|
149
|
+
placed.push(diagnosisAt(path, text, offset, category, finding.message))
|
|
150
|
+
}
|
|
151
|
+
return placed
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Orders findings by file, then line, then column. */
|
|
155
|
+
export function byPlace(one: Diagnosis, two: Diagnosis): number {
|
|
156
|
+
return (
|
|
157
|
+
one.path.localeCompare(two.path) ||
|
|
158
|
+
one.line - two.line ||
|
|
159
|
+
one.column - two.column
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* One finding as one `tsc`-style line.
|
|
165
|
+
*
|
|
166
|
+
* ONE LINE A FINDING, in the shape `tsc` and every editor's problem matcher already read:
|
|
167
|
+
* `file:line:column - error TS2322: …`, the path relative to `from`. A finding with no line — a
|
|
168
|
+
* whole-file or whole-app refusal — names the file alone.
|
|
169
|
+
*/
|
|
170
|
+
export function formatDiagnosis(diagnosis: Diagnosis, from: string): string {
|
|
171
|
+
const path = relative(from, diagnosis.path) || diagnosis.path
|
|
172
|
+
const where =
|
|
173
|
+
diagnosis.line === 0
|
|
174
|
+
? path
|
|
175
|
+
: `${path}:${diagnosis.line}:${diagnosis.column}`
|
|
176
|
+
const code = diagnosis.code === 0 ? '' : ` TS${diagnosis.code}`
|
|
177
|
+
return `${where} - ${diagnosis.category}${code}: ${diagnosis.message}`
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Writes each error and warning in `found` as its line, relative to where the command was run, and
|
|
182
|
+
* answers how many of each — what every command that reports findings prints. A suggestion or a
|
|
183
|
+
* message is neither, and is not written.
|
|
184
|
+
*/
|
|
185
|
+
export function printDiagnoses(
|
|
186
|
+
found: readonly Diagnosis[],
|
|
187
|
+
write: (line: string) => void,
|
|
188
|
+
): { errors: number; warnings: number } {
|
|
189
|
+
let errors = 0
|
|
190
|
+
let warnings = 0
|
|
191
|
+
for (const diagnosis of found) {
|
|
192
|
+
if (diagnosis.category === 'error') errors += 1
|
|
193
|
+
else if (diagnosis.category === 'warning') warnings += 1
|
|
194
|
+
else continue
|
|
195
|
+
write(formatDiagnosis(diagnosis, process.cwd()))
|
|
196
|
+
}
|
|
197
|
+
return { errors, warnings }
|
|
198
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { type FSWatcher, watch } from 'node:fs' // bun exposes no file watcher of its own
|
|
2
|
+
|
|
3
|
+
import { type AppChecker, openChecker } from './compileApp.ts'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* An app's checker kept for as long as a server runs, opened when first asked for and told what its
|
|
7
|
+
* watchers see move.
|
|
8
|
+
*
|
|
9
|
+
* `abide dev` and `abide lsp` keep one each, because spawning a checker is most of what a compile
|
|
10
|
+
* costs. WHAT KEEPING ONE TAKES is here rather than in either: one whose project moved
|
|
11
|
+
* (`AppChecker.current`) is closed and another opened, and one a question threw from is dropped —
|
|
12
|
+
* the checker's own process dying is a throw too, and a session nobody can tell apart from a dead
|
|
13
|
+
* one is not one to keep. `heard` is told every move after the checker is, so what it schedules
|
|
14
|
+
* reads a checker that already knows.
|
|
15
|
+
*/
|
|
16
|
+
export class KeptChecker {
|
|
17
|
+
readonly root: string
|
|
18
|
+
private readonly heard: () => void
|
|
19
|
+
private readonly watchers: FSWatcher[] = []
|
|
20
|
+
private readonly watched = new Set<string>()
|
|
21
|
+
private checker: Promise<AppChecker> | undefined
|
|
22
|
+
private readonly open: (root: string) => Promise<AppChecker>
|
|
23
|
+
|
|
24
|
+
constructor(
|
|
25
|
+
root: string,
|
|
26
|
+
heard: () => void,
|
|
27
|
+
open: (root: string) => Promise<AppChecker> = openChecker,
|
|
28
|
+
) {
|
|
29
|
+
this.root = root
|
|
30
|
+
this.heard = heard
|
|
31
|
+
this.open = open
|
|
32
|
+
this.watch(root, true)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** A checker on the project as it stands: the one kept where it still is, or a fresh one. */
|
|
36
|
+
async live(): Promise<AppChecker> {
|
|
37
|
+
const kept = this.checker
|
|
38
|
+
if (kept !== undefined) {
|
|
39
|
+
const checker = await kept.catch(() => undefined)
|
|
40
|
+
if (checker?.current()) return checker
|
|
41
|
+
if (this.checker === kept) await this.drop()
|
|
42
|
+
}
|
|
43
|
+
this.checker ??= this.open(this.root)
|
|
44
|
+
return this.checker
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** `question` asked of a live checker; a throw drops it, so the next question opens another. */
|
|
48
|
+
async run<Answer>(
|
|
49
|
+
question: (checker: AppChecker) => Promise<Answer>,
|
|
50
|
+
): Promise<Answer> {
|
|
51
|
+
try {
|
|
52
|
+
return await question(await this.live())
|
|
53
|
+
} catch (thrown) {
|
|
54
|
+
await this.drop()
|
|
55
|
+
throw thrown
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Watches `directory` too — a linked package's, outside the root — once for the session. */
|
|
60
|
+
watch(directory: string, recursive = false): void {
|
|
61
|
+
if (this.watched.has(directory)) return
|
|
62
|
+
this.watched.add(directory)
|
|
63
|
+
this.watchers.push(
|
|
64
|
+
watch(directory, { recursive }, (_event, filename) =>
|
|
65
|
+
this.saw(
|
|
66
|
+
filename === null ? undefined : `${directory}/${filename}`,
|
|
67
|
+
),
|
|
68
|
+
),
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Records a move at `path`, or one nobody could place, then tells `heard`. */
|
|
73
|
+
saw(path: string | undefined): void {
|
|
74
|
+
void this.checker?.then(
|
|
75
|
+
(checker) => checker.saw(path),
|
|
76
|
+
() => {},
|
|
77
|
+
)
|
|
78
|
+
this.heard()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Stops watching and closes the checker. */
|
|
82
|
+
async close(): Promise<void> {
|
|
83
|
+
for (const watcher of this.watchers) watcher.close()
|
|
84
|
+
this.watchers.length = 0
|
|
85
|
+
await this.drop()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private async drop(): Promise<void> {
|
|
89
|
+
const dropped = this.checker
|
|
90
|
+
this.checker = undefined
|
|
91
|
+
try {
|
|
92
|
+
await (await dropped)?.close()
|
|
93
|
+
} catch {
|
|
94
|
+
// Already gone, which is what closing it was for.
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
package/src/compiler/Lowering.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { SyntaxKind } from 'typescript/unstable/ast'
|
|
|
3
3
|
import type { CheckerAnswer } from './CheckerAnswer.ts'
|
|
4
4
|
import type { Read } from './classifyRead.ts'
|
|
5
5
|
import { holdsSlot } from './classifyTemplate.ts'
|
|
6
|
-
import type { Finding } from './
|
|
6
|
+
import type { Finding } from './Diagnostics.ts'
|
|
7
7
|
import { type AbideFile, scriptBodies } from './parseAbide.ts'
|
|
8
8
|
import {
|
|
9
9
|
type Origin,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* A refusal that knows WHERE in the file's text it stands, so a sink handed the text reports it on
|
|
3
|
-
* the line and column the author typed rather than at the file (`
|
|
3
|
+
* the line and column the author typed rather than at the file (`Diagnostics.at`).
|
|
4
4
|
*/
|
|
5
5
|
export class PositionedRefusal extends Error {
|
|
6
6
|
/** The offset into the file's text the refusal stands at. */
|