@abide/abide 0.123.3 → 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 +31 -0
- 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/LoweredFile.ts +609 -0
- package/src/compiler/Lowering.ts +17 -0
- 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 +71 -1715
- package/src/compiler/appLints.ts +109 -0
- package/src/compiler/checkedShadow.ts +1 -1
- package/src/compiler/classifyRead.ts +16 -1
- package/src/compiler/compileApp.ts +2 -2
- package/src/compiler/declaredExports.ts +44 -17
- package/src/compiler/diagnoseSources.ts +232 -0
- package/src/compiler/formatAbide.ts +5 -6
- package/src/compiler/handlersUnder.ts +16 -35
- package/src/compiler/reactiveReads.ts +235 -0
- package/src/compiler/reactiveTypes.ts +13 -0
- package/src/compiler/readsIn.ts +180 -81
- package/src/compiler/routesDeclaration.ts +46 -0
- package/src/compiler/scaffold.ts +7 -2
- package/src/compiler/serveLanguage.ts +26 -20
- package/src/compiler/splitImports.ts +2 -2
- 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/abideModules.ts +7 -7
- package/src/server/answerFailure.ts +3 -2
- package/src/server/answerRequest.ts +6 -70
- package/src/server/answerRpc.ts +48 -260
- package/src/server/answerSocket.ts +41 -463
- package/src/server/appKit.ts +1 -1
- package/src/server/argsContract.ts +1 -1
- package/src/server/createApp.ts +8 -2
- package/src/server/csp.ts +1 -1
- package/src/server/deferredBuilds.ts +3 -16
- package/src/server/faceOf.ts +1 -1
- package/src/server/generatedSurfaces.ts +2 -1
- package/src/server/handlerFileMount.ts +1 -1
- package/src/server/index.ts +1 -1
- 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/preload.ts +18 -0
- package/src/server/principal.ts +1 -1
- package/src/server/remote.ts +1 -1
- package/src/server/render.ts +31 -59
- package/src/server/requestScope.ts +5 -36
- package/src/server/runDesktop.ts +19 -447
- package/src/server/seedBuffer.ts +11 -11
- package/src/server/servedApp.ts +1 -1
- package/src/server/shell.ts +1 -1
- package/src/server/surfaceOf.ts +1 -1
- package/src/server/versionOf.ts +1 -1
- package/src/shared/RPC_SERVER.ts +27 -31
- package/src/shared/Schema.ts +1 -2
- 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/reactive/Keyed.ts +23 -15
- package/src/shared/reactive/Reactive.ts +34 -1001
- 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 -24
- package/src/shared/reactive/pairReactive.ts +5 -3
- package/src/shared/reactive/producer.ts +121 -0
- package/src/shared/reactive/scoped.ts +3 -2
- package/src/shared/reactive/selections.ts +209 -0
- package/src/shared/reactive/watch.ts +3 -4
- package/src/shared/rpc.ts +8 -5
- package/src/shared/validateJson.ts +1 -2
- package/src/shared/writePath.ts +29 -0
- package/src/ui/Router.ts +53 -69
- package/src/ui/outboxQueue.ts +2 -8
- package/src/ui/outcomeOf.ts +120 -0
- package/src/ui/socketWire.ts +3 -3
- package/src/ui/wire.ts +9 -10
- 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
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { AMBIENT_SOURCES, ambientReadsIn } from './ambientReadsIn.ts'
|
|
2
|
+
import { argsDefaultsIn } from './argsDefaultsIn.ts'
|
|
3
|
+
import {
|
|
4
|
+
byPlace,
|
|
5
|
+
type Diagnosis,
|
|
6
|
+
type Finding,
|
|
7
|
+
placedFindings,
|
|
8
|
+
} from './Diagnostics.ts'
|
|
9
|
+
import { projectShadow } from './projectShadow.ts'
|
|
10
|
+
import type { ParsedSource } from './readSources.ts'
|
|
11
|
+
import type { ShadowHost } from './ShadowHost.ts'
|
|
12
|
+
import { ShadowMap } from './ShadowMap.ts'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* What the build refuses and warns on in the app's `.ts` files and its `.abide` `sources`.
|
|
16
|
+
*
|
|
17
|
+
* 11.37 — `ambient`: every read of a request's ambient inside a `global` value's body, as errors.
|
|
18
|
+
* 11.30 — `unkeyed`: every `args` defaulting a member its body's parameter does not, as warnings.
|
|
19
|
+
* Both over every `.ts` under `within` and each of `sources` as the shadow its reads registered
|
|
20
|
+
* for it, because a keyed value is declared in a `.ts` as often as in a `.abide` — one walk of
|
|
21
|
+
* the files for the two, each finding on its own line of the file an author wrote. `scripts`
|
|
22
|
+
* as a diagnosis reads it: an editor asking about one page does not walk the app's `.ts`.
|
|
23
|
+
*/
|
|
24
|
+
export async function appLints(
|
|
25
|
+
host: ShadowHost,
|
|
26
|
+
within: string,
|
|
27
|
+
sources: readonly Omit<ParsedSource, 'quiet'>[],
|
|
28
|
+
scripts: boolean,
|
|
29
|
+
): Promise<{ ambient: Diagnosis[]; unkeyed: Diagnosis[] }> {
|
|
30
|
+
const ambient: Diagnosis[] = []
|
|
31
|
+
const unkeyed: Diagnosis[] = []
|
|
32
|
+
await host.ask(async (project) => {
|
|
33
|
+
const checker = project.checker
|
|
34
|
+
const ambientPaths = new Map<string, string>()
|
|
35
|
+
const declared = await Promise.all(
|
|
36
|
+
AMBIENT_SOURCES.map((source) =>
|
|
37
|
+
project.program.getSourceFile(source),
|
|
38
|
+
),
|
|
39
|
+
)
|
|
40
|
+
for (let at = 0; at < AMBIENT_SOURCES.length; at += 1) {
|
|
41
|
+
const found = declared[at]?.path
|
|
42
|
+
if (found !== undefined)
|
|
43
|
+
ambientPaths.set(AMBIENT_SOURCES[at] as string, found)
|
|
44
|
+
}
|
|
45
|
+
const authored = new Map<
|
|
46
|
+
string,
|
|
47
|
+
{ source: (typeof sources)[number]; map: ShadowMap }
|
|
48
|
+
>()
|
|
49
|
+
for (const source of sources) {
|
|
50
|
+
const map = new ShadowMap(
|
|
51
|
+
host.canonical(source.path),
|
|
52
|
+
projectShadow(source.file).origins,
|
|
53
|
+
)
|
|
54
|
+
authored.set(host.keyOf(map.shadowPath), { source, map })
|
|
55
|
+
}
|
|
56
|
+
// A file of the host's own is one of `sources`' shadows or not walked at all: a checked
|
|
57
|
+
// module is the lowering's, and placed nowhere an author wrote.
|
|
58
|
+
const files: string[] = []
|
|
59
|
+
if (scripts)
|
|
60
|
+
for (const path of host.rootsUnder(project, within)) {
|
|
61
|
+
if (authored.has(host.keyOf(path)) || !host.holds(path))
|
|
62
|
+
files.push(path)
|
|
63
|
+
}
|
|
64
|
+
else for (const { map } of authored.values()) files.push(map.shadowPath)
|
|
65
|
+
await Promise.all(
|
|
66
|
+
files.map(async (path) => {
|
|
67
|
+
const sourceFile = await project.program.getSourceFile(path)
|
|
68
|
+
if (sourceFile === undefined) return
|
|
69
|
+
const [reads, defaults] = await Promise.all([
|
|
70
|
+
ambientPaths.size === 0
|
|
71
|
+
? []
|
|
72
|
+
: ambientReadsIn(checker, sourceFile, ambientPaths),
|
|
73
|
+
argsDefaultsIn(checker, sourceFile),
|
|
74
|
+
])
|
|
75
|
+
if (reads.length === 0 && defaults.length === 0) return
|
|
76
|
+
const shadow = authored.get(host.keyOf(path))
|
|
77
|
+
const source = shadow?.source
|
|
78
|
+
const text =
|
|
79
|
+
source === undefined
|
|
80
|
+
? await Bun.file(path).text()
|
|
81
|
+
: source.text
|
|
82
|
+
const place = (at: number): number =>
|
|
83
|
+
shadow === undefined ? at : shadow.map.toSource(path, at)
|
|
84
|
+
const named = source?.path ?? path
|
|
85
|
+
const ambientReads: Finding[] = []
|
|
86
|
+
for (const read of reads)
|
|
87
|
+
ambientReads.push({
|
|
88
|
+
at: read.at,
|
|
89
|
+
message: `abide: a \`global\` value's body reads \`${read.ambient}\`, which belongs to the request it was built in — every later request would be answered with this one's. Take it out of the body, or drop \`global\`. RULEBOOK 11.37.`,
|
|
90
|
+
})
|
|
91
|
+
ambient.push(
|
|
92
|
+
...placedFindings(
|
|
93
|
+
named,
|
|
94
|
+
text,
|
|
95
|
+
ambientReads,
|
|
96
|
+
place,
|
|
97
|
+
'error',
|
|
98
|
+
),
|
|
99
|
+
)
|
|
100
|
+
unkeyed.push(
|
|
101
|
+
...placedFindings(named, text, defaults, place, 'warning'),
|
|
102
|
+
)
|
|
103
|
+
}),
|
|
104
|
+
)
|
|
105
|
+
})
|
|
106
|
+
ambient.sort(byPlace)
|
|
107
|
+
unkeyed.sort(byPlace)
|
|
108
|
+
return { ambient, unkeyed }
|
|
109
|
+
}
|
|
@@ -40,7 +40,7 @@ export type CheckedShadow = {
|
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
42
|
* What a checked module's default export is declared as. Scaffolding, so a jump to it is a jump to
|
|
43
|
-
* the component's file (`
|
|
43
|
+
* the component's file (`LoweredFile.inspect`).
|
|
44
44
|
*/
|
|
45
45
|
export const COMPONENT_STAND_IN = '__component'
|
|
46
46
|
|
|
@@ -46,7 +46,8 @@ export type Read = {
|
|
|
46
46
|
* `value` lowers to `n()`; `asWritten` stays as written (`n.set(v)`, 31.5); `member` is one of
|
|
47
47
|
* the `Reactive`'s own members referenced rather than called, named by `path`; `call` is the
|
|
48
48
|
* explicit `n()` (31.7), which `arguments` says was given any; `shorthand` is `{ n }`;
|
|
49
|
-
* `write` and `update` are 31.4, and 31.8 where `path` is not empty.
|
|
49
|
+
* `write` and `update` are 31.4, and 31.8 where `path` is not empty. `mutate` is 31.9: an
|
|
50
|
+
* array mutator called through `path`, its `value` the call from the receiver's end.
|
|
50
51
|
*
|
|
51
52
|
* 33's five: the `props<Props>()` call, which lowers to the record the caller passed; the pair
|
|
52
53
|
* that brackets a binding element's initializer, which together make a default an accessor;
|
|
@@ -63,6 +64,7 @@ export type Read = {
|
|
|
63
64
|
| 'shorthand'
|
|
64
65
|
| 'write'
|
|
65
66
|
| 'update'
|
|
67
|
+
| 'mutate'
|
|
66
68
|
| 'props'
|
|
67
69
|
| 'defaultOpen'
|
|
68
70
|
| 'defaultClose'
|
|
@@ -244,6 +246,7 @@ export function classifyPropRead(node: Node): Read | undefined {
|
|
|
244
246
|
export function classifyRead(
|
|
245
247
|
node: Node,
|
|
246
248
|
valueMember = false,
|
|
249
|
+
mutator = false,
|
|
247
250
|
): Read | undefined {
|
|
248
251
|
const parent = node.parent
|
|
249
252
|
if (isNotARead(parent)) return undefined
|
|
@@ -325,6 +328,18 @@ export function classifyRead(
|
|
|
325
328
|
top = up
|
|
326
329
|
}
|
|
327
330
|
|
|
331
|
+
// 31.9 — the walk took the mutator's own name as a step: it is the call, not the path.
|
|
332
|
+
if (
|
|
333
|
+
mutator &&
|
|
334
|
+
isPropertyAccessExpression(top) &&
|
|
335
|
+
isCallExpression(top.parent)
|
|
336
|
+
) {
|
|
337
|
+
read.path.pop()
|
|
338
|
+
read.shape = 'mutate'
|
|
339
|
+
read.to = top.parent.end
|
|
340
|
+
read.value = { start: top.expression.end, end: top.parent.end }
|
|
341
|
+
return read
|
|
342
|
+
}
|
|
328
343
|
const outer = top.parent
|
|
329
344
|
if (isAssigned(outer, top)) {
|
|
330
345
|
read.shape = 'write'
|
|
@@ -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'
|
|
@@ -84,7 +84,7 @@ export type AppChecker = {
|
|
|
84
84
|
* The same nearest tsconfig and package.json, neither touched since. Asked rather than heard,
|
|
85
85
|
* because either can sit above the watched root — the dogfood's do — and either changes what
|
|
86
86
|
* every file resolves to. Where it is, the session is told every move `saw` held
|
|
87
|
-
* (`
|
|
87
|
+
* (`ShadowHost.edited`); where it is not, `KeptChecker` opens another.
|
|
88
88
|
*/
|
|
89
89
|
current: () => boolean
|
|
90
90
|
close: () => Promise<void>
|
|
@@ -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,
|
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
isReactiveMember,
|
|
52
52
|
valueTypeOf,
|
|
53
53
|
} from './reactiveTypes.ts'
|
|
54
|
+
import type { ShadowHost } from './ShadowHost.ts'
|
|
54
55
|
|
|
55
56
|
/**
|
|
56
57
|
* Every value each module exports, with the abide method that declares it.
|
|
@@ -62,15 +63,40 @@ import {
|
|
|
62
63
|
* namespace is abide's, and an app's own function of that spelling is not. A type is not an
|
|
63
64
|
* export here: it is erased, and has no address to answer at.
|
|
64
65
|
*
|
|
65
|
-
* `
|
|
66
|
-
*
|
|
67
|
-
*
|
|
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.
|
|
68
73
|
*/
|
|
69
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,
|
|
70
98
|
project: Project,
|
|
71
|
-
|
|
72
|
-
paths: readonly string[],
|
|
73
|
-
named: readonly string[],
|
|
99
|
+
modules: readonly { path: string; file: string }[],
|
|
74
100
|
refusals: Diagnostics,
|
|
75
101
|
): Promise<(DeclaredExport[] | undefined)[]> {
|
|
76
102
|
const checker = project.checker
|
|
@@ -81,16 +107,17 @@ export async function declaredExports(
|
|
|
81
107
|
?.path
|
|
82
108
|
const abide = await abideIn(project)
|
|
83
109
|
return allSettled(
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
),
|
|
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)',
|
|
93
118
|
)
|
|
119
|
+
} catch (thrown) {
|
|
120
|
+
refusals.at(file, thrown)
|
|
94
121
|
return undefined
|
|
95
122
|
}
|
|
96
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
|
+
}
|
|
@@ -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
|
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
// each module exports and which call declares each (16.73) — which is also what makes an export
|
|
10
10
|
// that is not a handler a refusal here rather than an address that silently never answers.
|
|
11
11
|
|
|
12
|
-
import type { Mount } from '#server/answerRpc.ts'
|
|
13
12
|
import { frameworkRouteRefusal } from '#server/appTree.ts'
|
|
14
13
|
import { handlerFileMount } from '#server/handlerFileMount.ts'
|
|
14
|
+
import type { Mount } from '#server/mountHandlers.ts'
|
|
15
15
|
import { PRIVATE_EXPORT } from '#server/PRIVATE_EXPORT.ts'
|
|
16
16
|
import { surfaceName } from '#server/surfaceOf.ts'
|
|
17
17
|
import { PRIVATE_HANDLERS_ADDRESS } from '#shared/ADDRESSES.ts'
|
|
@@ -54,40 +54,22 @@ export async function handlersUnder(
|
|
|
54
54
|
const serving: Source[] = []
|
|
55
55
|
for (const source of sources)
|
|
56
56
|
if (source.file.servers.length > 0) serving.push(source)
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
for (const file of files)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
paths.push(session.projectionOf(source.path))
|
|
68
|
-
named.push(source.path)
|
|
69
|
-
}
|
|
57
|
+
const host = session.host
|
|
58
|
+
const modules: { path: string; file: string; onDisk: boolean }[] = []
|
|
59
|
+
for (const file of files)
|
|
60
|
+
modules.push({ path: file.path, file: file.relative, onDisk: true })
|
|
61
|
+
for (const source of serving)
|
|
62
|
+
modules.push({
|
|
63
|
+
path: host.projectionOf(source.path),
|
|
64
|
+
file: source.path,
|
|
65
|
+
onDisk: false,
|
|
66
|
+
})
|
|
70
67
|
// A module whose read was refused is `undefined`, refused at its file already.
|
|
71
|
-
const declared =
|
|
72
|
-
project === undefined || paths.length === 0
|
|
73
|
-
? paths.map(() => [])
|
|
74
|
-
: await declaredExports(
|
|
75
|
-
project,
|
|
76
|
-
session.tsconfig,
|
|
77
|
-
paths,
|
|
78
|
-
named,
|
|
79
|
-
diagnostics,
|
|
80
|
-
)
|
|
68
|
+
const declared = await declaredExports(host, modules, diagnostics)
|
|
81
69
|
const mounts: Mount[] = []
|
|
82
70
|
const names = new Map<string, string>()
|
|
83
|
-
for (
|
|
84
|
-
const
|
|
85
|
-
relative,
|
|
86
|
-
route: address,
|
|
87
|
-
scope,
|
|
88
|
-
matchers,
|
|
89
|
-
} = files[at] as (typeof files)[number]
|
|
90
|
-
const exported = declared[at]
|
|
71
|
+
for (const { path, relative, route: address, scope, matchers } of files) {
|
|
72
|
+
const exported = declared.get(path)
|
|
91
73
|
if (exported === undefined) continue
|
|
92
74
|
try {
|
|
93
75
|
const handler = exported.find((each) => each.name === 'default')
|
|
@@ -125,12 +107,11 @@ export async function handlersUnder(
|
|
|
125
107
|
diagnostics.at(relative, thrown)
|
|
126
108
|
}
|
|
127
109
|
}
|
|
128
|
-
for (
|
|
129
|
-
const { relative, position, place, scope } = serving[at] as Source
|
|
110
|
+
for (const { path, relative, position, place, scope } of serving) {
|
|
130
111
|
// 34.24 — a dependency's file answers where a mount places it, and its handlers below the
|
|
131
112
|
// path it has there.
|
|
132
113
|
const below = place === 'dependency' ? relative : position
|
|
133
|
-
for (const each of declared
|
|
114
|
+
for (const each of declared.get(host.projectionOf(path)) ?? []) {
|
|
134
115
|
if (each.method === undefined) continue
|
|
135
116
|
if (!each.name.startsWith(PRIVATE_EXPORT)) continue
|
|
136
117
|
const name = each.name.slice(PRIVATE_EXPORT.length)
|