@abide/abide 0.123.2 → 0.124.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/ABIDE.md +8 -5
- package/CHANGELOG.md +58 -0
- package/bin/abide.ts +36 -65
- package/docs/REGISTRY.md +45 -12
- package/docs/RULEBOOK.md +24 -14
- package/docs/abide.md +999 -459
- package/package.json +1 -1
- package/src/compiler/ARRAY_MUTATORS.ts +20 -0
- package/src/compiler/Diagnostics.ts +198 -0
- package/src/compiler/KeptChecker.ts +97 -0
- package/src/compiler/LoweredFile.ts +609 -0
- package/src/compiler/Lowering.ts +18 -1
- package/src/compiler/PositionedRefusal.ts +1 -1
- package/src/compiler/SHARED_NAMES.ts +2 -0
- package/src/compiler/ShadowHost.ts +670 -0
- package/src/compiler/ShadowMap.ts +1 -1
- package/src/compiler/TypeSession.ts +76 -1743
- package/src/compiler/appLayout.ts +28 -20
- package/src/compiler/appLints.ts +109 -0
- package/src/compiler/argsDefaultsIn.ts +1 -1
- package/src/compiler/checkApp.ts +64 -68
- package/src/compiler/checkedShadow.ts +2 -2
- package/src/compiler/classifyRead.ts +16 -1
- package/src/compiler/compileApp.ts +102 -71
- package/src/compiler/declaredExports.ts +46 -21
- package/src/compiler/diagnoseSources.ts +232 -0
- package/src/compiler/emitClient.ts +1 -1
- package/src/compiler/formatAbide.ts +7 -8
- package/src/compiler/handlersUnder.ts +32 -63
- package/src/compiler/lineOf.ts +0 -19
- package/src/compiler/matchersOf.ts +3 -3
- package/src/compiler/mountsOf.ts +3 -3
- package/src/compiler/reactiveReads.ts +235 -0
- package/src/compiler/reactiveTypes.ts +13 -0
- package/src/compiler/readSources.ts +16 -18
- package/src/compiler/readsIn.ts +181 -82
- package/src/compiler/routesDeclaration.ts +46 -0
- package/src/compiler/scaffold.ts +7 -2
- package/src/compiler/serveLanguage.ts +46 -74
- package/src/compiler/sharedModuleWrites.ts +1 -1
- package/src/compiler/splitImports.ts +2 -2
- 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/DesktopSession.ts +213 -0
- package/src/server/Followers.ts +399 -0
- package/src/server/MODULE_QUERIES.ts +6 -2
- package/src/server/PageRender.ts +201 -0
- package/src/server/RESUME_MILLISECONDS.ts +2 -0
- package/src/server/abideModules.ts +7 -7
- package/src/server/answerFailure.ts +3 -2
- package/src/server/answerRequest.ts +16 -369
- package/src/server/answerRpc.ts +48 -302
- package/src/server/answerSocket.ts +41 -490
- package/src/server/appBuild.ts +377 -0
- package/src/server/appKit.ts +1 -1
- 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 +38 -66
- package/src/server/csp.ts +1 -1
- package/src/server/deferredBuilds.ts +3 -16
- package/src/server/devWorker.ts +11 -15
- package/src/server/faceOf.ts +1 -1
- package/src/server/generatedSurfaces.ts +2 -1
- package/src/server/handlerFileMount.ts +40 -0
- package/src/server/index.ts +3 -3
- package/src/server/installServer.ts +5 -9
- package/src/server/invokeHandler.ts +230 -0
- package/src/server/mountHandlers.ts +184 -0
- package/src/server/openDesktopWindow.ts +308 -0
- package/src/server/packageDirectory.ts +14 -0
- package/src/server/preload.ts +21 -2
- package/src/server/principal.ts +5 -4
- package/src/server/remote.ts +2 -2
- package/src/server/render.ts +31 -59
- package/src/server/requestScope.ts +5 -36
- package/src/server/runCompiled.ts +1 -1
- package/src/server/runDesktop.ts +20 -448
- package/src/server/seedBuffer.ts +12 -12
- package/src/server/serveDev.ts +24 -58
- package/src/server/servedApp.ts +390 -0
- package/src/server/shell.ts +1 -1
- package/src/server/signInChanged.ts +20 -0
- package/src/server/surfaceOf.ts +3 -2
- package/src/server/versionOf.ts +7 -16
- package/src/shared/RPC_SERVER.ts +27 -31
- package/src/shared/Schema.ts +1 -2
- package/src/shared/applyPatch.ts +1 -1
- package/src/shared/index.ts +1 -1
- package/src/shared/{note.ts → issueReport.ts} +22 -0
- package/src/shared/loadCall.ts +66 -0
- package/src/shared/muxFrames.ts +1 -1
- package/src/shared/reactive/Keyed.ts +24 -16
- package/src/shared/reactive/Reactive.ts +37 -991
- package/src/shared/reactive/argsContract.ts +131 -4
- package/src/shared/reactive/byOf.ts +2 -5
- package/src/shared/reactive/callLoad.ts +8 -14
- package/src/shared/reactive/cellShape.ts +734 -0
- package/src/shared/reactive/index.ts +12 -23
- package/src/shared/reactive/pairReactive.ts +5 -3
- package/src/shared/reactive/patchLog.ts +79 -0
- package/src/shared/reactive/patchMarks.ts +34 -0
- package/src/shared/reactive/producer.ts +121 -0
- package/src/shared/reactive/recordWrites.ts +11 -14
- package/src/shared/reactive/scoped.ts +3 -2
- package/src/shared/reactive/selections.ts +209 -0
- package/src/shared/reactive/share.ts +4 -3
- package/src/shared/reactive/watch.ts +3 -4
- package/src/shared/rpc.ts +20 -24
- package/src/shared/validateJson.ts +1 -2
- package/src/shared/writePath.ts +29 -0
- 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 +106 -313
- 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 +23 -11
- package/src/ui/outcomeOf.ts +120 -0
- 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 +28 -13
- package/src/ui/wire.ts +9 -10
- 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/LOAD_OFFLINE.ts +0 -15
- package/src/shared/LOAD_SIGNAL.ts +0 -10
- package/src/shared/issuesOf.ts +0 -18
- package/src/shared/reactive/ENTRY_LIVES.ts +0 -7
- 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
|
@@ -6,9 +6,9 @@ import {
|
|
|
6
6
|
type CompiledFile,
|
|
7
7
|
resolvedFile,
|
|
8
8
|
} from '#server/abideModules.ts'
|
|
9
|
-
import type { Mount } from '#server/answerRpc.ts'
|
|
10
9
|
import { CLIENT_ENTRY } from '#server/CLIENT_BUILD.ts'
|
|
11
10
|
import { COMPONENT_QUERY } from '#server/MODULE_QUERIES.ts'
|
|
11
|
+
import type { Mount } from '#server/mountHandlers.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'
|
|
@@ -22,15 +22,19 @@ import {
|
|
|
22
22
|
type PlacedSource,
|
|
23
23
|
shapeOf,
|
|
24
24
|
} from './appLayout.ts'
|
|
25
|
+
import { type Diagnosis, Diagnostics } from './Diagnostics.ts'
|
|
25
26
|
import { emitSource } from './emitSource.ts'
|
|
26
27
|
import { handlersUnder } from './handlersUnder.ts'
|
|
27
28
|
import { isAbideSpecifier } from './hookCalls.ts'
|
|
28
29
|
import { nearest } from './nearest.ts'
|
|
29
30
|
import { projectShadow } from './projectShadow.ts'
|
|
30
|
-
import {
|
|
31
|
-
|
|
31
|
+
import {
|
|
32
|
+
type ParsedSource,
|
|
33
|
+
type ReadSource,
|
|
34
|
+
readSources,
|
|
35
|
+
} from './readSources.ts'
|
|
32
36
|
import { respecified } from './splitImports.ts'
|
|
33
|
-
import {
|
|
37
|
+
import { TypeSession } from './TypeSession.ts'
|
|
34
38
|
import { writeDefaultProject } from './writeDefaultProject.ts'
|
|
35
39
|
|
|
36
40
|
// AN APP'S `.abide` SOURCES, COMPILED — every file keyed by its path below the root, and loaded
|
|
@@ -80,7 +84,7 @@ export type AppChecker = {
|
|
|
80
84
|
* The same nearest tsconfig and package.json, neither touched since. Asked rather than heard,
|
|
81
85
|
* because either can sit above the watched root — the dogfood's do — and either changes what
|
|
82
86
|
* every file resolves to. Where it is, the session is told every move `saw` held
|
|
83
|
-
* (`
|
|
87
|
+
* (`ShadowHost.edited`); where it is not, `KeptChecker` opens another.
|
|
84
88
|
*/
|
|
85
89
|
current: () => boolean
|
|
86
90
|
close: () => Promise<void>
|
|
@@ -102,8 +106,7 @@ export async function openChecker(appRoot: string): Promise<AppChecker> {
|
|
|
102
106
|
found === undefined
|
|
103
107
|
? await writeDefaultProject(root)
|
|
104
108
|
: { tsconfig: found, remove: async (): Promise<void> => {} }
|
|
105
|
-
const session = await TypeSession.open(project.tsconfig)
|
|
106
|
-
session.root = root
|
|
109
|
+
const session = await TypeSession.open(project.tsconfig, root)
|
|
107
110
|
const moved = new Set<string>()
|
|
108
111
|
let movedSomewhere = false
|
|
109
112
|
let layout: Promise<AppLayout> | undefined
|
|
@@ -193,7 +196,7 @@ export async function compileApp(
|
|
|
193
196
|
|
|
194
197
|
/**
|
|
195
198
|
* The app at `appRoot` compiled — THE ONE PIPELINE `abide build` and `abide check` both run, the
|
|
196
|
-
* layout, the reads, the lints and the emit, differing only in what a refusal does (`
|
|
199
|
+
* layout, the reads, the lints and the emit, differing only in what a refusal does (`Diagnostics`) and
|
|
197
200
|
* in what is checked beside: a build stops at the first refusal, and a check (`checking`) reports
|
|
198
201
|
* every one at its file with what the checker says of each file (38.3), and goes on checking
|
|
199
202
|
* whatever a refusal does not stop (38.33).
|
|
@@ -204,11 +207,11 @@ export async function compiled(
|
|
|
204
207
|
checking: boolean,
|
|
205
208
|
): Promise<{ app: CompiledApp; findings: Diagnosis[] }> {
|
|
206
209
|
const root = realpathSync(appRoot)
|
|
207
|
-
const
|
|
210
|
+
const diagnostics = new Diagnostics(root, checking)
|
|
208
211
|
// ONE LAYOUT for the sources, the handler files and the public files, and what the tree refuses
|
|
209
212
|
// is met before the checker is asked anything.
|
|
210
213
|
const layout = await (kept?.layout() ?? appLayout(root))
|
|
211
|
-
|
|
214
|
+
diagnostics.adopt([...layout.unparsed, ...layout.refused])
|
|
212
215
|
const app: CompiledApp = {
|
|
213
216
|
files: {},
|
|
214
217
|
tables: {},
|
|
@@ -216,7 +219,7 @@ export async function compiled(
|
|
|
216
219
|
routes: [],
|
|
217
220
|
mounts: [],
|
|
218
221
|
sources: layout.own,
|
|
219
|
-
warnings:
|
|
222
|
+
warnings: diagnostics.warnings,
|
|
220
223
|
published: layout.published,
|
|
221
224
|
}
|
|
222
225
|
// 16.2 — an app of handler files alone is still an app; one of neither has nothing to build, and
|
|
@@ -231,62 +234,94 @@ export async function compiled(
|
|
|
231
234
|
// One session for the whole compile: opening it spawns the checker, and the cost is paid once
|
|
232
235
|
// rather than per file.
|
|
233
236
|
return withChecker(root, kept, async (session) => {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
const projected: PlacedSource[] = []
|
|
237
|
-
for (const source of layout.sources)
|
|
238
|
-
try {
|
|
239
|
-
projectShadow(source.file)
|
|
240
|
-
projected.push(source)
|
|
241
|
-
} catch (thrown) {
|
|
242
|
-
refusals.at(source.path, thrown)
|
|
243
|
-
}
|
|
244
|
-
const { read, warnings } = await readSources(
|
|
237
|
+
const emitted = await compiledSources(
|
|
238
|
+
layout,
|
|
245
239
|
session,
|
|
246
|
-
|
|
247
|
-
|
|
240
|
+
layout.sources,
|
|
241
|
+
[],
|
|
242
|
+
diagnostics,
|
|
243
|
+
checking,
|
|
244
|
+
true,
|
|
248
245
|
)
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
246
|
+
if (emitted !== undefined) Object.assign(app, emitted)
|
|
247
|
+
return { app, findings: diagnostics.findings }
|
|
248
|
+
})
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* What the compile makes of `own`, everything it finds in it kept in `diagnostics` — THE ONE STEP
|
|
253
|
+
* `abide check` runs over the app and an editor over the one file it holds (38.11), so the squiggle
|
|
254
|
+
* and the line CI fails on are one finding: the projection, the reads, the checker's diagnoses
|
|
255
|
+
* (`checking`), the lints and the emit.
|
|
256
|
+
*
|
|
257
|
+
* `beside` is lowered quietly with `own`, for the types it lends (33.4) and reported on by nobody;
|
|
258
|
+
* `scripts` is whether the app's `.ts` files are checked and read for handlers too, which an editor
|
|
259
|
+
* asking about one page does not want. `undefined` where the emit threw, which `diagnostics` holds
|
|
260
|
+
* at the root.
|
|
261
|
+
*/
|
|
262
|
+
export async function compiledSources(
|
|
263
|
+
layout: AppLayout,
|
|
264
|
+
session: TypeSession,
|
|
265
|
+
own: readonly PlacedSource[],
|
|
266
|
+
beside: readonly ParsedSource[],
|
|
267
|
+
diagnostics: Diagnostics,
|
|
268
|
+
checking: boolean,
|
|
269
|
+
scripts: boolean,
|
|
270
|
+
): Promise<Awaited<ReturnType<typeof emitApp>> | undefined> {
|
|
271
|
+
// 38.33 — A FILE WHOSE PROJECTION IS REFUSED (34.3, 34.17) is refused at its file, found here
|
|
272
|
+
// because every later step projects it and would refuse it for the whole app.
|
|
273
|
+
const projected: PlacedSource[] = []
|
|
274
|
+
for (const source of own)
|
|
270
275
|
try {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
emitWarnings = whole.warnings
|
|
276
|
+
projectShadow(source.file)
|
|
277
|
+
projected.push(source)
|
|
274
278
|
} catch (thrown) {
|
|
275
|
-
|
|
279
|
+
diagnostics.at(source.path, thrown)
|
|
276
280
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
281
|
+
const read = await readSources(
|
|
282
|
+
session,
|
|
283
|
+
[...projected, ...beside],
|
|
284
|
+
diagnostics,
|
|
285
|
+
)
|
|
286
|
+
const placed: ReadSource<PlacedSource>[] = []
|
|
287
|
+
for (const source of read)
|
|
288
|
+
if (!source.quiet) placed.push(source as ReadSource<PlacedSource>)
|
|
289
|
+
let emittable = placed
|
|
290
|
+
if (checking) {
|
|
291
|
+
// THE APP'S OWN FILES ARE REPORTED ON: a dependency's is compiled with them (34.23), and
|
|
292
|
+
// what is wrong inside a package is the package's to report.
|
|
293
|
+
const reported: typeof read = []
|
|
294
|
+
for (const source of read)
|
|
295
|
+
if (source.quiet || !(source as Partial<PlacedSource>).dependency)
|
|
296
|
+
reported.push(source)
|
|
297
|
+
const diagnosed = await session.diagnose(reported, layout, scripts)
|
|
298
|
+
diagnostics.check(diagnosed.diagnoses)
|
|
299
|
+
// A FILE WHOSE LOWERING REFUSED is reported already, at its line, and is not emitted.
|
|
300
|
+
emittable = []
|
|
301
|
+
for (const source of placed)
|
|
302
|
+
if (!diagnosed.refused.has(source.path)) emittable.push(source)
|
|
303
|
+
}
|
|
304
|
+
const { ambient, unkeyed } = await session.lints(
|
|
305
|
+
layout.root,
|
|
306
|
+
projected,
|
|
307
|
+
scripts,
|
|
308
|
+
)
|
|
309
|
+
// 11.37 — a build error, every read named. 11.30 — warned on, and the build goes on.
|
|
310
|
+
diagnostics.adopt(ambient)
|
|
311
|
+
diagnostics.warn(unkeyed)
|
|
312
|
+
// What the emit throws with no one file to name is refused at the root; a refusal of a file
|
|
313
|
+
// is refused at it.
|
|
314
|
+
try {
|
|
315
|
+
return await emitApp(
|
|
316
|
+
scripts ? layout : { ...layout, handlers: [] },
|
|
317
|
+
session,
|
|
318
|
+
emittable,
|
|
319
|
+
diagnostics,
|
|
320
|
+
)
|
|
321
|
+
} catch (thrown) {
|
|
322
|
+
diagnostics.at(layout.root, thrown)
|
|
323
|
+
return undefined
|
|
324
|
+
}
|
|
290
325
|
}
|
|
291
326
|
|
|
292
327
|
/** The emit half of `compiled`, over sources already laid out and read. */
|
|
@@ -297,12 +332,9 @@ async function emitApp(
|
|
|
297
332
|
>,
|
|
298
333
|
session: TypeSession,
|
|
299
334
|
sources: readonly ReadSource<PlacedSource>[],
|
|
300
|
-
refusals:
|
|
335
|
+
refusals: Diagnostics,
|
|
301
336
|
): Promise<
|
|
302
|
-
Pick<
|
|
303
|
-
CompiledApp,
|
|
304
|
-
'files' | 'tables' | 'handlers' | 'routes' | 'mounts' | 'warnings'
|
|
305
|
-
>
|
|
337
|
+
Pick<CompiledApp, 'files' | 'tables' | 'handlers' | 'routes' | 'mounts'>
|
|
306
338
|
> {
|
|
307
339
|
const { root, routes, errors } = layout
|
|
308
340
|
const files: Record<string, CompiledFile> = {}
|
|
@@ -311,7 +343,7 @@ async function emitApp(
|
|
|
311
343
|
// and the browser holds its wrapper in its place — a handler file's as that file, a private
|
|
312
344
|
// handler's in the server block's place in its own file's module. Written whether or not there
|
|
313
345
|
// is a page: a handler is answered over http to a caller that has none.
|
|
314
|
-
const
|
|
346
|
+
const mounts = await handlersUnder(
|
|
315
347
|
session,
|
|
316
348
|
sources,
|
|
317
349
|
layout.handlers,
|
|
@@ -387,7 +419,6 @@ async function emitApp(
|
|
|
387
419
|
handlers,
|
|
388
420
|
routes,
|
|
389
421
|
mounts,
|
|
390
|
-
warnings,
|
|
391
422
|
}
|
|
392
423
|
}
|
|
393
424
|
|
|
@@ -442,7 +473,7 @@ function tableImport(source: string): string {
|
|
|
442
473
|
function refuseSharedRoutes(
|
|
443
474
|
routes: AppRoute[],
|
|
444
475
|
mounts: Mount[],
|
|
445
|
-
refusals:
|
|
476
|
+
refusals: Diagnostics,
|
|
446
477
|
): void {
|
|
447
478
|
const owners = new Map<string, string>()
|
|
448
479
|
const claim = (method: string, route: string, source: string): void => {
|
|
@@ -476,7 +507,7 @@ function refuseAddressCollision(
|
|
|
476
507
|
routes: AppRoute[],
|
|
477
508
|
mounts: Mount[],
|
|
478
509
|
published: Map<string, string>,
|
|
479
|
-
refusals:
|
|
510
|
+
refusals: Diagnostics,
|
|
480
511
|
): void {
|
|
481
512
|
const patterns = new Set<string>()
|
|
482
513
|
for (const route of routes) patterns.add(route.pattern)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CallExpression, Node } from 'typescript/unstable/ast'
|
|
1
|
+
import type { CallExpression, Node, SourceFile } from 'typescript/unstable/ast'
|
|
2
2
|
import { SyntaxKind } from 'typescript/unstable/ast'
|
|
3
3
|
import {
|
|
4
4
|
isArrayLiteralExpression,
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
type UnionOrIntersectionType,
|
|
28
28
|
} from 'typescript/unstable/async'
|
|
29
29
|
|
|
30
|
-
import type { Mount } from '#server/
|
|
30
|
+
import type { Mount } from '#server/mountHandlers.ts'
|
|
31
31
|
import type { CopyOptions } from '#shared/CopyOptions.ts'
|
|
32
32
|
import {
|
|
33
33
|
type Defaults,
|
|
@@ -43,7 +43,7 @@ import {
|
|
|
43
43
|
resultSchemaOf,
|
|
44
44
|
type Walk,
|
|
45
45
|
} from './argsSchemaOf.ts'
|
|
46
|
-
import { diagnosisAt } from './
|
|
46
|
+
import { type Diagnosis, diagnosisAt, type Diagnostics } from './Diagnostics.ts'
|
|
47
47
|
import { isReactiveCall, METHODS, optionNamed } from './reactiveOptions.ts'
|
|
48
48
|
import {
|
|
49
49
|
abideIn,
|
|
@@ -51,8 +51,7 @@ import {
|
|
|
51
51
|
isReactiveMember,
|
|
52
52
|
valueTypeOf,
|
|
53
53
|
} from './reactiveTypes.ts'
|
|
54
|
-
import type {
|
|
55
|
-
import type { Diagnosis } from './TypeSession.ts'
|
|
54
|
+
import type { ShadowHost } from './ShadowHost.ts'
|
|
56
55
|
|
|
57
56
|
/**
|
|
58
57
|
* Every value each module exports, with the abide method that declares it.
|
|
@@ -64,16 +63,41 @@ import type { Diagnosis } from './TypeSession.ts'
|
|
|
64
63
|
* namespace is abide's, and an app's own function of that spelling is not. A type is not an
|
|
65
64
|
* export here: it is erased, and has no address to answer at.
|
|
66
65
|
*
|
|
67
|
-
* `
|
|
68
|
-
*
|
|
69
|
-
*
|
|
66
|
+
* Per module's `path`: a module `onDisk` is a file the host may not hold yet, which is made a root
|
|
67
|
+
* of the project where it has to be (`ShadowHost.ask`); a projection is one the host registered.
|
|
68
|
+
* Where the checker opened no project, every module exports nothing.
|
|
69
|
+
*
|
|
70
|
+
* 38.33 — WHAT IS REFUSED OF A MODULE IS REFUSED AT ITS `file`, every export read either way: a
|
|
71
|
+
* module with a refusal — one outside the project among them — is `undefined`, so nothing refuses
|
|
72
|
+
* it twice.
|
|
70
73
|
*/
|
|
71
74
|
export async function declaredExports(
|
|
75
|
+
host: ShadowHost,
|
|
76
|
+
modules: readonly { path: string; file: string; onDisk: boolean }[],
|
|
77
|
+
refusals: Diagnostics,
|
|
78
|
+
): Promise<Map<string, DeclaredExport[] | undefined>> {
|
|
79
|
+
const declared = new Map<string, DeclaredExport[] | undefined>()
|
|
80
|
+
if (modules.length === 0) return declared
|
|
81
|
+
const onDisk: string[] = []
|
|
82
|
+
for (const module of modules) if (module.onDisk) onDisk.push(module.path)
|
|
83
|
+
const asked = await host.ask(
|
|
84
|
+
(project) => exportsIn(host, project, modules, refusals),
|
|
85
|
+
{ modules: onDisk },
|
|
86
|
+
)
|
|
87
|
+
for (let at = 0; at < modules.length; at += 1)
|
|
88
|
+
declared.set(
|
|
89
|
+
(modules[at] as (typeof modules)[number]).path,
|
|
90
|
+
asked === undefined ? [] : asked[at],
|
|
91
|
+
)
|
|
92
|
+
return declared
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** What each of `modules` exports in `project`, in their order (`declaredExports`). */
|
|
96
|
+
async function exportsIn(
|
|
97
|
+
host: ShadowHost,
|
|
72
98
|
project: Project,
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
named: readonly string[],
|
|
76
|
-
refusals: Refusals,
|
|
99
|
+
modules: readonly { path: string; file: string }[],
|
|
100
|
+
refusals: Diagnostics,
|
|
77
101
|
): Promise<(DeclaredExport[] | undefined)[]> {
|
|
78
102
|
const checker = project.checker
|
|
79
103
|
// ABIDE'S FILE AS THE CHECKER NAMES IT, not as this module does: it canonicalizes a path to
|
|
@@ -83,16 +107,17 @@ export async function declaredExports(
|
|
|
83
107
|
?.path
|
|
84
108
|
const abide = await abideIn(project)
|
|
85
109
|
return allSettled(
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
),
|
|
110
|
+
modules.map(async ({ path, file }) => {
|
|
111
|
+
let sourceFile: SourceFile
|
|
112
|
+
try {
|
|
113
|
+
sourceFile = await host.sourceFile(
|
|
114
|
+
project,
|
|
115
|
+
path,
|
|
116
|
+
path,
|
|
117
|
+
'what it exports cannot be read (RULEBOOK 16.73)',
|
|
95
118
|
)
|
|
119
|
+
} catch (thrown) {
|
|
120
|
+
refusals.at(file, thrown)
|
|
96
121
|
return undefined
|
|
97
122
|
}
|
|
98
123
|
const module = await checker.getSymbolAtLocation(sourceFile)
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import type { AppLayout, PlacedSource } from './appLayout.ts'
|
|
2
|
+
import { checkedShadow, moduleConfigShadow } from './checkedShadow.ts'
|
|
3
|
+
import {
|
|
4
|
+
byPlace,
|
|
5
|
+
type Diagnosis,
|
|
6
|
+
type Finding,
|
|
7
|
+
placedFindings,
|
|
8
|
+
} from './Diagnostics.ts'
|
|
9
|
+
import { lineOf } from './lineOf.ts'
|
|
10
|
+
import { LoweredFile } from './LoweredFile.ts'
|
|
11
|
+
import { CHECKED_VALUES } from './Lowering.ts'
|
|
12
|
+
import { projectShadow } from './projectShadow.ts'
|
|
13
|
+
import type { ReadSource } from './readSources.ts'
|
|
14
|
+
import { routesDeclaration } from './routesDeclaration.ts'
|
|
15
|
+
import type { ShadowEdit, ShadowHost } from './ShadowHost.ts'
|
|
16
|
+
import { ShadowMap } from './ShadowMap.ts'
|
|
17
|
+
|
|
18
|
+
/** One source a diagnosis lowers: read, placed where it is a page or a layout, and maybe quiet. */
|
|
19
|
+
export type DiagnosedSource = Omit<ReadSource, 'quiet'> &
|
|
20
|
+
Partial<Pick<PlacedSource, 'kind' | 'route'>> & {
|
|
21
|
+
quiet?: boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The checker's diagnostics for each `.abide` source and the app's `.ts` roots, and each source's
|
|
26
|
+
* lowering.
|
|
27
|
+
*
|
|
28
|
+
* 38.3 — WHAT THE CHECKER SAYS ABOUT EACH FILE AS IT RUNS, placed on the `.abide` line. Each
|
|
29
|
+
* source is checked as its lowered program (`checkedShadow`), which is registered for the life
|
|
30
|
+
* of the host beside its shadow for the reason a shadow is; and every `.ts` root of the
|
|
31
|
+
* project under the app's root is checked as it stands, where `scripts` asks for them — an editor
|
|
32
|
+
* asking about one page does not want the whole app. `sources` carry their text, which is what
|
|
33
|
+
* a position is a line of. A `quiet` source is lowered and not reported on: a component an
|
|
34
|
+
* editor's page imports, which is there for the props the page is held to (33.4).
|
|
35
|
+
*
|
|
36
|
+
* `app` is the app the sources are checked in (`appLayout`): its route table, which `url`'s
|
|
37
|
+
* literal is held to (30.26), and its handler files, which type `Config` (27.13). A source placed
|
|
38
|
+
* as a page or a layout carries its `kind` and `route`, which its `route.params` is typed by (23.9).
|
|
39
|
+
*
|
|
40
|
+
* `refused` is the path of every source whose checked lowering refused, which `diagnoses` has
|
|
41
|
+
* reported at its line — `abide check` emits the rest of the app without them. `lowered` is each
|
|
42
|
+
* source's lowering, which the host keeps for an editor's questions about it (`LoweredFile`) —
|
|
43
|
+
* none where the checker opened no project.
|
|
44
|
+
*/
|
|
45
|
+
export async function diagnoseSources(
|
|
46
|
+
host: ShadowHost,
|
|
47
|
+
sources: readonly DiagnosedSource[],
|
|
48
|
+
app: Pick<AppLayout, 'root' | 'addresses' | 'handlers' | 'routes'>,
|
|
49
|
+
scripts: boolean,
|
|
50
|
+
): Promise<{
|
|
51
|
+
diagnoses: Diagnosis[]
|
|
52
|
+
refused: Set<string>
|
|
53
|
+
lowered: LoweredFile[]
|
|
54
|
+
}> {
|
|
55
|
+
const within = app.root
|
|
56
|
+
const edit: ShadowEdit = { created: [], changed: [] }
|
|
57
|
+
const checked: {
|
|
58
|
+
source: DiagnosedSource
|
|
59
|
+
map: ShadowMap
|
|
60
|
+
refusals: readonly Finding[]
|
|
61
|
+
}[] = []
|
|
62
|
+
for (const source of sources) {
|
|
63
|
+
const shadow = checkedShadow(
|
|
64
|
+
source.file,
|
|
65
|
+
source.answer,
|
|
66
|
+
(source.kind === 'page' || source.kind === 'layout') &&
|
|
67
|
+
source.route !== undefined
|
|
68
|
+
? { route: source.route, kind: source.kind }
|
|
69
|
+
: undefined,
|
|
70
|
+
)
|
|
71
|
+
const map = new ShadowMap(
|
|
72
|
+
host.canonical(source.path),
|
|
73
|
+
projectShadow(source.file).origins,
|
|
74
|
+
shadow.origins,
|
|
75
|
+
)
|
|
76
|
+
host.write(map.checkedPath, shadow.text, edit)
|
|
77
|
+
// 23.19 — a package's page an editor opens is reported on, so its checked module is a
|
|
78
|
+
// root of the project as its shadow is (`reactiveReads`).
|
|
79
|
+
if (!source.quiet) host.openOutside(source.path, map.checkedPath, edit)
|
|
80
|
+
checked.push({ source, map, refusals: shadow.refusals })
|
|
81
|
+
}
|
|
82
|
+
// 34.6 — WHAT THE BUNDLER RESOLVES AND THE CHECKER CANNOT: a stylesheet imported for its
|
|
83
|
+
// effect, and (34.21) markdown imported as text into a `.ts`, which bun's own types leave
|
|
84
|
+
// out where they declare `.txt`. Declared in a file of its own at the app's root, which is
|
|
85
|
+
// a script, because a pattern module can only be declared outside a module.
|
|
86
|
+
host.write(
|
|
87
|
+
`${host.canonical(within)}/${AMBIENT}`,
|
|
88
|
+
AMBIENT_DECLARATIONS,
|
|
89
|
+
edit,
|
|
90
|
+
)
|
|
91
|
+
// 27.13 — what a handler file defaults, beside it.
|
|
92
|
+
for (const handler of app.handlers) {
|
|
93
|
+
const at = `${host.canonical(handler.path)}${CONFIG_SUFFIX}`
|
|
94
|
+
const shadow = handler.hooks.includes('onConfig')
|
|
95
|
+
? moduleConfigShadow(handler.text)
|
|
96
|
+
: undefined
|
|
97
|
+
// One that stops filing keeps its place and declares nothing, so the next snapshot is
|
|
98
|
+
// told it moved.
|
|
99
|
+
if (shadow !== undefined || host.holds(at))
|
|
100
|
+
host.write(at, shadow ?? 'export {}\n', edit)
|
|
101
|
+
}
|
|
102
|
+
// 30.26 — THE APP'S ROUTE TABLE, which `url`'s literal is held to: every page the build
|
|
103
|
+
// routes, declared into abide's own `Routes`. A module rather than a script, since only a
|
|
104
|
+
// module augments one.
|
|
105
|
+
host.write(
|
|
106
|
+
`${host.canonical(within)}/${ROUTES_DECLARATION}`,
|
|
107
|
+
routesDeclaration(app.addresses, app.routes),
|
|
108
|
+
edit,
|
|
109
|
+
)
|
|
110
|
+
const refused = new Set<string>()
|
|
111
|
+
for (const { source, refusals } of checked)
|
|
112
|
+
if (refusals.length > 0) refused.add(source.path)
|
|
113
|
+
const answered = await host.ask(
|
|
114
|
+
async (project) => {
|
|
115
|
+
const program = project.program
|
|
116
|
+
// `path` is the file the author wrote, which for a checked module is not the file asked.
|
|
117
|
+
const report = async (
|
|
118
|
+
asked: string,
|
|
119
|
+
path: string,
|
|
120
|
+
place: (position: number) => { line: number; column: number },
|
|
121
|
+
): Promise<Diagnosis[]> => {
|
|
122
|
+
const [syntactic, semantic] = await Promise.all([
|
|
123
|
+
program.getSyntacticDiagnostics(asked),
|
|
124
|
+
program.getSemanticDiagnostics(asked),
|
|
125
|
+
])
|
|
126
|
+
const said: Diagnosis[] = []
|
|
127
|
+
for (const diagnostic of [...syntactic, ...semantic])
|
|
128
|
+
said.push({
|
|
129
|
+
path,
|
|
130
|
+
...place(diagnostic.pos),
|
|
131
|
+
code: diagnostic.code,
|
|
132
|
+
category: CATEGORIES[diagnostic.category] ?? 'error',
|
|
133
|
+
// A checked module's own names are the lowering's, and read as the author's.
|
|
134
|
+
message: flatten(diagnostic).replaceAll(
|
|
135
|
+
`${CHECKED_VALUES}.`,
|
|
136
|
+
'',
|
|
137
|
+
),
|
|
138
|
+
})
|
|
139
|
+
return said
|
|
140
|
+
}
|
|
141
|
+
const reports: Promise<Diagnosis[]>[] = []
|
|
142
|
+
const refusing: Diagnosis[] = []
|
|
143
|
+
const lowered: LoweredFile[] = []
|
|
144
|
+
for (const { source, map, refusals } of checked) {
|
|
145
|
+
const lowering = new LoweredFile(host, source.text, map)
|
|
146
|
+
host.keep(lowering)
|
|
147
|
+
lowered.push(lowering)
|
|
148
|
+
if (source.quiet) continue
|
|
149
|
+
// A refusal is at a projection offset (`Lowering.refusals`).
|
|
150
|
+
refusing.push(
|
|
151
|
+
...placedFindings(
|
|
152
|
+
source.path,
|
|
153
|
+
source.text,
|
|
154
|
+
refusals,
|
|
155
|
+
(at) => map.toSource(map.shadowPath, at),
|
|
156
|
+
'error',
|
|
157
|
+
),
|
|
158
|
+
)
|
|
159
|
+
reports.push(
|
|
160
|
+
report(map.checkedPath, source.path, (position) =>
|
|
161
|
+
lineOf(
|
|
162
|
+
source.text,
|
|
163
|
+
map.toSource(map.checkedPath, position),
|
|
164
|
+
),
|
|
165
|
+
),
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
const standing = scripts ? host.rootsUnder(project, within) : []
|
|
169
|
+
for (const path of standing)
|
|
170
|
+
if (!host.holds(path))
|
|
171
|
+
reports.push(
|
|
172
|
+
Bun.file(path)
|
|
173
|
+
.text()
|
|
174
|
+
.then((text) =>
|
|
175
|
+
report(path, path, (position) =>
|
|
176
|
+
lineOf(text, position),
|
|
177
|
+
),
|
|
178
|
+
),
|
|
179
|
+
)
|
|
180
|
+
const reported = [
|
|
181
|
+
...refusing,
|
|
182
|
+
...(await Promise.all(reports)).flat(),
|
|
183
|
+
]
|
|
184
|
+
reported.sort(byPlace)
|
|
185
|
+
// A component's props type is checked twice — in its `props` call and in what a tag is
|
|
186
|
+
// held to (`checkedShadow`) — and both copies place a finding in it at the one place
|
|
187
|
+
// written.
|
|
188
|
+
const diagnoses: Diagnosis[] = []
|
|
189
|
+
const placed = new Set<string>()
|
|
190
|
+
for (const diagnosis of reported) {
|
|
191
|
+
const key = `${diagnosis.path}:${diagnosis.line}:${diagnosis.column}:${diagnosis.code}:${diagnosis.message}`
|
|
192
|
+
if (placed.has(key)) continue
|
|
193
|
+
placed.add(key)
|
|
194
|
+
diagnoses.push(diagnosis)
|
|
195
|
+
}
|
|
196
|
+
return { diagnoses, lowered }
|
|
197
|
+
},
|
|
198
|
+
{ edit },
|
|
199
|
+
)
|
|
200
|
+
return {
|
|
201
|
+
diagnoses: answered?.diagnoses ?? [],
|
|
202
|
+
refused,
|
|
203
|
+
lowered: answered?.lowered ?? [],
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const AMBIENT = 'abide-check.d.ts'
|
|
208
|
+
const ROUTES_DECLARATION = 'abide-routes.d.ts'
|
|
209
|
+
const AMBIENT_DECLARATIONS = `declare module '*.css' {}
|
|
210
|
+
declare module '*.md' {
|
|
211
|
+
const text: string
|
|
212
|
+
export default text
|
|
213
|
+
}
|
|
214
|
+
`
|
|
215
|
+
|
|
216
|
+
/** What a module that is no `.abide` is checked beside as, for the `Config` it files (27.13). */
|
|
217
|
+
const CONFIG_SUFFIX = '.config-shadow.ts'
|
|
218
|
+
|
|
219
|
+
/** Indexed by the API's `DiagnosticCategory`. */
|
|
220
|
+
const CATEGORIES = ['warning', 'error', 'suggestion', 'message'] as const
|
|
221
|
+
|
|
222
|
+
function flatten(diagnostic: {
|
|
223
|
+
text: string
|
|
224
|
+
messageChain?:
|
|
225
|
+
readonly { text: string; messageChain?: unknown }[] | undefined
|
|
226
|
+
}): string {
|
|
227
|
+
let message = diagnostic.text
|
|
228
|
+
const chain = diagnostic.messageChain ?? []
|
|
229
|
+
for (const link of chain)
|
|
230
|
+
message += `\n ${flatten(link as Parameters<typeof flatten>[0]).replaceAll('\n', '\n ')}`
|
|
231
|
+
return message
|
|
232
|
+
}
|
|
@@ -143,7 +143,7 @@ class RegionEmission {
|
|
|
143
143
|
*/
|
|
144
144
|
export type SelfFill = 'none' | 'routed' | 'invoked' | 'both'
|
|
145
145
|
|
|
146
|
-
// The mount's third argument, which only the router passes (`
|
|
146
|
+
// The mount's third argument, which only the router passes (`LevelChain.mountFrom`).
|
|
147
147
|
const ROUTED = '__routed'
|
|
148
148
|
|
|
149
149
|
/**
|
|
@@ -2,7 +2,8 @@ import { dirname } from 'node:path' // bun exposes no path helpers of its own
|
|
|
2
2
|
|
|
3
3
|
import { SyntaxKind } from 'typescript/unstable/ast'
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { type Diagnosis, diagnosisAt } from './Diagnostics.ts'
|
|
6
|
+
import { offsetOf } from './lineOf.ts'
|
|
6
7
|
import { nearest } from './nearest.ts'
|
|
7
8
|
import {
|
|
8
9
|
type AbideFile,
|
|
@@ -13,7 +14,6 @@ import {
|
|
|
13
14
|
} from './parseAbide.ts'
|
|
14
15
|
import { sameTemplate } from './sameTemplate.ts'
|
|
15
16
|
import { isSpace, slotEnd, tokenize } from './tokenize.ts'
|
|
16
|
-
import type { Diagnosis } from './TypeSession.ts'
|
|
17
17
|
import { VOID_ELEMENTS } from './VOID_ELEMENTS.ts'
|
|
18
18
|
|
|
19
19
|
// 38.125, 38.126 — A `.abide` IS FORMATTED AS PIECES, and `bun fmt` formats every piece that is code: a
|
|
@@ -669,12 +669,11 @@ function unwrap(entry: string, unit: string): string {
|
|
|
669
669
|
if (inner.endsWith(';')) inner = inner.slice(0, -1)
|
|
670
670
|
if (inner.endsWith(')')) inner = inner.slice(0, -1)
|
|
671
671
|
if (!inner.startsWith('\n')) return inner.trimEnd()
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
).trimEnd()
|
|
672
|
+
const outdent = (line: string): string =>
|
|
673
|
+
line.startsWith(unit) ? line.slice(unit.length) : line.trimStart()
|
|
674
|
+
// The first line opens the expression, so it is never inside a template literal — and
|
|
675
|
+
// `shiftLines` leaves it alone for that reason, which is why it is outdented here.
|
|
676
|
+
inner = outdent(shiftLines(inner.slice(1), outdent, 'ts')).trimEnd()
|
|
678
677
|
return inner.endsWith(',') ? inner.slice(0, -1) : inner
|
|
679
678
|
}
|
|
680
679
|
|