@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
|
@@ -1,237 +1,38 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { basename, dirname, join, resolve } from 'node:path' // Bun exposes no path helpers of its own
|
|
1
|
+
import type { CompletionInfo } from 'typescript/unstable/async'
|
|
3
2
|
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
type Node,
|
|
7
|
-
NodeFlags,
|
|
8
|
-
SyntaxKind,
|
|
9
|
-
} from 'typescript/unstable/ast'
|
|
10
|
-
import {
|
|
11
|
-
API,
|
|
12
|
-
type Checker,
|
|
13
|
-
type CompletionEntry,
|
|
14
|
-
type CompletionInfo,
|
|
15
|
-
CompletionItemKind,
|
|
16
|
-
NodeBuilderFlags,
|
|
17
|
-
type Project,
|
|
18
|
-
SignatureKind,
|
|
19
|
-
type Snapshot,
|
|
20
|
-
type Symbol as CheckedSymbol,
|
|
21
|
-
SymbolFlags,
|
|
22
|
-
type Type,
|
|
23
|
-
} from 'typescript/unstable/async'
|
|
24
|
-
|
|
25
|
-
import { placeOf } from '#server/placement.ts'
|
|
26
|
-
import { parseRoutePattern } from '#shared/routePattern.ts'
|
|
27
|
-
|
|
28
|
-
import { aliasedSymbol } from './aliasedSymbol.ts'
|
|
29
|
-
import { AMBIENT_SOURCES, ambientReadsIn } from './ambientReadsIn.ts'
|
|
30
|
-
import type { AppLayout, AppRoute, PlacedSource } from './appLayout.ts'
|
|
31
|
-
import { argsDefaultsIn } from './argsDefaultsIn.ts'
|
|
32
|
-
import {
|
|
33
|
-
checkedShadow,
|
|
34
|
-
COMPONENT_MODULE,
|
|
35
|
-
COMPONENT_STAND_IN,
|
|
36
|
-
moduleConfigShadow,
|
|
37
|
-
} from './checkedShadow.ts'
|
|
3
|
+
import type { AppLayout } from './appLayout.ts'
|
|
4
|
+
import { appLints } from './appLints.ts'
|
|
38
5
|
import { type CheckerAnswer, UNCHECKED } from './CheckerAnswer.ts'
|
|
6
|
+
import { type DiagnosedSource, diagnoseSources } from './diagnoseSources.ts'
|
|
7
|
+
import type { Diagnosis } from './Diagnostics.ts'
|
|
39
8
|
import {
|
|
40
|
-
|
|
41
|
-
type
|
|
42
|
-
type
|
|
43
|
-
|
|
44
|
-
} from './
|
|
45
|
-
import { lineOf } from './lineOf.ts'
|
|
46
|
-
import { CHECKED_VALUES } from './Lowering.ts'
|
|
9
|
+
completionAt,
|
|
10
|
+
type Inspection,
|
|
11
|
+
type LoweredFile,
|
|
12
|
+
type References,
|
|
13
|
+
} from './LoweredFile.ts'
|
|
47
14
|
import type { AbideFile } from './parseAbide.ts'
|
|
48
|
-
import {
|
|
49
|
-
import
|
|
50
|
-
import {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
} from './ShadowMap.ts'
|
|
58
|
-
import { SOURCE_SUFFIX } from './splitImports.ts'
|
|
59
|
-
import { type TemplateContext, templateContext } from './templateContext.ts'
|
|
60
|
-
import { templateWarnings } from './templateWarnings.ts'
|
|
61
|
-
|
|
62
|
-
// Where 31.3 gets its answer. A `.abide` file is projected into a SHADOW `.ts` held in a virtual
|
|
63
|
-
// filesystem and never written to disk, and TypeScript's own checker is asked what each declared
|
|
64
|
-
// name's type is. That is the whole reason the checker is here rather than a local inference over
|
|
65
|
-
// annotations: the corpus writes `let rows = reactive([...])` with no annotation anywhere, so the
|
|
66
|
-
// answer comes from the initializer's INFERRED type or it does not come at all.
|
|
15
|
+
import { reactiveReads } from './reactiveReads.ts'
|
|
16
|
+
import type { ParsedSource } from './readSources.ts'
|
|
17
|
+
import { ShadowHost } from './ShadowHost.ts'
|
|
18
|
+
|
|
19
|
+
// Where 31.3 gets its answer: TypeScript's own checker, asked about an app's `.abide` files through
|
|
20
|
+
// the shadows a `ShadowHost` holds for it. That is the whole reason the checker is here rather than
|
|
21
|
+
// a local inference over annotations: the corpus writes `let rows = reactive([...])` with no
|
|
22
|
+
// annotation anywhere, so the answer comes from the initializer's INFERRED type or it does not come
|
|
23
|
+
// at all.
|
|
67
24
|
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
// a
|
|
71
|
-
|
|
72
|
-
/** Whether a script can bind a name at all — a file that cannot is never handed to the checker. */
|
|
73
|
-
const DECLARES = /\b(?:const|let|var|import)\b/
|
|
25
|
+
// ONE OBJECT FOR WHAT A BUILD, A CHECK AND AN EDITOR ASK, each question answered by the module that
|
|
26
|
+
// owns it — the reads (`reactiveReads`), the lints (`appLints`), the diagnosis (`diagnoseSources`)
|
|
27
|
+
// and an editor's questions of a file's lowering (`LoweredFile`) — over the one host.
|
|
74
28
|
|
|
75
29
|
/** A TypeScript checker session over an app, asked about its `.abide` files through shadows. */
|
|
76
30
|
export class TypeSession {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
readonly tsconfig: string
|
|
80
|
-
/**
|
|
81
|
-
* Per checker key, the text the checker reads for a file this session wrote, and the path it
|
|
82
|
-
* was registered under.
|
|
83
|
-
*
|
|
84
|
-
* KEYED AS THE CHECKER KEYS ITS OWN CACHE (`keyOf`), not by the string a caller handed in. On a
|
|
85
|
-
* case-insensitive disk the checker folds `Page.abide.shadow.ts` and `page.abide.shadow.ts` to
|
|
86
|
-
* one file, so a map telling them apart reported the second spelling `created` for a file the
|
|
87
|
-
* checker already held — it read nothing, and answered the second text's reads at the first
|
|
88
|
-
* text's offsets.
|
|
89
|
-
*/
|
|
90
|
-
private shadows = new Map<string, { path: string; text: string }>()
|
|
91
|
-
/** The checker's own key for a path: resolved, and case-folded where its host folds case. */
|
|
92
|
-
private keyOf: (path: string) => string
|
|
93
|
-
/**
|
|
94
|
-
* Per `.abide` path as the checker keys it (`keyOf`), what `diagnose` last lowered it to —
|
|
95
|
-
* kept for `inspect` alone.
|
|
96
|
-
*
|
|
97
|
-
* THE ONE THING CARRIED FROM ONE CALL TO ANOTHER, because an editor's hover is a request of
|
|
98
|
-
* its own that arrives after the check that lowered the file, and has to map its offset
|
|
99
|
-
* through the lowering the checker's program holds rather than one made again. Everything a
|
|
100
|
-
* check's own caller needs of the lowering, `diagnose` hands back.
|
|
101
|
-
*/
|
|
102
|
-
private loweredForInspect = new Map<
|
|
103
|
-
string,
|
|
104
|
-
{ text: string; map: ShadowMap }
|
|
105
|
-
>()
|
|
31
|
+
/** The checker, the files it reads out of this session, and the snapshots it holds of them. */
|
|
32
|
+
readonly host: ShadowHost
|
|
106
33
|
|
|
107
|
-
private constructor(
|
|
108
|
-
|
|
109
|
-
tsconfig: string,
|
|
110
|
-
keyOf: (path: string) => string,
|
|
111
|
-
root: string,
|
|
112
|
-
) {
|
|
113
|
-
this.api = api
|
|
114
|
-
this.tsconfig = tsconfig
|
|
115
|
-
this.root = root
|
|
116
|
-
this.opened = `${dirname(tsconfig)}/${SESSION_CONFIG}`
|
|
117
|
-
this.keyOf = keyOf
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* The project this session opens: the app's tsconfig, with every shadow in `outside` a root.
|
|
122
|
-
*
|
|
123
|
-
* 34.25 — A DEPENDENCY'S `.abide` IS TYPED AS THE APP'S OWN, and its tsconfig's `include` never
|
|
124
|
-
* reaches `node_modules` or a linked package's directory. The session opens a config of its own —
|
|
125
|
-
* served from here, never written — that extends the app's, names those shadows as files, which
|
|
126
|
-
* TypeScript adds to what the base config includes (a linked package's shadow, asked for through
|
|
127
|
-
* the link, is reached no other way), and allows the declaration a package's `exports` is asked
|
|
128
|
-
* for by (`standIn`).
|
|
129
|
-
*/
|
|
130
|
-
private readonly opened: string
|
|
131
|
-
|
|
132
|
-
/** The shadows of the files outside the app's own, as `opened` names them. */
|
|
133
|
-
private outside = new Set<string>()
|
|
134
|
-
|
|
135
|
-
private sessionConfig(): string {
|
|
136
|
-
const config: {
|
|
137
|
-
extends: string
|
|
138
|
-
compilerOptions: Record<string, unknown>
|
|
139
|
-
files?: string[]
|
|
140
|
-
} = {
|
|
141
|
-
extends: `./${basename(this.tsconfig)}`,
|
|
142
|
-
// A package's `exports` names `./x.abide` exactly, so the checker asks for its
|
|
143
|
-
// declaration as `./x.d.abide.ts` (`standIn`) — and takes the answer only with this.
|
|
144
|
-
compilerOptions: { allowArbitraryExtensions: true },
|
|
145
|
-
}
|
|
146
|
-
// An empty `files` is a config error, and the base's `include` alone is the app.
|
|
147
|
-
if (this.outside.size > 0) config.files = [...this.outside]
|
|
148
|
-
return JSON.stringify(config)
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Checked-module paths a stand-in answered for, without the `.abide` being lowered.
|
|
153
|
-
*
|
|
154
|
-
* 38.39 — A `.abide` THIS SESSION HAS NOT LOWERED IS STILL A MODULE ANOTHER CAN IMPORT. What its
|
|
155
|
-
* checked module exports does not depend on what the component holds (`COMPONENT_MODULE`), so an
|
|
156
|
-
* editor checking one page resolves the components it imports without lowering each of them.
|
|
157
|
-
* Remembered, so the lowering that replaces a stand-in is reported as a change to it.
|
|
158
|
-
*/
|
|
159
|
-
private stoodIn = new Set<string>()
|
|
160
|
-
|
|
161
|
-
/** The app's root, real. */
|
|
162
|
-
readonly root: string
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* `path` below the app's root, where it lies under it.
|
|
166
|
-
*
|
|
167
|
-
* 23.14 — what a page's route is read off: the directories below the root are it, and the
|
|
168
|
-
* machine's directories above the root are not.
|
|
169
|
-
*/
|
|
170
|
-
belowRoot(path: string): string {
|
|
171
|
-
return path.startsWith(`${this.root}/`)
|
|
172
|
-
? path.slice(this.root.length + 1)
|
|
173
|
-
: path
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* The shadow registered at `name`, or at the same file through its directory's real path.
|
|
178
|
-
*
|
|
179
|
-
* 34.25 — A LINKED PACKAGE IS ASKED FOR THROUGH ITS LINK: the checker resolves `kit/x.abide` to
|
|
180
|
-
* `node_modules/kit/x.d.abide.ts` and the re-export there to the shadow beside it, while the
|
|
181
|
-
* shadow is registered where bun resolved the file — the package's real directory.
|
|
182
|
-
*/
|
|
183
|
-
private shadowAt(name: string): { path: string; text: string } | undefined {
|
|
184
|
-
const found = this.shadows.get(this.keyOf(name))
|
|
185
|
-
// Only a shadow is registered through a link, so any other name is spared the realpath.
|
|
186
|
-
if (
|
|
187
|
-
found !== undefined ||
|
|
188
|
-
!name.includes('/node_modules/') ||
|
|
189
|
-
!(name.endsWith(SHADOW_SUFFIX) || name.endsWith(CHECKED_SUFFIX))
|
|
190
|
-
)
|
|
191
|
-
return found
|
|
192
|
-
let real: string
|
|
193
|
-
try {
|
|
194
|
-
real = realpathSync(dirname(name))
|
|
195
|
-
} catch {
|
|
196
|
-
return undefined
|
|
197
|
-
}
|
|
198
|
-
return this.shadows.get(this.keyOf(`${real}/${basename(name)}`))
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
private standIn(name: string): string | undefined {
|
|
202
|
-
// 34.21 — a file imported as text is its text, which the projection names apart.
|
|
203
|
-
if (name.endsWith(`${SOURCE_SUFFIX}.d.ts`))
|
|
204
|
-
return isFile(name.slice(0, -`${SOURCE_SUFFIX}.d.ts`.length))
|
|
205
|
-
? 'declare const text: string\nexport default text\n'
|
|
206
|
-
: undefined
|
|
207
|
-
// 34.17 — WHAT A `.abide` EXPORTS, to anything importing it: the component its checked
|
|
208
|
-
// module declares, and nothing else. The checker resolves `./x.abide` by trying
|
|
209
|
-
// `./x.abide.d.ts`; the bindings the projection re-exports for the build (16.70) are not
|
|
210
|
-
// the importer's to read.
|
|
211
|
-
//
|
|
212
|
-
// 34.25 — AND THROUGH A PACKAGE'S `exports`, which the checker follows to the exact file and
|
|
213
|
-
// so asks for the arbitrary extension's declaration, `./x.d.abide.ts`, instead.
|
|
214
|
-
const abide = name.endsWith(DECLARATION_SUFFIX)
|
|
215
|
-
? name.slice(0, -'.d.ts'.length)
|
|
216
|
-
: name.endsWith(EXPORTED_DECLARATION_SUFFIX)
|
|
217
|
-
? `${name.slice(0, -EXPORTED_DECLARATION_SUFFIX.length)}.abide`
|
|
218
|
-
: undefined
|
|
219
|
-
if (abide !== undefined) {
|
|
220
|
-
if (!isFile(abide)) return undefined
|
|
221
|
-
const own = `./${basename(abide)}`
|
|
222
|
-
return `export { default } from '${own}${CHECKED_SUFFIX.slice(0, -'.ts'.length)}'
|
|
223
|
-
`
|
|
224
|
-
}
|
|
225
|
-
// A projection not registered — a file with nothing to read, or one this session has not
|
|
226
|
-
// projected yet — exports nothing, so the declaration above always resolves.
|
|
227
|
-
if (name.endsWith(`.abide${SHADOW_SUFFIX}`))
|
|
228
|
-
return isFile(name.slice(0, -SHADOW_SUFFIX.length))
|
|
229
|
-
? 'export {}\n'
|
|
230
|
-
: undefined
|
|
231
|
-
if (!name.endsWith(`.abide${CHECKED_SUFFIX}`)) return undefined
|
|
232
|
-
if (!isFile(name.slice(0, -CHECKED_SUFFIX.length))) return undefined
|
|
233
|
-
this.stoodIn.add(this.keyOf(name))
|
|
234
|
-
return COMPONENT_MODULE
|
|
34
|
+
private constructor(host: ShadowHost) {
|
|
35
|
+
this.host = host
|
|
235
36
|
}
|
|
236
37
|
|
|
237
38
|
/**
|
|
@@ -239,1558 +40,113 @@ export class TypeSession {
|
|
|
239
40
|
* for a session asked about no app's routes.
|
|
240
41
|
*/
|
|
241
42
|
static async open(tsconfig: string, root = ''): Promise<TypeSession> {
|
|
242
|
-
|
|
243
|
-
cwd: dirname(tsconfig),
|
|
244
|
-
fs: {
|
|
245
|
-
readFile: (name) =>
|
|
246
|
-
name === session.opened
|
|
247
|
-
? session.sessionConfig()
|
|
248
|
-
: (session.shadowAt(name)?.text ??
|
|
249
|
-
session.standIn(name)),
|
|
250
|
-
fileExists: (name) =>
|
|
251
|
-
name === session.opened ||
|
|
252
|
-
session.shadowAt(name) !== undefined ||
|
|
253
|
-
session.standIn(name) !== undefined
|
|
254
|
-
? true
|
|
255
|
-
: undefined,
|
|
256
|
-
getAccessibleEntries: (directory) =>
|
|
257
|
-
session.entriesIn(directory),
|
|
258
|
-
},
|
|
259
|
-
})
|
|
260
|
-
// Whether the host folds case is the checker process's answer to `initialize`, and the API
|
|
261
|
-
// keeps it only inside the private `toPath` its own `getSourceFile` keys by — so that is
|
|
262
|
-
// what is read, rather than a second guess at the disk that could disagree with it.
|
|
263
|
-
const internals = api as unknown as {
|
|
264
|
-
ensureInitialized(): Promise<void>
|
|
265
|
-
toPath?: (path: string) => string
|
|
266
|
-
}
|
|
267
|
-
await internals.ensureInitialized()
|
|
268
|
-
const toPath = internals.toPath
|
|
269
|
-
if (typeof toPath !== 'function')
|
|
270
|
-
throw new Error(
|
|
271
|
-
'abide: the TypeScript API no longer keeps `toPath` where the type session reads it, so a path cannot be keyed as the checker keys it.',
|
|
272
|
-
)
|
|
273
|
-
const session = new TypeSession(api, tsconfig, toPath, root)
|
|
274
|
-
return session
|
|
43
|
+
return new TypeSession(await ShadowHost.open(tsconfig, root))
|
|
275
44
|
}
|
|
276
45
|
|
|
277
|
-
/**
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
*/
|
|
281
|
-
private latest: Project | undefined = undefined
|
|
282
|
-
private latestSnapshot: Snapshot | undefined = undefined
|
|
283
|
-
|
|
284
|
-
/**
|
|
285
|
-
* Snapshots a later one replaced, released once no call is in flight (`hold`).
|
|
286
|
-
*
|
|
287
|
-
* THE CHECKER PROCESS KEEPS EVERY SNAPSHOT UNTIL IT IS RELEASED — `updateSnapshot` releases
|
|
288
|
-
* nothing — and each holds a whole program and checker. Dropped unreleased, a kept session grew
|
|
289
|
-
* without bound: an `abide lsp` checker reached 59 GB in two hours of watcher-driven snapshots.
|
|
290
|
-
* Released the moment it is replaced, a question still being answered over it throws, so the
|
|
291
|
-
* release waits for the last call out.
|
|
292
|
-
*/
|
|
293
|
-
private retired: Snapshot[] = []
|
|
294
|
-
private inFlight = 0
|
|
295
|
-
|
|
296
|
-
/**
|
|
297
|
-
* Marks a call in flight until the returned value is disposed (`using`); the last one out
|
|
298
|
-
* releases what was retired meanwhile. A caller holding a `Project` across calls (`projectWith`)
|
|
299
|
-
* holds the session for as long.
|
|
300
|
-
*/
|
|
301
|
-
hold(): Disposable {
|
|
302
|
-
this.inFlight += 1
|
|
303
|
-
return {
|
|
304
|
-
[Symbol.dispose]: () => {
|
|
305
|
-
this.inFlight -= 1
|
|
306
|
-
if (this.inFlight === 0) this.releaseRetired()
|
|
307
|
-
},
|
|
308
|
-
}
|
|
46
|
+
/** The `tsconfig` the session was opened on, which a file outside its project is refused by. */
|
|
47
|
+
get tsconfig(): string {
|
|
48
|
+
return this.host.tsconfig
|
|
309
49
|
}
|
|
310
50
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
51
|
+
/** The app's root, real. */
|
|
52
|
+
get root(): string {
|
|
53
|
+
return this.host.root
|
|
314
54
|
}
|
|
315
55
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
const release = snapshot.dispose().catch(() => {})
|
|
320
|
-
this.releasing.add(release)
|
|
321
|
-
void release.finally(() => this.releasing.delete(release))
|
|
322
|
-
}
|
|
323
|
-
this.retired = []
|
|
56
|
+
/** How many snapshots this session has taken (`ShadowHost.snapshots`). */
|
|
57
|
+
get snapshots(): number {
|
|
58
|
+
return this.host.snapshots
|
|
324
59
|
}
|
|
325
60
|
|
|
326
|
-
/**
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
/**
|
|
330
|
-
* Paths edited or deleted on disk since the last snapshot, or `everything`.
|
|
331
|
-
*
|
|
332
|
-
* WHAT THE AUTHOR CHANGED ON DISK since the last snapshot, for a session kept across compiles
|
|
333
|
-
* (`abide dev`). The checker caches every file it has read and is told only about the shadows
|
|
334
|
-
* this class writes, so an edit to a module a page imports answered with the type it had when
|
|
335
|
-
* the session opened: a name made reactive in a `.ts` stayed static, and a handler exported
|
|
336
|
-
* since was not there. `everything` is the edit nobody could name, and drops the whole cache.
|
|
337
|
-
*/
|
|
338
|
-
private edits = { present: [] as string[], deleted: [] as string[] }
|
|
339
|
-
private everything = false
|
|
340
|
-
|
|
341
|
-
/**
|
|
342
|
-
* How many snapshots this session has taken.
|
|
343
|
-
*
|
|
344
|
-
* SNAPSHOTS TAKEN, and it is a counter rather than a timing because that is the only form of
|
|
345
|
-
* this claim that survives a loaded machine. One per build is the contract; one per FILE is
|
|
346
|
-
* the shape that made the checker 99.8% of compile time, and it reports the same answers.
|
|
347
|
-
*/
|
|
348
|
-
snapshots = 0
|
|
349
|
-
|
|
350
|
-
/**
|
|
351
|
-
* A directory's listing as the checker sees it, with this session's shadows added.
|
|
352
|
-
*
|
|
353
|
-
* The shadow has to appear in its directory's listing or it is not a root of the project and
|
|
354
|
-
* the checker never reads it.
|
|
355
|
-
*/
|
|
356
|
-
private entriesIn(
|
|
357
|
-
directory: string,
|
|
358
|
-
): { files: string[]; directories: string[] } | undefined {
|
|
359
|
-
// TWO PATHS TO ONE DIRECTORY ARE ONE DIRECTORY. A raw string compare said otherwise on a
|
|
360
|
-
// doubled separator, a trailing slash or a relative path, and the shadow then appeared in
|
|
361
|
-
// nobody's listing — so the checker never read it and the refusal below blamed the app's
|
|
362
|
-
// tsconfig for a path this class had mangled.
|
|
363
|
-
const here = this.keyOf(directory)
|
|
364
|
-
this.listed.add(here)
|
|
365
|
-
const mine: string[] = []
|
|
366
|
-
// The key is the checker's where it is registered, so only the incoming directory needs
|
|
367
|
-
// keying — one `toPath` a call rather than one a shadow.
|
|
368
|
-
for (const [key, shadow] of this.shadows)
|
|
369
|
-
if (dirname(key) === here) mine.push(basename(shadow.path))
|
|
370
|
-
if (mine.length === 0) return undefined
|
|
371
|
-
|
|
372
|
-
const files: string[] = []
|
|
373
|
-
const directories: string[] = []
|
|
374
|
-
// A DIRECTORY THAT IS NOT THERE IS A LISTING OF THE SHADOWS ALONE, not a throw. The
|
|
375
|
-
// checker calls this over the api and a throw crosses back as a remote error, which
|
|
376
|
-
// PANICS the checker process — so a directory deleted between the glob and the ask took
|
|
377
|
-
// the whole command down with it. `abide dev` is where that happens as a matter of
|
|
378
|
-
// course: a generator that rewrites a tree of pages deletes every one of them first, and
|
|
379
|
-
// the watcher fires while it is doing it.
|
|
380
|
-
let entries: Dirent[]
|
|
381
|
-
try {
|
|
382
|
-
entries = readdirSync(directory, { withFileTypes: true })
|
|
383
|
-
} catch (thrown) {
|
|
384
|
-
if ((thrown as { code?: unknown }).code !== 'ENOENT') throw thrown
|
|
385
|
-
return { files: mine, directories: [] }
|
|
386
|
-
}
|
|
387
|
-
// THE LISTING ALREADY KNOWS. A `statSync` per entry was N syscalls for an answer
|
|
388
|
-
// `readdirSync` hands back with the name, and it THREW on a dangling symlink — an Emacs
|
|
389
|
-
// `.#page.abide` lock beside a page took the whole build down with it.
|
|
390
|
-
for (const entry of entries) {
|
|
391
|
-
// A symlink is the one entry the listing cannot answer for, and a dangling one has no
|
|
392
|
-
// answer at all — which is a file as far as this is concerned.
|
|
393
|
-
if (entry.isSymbolicLink()) {
|
|
394
|
-
let followed = false
|
|
395
|
-
try {
|
|
396
|
-
followed = statSync(
|
|
397
|
-
join(directory, entry.name),
|
|
398
|
-
).isDirectory()
|
|
399
|
-
} catch {
|
|
400
|
-
// Dangling: a file, as `followed` already says.
|
|
401
|
-
}
|
|
402
|
-
if (followed) directories.push(entry.name)
|
|
403
|
-
else files.push(entry.name)
|
|
404
|
-
continue
|
|
405
|
-
}
|
|
406
|
-
if (entry.isDirectory()) directories.push(entry.name)
|
|
407
|
-
else files.push(entry.name)
|
|
408
|
-
}
|
|
409
|
-
for (const name of mine) if (!files.includes(name)) files.push(name)
|
|
410
|
-
return { files, directories }
|
|
61
|
+
/** `path` below the app's root, where it lies under it (23.14). */
|
|
62
|
+
belowRoot(path: string): string {
|
|
63
|
+
return this.host.belowRoot(path)
|
|
411
64
|
}
|
|
412
65
|
|
|
413
|
-
/**
|
|
414
|
-
* Records the paths a watcher saw move, for the next snapshot to invalidate.
|
|
415
|
-
*
|
|
416
|
-
* THE PATHS A WATCHER SAW MOVE, or `undefined` where it saw a move and could not say where.
|
|
417
|
-
* A file that is there is reported CREATED AND CHANGED: `changed` alone leaves a new file out
|
|
418
|
-
* of the project's root set (a `.d.ts` nothing imports is never read), and `created` alone
|
|
419
|
-
* leaves an edited file's cached text in place. A directory is dropped wholesale, because the
|
|
420
|
-
* checker is not told what was under it.
|
|
421
|
-
*/
|
|
66
|
+
/** Records the paths a watcher saw move, for the next snapshot to invalidate. */
|
|
422
67
|
edited(paths: string[] | undefined): void {
|
|
423
|
-
|
|
424
|
-
this.everything = true
|
|
425
|
-
return
|
|
426
|
-
}
|
|
427
|
-
for (const path of paths) {
|
|
428
|
-
let directory: boolean
|
|
429
|
-
try {
|
|
430
|
-
directory = statSync(path).isDirectory()
|
|
431
|
-
} catch {
|
|
432
|
-
this.edits.deleted.push(path)
|
|
433
|
-
// A `.abide` that is gone takes its shadows with it, or the checker keeps a page
|
|
434
|
-
// nobody has as a root of the project for the rest of the session — and a page
|
|
435
|
-
// importing it goes on reading the props its checked module declared.
|
|
436
|
-
const gone = this.keyOf(path)
|
|
437
|
-
for (const [key, shadow] of this.shadows)
|
|
438
|
-
if (
|
|
439
|
-
ShadowMap.isShadowOf(key, gone) ||
|
|
440
|
-
key.startsWith(`${gone}/`)
|
|
441
|
-
) {
|
|
442
|
-
this.shadows.delete(key)
|
|
443
|
-
this.edits.deleted.push(shadow.path)
|
|
444
|
-
}
|
|
445
|
-
continue
|
|
446
|
-
}
|
|
447
|
-
// A FILE IN A DIRECTORY THE CHECKER HAS NEVER LISTED is a directory move the watcher
|
|
448
|
-
// reported as the file alone — a save that creates `late/` and the page in it
|
|
449
|
-
// at once — and the listing it already holds of the parent does not have it.
|
|
450
|
-
if (directory || !this.listed.has(this.keyOf(dirname(path))))
|
|
451
|
-
this.everything = true
|
|
452
|
-
else this.edits.present.push(path)
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
/** Every directory the checker has asked this session to list (`entriesIn`). */
|
|
457
|
-
private listed = new Set<string>()
|
|
458
|
-
|
|
459
|
-
/**
|
|
460
|
-
* ONE SNAPSHOT TAKES EVERY EDIT WITH IT: the shadows written for it and whatever moved on disk.
|
|
461
|
-
*/
|
|
462
|
-
private async snapshot(
|
|
463
|
-
created: string[],
|
|
464
|
-
changed: string[],
|
|
465
|
-
): Promise<Project | undefined> {
|
|
466
|
-
const edits = this.edits
|
|
467
|
-
const everything = this.everything
|
|
468
|
-
this.edits = { present: [], deleted: [] }
|
|
469
|
-
this.everything = false
|
|
470
|
-
this.snapshots += 1
|
|
471
|
-
// AN EDIT NOBODY COULD NAME DROPS THE CACHE, and the files created with it are still named
|
|
472
|
-
// after it: a dropped cache re-reads what the project already listed, and a shadow written
|
|
473
|
-
// for a page in a directory it never listed is not among them.
|
|
474
|
-
if (everything)
|
|
475
|
-
this.retire(
|
|
476
|
-
await this.api.updateSnapshot({
|
|
477
|
-
openProjects: [this.opened],
|
|
478
|
-
fileChanges: { invalidateAll: true },
|
|
479
|
-
}),
|
|
480
|
-
)
|
|
481
|
-
const snapshot = await this.api.updateSnapshot({
|
|
482
|
-
openProjects: [this.opened],
|
|
483
|
-
fileChanges: {
|
|
484
|
-
created: [...created, ...edits.present],
|
|
485
|
-
changed: [...changed, ...edits.present],
|
|
486
|
-
deleted: edits.deleted,
|
|
487
|
-
},
|
|
488
|
-
})
|
|
489
|
-
if (this.latestSnapshot !== undefined) this.retire(this.latestSnapshot)
|
|
490
|
-
this.latestSnapshot = snapshot
|
|
491
|
-
this.latest = snapshot.getProjects()[0]
|
|
492
|
-
return this.latest
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
/** Whether the last snapshot still answers: there is one, and nothing has moved since. */
|
|
496
|
-
private get current(): boolean {
|
|
497
|
-
return (
|
|
498
|
-
this.latest !== undefined &&
|
|
499
|
-
!this.everything &&
|
|
500
|
-
this.edits.present.length === 0 &&
|
|
501
|
-
this.edits.deleted.length === 0
|
|
502
|
-
)
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
private async currentProject(
|
|
506
|
-
created: string[] = [],
|
|
507
|
-
changed: string[] = [],
|
|
508
|
-
): Promise<Project | undefined> {
|
|
509
|
-
return created.length === 0 && changed.length === 0 && this.current
|
|
510
|
-
? this.latest
|
|
511
|
-
: await this.snapshot(created, changed)
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
/**
|
|
515
|
-
* 34.25, 23.19 — where `source` is a dependency's file, makes `path` — it, or what it lowers to
|
|
516
|
-
* — a root of the opened config, telling `changed` the config moved where it was not one
|
|
517
|
-
* already. The app's own is a root already: telling the config it moved reloads the project.
|
|
518
|
-
*/
|
|
519
|
-
private openOutside(source: string, path: string, changed: string[]): void {
|
|
520
|
-
if (placeOf(this.belowRoot(source)) !== 'dependency') return
|
|
521
|
-
if (this.outside.has(path)) return
|
|
522
|
-
this.outside.add(path)
|
|
523
|
-
if (!changed.includes(this.opened)) changed.push(this.opened)
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
/**
|
|
527
|
-
* The project, with each of `modules` outside the app's own a root of it.
|
|
528
|
-
*
|
|
529
|
-
* THE SNAPSHOT IS THE SESSION'S, AND A QUESTION IS ANY MODULE'S: what a handler declares
|
|
530
|
-
* (`declaredExports`) is asked of the same snapshot the reads were, taken only where something
|
|
531
|
-
* moved since.
|
|
532
|
-
*
|
|
533
|
-
* 23.19 — a handler file a mount places is a package's, which the app's `include` does not reach:
|
|
534
|
-
* what it exports is asked all the same (16.73), as a mounted `.abide`'s shadow is.
|
|
535
|
-
*/
|
|
536
|
-
async projectWith(
|
|
537
|
-
modules: readonly string[],
|
|
538
|
-
): Promise<Project | undefined> {
|
|
539
|
-
using _held = this.hold()
|
|
540
|
-
const changed: string[] = []
|
|
541
|
-
for (const path of modules) {
|
|
542
|
-
const canonical = this.canonical(path)
|
|
543
|
-
this.openOutside(canonical, canonical, changed)
|
|
544
|
-
}
|
|
545
|
-
const project = await this.currentProject([], changed)
|
|
546
|
-
if (project === undefined) return project
|
|
547
|
-
// A KEPT SESSION HEARS OF A FILE ONLY AS A WATCHER REPORTS IT, and one saved into a
|
|
548
|
-
// directory made since the last snapshot is reported as the directory: it is no root yet,
|
|
549
|
-
// and is told to the next snapshot as created.
|
|
550
|
-
const found = await Promise.all(
|
|
551
|
-
modules.map((path) => project.program.getSourceFile(path)),
|
|
552
|
-
)
|
|
553
|
-
const missing: string[] = []
|
|
554
|
-
for (let at = 0; at < modules.length; at += 1)
|
|
555
|
-
if (found[at] === undefined) missing.push(modules[at] as string)
|
|
556
|
-
return missing.length === 0
|
|
557
|
-
? project
|
|
558
|
-
: await this.currentProject(missing, [...missing])
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
/**
|
|
562
|
-
* THE APP'S OWN FILES the checker holds as roots: under `within`, and neither a dependency nor a
|
|
563
|
-
* declaration file. Resolved against the tsconfig's directory; a caller asking `shadows` keys it.
|
|
564
|
-
*/
|
|
565
|
-
private rootsUnder(project: Project, within: string): string[] {
|
|
566
|
-
const root = `${this.canonical(within)}/`
|
|
567
|
-
const roots: string[] = []
|
|
568
|
-
for (const path of project.rootFiles) {
|
|
569
|
-
const canonical = this.canonical(path)
|
|
570
|
-
if (
|
|
571
|
-
canonical.startsWith(root) &&
|
|
572
|
-
!path.includes('/node_modules/') &&
|
|
573
|
-
!path.endsWith('.d.ts')
|
|
574
|
-
)
|
|
575
|
-
roots.push(canonical)
|
|
576
|
-
}
|
|
577
|
-
return roots
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
/**
|
|
581
|
-
* A relative path from the checker is relative to the API's own cwd, which is the tsconfig's
|
|
582
|
-
* directory rather than the process's.
|
|
583
|
-
*/
|
|
584
|
-
private canonical(path: string): string {
|
|
585
|
-
return resolve(dirname(this.tsconfig), path)
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
/**
|
|
589
|
-
* Where the checker holds a `.abide`'s projection, once `readsFor` has registered it.
|
|
590
|
-
*
|
|
591
|
-
* 16.70 — what a `<script server>` binds is read off it as a `.ts` module's exports are.
|
|
592
|
-
*/
|
|
593
|
-
projectionOf(abidePath: string): string {
|
|
594
|
-
return `${this.canonical(abidePath)}${SHADOW_SUFFIX}`
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
/**
|
|
598
|
-
* A file the checker reads out of `shadows`, reported to the next snapshot only when it is new,
|
|
599
|
-
* its text moved, or a stand-in answered for it until now.
|
|
600
|
-
*/
|
|
601
|
-
private register(
|
|
602
|
-
path: string,
|
|
603
|
-
text: string,
|
|
604
|
-
created: string[],
|
|
605
|
-
changed: string[],
|
|
606
|
-
): void {
|
|
607
|
-
const key = this.keyOf(path)
|
|
608
|
-
const previous = this.shadows.get(key)
|
|
609
|
-
const stoodIn = this.stoodIn.delete(key)
|
|
610
|
-
if (previous === undefined) created.push(path)
|
|
611
|
-
if (stoodIn || (previous !== undefined && previous.text !== text))
|
|
612
|
-
changed.push(path)
|
|
613
|
-
this.shadows.set(key, { path, text })
|
|
68
|
+
this.host.edited(paths)
|
|
614
69
|
}
|
|
615
70
|
|
|
616
71
|
/**
|
|
617
72
|
* The classified reactive reads in one `.abide` file.
|
|
618
73
|
*
|
|
619
|
-
*
|
|
620
|
-
* its projection (`projectShadow`). Everything else is left exactly as written. ONE FILE; a
|
|
621
|
-
* build wants `readsFor`.
|
|
74
|
+
* ONE FILE; a build wants `readsFor`.
|
|
622
75
|
*/
|
|
623
76
|
async reads(abidePath: string, file: AbideFile): Promise<CheckerAnswer> {
|
|
624
77
|
const answered = await this.readsFor([{ path: abidePath, file }])
|
|
625
78
|
return answered.answers[0] ?? UNCHECKED
|
|
626
79
|
}
|
|
627
80
|
|
|
628
|
-
/**
|
|
629
|
-
|
|
630
|
-
*
|
|
631
|
-
* EVERY FILE IN ONE SNAPSHOT, which is the whole of the compiler's build time. Measured at
|
|
632
|
-
* 19.7 ms a file taking a snapshot each against 0.8 ms a file sharing one — 25x, because
|
|
633
|
-
* adding a file to the project's root set is what costs, not asking about it. What each file's
|
|
634
|
-
* shadow is asked is `readsIn`'s.
|
|
635
|
-
*
|
|
636
|
-
* WHAT IT HAS TO SAY THAT IS NOT A TYPE ERROR comes back beside the reads: the compiler's own
|
|
637
|
-
* warnings (14.14 to 14.17, 32.14), each on the `.abide` line where the source's text
|
|
638
|
-
* was handed in.
|
|
639
|
-
*/
|
|
640
|
-
async readsFor(
|
|
641
|
-
sources: readonly (Pick<ParsedSource, 'path' | 'file'> & {
|
|
642
|
-
text?: string
|
|
643
|
-
})[],
|
|
644
|
-
): Promise<{ answers: CheckerAnswer[]; warnings: Diagnosis[] }> {
|
|
645
|
-
using _held = this.hold()
|
|
646
|
-
const answered = await this.classify(sources)
|
|
647
|
-
for (let at = 0; at < sources.length; at += 1) {
|
|
648
|
-
const source = sources[at] as (typeof sources)[number]
|
|
649
|
-
if (source.text !== undefined)
|
|
650
|
-
answered.warnings.push(
|
|
651
|
-
...templateWarnings(
|
|
652
|
-
source.path,
|
|
653
|
-
source.text,
|
|
654
|
-
source.file,
|
|
655
|
-
answered.answers[at] ?? UNCHECKED,
|
|
656
|
-
),
|
|
657
|
-
)
|
|
658
|
-
}
|
|
659
|
-
answered.warnings.sort(byPlace)
|
|
660
|
-
return answered
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
private async classify(
|
|
81
|
+
/** Reads and compiler warnings for every file, in one checker snapshot (`reactiveReads`). */
|
|
82
|
+
readsFor(
|
|
664
83
|
sources: readonly (Pick<ParsedSource, 'path' | 'file'> & {
|
|
665
84
|
text?: string
|
|
666
85
|
})[],
|
|
667
86
|
): Promise<{ answers: CheckerAnswer[]; warnings: Diagnosis[] }> {
|
|
668
|
-
|
|
669
|
-
// Per source, its shadow and what is asked of it, or `undefined` where nothing is declared.
|
|
670
|
-
const queries: ({ map: ShadowMap; spans: ShadowSpans } | undefined)[] =
|
|
671
|
-
[]
|
|
672
|
-
const created: string[] = []
|
|
673
|
-
const changed: string[] = []
|
|
674
|
-
|
|
675
|
-
// EVERY SOURCE PROJECTS BEFORE ANY SHADOW IS REGISTERED. A projection refuses (a top-level
|
|
676
|
-
// `await`), and a shadow registered ahead of the refusal was in `shadows` with the lists
|
|
677
|
-
// that would have reported it dropped — so the retry found nothing moved, and the checker
|
|
678
|
-
// answered from the old shadow at the new one's offsets, or never had the file at all.
|
|
679
|
-
const projected = sources.map((source) => ({
|
|
680
|
-
source,
|
|
681
|
-
projection: projectShadow(source.file),
|
|
682
|
-
}))
|
|
683
|
-
for (const { source, projection } of projected) {
|
|
684
|
-
// 34.2 — A `<script module>` DECLARES NAMES TOO, and the `<script>` under it is inside
|
|
685
|
-
// that scope on both sides. The projection holds both from offset 0.
|
|
686
|
-
const scripts = projection.text.slice(0, projection.scripts.end)
|
|
687
|
-
// A TEMPLATE POSITION IS A QUESTION TOO (14.2): a file that declares nothing but
|
|
688
|
-
// writes `{format(total)}` has a position only the checker can prove static. And a
|
|
689
|
-
// `<script server>` always is (16.70): what it binds is read off this shadow, re-exported
|
|
690
|
-
// under `PRIVATE_EXPORT`.
|
|
691
|
-
if (
|
|
692
|
-
!DECLARES.test(scripts) &&
|
|
693
|
-
projection.slots.size === 0 &&
|
|
694
|
-
source.file.servers.length === 0
|
|
695
|
-
) {
|
|
696
|
-
queries.push(undefined)
|
|
697
|
-
continue
|
|
698
|
-
}
|
|
699
|
-
// CANONICAL AT THE KEY, not at the comparison. A doubled separator or a relative
|
|
700
|
-
// path is the same file and a raw string is not the same string — registered as it
|
|
701
|
-
// arrived, the shadow appeared in nobody's directory listing AND named a file the
|
|
702
|
-
// checker could not open, and the refusal below blamed the app's tsconfig for it.
|
|
703
|
-
const map = new ShadowMap(
|
|
704
|
-
this.canonical(source.path),
|
|
705
|
-
projection.origins,
|
|
706
|
-
)
|
|
707
|
-
// A shadow STAYS registered for the life of the session and the snapshot is told it
|
|
708
|
-
// changed. Deleting and re-adding under the same path does not invalidate the
|
|
709
|
-
// source-file cache, and the checker then answers about the PREVIOUS file at these
|
|
710
|
-
// offsets — right-looking types for the wrong names, with nothing to say so.
|
|
711
|
-
// AN UNCHANGED SHADOW IS NOT REPORTED, which is what a kept session saves on a save:
|
|
712
|
-
// the one page that moved is re-read rather than all of them.
|
|
713
|
-
this.register(map.shadowPath, projection.text, created, changed)
|
|
714
|
-
this.openOutside(source.path, map.shadowPath, changed)
|
|
715
|
-
queries.push({
|
|
716
|
-
map,
|
|
717
|
-
spans: {
|
|
718
|
-
scriptsEnd: projection.scripts.end,
|
|
719
|
-
modules: projection.modules,
|
|
720
|
-
servers: projection.servers,
|
|
721
|
-
scripts: projection.scripts,
|
|
722
|
-
positions: projection.positions,
|
|
723
|
-
items: new Set(projection.items.values()),
|
|
724
|
-
},
|
|
725
|
-
})
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
const answers = sources.map(() => UNCHECKED)
|
|
729
|
-
if (queries.every((query) => query === undefined))
|
|
730
|
-
return { answers, warnings }
|
|
731
|
-
|
|
732
|
-
let project = await this.currentProject(created, changed)
|
|
733
|
-
if (project === undefined) return { answers, warnings }
|
|
734
|
-
// Per source, its compiler warnings, the bindings the projection could not type
|
|
735
|
-
// (`bindingsOfReads`), and those the lowered program proved a `Reactive`.
|
|
736
|
-
const warned: Diagnosis[][] = sources.map(() => [])
|
|
737
|
-
const suspects: number[][] = sources.map(() => [])
|
|
738
|
-
const held: Set<number>[] = sources.map(() => new Set())
|
|
739
|
-
const answer = async (
|
|
740
|
-
asked: Project,
|
|
741
|
-
abide: Abide,
|
|
742
|
-
at: number,
|
|
743
|
-
): Promise<void> => {
|
|
744
|
-
const query = queries[at]
|
|
745
|
-
if (query === undefined) return
|
|
746
|
-
const source = sources[at] as (typeof sources)[number]
|
|
747
|
-
const sourceFile = await asked.program.getSourceFile(
|
|
748
|
-
query.map.shadowPath,
|
|
749
|
-
)
|
|
750
|
-
// The shadow has to be INSIDE the opened project or the checker never reads it.
|
|
751
|
-
// Refusing loudly rather than answering nothing: no reads is a file whose every
|
|
752
|
-
// name lowers as written, which renders `[object Object]` where a value was due —
|
|
753
|
-
// a wrong page, with nothing about it saying so.
|
|
754
|
-
if (sourceFile === undefined)
|
|
755
|
-
throw new Error(
|
|
756
|
-
`abide: ${source.path} is not inside the project ${this.tsconfig}, so no name in it can be typed (RULEBOOK 31.3). Give the app a tsconfig whose include reaches it.`,
|
|
757
|
-
)
|
|
758
|
-
const read = await readsIn(
|
|
759
|
-
asked.checker,
|
|
760
|
-
abide,
|
|
761
|
-
sourceFile,
|
|
762
|
-
query.map.shadowPath,
|
|
763
|
-
query.spans,
|
|
764
|
-
source.file,
|
|
765
|
-
held[at] as Set<number>,
|
|
766
|
-
)
|
|
767
|
-
answers[at] = read.answer
|
|
768
|
-
suspects[at] = read.suspects
|
|
769
|
-
warned[at] = placedFindings(
|
|
770
|
-
source.path,
|
|
771
|
-
source.text,
|
|
772
|
-
read.warnings,
|
|
773
|
-
(offset) => query.map.toSource(query.map.shadowPath, offset),
|
|
774
|
-
'warning',
|
|
775
|
-
true,
|
|
776
|
-
)
|
|
777
|
-
}
|
|
778
|
-
const every = queries.map((_, at) => at)
|
|
779
|
-
const first = project
|
|
780
|
-
const firstAbide = await abideIn(first)
|
|
781
|
-
await Promise.all(every.map((at) => answer(first, firstAbide, at)))
|
|
782
|
-
// 31.1 — A BINDING THE PROJECTION TYPED FROM A `Reactive` IS ASKED OF THE PROGRAM IT RUNS
|
|
783
|
-
// AS: each file holding one is lowered with what it has answered so far, and a binding that
|
|
784
|
-
// is a `Reactive` there is one in the projection too. A binding typed from one that just
|
|
785
|
-
// turned out to be a `Reactive` is asked again, until a round turns none. Nothing is asked,
|
|
786
|
-
// and no second snapshot taken, where no file holds one (`bindingsOfReads`).
|
|
787
|
-
for (;;) {
|
|
788
|
-
const asking = every.filter(
|
|
789
|
-
(at) => (suspects[at] as number[]).length > 0,
|
|
790
|
-
)
|
|
791
|
-
if (asking.length === 0) break
|
|
792
|
-
const loweredCreated: string[] = []
|
|
793
|
-
const loweredChanged: string[] = []
|
|
794
|
-
const lowered = asking.map((at) => {
|
|
795
|
-
const source = sources[at] as (typeof sources)[number]
|
|
796
|
-
const query = queries[at] as { map: ShadowMap }
|
|
797
|
-
const shadow = checkedShadow(
|
|
798
|
-
source.file,
|
|
799
|
-
answers[at] as CheckerAnswer,
|
|
800
|
-
)
|
|
801
|
-
this.register(
|
|
802
|
-
query.map.checkedPath,
|
|
803
|
-
shadow.text,
|
|
804
|
-
loweredCreated,
|
|
805
|
-
loweredChanged,
|
|
806
|
-
)
|
|
807
|
-
this.openOutside(
|
|
808
|
-
source.path,
|
|
809
|
-
query.map.checkedPath,
|
|
810
|
-
loweredChanged,
|
|
811
|
-
)
|
|
812
|
-
return {
|
|
813
|
-
at,
|
|
814
|
-
path: query.map.checkedPath,
|
|
815
|
-
origins: shadow.origins,
|
|
816
|
-
}
|
|
817
|
-
})
|
|
818
|
-
project = await this.currentProject(loweredCreated, loweredChanged)
|
|
819
|
-
if (project === undefined) return { answers, warnings }
|
|
820
|
-
const asked = project
|
|
821
|
-
const abide = await abideIn(asked)
|
|
822
|
-
const turned = new Set<number>()
|
|
823
|
-
await Promise.all(
|
|
824
|
-
lowered.map(async ({ at, path, origins }) => {
|
|
825
|
-
const sourceFile = await asked.program.getSourceFile(path)
|
|
826
|
-
if (sourceFile === undefined) return
|
|
827
|
-
for (const start of suspects[at] as number[]) {
|
|
828
|
-
const position = mapForward(origins, start)
|
|
829
|
-
if (position === undefined) continue
|
|
830
|
-
const symbol = await asked.checker.getSymbolAtLocation(
|
|
831
|
-
getTouchingPropertyName(sourceFile, position),
|
|
832
|
-
)
|
|
833
|
-
const type =
|
|
834
|
-
symbol === undefined
|
|
835
|
-
? undefined
|
|
836
|
-
: await asked.checker.getTypeOfSymbol(symbol)
|
|
837
|
-
if (
|
|
838
|
-
type !== undefined &&
|
|
839
|
-
(await isReactiveType(
|
|
840
|
-
asked.checker,
|
|
841
|
-
abide,
|
|
842
|
-
type,
|
|
843
|
-
false,
|
|
844
|
-
))
|
|
845
|
-
) {
|
|
846
|
-
;(held[at] as Set<number>).add(start)
|
|
847
|
-
turned.add(at)
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
}),
|
|
851
|
-
)
|
|
852
|
-
if (turned.size === 0) break
|
|
853
|
-
await Promise.all([...turned].map((at) => answer(asked, abide, at)))
|
|
854
|
-
for (const at of asking) if (!turned.has(at)) suspects[at] = []
|
|
855
|
-
}
|
|
856
|
-
for (const placed of warned) warnings.push(...placed)
|
|
857
|
-
return { answers, warnings }
|
|
87
|
+
return reactiveReads(this.host, sources)
|
|
858
88
|
}
|
|
859
89
|
|
|
860
|
-
/**
|
|
861
|
-
|
|
862
|
-
*
|
|
863
|
-
* 11.37 — `ambient`: every read of a request's ambient inside a `global` value's body, as errors.
|
|
864
|
-
* 11.30 — `unkeyed`: every `args` defaulting a member its body's parameter does not, as warnings.
|
|
865
|
-
* Both over every `.ts` under `within` and each of `sources` as the shadow `readsFor` registered
|
|
866
|
-
* for it, because a keyed value is declared in a `.ts` as often as in a `.abide` — one walk of
|
|
867
|
-
* the files for the two, each finding on its own line of the file an author wrote. `scripts`
|
|
868
|
-
* as `diagnose` reads it: an editor asking about one page does not walk the app's `.ts`.
|
|
869
|
-
*/
|
|
870
|
-
async lints(
|
|
90
|
+
/** What the build refuses and warns on in the app's `.ts` and `.abide` files (`appLints`). */
|
|
91
|
+
lints(
|
|
871
92
|
within: string,
|
|
872
93
|
sources: readonly Omit<ParsedSource, 'quiet'>[],
|
|
873
94
|
scripts = true,
|
|
874
95
|
): Promise<{ ambient: Diagnosis[]; unkeyed: Diagnosis[] }> {
|
|
875
|
-
|
|
876
|
-
const ambient: Diagnosis[] = []
|
|
877
|
-
const unkeyed: Diagnosis[] = []
|
|
878
|
-
const project = await this.currentProject()
|
|
879
|
-
if (project === undefined) return { ambient, unkeyed }
|
|
880
|
-
const checker = project.checker
|
|
881
|
-
const ambientPaths = new Map<string, string>()
|
|
882
|
-
const declared = await Promise.all(
|
|
883
|
-
AMBIENT_SOURCES.map((source) =>
|
|
884
|
-
project.program.getSourceFile(source),
|
|
885
|
-
),
|
|
886
|
-
)
|
|
887
|
-
for (let at = 0; at < AMBIENT_SOURCES.length; at += 1) {
|
|
888
|
-
const found = declared[at]?.path
|
|
889
|
-
if (found !== undefined)
|
|
890
|
-
ambientPaths.set(AMBIENT_SOURCES[at] as string, found)
|
|
891
|
-
}
|
|
892
|
-
const authored = new Map<
|
|
893
|
-
string,
|
|
894
|
-
{ source: (typeof sources)[number]; map: ShadowMap }
|
|
895
|
-
>()
|
|
896
|
-
for (const source of sources) {
|
|
897
|
-
const map = new ShadowMap(
|
|
898
|
-
this.canonical(source.path),
|
|
899
|
-
projectShadow(source.file).origins,
|
|
900
|
-
)
|
|
901
|
-
authored.set(this.keyOf(map.shadowPath), { source, map })
|
|
902
|
-
}
|
|
903
|
-
// A file of this session's own is one of `sources`' shadows or not walked at all: a
|
|
904
|
-
// checked module is the lowering's, and placed nowhere an author wrote.
|
|
905
|
-
const files: string[] = []
|
|
906
|
-
if (scripts)
|
|
907
|
-
for (const path of this.rootsUnder(project, within)) {
|
|
908
|
-
const key = this.keyOf(path)
|
|
909
|
-
if (authored.has(key) || !this.shadows.has(key))
|
|
910
|
-
files.push(path)
|
|
911
|
-
}
|
|
912
|
-
else for (const { map } of authored.values()) files.push(map.shadowPath)
|
|
913
|
-
await Promise.all(
|
|
914
|
-
files.map(async (path) => {
|
|
915
|
-
const sourceFile = await project.program.getSourceFile(path)
|
|
916
|
-
if (sourceFile === undefined) return
|
|
917
|
-
const [reads, defaults] = await Promise.all([
|
|
918
|
-
ambientPaths.size === 0
|
|
919
|
-
? []
|
|
920
|
-
: ambientReadsIn(checker, sourceFile, ambientPaths),
|
|
921
|
-
argsDefaultsIn(checker, sourceFile),
|
|
922
|
-
])
|
|
923
|
-
if (reads.length === 0 && defaults.length === 0) return
|
|
924
|
-
const shadow = authored.get(this.keyOf(path))
|
|
925
|
-
const source = shadow?.source
|
|
926
|
-
const text =
|
|
927
|
-
source === undefined
|
|
928
|
-
? await Bun.file(path).text()
|
|
929
|
-
: source.text
|
|
930
|
-
const place = (at: number): number =>
|
|
931
|
-
shadow === undefined ? at : shadow.map.toSource(path, at)
|
|
932
|
-
const named = source?.path ?? path
|
|
933
|
-
const ambientReads: Finding[] = []
|
|
934
|
-
for (const read of reads)
|
|
935
|
-
ambientReads.push({
|
|
936
|
-
at: read.at,
|
|
937
|
-
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.`,
|
|
938
|
-
})
|
|
939
|
-
ambient.push(
|
|
940
|
-
...placedFindings(
|
|
941
|
-
named,
|
|
942
|
-
text,
|
|
943
|
-
ambientReads,
|
|
944
|
-
place,
|
|
945
|
-
'error',
|
|
946
|
-
),
|
|
947
|
-
)
|
|
948
|
-
unkeyed.push(
|
|
949
|
-
...placedFindings(named, text, defaults, place, 'warning'),
|
|
950
|
-
)
|
|
951
|
-
}),
|
|
952
|
-
)
|
|
953
|
-
ambient.sort(byPlace)
|
|
954
|
-
unkeyed.sort(byPlace)
|
|
955
|
-
return { ambient, unkeyed }
|
|
96
|
+
return appLints(this.host, within, sources, scripts)
|
|
956
97
|
}
|
|
957
98
|
|
|
958
99
|
/**
|
|
959
|
-
* The checker's diagnostics for each `.abide` source and the app's `.ts` roots
|
|
960
|
-
*
|
|
961
|
-
* 38.3 — WHAT THE CHECKER SAYS ABOUT EACH FILE AS IT RUNS, placed on the `.abide` line. Each
|
|
962
|
-
* source is checked as its lowered program (`checkedShadow`), which is registered for the life
|
|
963
|
-
* of the session beside its shadow for the reason a shadow is; and every `.ts` root of the
|
|
964
|
-
* project under the app's root is checked as it stands, where `scripts` asks for them — an editor
|
|
965
|
-
* asking about one page does not want the whole app. `sources` carry their text, which is what
|
|
966
|
-
* a position is a line of. A `quiet` source is lowered and not reported on: a component an
|
|
967
|
-
* editor's page imports, which is there for the props the page is held to (33.4).
|
|
968
|
-
*
|
|
969
|
-
* `app` is the app the sources are checked in (`appLayout`): its route table, which `url`'s
|
|
970
|
-
* literal is held to (30.26), and its handler files, which type `Config` (27.13). A source placed
|
|
971
|
-
* as a page or a layout carries its `kind` and `route`, which its `route.params` is typed by (23.9).
|
|
972
|
-
*
|
|
973
|
-
* `refused` is the path of every source whose checked lowering refused, which `diagnoses` has
|
|
974
|
-
* reported at its line — `abide check` emits the rest of the app without them.
|
|
100
|
+
* The checker's diagnostics for each `.abide` source and the app's `.ts` roots, and each
|
|
101
|
+
* source's lowering (`diagnoseSources`).
|
|
975
102
|
*/
|
|
976
|
-
|
|
977
|
-
sources:
|
|
978
|
-
Partial<Pick<PlacedSource, 'kind' | 'route'>> & {
|
|
979
|
-
quiet?: boolean
|
|
980
|
-
})[],
|
|
103
|
+
diagnose(
|
|
104
|
+
sources: readonly DiagnosedSource[],
|
|
981
105
|
app: Pick<AppLayout, 'root' | 'addresses' | 'handlers' | 'routes'>,
|
|
982
106
|
scripts = true,
|
|
983
|
-
): Promise<{
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
source: (typeof sources)[number]
|
|
990
|
-
map: ShadowMap
|
|
991
|
-
refusals: readonly Finding[]
|
|
992
|
-
}[] = []
|
|
993
|
-
for (const source of sources) {
|
|
994
|
-
const shadow = checkedShadow(
|
|
995
|
-
source.file,
|
|
996
|
-
source.answer,
|
|
997
|
-
(source.kind === 'page' || source.kind === 'layout') &&
|
|
998
|
-
source.route !== undefined
|
|
999
|
-
? { route: source.route, kind: source.kind }
|
|
1000
|
-
: undefined,
|
|
1001
|
-
)
|
|
1002
|
-
const map = new ShadowMap(
|
|
1003
|
-
this.canonical(source.path),
|
|
1004
|
-
projectShadow(source.file).origins,
|
|
1005
|
-
shadow.origins,
|
|
1006
|
-
)
|
|
1007
|
-
this.register(map.checkedPath, shadow.text, created, changed)
|
|
1008
|
-
// 23.19 — a package's page an editor opens is reported on, so its checked module is a
|
|
1009
|
-
// root of the project as its shadow is (`classify`).
|
|
1010
|
-
if (!source.quiet)
|
|
1011
|
-
this.openOutside(source.path, map.checkedPath, changed)
|
|
1012
|
-
checked.push({ source, map, refusals: shadow.refusals })
|
|
1013
|
-
}
|
|
1014
|
-
// 34.6 — WHAT THE BUNDLER RESOLVES AND THE CHECKER CANNOT: a stylesheet imported for its
|
|
1015
|
-
// effect, and (34.21) markdown imported as text into a `.ts`, which bun's own types leave
|
|
1016
|
-
// out where they declare `.txt`. Declared in a file of its own at the app's root, which is
|
|
1017
|
-
// a script, because a pattern module can only be declared outside a module.
|
|
1018
|
-
this.register(
|
|
1019
|
-
`${this.canonical(within)}/${AMBIENT}`,
|
|
1020
|
-
AMBIENT_DECLARATIONS,
|
|
1021
|
-
created,
|
|
1022
|
-
changed,
|
|
1023
|
-
)
|
|
1024
|
-
// 27.13 — what a handler file defaults, beside it.
|
|
1025
|
-
for (const handler of app.handlers) {
|
|
1026
|
-
const at = `${this.canonical(handler.path)}${CONFIG_SUFFIX}`
|
|
1027
|
-
const shadow = handler.hooks.includes('onConfig')
|
|
1028
|
-
? moduleConfigShadow(handler.text)
|
|
1029
|
-
: undefined
|
|
1030
|
-
// One that stops filing keeps its place and declares nothing, so the next snapshot is
|
|
1031
|
-
// told it moved.
|
|
1032
|
-
if (shadow !== undefined || this.shadows.has(this.keyOf(at)))
|
|
1033
|
-
this.register(at, shadow ?? 'export {}\n', created, changed)
|
|
1034
|
-
}
|
|
1035
|
-
// 30.26 — THE APP'S ROUTE TABLE, which `url`'s literal is held to: every page the build
|
|
1036
|
-
// routes, declared into abide's own `Routes`. A module rather than a script, since only a
|
|
1037
|
-
// module augments one.
|
|
1038
|
-
this.register(
|
|
1039
|
-
`${this.canonical(within)}/${ROUTES_DECLARATION}`,
|
|
1040
|
-
routesDeclaration(app.addresses, app.routes),
|
|
1041
|
-
created,
|
|
1042
|
-
changed,
|
|
1043
|
-
)
|
|
1044
|
-
const refusing = new Set<string>()
|
|
1045
|
-
for (const { source, refusals } of checked)
|
|
1046
|
-
if (refusals.length > 0) refusing.add(source.path)
|
|
1047
|
-
const project = await this.currentProject(created, changed)
|
|
1048
|
-
if (project === undefined) return { diagnoses: [], refused: refusing }
|
|
1049
|
-
const program = project.program
|
|
1050
|
-
// `path` is the file the author wrote, which for a checked module is not the file asked.
|
|
1051
|
-
const report = async (
|
|
1052
|
-
asked: string,
|
|
1053
|
-
path: string,
|
|
1054
|
-
place: (position: number) => { line: number; column: number },
|
|
1055
|
-
): Promise<Diagnosis[]> => {
|
|
1056
|
-
const [syntactic, semantic] = await Promise.all([
|
|
1057
|
-
program.getSyntacticDiagnostics(asked),
|
|
1058
|
-
program.getSemanticDiagnostics(asked),
|
|
1059
|
-
])
|
|
1060
|
-
const said: Diagnosis[] = []
|
|
1061
|
-
for (const diagnostic of [...syntactic, ...semantic])
|
|
1062
|
-
said.push({
|
|
1063
|
-
path,
|
|
1064
|
-
...place(diagnostic.pos),
|
|
1065
|
-
code: diagnostic.code,
|
|
1066
|
-
category: CATEGORIES[diagnostic.category] ?? 'error',
|
|
1067
|
-
// A checked module's own names are the lowering's, and read as the author's.
|
|
1068
|
-
message: flatten(diagnostic).replaceAll(
|
|
1069
|
-
`${CHECKED_VALUES}.`,
|
|
1070
|
-
'',
|
|
1071
|
-
),
|
|
1072
|
-
})
|
|
1073
|
-
return said
|
|
1074
|
-
}
|
|
1075
|
-
const reports: Promise<Diagnosis[]>[] = []
|
|
1076
|
-
const refused: Diagnosis[] = []
|
|
1077
|
-
for (const { source, map, refusals } of checked) {
|
|
1078
|
-
this.loweredForInspect.set(this.keyOf(map.abidePath), {
|
|
1079
|
-
text: source.text,
|
|
1080
|
-
map,
|
|
1081
|
-
})
|
|
1082
|
-
if (source.quiet) continue
|
|
1083
|
-
// A refusal is at a projection offset (`Lowering.refusals`).
|
|
1084
|
-
refused.push(
|
|
1085
|
-
...placedFindings(
|
|
1086
|
-
source.path,
|
|
1087
|
-
source.text,
|
|
1088
|
-
refusals,
|
|
1089
|
-
(at) => map.toSource(map.shadowPath, at),
|
|
1090
|
-
'error',
|
|
1091
|
-
),
|
|
1092
|
-
)
|
|
1093
|
-
reports.push(
|
|
1094
|
-
report(map.checkedPath, source.path, (position) =>
|
|
1095
|
-
lineOf(
|
|
1096
|
-
source.text,
|
|
1097
|
-
map.toSource(map.checkedPath, position),
|
|
1098
|
-
),
|
|
1099
|
-
),
|
|
1100
|
-
)
|
|
1101
|
-
}
|
|
1102
|
-
const standing = scripts ? this.rootsUnder(project, within) : []
|
|
1103
|
-
for (const path of standing)
|
|
1104
|
-
if (!this.shadows.has(this.keyOf(path)))
|
|
1105
|
-
reports.push(
|
|
1106
|
-
Bun.file(path)
|
|
1107
|
-
.text()
|
|
1108
|
-
.then((text) =>
|
|
1109
|
-
report(path, path, (position) =>
|
|
1110
|
-
lineOf(text, position),
|
|
1111
|
-
),
|
|
1112
|
-
),
|
|
1113
|
-
)
|
|
1114
|
-
const reported = [...refused, ...(await Promise.all(reports)).flat()]
|
|
1115
|
-
reported.sort(byPlace)
|
|
1116
|
-
// A component's props type is checked twice — in its `props` call and in what a tag is held
|
|
1117
|
-
// to (`checkedShadow`) — and both copies place a finding in it at the one place written.
|
|
1118
|
-
const found: Diagnosis[] = []
|
|
1119
|
-
const placed = new Set<string>()
|
|
1120
|
-
for (const diagnosis of reported) {
|
|
1121
|
-
const key = `${diagnosis.path}:${diagnosis.line}:${diagnosis.column}:${diagnosis.code}:${diagnosis.message}`
|
|
1122
|
-
if (placed.has(key)) continue
|
|
1123
|
-
placed.add(key)
|
|
1124
|
-
found.push(diagnosis)
|
|
1125
|
-
}
|
|
1126
|
-
return { diagnoses: found, refused: refusing }
|
|
107
|
+
): Promise<{
|
|
108
|
+
diagnoses: Diagnosis[]
|
|
109
|
+
refused: Set<string>
|
|
110
|
+
lowered: LoweredFile[]
|
|
111
|
+
}> {
|
|
112
|
+
return diagnoseSources(this.host, sources, app, scripts)
|
|
1127
113
|
}
|
|
1128
114
|
|
|
1129
|
-
/**
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
*/
|
|
1133
|
-
private askedAt(
|
|
1134
|
-
path: string,
|
|
1135
|
-
text: string,
|
|
1136
|
-
offset: number,
|
|
1137
|
-
):
|
|
1138
|
-
| { project: Project; asked: { path: string; position: number } }
|
|
1139
|
-
| undefined {
|
|
1140
|
-
const lowered = this.loweredForInspect.get(this.keyOf(path))
|
|
1141
|
-
const project = this.latest
|
|
1142
|
-
if (
|
|
1143
|
-
lowered === undefined ||
|
|
1144
|
-
lowered.text !== text ||
|
|
1145
|
-
project === undefined
|
|
1146
|
-
)
|
|
1147
|
-
return undefined
|
|
1148
|
-
const asked = lowered.map.fromSource(offset)
|
|
1149
|
-
if (asked === undefined || !this.shadows.has(this.keyOf(asked.path)))
|
|
1150
|
-
return undefined
|
|
1151
|
-
return { project, asked }
|
|
115
|
+
/** The lowering a diagnosis last registered for the `.abide` at `path`, whatever its text. */
|
|
116
|
+
lowered(path: string): LoweredFile | undefined {
|
|
117
|
+
return this.host.lowered(path)
|
|
1152
118
|
}
|
|
1153
119
|
|
|
1154
|
-
/**
|
|
1155
|
-
|
|
1156
|
-
*
|
|
1157
|
-
* 38.98 — ASKED WHERE A HOVER IS (`askedAt`), so `config().` offers the fields 27.13 declared and
|
|
1158
|
-
* a name read by name offers its value's members (31.1). MEMBERS ALONE in a script or an
|
|
1159
|
-
* expression: at an identifier the API rejects ("completion list needs auto imports"), which
|
|
1160
|
-
* the language server answers as nothing offered. Markup is answered apart (38.117).
|
|
1161
|
-
*/
|
|
1162
|
-
async complete(
|
|
120
|
+
/** The members or markup that may be written at `offset` in a `.abide` (`completionAt`). */
|
|
121
|
+
complete(
|
|
1163
122
|
path: string,
|
|
1164
123
|
text: string,
|
|
1165
124
|
offset: number,
|
|
1166
125
|
triggerCharacter?: string,
|
|
1167
126
|
): Promise<CompletionInfo | undefined> {
|
|
1168
|
-
|
|
1169
|
-
const template = templateContext(text, offset)
|
|
1170
|
-
if (template !== undefined)
|
|
1171
|
-
return await this.completeTemplate(path, template)
|
|
1172
|
-
const at = this.askedAt(path, text, offset)
|
|
1173
|
-
if (at === undefined) return undefined
|
|
1174
|
-
return await at.project.checker.getCompletionsAtPosition(
|
|
1175
|
-
at.asked.path,
|
|
1176
|
-
at.asked.position,
|
|
1177
|
-
{ triggerCharacter },
|
|
1178
|
-
)
|
|
127
|
+
return completionAt(this.lowered(path), text, offset, triggerCharacter)
|
|
1179
128
|
}
|
|
1180
129
|
|
|
1181
|
-
/**
|
|
1182
|
-
* 38.117 — what markup offers: a block keyword, a component in scope, or a component's props.
|
|
1183
|
-
*
|
|
1184
|
-
* A PROP IS ASKED OF THE LAST LOWERING, by the component's name: the tag being typed is text the
|
|
1185
|
-
* parser refuses, and the import the name resolves through is the same in the lowering before it.
|
|
1186
|
-
*/
|
|
1187
|
-
private async completeTemplate(
|
|
1188
|
-
path: string,
|
|
1189
|
-
template: TemplateContext,
|
|
1190
|
-
): Promise<CompletionInfo> {
|
|
1191
|
-
const entries: CompletionEntry[] = []
|
|
1192
|
-
if (template.kind !== 'attribute') {
|
|
1193
|
-
const kind =
|
|
1194
|
-
template.kind === 'block'
|
|
1195
|
-
? CompletionItemKind.Keyword
|
|
1196
|
-
: CompletionItemKind.Class
|
|
1197
|
-
for (const name of template.offered) entries.push({ name, kind })
|
|
1198
|
-
return { isIncomplete: false, entries }
|
|
1199
|
-
}
|
|
1200
|
-
const lowered = this.loweredForInspect.get(this.keyOf(path))
|
|
1201
|
-
const project = this.latest
|
|
1202
|
-
if (lowered === undefined || project === undefined)
|
|
1203
|
-
return { isIncomplete: false, entries }
|
|
1204
|
-
const checker = project.checker
|
|
1205
|
-
const found = await checker.resolveName(
|
|
1206
|
-
template.tag,
|
|
1207
|
-
SymbolFlags.Value,
|
|
1208
|
-
{ document: lowered.map.shadowPath, position: 0 },
|
|
1209
|
-
)
|
|
1210
|
-
const record =
|
|
1211
|
-
found === undefined
|
|
1212
|
-
? undefined
|
|
1213
|
-
: await propsRecordOf(
|
|
1214
|
-
checker,
|
|
1215
|
-
await aliasedSymbol(checker, found),
|
|
1216
|
-
)
|
|
1217
|
-
if (record === undefined) return { isIncomplete: false, entries }
|
|
1218
|
-
const unwritten = (await checker.getPropertiesOfType(record)).filter(
|
|
1219
|
-
(prop) => !template.written.includes(prop.name),
|
|
1220
|
-
)
|
|
1221
|
-
for (const entry of await Promise.all(
|
|
1222
|
-
unwritten.map(async (prop) => {
|
|
1223
|
-
const type = await checker.getTypeOfSymbol(prop)
|
|
1224
|
-
return {
|
|
1225
|
-
name: prop.name,
|
|
1226
|
-
kind: CompletionItemKind.Property,
|
|
1227
|
-
detail:
|
|
1228
|
-
type === undefined
|
|
1229
|
-
? undefined
|
|
1230
|
-
: await checker.typeToString(
|
|
1231
|
-
type,
|
|
1232
|
-
undefined,
|
|
1233
|
-
NodeBuilderFlags.NoTruncation,
|
|
1234
|
-
),
|
|
1235
|
-
}
|
|
1236
|
-
}),
|
|
1237
|
-
))
|
|
1238
|
-
entries.push(entry)
|
|
1239
|
-
return { isIncomplete: false, entries }
|
|
1240
|
-
}
|
|
1241
|
-
|
|
1242
|
-
/**
|
|
1243
|
-
* The hover signature, documentation and definitions of the name at `offset` in a `.abide`.
|
|
1244
|
-
*
|
|
1245
|
-
* 38.39 — WHAT THE NAME AT `offset` IN A `.abide` IS, asked of the program `diagnose` last lowered
|
|
1246
|
-
* it to: its type and documentation for a hover, and where it is declared for a jump. The
|
|
1247
|
-
* checked module is asked where the position was copied into it, which is what a member hangs
|
|
1248
|
-
* off — `regions.join` is `Array`'s — and the projection where the lowering rewrote it, so a
|
|
1249
|
-
* name read by name is the `Reactive` it was declared as.
|
|
1250
|
-
*
|
|
1251
|
-
* `text` IS WHAT THE EDITOR SHOWS, and an offset into it maps through a lowering of the same
|
|
1252
|
-
* text or not at all: a file the parser refuses keeps its last lowering, whose offsets have
|
|
1253
|
-
* moved under every edit since.
|
|
1254
|
-
*/
|
|
130
|
+
/** The name at `offset` in a `.abide` as its last lowering holds it (`LoweredFile.inspect`). */
|
|
1255
131
|
async inspect(
|
|
1256
132
|
path: string,
|
|
1257
133
|
text: string,
|
|
1258
134
|
offset: number,
|
|
1259
135
|
): Promise<Inspection | undefined> {
|
|
1260
|
-
|
|
1261
|
-
const at = this.askedAt(path, text, offset)
|
|
1262
|
-
if (at === undefined) return undefined
|
|
1263
|
-
const { project, asked } = at
|
|
1264
|
-
const checker = project.checker
|
|
1265
|
-
const found = await checker.getSymbolAtPosition(
|
|
1266
|
-
asked.path,
|
|
1267
|
-
asked.position,
|
|
1268
|
-
)
|
|
1269
|
-
if (found === undefined) return undefined
|
|
1270
|
-
const symbol = await aliasedSymbol(checker, found)
|
|
1271
|
-
const declarations: Node[] = []
|
|
1272
|
-
for (const handle of symbol.declarations) {
|
|
1273
|
-
const node = await handle.resolve(project)
|
|
1274
|
-
if (node !== undefined) declarations.push(node)
|
|
1275
|
-
}
|
|
1276
|
-
// A `.abide` import resolves to its checked module's stand-in, which is the component.
|
|
1277
|
-
const component =
|
|
1278
|
-
symbol.name === COMPONENT_STAND_IN &&
|
|
1279
|
-
declarations[0]?.getSourceFile().fileName.endsWith(CHECKED_SUFFIX)
|
|
1280
|
-
const [signature, documentation] = component
|
|
1281
|
-
? [
|
|
1282
|
-
`(component) ${found.name}(props: ${await propsTypeOf(checker, symbol)})`,
|
|
1283
|
-
'',
|
|
1284
|
-
]
|
|
1285
|
-
: await Promise.all([
|
|
1286
|
-
describe(project, symbol, declarations[0]),
|
|
1287
|
-
symbol.getDocumentationComment(checker),
|
|
1288
|
-
])
|
|
1289
|
-
const definitions: Place[] = []
|
|
1290
|
-
for (const node of declarations) {
|
|
1291
|
-
const named = (node as { name?: Node }).name ?? node
|
|
1292
|
-
const file = node.getSourceFile()
|
|
1293
|
-
definitions.push(
|
|
1294
|
-
this.spanInSource(
|
|
1295
|
-
file.fileName,
|
|
1296
|
-
file.text,
|
|
1297
|
-
named.getStart(file),
|
|
1298
|
-
named.end,
|
|
1299
|
-
),
|
|
1300
|
-
)
|
|
1301
|
-
}
|
|
1302
|
-
return {
|
|
1303
|
-
signature: signature.replaceAll(`${CHECKED_VALUES}.`, ''),
|
|
1304
|
-
documentation,
|
|
1305
|
-
definitions,
|
|
1306
|
-
}
|
|
136
|
+
return this.lowered(path)?.inspect(text, offset)
|
|
1307
137
|
}
|
|
1308
138
|
|
|
1309
|
-
/**
|
|
1310
|
-
* Every place the name at `offset` in a `.abide` is written, across the files the checker holds,
|
|
1311
|
-
* or `undefined` where nothing is named there.
|
|
1312
|
-
*
|
|
1313
|
-
* 38.116 — ASKED OF EVERY PLACE THE CHECKER READS THE OFFSET AT (`ShadowMap.placesFrom`), because
|
|
1314
|
-
* the checked module rewrites a name read by name into another symbol's read: the projection is
|
|
1315
|
-
* where a reactive's markup uses are the declaration's references. A place is kept only where
|
|
1316
|
-
* both layers copied it from the author's text and that text is the name, so scaffolding a
|
|
1317
|
-
* lowering wrote is never a place to rename.
|
|
1318
|
-
*
|
|
1319
|
-
* AND ASKED AGAIN WHERE A REFERENCE IS TEXT THE CHECKED MODULE COPIES TWICE. A component's props
|
|
1320
|
-
* type is a second copy of its `props` call (`checkedShadow`), and a tag's keys are typed by
|
|
1321
|
-
* that copy: the checker answers them as another symbol's references, so a reference landing
|
|
1322
|
-
* on one copy is asked at every place the author's text is read.
|
|
1323
|
-
*/
|
|
139
|
+
/** Every place the name at `offset` in a `.abide` is written (`LoweredFile.references`). */
|
|
1324
140
|
async references(
|
|
1325
141
|
path: string,
|
|
1326
142
|
text: string,
|
|
1327
143
|
offset: number,
|
|
1328
144
|
): Promise<References | undefined> {
|
|
1329
|
-
|
|
1330
|
-
const lowered = this.loweredForInspect.get(this.keyOf(path))
|
|
1331
|
-
const project = this.latest
|
|
1332
|
-
if (
|
|
1333
|
-
lowered === undefined ||
|
|
1334
|
-
lowered.text !== text ||
|
|
1335
|
-
project === undefined
|
|
1336
|
-
)
|
|
1337
|
-
return undefined
|
|
1338
|
-
const asked = lowered.map.placesFrom(offset)
|
|
1339
|
-
if (asked.length === 0) return undefined
|
|
1340
|
-
const askedAt = new Set<string>()
|
|
1341
|
-
for (const place of asked)
|
|
1342
|
-
askedAt.add(`${place.path}:${place.position}`)
|
|
1343
|
-
let name: Place | undefined
|
|
1344
|
-
let spelled = ''
|
|
1345
|
-
let renamable = true
|
|
1346
|
-
const seen = new Set<string>()
|
|
1347
|
-
const occurrences: Occurrence[] = []
|
|
1348
|
-
for (let index = 0; index < asked.length; index += 1) {
|
|
1349
|
-
const { path: file, position } = asked[index] as {
|
|
1350
|
-
path: string
|
|
1351
|
-
position: number
|
|
1352
|
-
}
|
|
1353
|
-
const sourceFile = await project.program.getSourceFile(file)
|
|
1354
|
-
if (sourceFile === undefined) continue
|
|
1355
|
-
const node = getTouchingPropertyName(sourceFile, position)
|
|
1356
|
-
if (node.kind !== SyntaxKind.Identifier) continue
|
|
1357
|
-
const written = node.getText(sourceFile)
|
|
1358
|
-
if (name === undefined) {
|
|
1359
|
-
const copied = this.copiedSpan(node, written)
|
|
1360
|
-
name = copied === undefined ? undefined : placeOfSpan(copied)
|
|
1361
|
-
spelled = written
|
|
1362
|
-
}
|
|
1363
|
-
const entries = await project.checker.getReferencedSymbolsForNode(
|
|
1364
|
-
node,
|
|
1365
|
-
position,
|
|
1366
|
-
)
|
|
1367
|
-
for (const entry of entries) {
|
|
1368
|
-
const definition = await entry.definition.resolve(project)
|
|
1369
|
-
if (definition === undefined) continue
|
|
1370
|
-
const declaredIn = definition.getSourceFile().fileName
|
|
1371
|
-
if (!this.authored(declaredIn)) renamable = false
|
|
1372
|
-
const declared = (definition as { name?: Node }).name
|
|
1373
|
-
const references = await Promise.all(
|
|
1374
|
-
entry.references.map((handle) => handle.resolve(project)),
|
|
1375
|
-
)
|
|
1376
|
-
for (const reference of references) {
|
|
1377
|
-
if (reference === undefined) continue
|
|
1378
|
-
const copied = this.copiedSpan(reference, written)
|
|
1379
|
-
if (copied === undefined) continue
|
|
1380
|
-
const declaration =
|
|
1381
|
-
declared !== undefined &&
|
|
1382
|
-
declared.pos === reference.pos &&
|
|
1383
|
-
declaredIn === reference.getSourceFile().fileName
|
|
1384
|
-
// Text the checker reads at more than one place is asked at every one.
|
|
1385
|
-
const copies = copied.map?.placesFrom(copied.start) ?? []
|
|
1386
|
-
if (copies.length > 2)
|
|
1387
|
-
for (const place of copies) {
|
|
1388
|
-
const at = `${place.path}:${place.position}`
|
|
1389
|
-
if (askedAt.has(at)) continue
|
|
1390
|
-
askedAt.add(at)
|
|
1391
|
-
asked.push(place)
|
|
1392
|
-
}
|
|
1393
|
-
const placed = placeOfSpan(copied)
|
|
1394
|
-
const key = `${placed.path}:${placed.start.line}:${placed.start.column}`
|
|
1395
|
-
if (seen.has(key)) continue
|
|
1396
|
-
seen.add(key)
|
|
1397
|
-
const holder = reference.parent
|
|
1398
|
-
const declaresHolder =
|
|
1399
|
-
definition.pos === holder.pos &&
|
|
1400
|
-
declaredIn === reference.getSourceFile().fileName
|
|
1401
|
-
const shorthand =
|
|
1402
|
-
holder.kind ===
|
|
1403
|
-
SyntaxKind.ShorthandPropertyAssignment ||
|
|
1404
|
-
(holder.kind === SyntaxKind.BindingElement &&
|
|
1405
|
-
(holder as { propertyName?: Node }).propertyName ===
|
|
1406
|
-
undefined)
|
|
1407
|
-
occurrences.push({
|
|
1408
|
-
...placed,
|
|
1409
|
-
declaration,
|
|
1410
|
-
// A shorthand names a property and a binding at once, and renaming one
|
|
1411
|
-
// writes the other out beside it: the binding element declares the local
|
|
1412
|
-
// and the shorthand assignment the property.
|
|
1413
|
-
shorthand: !shorthand
|
|
1414
|
-
? undefined
|
|
1415
|
-
: declaresHolder ===
|
|
1416
|
-
(holder.kind === SyntaxKind.BindingElement)
|
|
1417
|
-
? 'local'
|
|
1418
|
-
: 'property',
|
|
1419
|
-
})
|
|
1420
|
-
}
|
|
1421
|
-
}
|
|
1422
|
-
}
|
|
1423
|
-
if (name === undefined) return undefined
|
|
1424
|
-
return { name, written: spelled, renamable, occurrences }
|
|
145
|
+
return this.lowered(path)?.references(text, offset)
|
|
1425
146
|
}
|
|
1426
147
|
|
|
1427
|
-
/**
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
*/
|
|
1431
|
-
private authored(fileName: string): boolean {
|
|
1432
|
-
if (fileName.endsWith('.d.ts') || fileName.includes('/node_modules/'))
|
|
1433
|
-
return false
|
|
1434
|
-
return (
|
|
1435
|
-
ShadowMap.fileFor(this.canonical(fileName)) !== undefined ||
|
|
1436
|
-
!this.shadows.has(this.keyOf(fileName))
|
|
1437
|
-
)
|
|
148
|
+
/** Closes the checker, and ends its process. */
|
|
149
|
+
close(): Promise<void> {
|
|
150
|
+
return this.host.close()
|
|
1438
151
|
}
|
|
1439
|
-
|
|
1440
|
-
/**
|
|
1441
|
-
* Where a node the checker read stands in the file the author wrote, with that file's map where
|
|
1442
|
-
* it is a `.abide`, or `undefined` where the author did not write `name` there: in scaffolding,
|
|
1443
|
-
* or in a file this session serves.
|
|
1444
|
-
*/
|
|
1445
|
-
private copiedSpan(node: Node, name: string): CopiedSpan | undefined {
|
|
1446
|
-
const file = node.getSourceFile()
|
|
1447
|
-
let start = node.getStart(file)
|
|
1448
|
-
let end = node.end
|
|
1449
|
-
// A string literal names a property by its text, inside the quotes.
|
|
1450
|
-
if (node.kind === SyntaxKind.StringLiteral) {
|
|
1451
|
-
start += 1
|
|
1452
|
-
end -= 1
|
|
1453
|
-
}
|
|
1454
|
-
const canonical = this.canonical(file.fileName)
|
|
1455
|
-
const abidePath = ShadowMap.fileFor(canonical)
|
|
1456
|
-
if (abidePath === undefined) {
|
|
1457
|
-
if (!this.authored(file.fileName)) return undefined
|
|
1458
|
-
if (file.text.slice(start, end) !== name) return undefined
|
|
1459
|
-
return {
|
|
1460
|
-
path: file.fileName,
|
|
1461
|
-
text: file.text,
|
|
1462
|
-
start,
|
|
1463
|
-
end,
|
|
1464
|
-
map: undefined,
|
|
1465
|
-
}
|
|
1466
|
-
}
|
|
1467
|
-
const lowered = this.loweredForInspect.get(this.keyOf(abidePath))
|
|
1468
|
-
const span = lowered?.map.copiedToSource(canonical, start, end)
|
|
1469
|
-
if (
|
|
1470
|
-
lowered === undefined ||
|
|
1471
|
-
span === undefined ||
|
|
1472
|
-
lowered.text.slice(span.start, span.end) !== name
|
|
1473
|
-
)
|
|
1474
|
-
return undefined
|
|
1475
|
-
return {
|
|
1476
|
-
path: abidePath,
|
|
1477
|
-
text: lowered.text,
|
|
1478
|
-
start: span.start,
|
|
1479
|
-
end: span.end,
|
|
1480
|
-
map: lowered.map,
|
|
1481
|
-
}
|
|
1482
|
-
}
|
|
1483
|
-
|
|
1484
|
-
/**
|
|
1485
|
-
* A span the checker read, placed in the file the author wrote.
|
|
1486
|
-
*
|
|
1487
|
-
* A span of a file the checker read, as the file the author wrote: a lowered `.abide` mapped
|
|
1488
|
-
* back through both of its maps, and one this session has not lowered — or a span of the
|
|
1489
|
-
* scaffolding a lowering adds, which the author never wrote — placed at its top.
|
|
1490
|
-
*/
|
|
1491
|
-
private spanInSource(
|
|
1492
|
-
fileName: string,
|
|
1493
|
-
text: string,
|
|
1494
|
-
start: number,
|
|
1495
|
-
end: number,
|
|
1496
|
-
): Place {
|
|
1497
|
-
const canonical = this.canonical(fileName)
|
|
1498
|
-
const abidePath = ShadowMap.fileFor(canonical)
|
|
1499
|
-
if (abidePath === undefined)
|
|
1500
|
-
return {
|
|
1501
|
-
path: fileName,
|
|
1502
|
-
start: lineOf(text, start),
|
|
1503
|
-
end: lineOf(text, end),
|
|
1504
|
-
}
|
|
1505
|
-
const lowered = this.loweredForInspect.get(this.keyOf(abidePath))
|
|
1506
|
-
const placed = lowered?.map.spanToSource(canonical, start, end)
|
|
1507
|
-
if (lowered === undefined || placed === undefined) {
|
|
1508
|
-
const top = { line: 1, column: 1 }
|
|
1509
|
-
return { path: abidePath, start: top, end: top }
|
|
1510
|
-
}
|
|
1511
|
-
return {
|
|
1512
|
-
path: abidePath,
|
|
1513
|
-
start: lineOf(lowered.text, placed.start),
|
|
1514
|
-
end: lineOf(lowered.text, placed.end),
|
|
1515
|
-
}
|
|
1516
|
-
}
|
|
1517
|
-
|
|
1518
|
-
/**
|
|
1519
|
-
* Closes the checker, and ends its process.
|
|
1520
|
-
*
|
|
1521
|
-
* KILLED, NOT LEFT TO END ITSELF. The API's own close only ends the process's stdin, and a
|
|
1522
|
-
* `tsgo` that does not read the end of it lives on with this process's stderr in hand — one
|
|
1523
|
-
* outlived its build by eight hours, and anything reading that build's stderr to its end
|
|
1524
|
-
* (`abide build 2>&1 | tee`, a test's pipe) waits as long. The API holds the process on a
|
|
1525
|
-
* private field and forgets it as it closes, so it is taken first; `typeSession.test.ts` gates
|
|
1526
|
-
* that the field is still where this reads it.
|
|
1527
|
-
*
|
|
1528
|
-
* THE CLIENT IS CLOSED, NOT THE API: the API's close first asks the process to drop each
|
|
1529
|
-
* snapshot, and one that has stopped answering never settles it. The client's asks nothing —
|
|
1530
|
-
* it ends the connection, which rejects what was in flight — and the snapshots die with the
|
|
1531
|
-
* process.
|
|
1532
|
-
*/
|
|
1533
|
-
async close(): Promise<void> {
|
|
1534
|
-
const client = (
|
|
1535
|
-
this.api as unknown as {
|
|
1536
|
-
client: {
|
|
1537
|
-
process?: { kill(): boolean }
|
|
1538
|
-
close(): Promise<void>
|
|
1539
|
-
}
|
|
1540
|
-
}
|
|
1541
|
-
).client
|
|
1542
|
-
const child = client.process
|
|
1543
|
-
// A release still being written when the connection ends is a write after end, thrown
|
|
1544
|
-
// where nothing catches it. Bounded, for the same process that has stopped answering.
|
|
1545
|
-
if (this.releasing.size > 0)
|
|
1546
|
-
await Promise.race([
|
|
1547
|
-
Promise.allSettled(this.releasing),
|
|
1548
|
-
Bun.sleep(RELEASE_GRACE_MS),
|
|
1549
|
-
])
|
|
1550
|
-
await client.close()
|
|
1551
|
-
child?.kill()
|
|
1552
|
-
}
|
|
1553
|
-
}
|
|
1554
|
-
|
|
1555
|
-
const AMBIENT = 'abide-check.d.ts'
|
|
1556
|
-
|
|
1557
|
-
// How long `close` waits on releases in flight before ending the connection under them.
|
|
1558
|
-
const RELEASE_GRACE_MS = 1000
|
|
1559
|
-
|
|
1560
|
-
// What the checker asks for when it resolves an import of `x.abide` (`standIn`).
|
|
1561
|
-
const DECLARATION_SUFFIX = '.abide.d.ts'
|
|
1562
|
-
// Beside the app's tsconfig, so `extends` and every path in it resolve as they would from there.
|
|
1563
|
-
const SESSION_CONFIG = 'tsconfig.abide-session.json'
|
|
1564
|
-
const EXPORTED_DECLARATION_SUFFIX = '.d.abide.ts'
|
|
1565
|
-
|
|
1566
|
-
function isFile(path: string): boolean {
|
|
1567
|
-
try {
|
|
1568
|
-
return statSync(path).isFile()
|
|
1569
|
-
} catch {
|
|
1570
|
-
return false
|
|
1571
|
-
}
|
|
1572
|
-
}
|
|
1573
|
-
const ROUTES_DECLARATION = 'abide-routes.d.ts'
|
|
1574
|
-
const URL_SOURCE = Bun.fileURLToPath(new URL('../shared/url', import.meta.url))
|
|
1575
|
-
|
|
1576
|
-
/**
|
|
1577
|
-
* 30.26 — each of `addresses` as a key of abide's `Routes`, and 23.9, 30.3 — a page route's params
|
|
1578
|
-
* narrowed where its matcher is an alternation of literals, which is the one matcher a type can say.
|
|
1579
|
-
*/
|
|
1580
|
-
function routesDeclaration(
|
|
1581
|
-
addresses: readonly string[],
|
|
1582
|
-
routes: readonly AppRoute[],
|
|
1583
|
-
): string {
|
|
1584
|
-
const narrowed = new Map<string, string>()
|
|
1585
|
-
for (const route of routes) {
|
|
1586
|
-
let members = ''
|
|
1587
|
-
for (const segment of parseRoutePattern(route.pattern)) {
|
|
1588
|
-
if (segment.kind !== 'required' && segment.kind !== 'optional')
|
|
1589
|
-
continue
|
|
1590
|
-
const literals = literalsOf(
|
|
1591
|
-
segment.matcher === undefined
|
|
1592
|
-
? undefined
|
|
1593
|
-
: route.matchers[segment.matcher],
|
|
1594
|
-
)
|
|
1595
|
-
if (literals === undefined) continue
|
|
1596
|
-
members += ` ${JSON.stringify(segment.name)}${segment.kind === 'optional' ? '?' : ''}: ${literals};`
|
|
1597
|
-
}
|
|
1598
|
-
if (members !== '') narrowed.set(route.pattern, `{${members} }`)
|
|
1599
|
-
}
|
|
1600
|
-
let keys = ''
|
|
1601
|
-
for (const route of addresses)
|
|
1602
|
-
keys += ` ${JSON.stringify(route)}: ${narrowed.get(route) ?? 'true'}\n`
|
|
1603
|
-
return `export {}\ndeclare module ${JSON.stringify(URL_SOURCE)} {\n interface Routes {\n${keys} }\n}\n`
|
|
1604
|
-
}
|
|
1605
|
-
|
|
1606
|
-
/** The union of the literals an anchored matcher source alternates between, or `undefined`. */
|
|
1607
|
-
function literalsOf(source: string | undefined): string | undefined {
|
|
1608
|
-
const body = source?.match(/^\^\(\?!\\\.\\\.\?\$\)\(\?:(.*)\)\$$/)?.[1]
|
|
1609
|
-
if (body === undefined || !/^[\w~-]+(?:\|[\w~-]+)*$/.test(body))
|
|
1610
|
-
return undefined
|
|
1611
|
-
return body
|
|
1612
|
-
.split('|')
|
|
1613
|
-
.map((literal) => JSON.stringify(literal))
|
|
1614
|
-
.join(' | ')
|
|
1615
|
-
}
|
|
1616
|
-
const AMBIENT_DECLARATIONS = `declare module '*.css' {}
|
|
1617
|
-
declare module '*.md' {
|
|
1618
|
-
const text: string
|
|
1619
|
-
export default text
|
|
1620
152
|
}
|
|
1621
|
-
`
|
|
1622
|
-
|
|
1623
|
-
/**
|
|
1624
|
-
* Where something is in a file the author wrote, line and column from 1.
|
|
1625
|
-
*
|
|
1626
|
-
* 38.39.
|
|
1627
|
-
*/
|
|
1628
|
-
export type Place = {
|
|
1629
|
-
path: string
|
|
1630
|
-
start: { line: number; column: number }
|
|
1631
|
-
end: { line: number; column: number }
|
|
1632
|
-
}
|
|
1633
|
-
|
|
1634
|
-
/**
|
|
1635
|
-
* The name under a cursor: how it is declared, what its doc comment says, and where.
|
|
1636
|
-
*
|
|
1637
|
-
* 38.39.
|
|
1638
|
-
*/
|
|
1639
|
-
export type Inspection = {
|
|
1640
|
-
signature: string
|
|
1641
|
-
documentation: string
|
|
1642
|
-
definitions: Place[]
|
|
1643
|
-
}
|
|
1644
|
-
|
|
1645
|
-
/**
|
|
1646
|
-
* The name under a cursor and every place it is written.
|
|
1647
|
-
*
|
|
1648
|
-
* 38.116 — `name` is where the cursor's own name stands; `renamable` is false where any symbol it
|
|
1649
|
-
* names is declared somewhere an author does not write.
|
|
1650
|
-
*/
|
|
1651
|
-
export type References = {
|
|
1652
|
-
name: Place
|
|
1653
|
-
/** The name as it is written. */
|
|
1654
|
-
written: string
|
|
1655
|
-
renamable: boolean
|
|
1656
|
-
occurrences: Occurrence[]
|
|
1657
|
-
}
|
|
1658
|
-
|
|
1659
|
-
/**
|
|
1660
|
-
* One place a name is written: whether it is where the name is declared, and where it is a
|
|
1661
|
-
* shorthand, which side of it a rename renames — the `local` binding or the `property`.
|
|
1662
|
-
*/
|
|
1663
|
-
export type Occurrence = Place & {
|
|
1664
|
-
declaration: boolean
|
|
1665
|
-
shorthand: 'local' | 'property' | undefined
|
|
1666
|
-
}
|
|
1667
|
-
|
|
1668
|
-
/** A span of a file the author wrote, and the map of the `.abide` it is in, where it is in one. */
|
|
1669
|
-
type CopiedSpan = Span & {
|
|
1670
|
-
path: string
|
|
1671
|
-
text: string
|
|
1672
|
-
map: ShadowMap | undefined
|
|
1673
|
-
}
|
|
1674
|
-
|
|
1675
|
-
/** A `CopiedSpan` as lines and columns. */
|
|
1676
|
-
function placeOfSpan(span: CopiedSpan): Place {
|
|
1677
|
-
return {
|
|
1678
|
-
path: span.path,
|
|
1679
|
-
start: lineOf(span.text, span.start),
|
|
1680
|
-
end: lineOf(span.text, span.end),
|
|
1681
|
-
}
|
|
1682
|
-
}
|
|
1683
|
-
|
|
1684
|
-
/**
|
|
1685
|
-
* The props type a component's tag is held to, printed.
|
|
1686
|
-
*
|
|
1687
|
-
* 33.4 — the record its stand-in is called with.
|
|
1688
|
-
*/
|
|
1689
|
-
async function propsTypeOf(
|
|
1690
|
-
checker: Checker,
|
|
1691
|
-
component: CheckedSymbol,
|
|
1692
|
-
): Promise<string> {
|
|
1693
|
-
const record = await propsRecordOf(checker, component)
|
|
1694
|
-
return record === undefined
|
|
1695
|
-
? 'any'
|
|
1696
|
-
: checker.typeToString(record, undefined, NodeBuilderFlags.NoTruncation)
|
|
1697
|
-
}
|
|
1698
|
-
|
|
1699
|
-
/** The record a component is called with — the props its tag is held to (33.4). */
|
|
1700
|
-
async function propsRecordOf(
|
|
1701
|
-
checker: Checker,
|
|
1702
|
-
component: CheckedSymbol,
|
|
1703
|
-
): Promise<Type | undefined> {
|
|
1704
|
-
const type = await checker.getTypeOfSymbol(component)
|
|
1705
|
-
const call =
|
|
1706
|
-
type === undefined
|
|
1707
|
-
? undefined
|
|
1708
|
-
: (await checker.getSignaturesOfType(type, SignatureKind.Call))[0]
|
|
1709
|
-
return call === undefined ? undefined : checker.getParameterType(call, 0)
|
|
1710
|
-
}
|
|
1711
|
-
|
|
1712
|
-
/**
|
|
1713
|
-
* How a hover spells a symbol, as tsserver does: the keyword it was declared with, its name, and its
|
|
1714
|
-
* type — a function's first signature, with how many more it has.
|
|
1715
|
-
*/
|
|
1716
|
-
async function describe(
|
|
1717
|
-
project: Project,
|
|
1718
|
-
symbol: CheckedSymbol,
|
|
1719
|
-
declaration: Node | undefined,
|
|
1720
|
-
): Promise<string> {
|
|
1721
|
-
const checker = project.checker
|
|
1722
|
-
const flags = symbol.flags
|
|
1723
|
-
const name = symbol.name
|
|
1724
|
-
if (flags & SymbolFlags.Interface) return `interface ${name}`
|
|
1725
|
-
if (flags & SymbolFlags.Class) return `class ${name}`
|
|
1726
|
-
if (flags & SymbolFlags.TypeAlias)
|
|
1727
|
-
return `type ${name} = ${await checker.typeToString(
|
|
1728
|
-
await checker.getDeclaredTypeOfSymbol(symbol),
|
|
1729
|
-
undefined,
|
|
1730
|
-
NodeBuilderFlags.NoTruncation,
|
|
1731
|
-
)}`
|
|
1732
|
-
const type = await checker.getTypeOfSymbol(symbol)
|
|
1733
|
-
if (type === undefined) return name
|
|
1734
|
-
if (flags & (SymbolFlags.Function | SymbolFlags.Method)) {
|
|
1735
|
-
const signatures = await checker.getSignaturesOfType(
|
|
1736
|
-
type,
|
|
1737
|
-
SignatureKind.Call,
|
|
1738
|
-
)
|
|
1739
|
-
const first = signatures[0]
|
|
1740
|
-
const declared =
|
|
1741
|
-
first === undefined
|
|
1742
|
-
? undefined
|
|
1743
|
-
: await checker.signatureToSignatureDeclaration(
|
|
1744
|
-
first,
|
|
1745
|
-
SyntaxKind.MethodSignature,
|
|
1746
|
-
undefined,
|
|
1747
|
-
NodeBuilderFlags.NoTruncation,
|
|
1748
|
-
)
|
|
1749
|
-
if (declared !== undefined) {
|
|
1750
|
-
const printed = (await project.emitter.printNode(declared))
|
|
1751
|
-
.trim()
|
|
1752
|
-
.replace(/;$/, '')
|
|
1753
|
-
const more = signatures.length - 1
|
|
1754
|
-
return `${flags & SymbolFlags.Function ? 'function' : '(method)'} ${name}${printed}${
|
|
1755
|
-
more > 0 ? ` (+${more} overload${more === 1 ? '' : 's'})` : ''
|
|
1756
|
-
}`
|
|
1757
|
-
}
|
|
1758
|
-
}
|
|
1759
|
-
const spelled = await checker.typeToString(
|
|
1760
|
-
type,
|
|
1761
|
-
undefined,
|
|
1762
|
-
NodeBuilderFlags.NoTruncation,
|
|
1763
|
-
)
|
|
1764
|
-
let keyword = '(property)'
|
|
1765
|
-
if (flags & SymbolFlags.Function) keyword = 'function'
|
|
1766
|
-
else if (flags & SymbolFlags.Method) keyword = '(method)'
|
|
1767
|
-
else if (declaration?.kind === SyntaxKind.Parameter) keyword = '(parameter)'
|
|
1768
|
-
else if (flags & SymbolFlags.Variable) {
|
|
1769
|
-
const list = declaration?.parent.flags ?? 0
|
|
1770
|
-
keyword =
|
|
1771
|
-
list & NodeFlags.Const
|
|
1772
|
-
? 'const'
|
|
1773
|
-
: list & NodeFlags.Let
|
|
1774
|
-
? 'let'
|
|
1775
|
-
: 'var'
|
|
1776
|
-
}
|
|
1777
|
-
return `${keyword} ${name}: ${spelled}`
|
|
1778
|
-
}
|
|
1779
|
-
|
|
1780
|
-
/** Indexed by the API's `DiagnosticCategory`. */
|
|
1781
|
-
const CATEGORIES = ['warning', 'error', 'suggestion', 'message'] as const
|
|
1782
|
-
|
|
1783
|
-
function flatten(diagnostic: {
|
|
1784
|
-
text: string
|
|
1785
|
-
messageChain?:
|
|
1786
|
-
readonly { text: string; messageChain?: unknown }[] | undefined
|
|
1787
|
-
}): string {
|
|
1788
|
-
let message = diagnostic.text
|
|
1789
|
-
const chain = diagnostic.messageChain ?? []
|
|
1790
|
-
for (const link of chain)
|
|
1791
|
-
message += `\n ${flatten(link as Parameters<typeof flatten>[0]).replaceAll('\n', '\n ')}`
|
|
1792
|
-
return message
|
|
1793
|
-
}
|
|
1794
|
-
|
|
1795
|
-
/** What a module that is no `.abide` is checked beside as, for the `Config` it files (27.13). */
|
|
1796
|
-
const CONFIG_SUFFIX = '.config-shadow.ts'
|