@abide/abide 0.100.0 → 0.102.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/bin/abide.ts +227 -163
- package/docs/REGISTRY.md +726 -0
- package/docs/RULEBOOK.md +2753 -0
- package/docs/abide.md +12878 -0
- package/package.json +10 -3
- package/scaffold/AGENTS.md +31 -0
- package/scaffold/CLAUDE.md +1 -0
- package/scaffold/src/layout.abide +18 -0
- package/src/compiler/Lowering.ts +78 -9
- package/src/compiler/ShadowMap.ts +2 -2
- package/src/compiler/TypeSession.ts +262 -67
- package/src/compiler/argsSchemaOf.ts +1 -1
- package/src/compiler/checkApp.ts +43 -15
- package/src/compiler/checkedShadow.ts +39 -5
- package/src/compiler/classifyRead.ts +6 -4
- package/src/compiler/classifyTemplate.ts +1 -1
- package/src/compiler/compileApp.ts +433 -193
- package/src/compiler/compiledEntry.ts +43 -39
- package/src/compiler/componentGraph.ts +58 -71
- package/src/compiler/declaredExports.ts +24 -7
- package/src/compiler/emitClient.ts +141 -71
- package/src/compiler/emitComponent.ts +37 -8
- package/src/compiler/emitSource.ts +106 -13
- package/src/compiler/handlersUnder.ts +106 -56
- package/src/compiler/index.ts +7 -0
- package/src/compiler/mountsOf.ts +138 -0
- package/src/compiler/nearest.ts +14 -0
- package/src/compiler/parseAbide.ts +157 -20
- package/src/compiler/projectShadow.ts +213 -7
- package/src/compiler/reactiveTypes.ts +20 -7
- package/src/compiler/readSources.ts +15 -2
- package/src/compiler/readsIn.ts +61 -7
- package/src/compiler/renderMarkup.ts +3 -3
- package/src/compiler/scaffold.ts +30 -11
- package/src/compiler/serveLanguage.ts +125 -34
- package/src/compiler/sourceMapOf.ts +193 -0
- package/src/compiler/splitImports.ts +205 -83
- package/src/compiler/syncTypes.ts +32 -0
- package/src/compiler/tokenize.ts +14 -0
- package/src/compiler/writeDefaultProject.ts +1 -1
- package/src/compiler/writeTypes.ts +82 -0
- package/src/server/CLIENT_BUILD.ts +11 -13
- package/src/server/DEFAULT_PORT.ts +1 -1
- package/src/server/LAUNCHER_FILES.ts +9 -0
- package/src/server/MODULE_QUERIES.ts +18 -0
- package/src/server/TABLES.ts +7 -0
- package/src/server/VIRTUAL_DIRECTORY.ts +4 -5
- package/src/server/abideModules.ts +391 -0
- package/src/server/answerFailure.ts +16 -6
- package/src/server/answerRequest.ts +235 -106
- package/src/server/answerRpc.ts +124 -39
- package/src/server/answerSocket.ts +141 -48
- package/src/server/answerValue.ts +22 -9
- package/src/server/appDirectories.ts +1 -1
- package/src/server/appPlugins.ts +1 -1
- package/src/server/appTree.ts +179 -0
- package/src/server/argsFor.ts +2 -2
- package/src/server/assistant.ts +2 -2
- package/src/server/bridgeRelay.ts +6 -8
- package/src/server/claudeCode.ts +0 -6
- package/src/server/clientBuildOptions.ts +8 -3
- package/src/server/coerceArgs.ts +1 -1
- package/src/server/commands.ts +37 -37
- package/src/server/componentModules.d.ts +3 -1
- package/src/server/compressAnswer.ts +48 -14
- package/src/server/config.ts +25 -29
- package/src/server/configFields.ts +31 -25
- package/src/server/createApp.ts +59 -73
- package/src/server/devWorker.ts +144 -183
- package/src/server/executableName.ts +4 -0
- package/src/server/fenceServer.ts +37 -35
- package/src/server/generatedSurfaces.ts +99 -66
- package/src/server/hooks.ts +124 -52
- package/src/server/iconAccent.ts +191 -0
- package/src/server/index.ts +11 -7
- package/src/server/installSourceMaps.ts +88 -0
- package/src/server/launcherOptions.ts +34 -31
- package/src/server/launcherPage.ts +107 -64
- package/src/server/manifestOf.ts +16 -13
- package/src/server/namesUnder.ts +2 -2
- package/src/server/nativeMenu.ts +5 -5
- package/src/server/negotiateEncoding.ts +1 -1
- package/src/server/principal.ts +10 -5
- package/src/server/realOrAsGiven.ts +10 -0
- package/src/server/refuseAddressCollision.ts +21 -14
- package/src/server/remote.ts +49 -52
- package/src/server/render.ts +65 -94
- package/src/server/renderComponent.ts +5 -0
- package/src/server/reportHealth.ts +2 -2
- package/src/server/requestScope.ts +12 -1
- package/src/server/runBuild.ts +24 -0
- package/src/server/runCompiled.ts +5 -5
- package/src/server/runDesktop.ts +52 -27
- package/src/server/serveBuild.ts +43 -52
- package/src/server/serveDev.ts +81 -39
- package/src/server/shell.ts +7 -22
- package/src/server/sourceMappings.ts +139 -0
- package/src/server/surfaceOf.ts +18 -3
- package/src/server/testBuild.ts +1 -19
- package/src/server/withAbideSources.ts +117 -0
- package/src/shared/ADDRESSES.ts +7 -18
- package/src/shared/APP_META.ts +7 -0
- package/src/shared/CopyOptions.ts +1 -1
- package/src/shared/DEBUG_META.ts +1 -1
- package/src/shared/GLOBAL_GETS.ts +9 -3
- package/src/shared/RESERVED_DIRECTORIES.ts +15 -0
- package/src/shared/RPC_SERVER.ts +2 -2
- package/src/shared/cookies.ts +2 -2
- package/src/shared/csp.ts +1 -1
- package/src/shared/escapeText.ts +1 -1
- package/src/shared/filledPath.ts +85 -0
- package/src/shared/framing.ts +79 -6
- package/src/shared/headKeyOf.ts +2 -0
- package/src/shared/index.ts +1 -1
- package/src/shared/log.ts +27 -13
- package/src/shared/matchRoute.ts +23 -4
- package/src/shared/menuItem.ts +3 -3
- package/src/shared/reactive/Cursor.ts +3 -9
- package/src/shared/reactive/Keyed.ts +4 -3
- package/src/shared/reactive/NotLanded.ts +1 -1
- package/src/shared/reactive/Reactive.ts +81 -60
- package/src/shared/reactive/argsContract.ts +4 -4
- package/src/shared/reactive/canonicalKey.ts +4 -4
- package/src/shared/reactive/chunksOfStream.ts +6 -0
- package/src/shared/reactive/index.ts +1 -1
- package/src/shared/reactive/recordWrites.ts +103 -5
- package/src/shared/reactive/structural.ts +1 -1
- package/src/shared/reactive/tracking.ts +155 -21
- package/src/shared/reactive/watch.ts +10 -6
- package/src/shared/refuse.ts +1 -1
- package/src/shared/routePattern.ts +82 -46
- package/src/shared/rpc.ts +47 -13
- package/src/shared/rpcRequest.ts +27 -3
- package/src/shared/url.ts +4 -44
- package/src/shared/writePath.ts +1 -1
- package/src/ui/LAYER_NAMES.ts +1 -1
- package/src/ui/List.ts +3 -3
- package/src/ui/MOUNTING_LEVEL.ts +18 -0
- package/src/ui/Mismatch.ts +1 -1
- package/src/ui/RawRun.ts +1 -1
- package/src/ui/Router.ts +38 -31
- package/src/ui/Scope.ts +1 -1
- package/src/ui/TryRegion.ts +2 -2
- package/src/ui/applyHead.ts +33 -0
- package/src/ui/index.ts +1 -1
- package/src/ui/menuBridge.ts +1 -1
- package/src/ui/moveNode.ts +1 -1
- package/src/ui/reconcile.ts +2 -2
- package/src/ui/reportMenu.ts +2 -2
- package/src/ui/routing.ts +2 -2
- package/src/ui/setAttribute.ts +1 -1
- package/src/ui/setText.ts +1 -1
- package/src/ui/socketWire.ts +9 -7
- package/src/ui/wire.ts +1 -1
- package/scaffold/src/server/app.ts +0 -6
- package/scaffold/src/shared/.gitkeep +0 -0
- package/scaffold/src/ui/app.html +0 -8
- package/src/server/componentImports.ts +0 -56
- /package/scaffold/src/{ui/pages/page.abide → page.abide} +0 -0
|
@@ -1,16 +1,18 @@
|
|
|
1
|
-
import { type Dirent, readdirSync, statSync } from 'node:fs' // Bun exposes no sync directory listing of its own
|
|
1
|
+
import { type Dirent, readdirSync, realpathSync, statSync } from 'node:fs' // Bun exposes no sync directory listing or realpath of its own
|
|
2
2
|
import { basename, dirname, join, resolve } from 'node:path' // Bun exposes no path helpers of its own
|
|
3
3
|
import { type Node, NodeFlags, SyntaxKind } from 'typescript/unstable/ast'
|
|
4
4
|
import {
|
|
5
5
|
API,
|
|
6
6
|
type Symbol as CheckedSymbol,
|
|
7
7
|
type Checker,
|
|
8
|
+
type CompletionInfo,
|
|
8
9
|
NodeBuilderFlags,
|
|
9
10
|
type Project,
|
|
10
11
|
SignatureKind,
|
|
11
12
|
SymbolFlags,
|
|
12
13
|
} from 'typescript/unstable/async'
|
|
13
|
-
import {
|
|
14
|
+
import { appFiles } from '#server/appTree.ts'
|
|
15
|
+
import { fileKind, placeOf, routeOf } from '#shared/routePattern.ts'
|
|
14
16
|
import { aliasedSymbol } from './aliasedSymbol.ts'
|
|
15
17
|
import { AMBIENT_SOURCES, ambientReadsIn } from './ambientReadsIn.ts'
|
|
16
18
|
import { argsDefaultsIn } from './argsDefaultsIn.ts'
|
|
@@ -23,12 +25,14 @@ import {
|
|
|
23
25
|
import type { Finding } from './Finding.ts'
|
|
24
26
|
import { CHECKED_VALUES } from './Lowering.ts'
|
|
25
27
|
import { diagnosisAt, lineOf } from './lineOf.ts'
|
|
28
|
+
import { mountedUnder } from './mountsOf.ts'
|
|
26
29
|
import type { AbideFile } from './parseAbide.ts'
|
|
27
30
|
import { projectShadow } from './projectShadow.ts'
|
|
28
31
|
import { abideIn } from './reactiveTypes.ts'
|
|
29
32
|
import type { ParsedSource, ReadSource } from './readSources.ts'
|
|
30
33
|
import { readsIn, type ShadowSpans } from './readsIn.ts'
|
|
31
|
-
import { CHECKED_SUFFIX, ShadowMap } from './ShadowMap.ts'
|
|
34
|
+
import { CHECKED_SUFFIX, SHADOW_SUFFIX, ShadowMap } from './ShadowMap.ts'
|
|
35
|
+
import { SOURCE_SUFFIX } from './splitImports.ts'
|
|
32
36
|
import { templateWarnings } from './templateWarnings.ts'
|
|
33
37
|
|
|
34
38
|
// Where 31.3 gets its answer. A `.abide` file is projected into a SHADOW `.ts` held in a virtual
|
|
@@ -60,6 +64,13 @@ export class TypeSession {
|
|
|
60
64
|
* text's offsets.
|
|
61
65
|
*/
|
|
62
66
|
private shadows = new Map<string, { path: string; text: string }>()
|
|
67
|
+
/**
|
|
68
|
+
* 30.26 — the route table `diagnose` last declared, and the root it walked for it: kept until
|
|
69
|
+
* `edited` hears anything move, so a check asked again and again while an editor types walks and
|
|
70
|
+
* reads nothing on disk.
|
|
71
|
+
*/
|
|
72
|
+
private routes: { within: string; declaration: string } | undefined =
|
|
73
|
+
undefined
|
|
63
74
|
/** The checker's own key for a path: resolved, and case-folded where its host folds case. */
|
|
64
75
|
private keyOf: (path: string) => string
|
|
65
76
|
/**
|
|
@@ -83,27 +94,124 @@ export class TypeSession {
|
|
|
83
94
|
) {
|
|
84
95
|
this.api = api
|
|
85
96
|
this.tsconfig = tsconfig
|
|
97
|
+
this.opened = `${dirname(tsconfig)}/${SESSION_CONFIG}`
|
|
86
98
|
this.keyOf = keyOf
|
|
87
99
|
}
|
|
88
100
|
|
|
101
|
+
/**
|
|
102
|
+
* The project this session opens: the app's tsconfig, with every shadow in `outside` a root.
|
|
103
|
+
*
|
|
104
|
+
* 34.29 — A DEPENDENCY'S `.abide` IS TYPED AS THE APP'S OWN, and its tsconfig's `include` never
|
|
105
|
+
* reaches `node_modules` or a linked package's directory. The session opens a config of its own —
|
|
106
|
+
* served from here, never written — that extends the app's, names those shadows as files, which
|
|
107
|
+
* TypeScript adds to what the base config includes (a linked package's shadow, asked for through
|
|
108
|
+
* the link, is reached no other way), and allows the declaration a package's `exports` is asked
|
|
109
|
+
* for by (`standIn`).
|
|
110
|
+
*/
|
|
111
|
+
private readonly opened: string
|
|
112
|
+
|
|
113
|
+
/** The shadows of the files outside the app's own, as `opened` names them. */
|
|
114
|
+
private outside = new Set<string>()
|
|
115
|
+
|
|
116
|
+
private sessionConfig(): string {
|
|
117
|
+
const config: {
|
|
118
|
+
extends: string
|
|
119
|
+
compilerOptions: Record<string, unknown>
|
|
120
|
+
files?: string[]
|
|
121
|
+
} = {
|
|
122
|
+
extends: `./${basename(this.tsconfig)}`,
|
|
123
|
+
// A package's `exports` names `./x.abide` exactly, so the checker asks for its
|
|
124
|
+
// declaration as `./x.d.abide.ts` (`standIn`) — and takes the answer only with this.
|
|
125
|
+
compilerOptions: { allowArbitraryExtensions: true },
|
|
126
|
+
}
|
|
127
|
+
// An empty `files` is a config error, and the base's `include` alone is the app.
|
|
128
|
+
if (this.outside.size > 0) config.files = [...this.outside]
|
|
129
|
+
return JSON.stringify(config)
|
|
130
|
+
}
|
|
131
|
+
|
|
89
132
|
/**
|
|
90
133
|
* Checked-module paths a stand-in answered for, without the `.abide` being lowered.
|
|
91
134
|
*
|
|
92
|
-
* 38.
|
|
135
|
+
* 38.39 — A `.abide` THIS SESSION HAS NOT LOWERED IS STILL A MODULE ANOTHER CAN IMPORT. What its
|
|
93
136
|
* checked module exports does not depend on what the component holds (`COMPONENT_MODULE`), so an
|
|
94
137
|
* editor checking one page resolves the components it imports without lowering each of them.
|
|
95
138
|
* Remembered, so the lowering that replaces a stand-in is reported as a change to it.
|
|
96
139
|
*/
|
|
97
140
|
private stoodIn = new Set<string>()
|
|
98
141
|
|
|
99
|
-
|
|
100
|
-
|
|
142
|
+
/** The app's root, real — set by `openChecker`. */
|
|
143
|
+
root = ''
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* `path` below the app's root, where it lies under it.
|
|
147
|
+
*
|
|
148
|
+
* 23.14 — what a page's route is read off: the directories below the root are it, and the
|
|
149
|
+
* machine's directories above the root are not.
|
|
150
|
+
*/
|
|
151
|
+
belowRoot(path: string): string {
|
|
152
|
+
return path.startsWith(`${this.root}/`)
|
|
153
|
+
? path.slice(this.root.length + 1)
|
|
154
|
+
: path
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* The shadow registered at `name`, or at the same file through its directory's real path.
|
|
159
|
+
*
|
|
160
|
+
* 34.29 — A LINKED PACKAGE IS ASKED FOR THROUGH ITS LINK: the checker resolves `kit/x.abide` to
|
|
161
|
+
* `node_modules/kit/x.d.abide.ts` and the re-export there to the shadow beside it, while the
|
|
162
|
+
* shadow is registered where bun resolved the file — the package's real directory.
|
|
163
|
+
*/
|
|
164
|
+
private shadowAt(name: string): { path: string; text: string } | undefined {
|
|
165
|
+
const found = this.shadows.get(this.keyOf(name))
|
|
166
|
+
// Only a shadow is registered through a link, so any other name is spared the realpath.
|
|
167
|
+
if (
|
|
168
|
+
found !== undefined ||
|
|
169
|
+
!name.includes('/node_modules/') ||
|
|
170
|
+
!(name.endsWith(SHADOW_SUFFIX) || name.endsWith(CHECKED_SUFFIX))
|
|
171
|
+
)
|
|
172
|
+
return found
|
|
173
|
+
let real: string
|
|
101
174
|
try {
|
|
102
|
-
|
|
103
|
-
return undefined
|
|
175
|
+
real = realpathSync(dirname(name))
|
|
104
176
|
} catch {
|
|
105
177
|
return undefined
|
|
106
178
|
}
|
|
179
|
+
return this.shadows.get(this.keyOf(`${real}/${basename(name)}`))
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private standIn(name: string): string | undefined {
|
|
183
|
+
// 34.25 — a file imported as text is its text, which the projection names apart.
|
|
184
|
+
if (name.endsWith(`${SOURCE_SUFFIX}.d.ts`))
|
|
185
|
+
return isFile(name.slice(0, -`${SOURCE_SUFFIX}.d.ts`.length))
|
|
186
|
+
? 'declare const text: string\nexport default text\n'
|
|
187
|
+
: undefined
|
|
188
|
+
// 34.17, 34.23 — WHAT A `.abide` EXPORTS BY NAME, to anything importing it: the checker
|
|
189
|
+
// resolves `./x.abide` by trying `./x.abide.d.ts`, and is answered with the file's own
|
|
190
|
+
// module and server blocks — its projection — beside the component its checked module
|
|
191
|
+
// declares. The wildcard declaration a plain `.ts` is typed by carries the default alone.
|
|
192
|
+
//
|
|
193
|
+
// 34.29 — AND THROUGH A PACKAGE'S `exports`, which the checker follows to the exact file and
|
|
194
|
+
// so asks for the arbitrary extension's declaration, `./x.d.abide.ts`, instead.
|
|
195
|
+
const abide = name.endsWith(DECLARATION_SUFFIX)
|
|
196
|
+
? name.slice(0, -'.d.ts'.length)
|
|
197
|
+
: name.endsWith(EXPORTED_DECLARATION_SUFFIX)
|
|
198
|
+
? `${name.slice(0, -EXPORTED_DECLARATION_SUFFIX.length)}.abide`
|
|
199
|
+
: undefined
|
|
200
|
+
if (abide !== undefined) {
|
|
201
|
+
if (!isFile(abide)) return undefined
|
|
202
|
+
const own = `./${basename(abide)}`
|
|
203
|
+
return `export * from '${own}${SHADOW_SUFFIX.slice(0, -'.ts'.length)}'
|
|
204
|
+
export { default } from '${own}${CHECKED_SUFFIX.slice(0, -'.ts'.length)}'
|
|
205
|
+
`
|
|
206
|
+
}
|
|
207
|
+
// A projection not registered — a file with nothing to read, or one this session has not
|
|
208
|
+
// projected yet — exports nothing, so the declaration above always resolves.
|
|
209
|
+
if (name.endsWith(`.abide${SHADOW_SUFFIX}`))
|
|
210
|
+
return isFile(name.slice(0, -SHADOW_SUFFIX.length))
|
|
211
|
+
? 'export {}\n'
|
|
212
|
+
: undefined
|
|
213
|
+
if (!name.endsWith(`.abide${CHECKED_SUFFIX}`)) return undefined
|
|
214
|
+
if (!isFile(name.slice(0, -CHECKED_SUFFIX.length))) return undefined
|
|
107
215
|
this.stoodIn.add(this.keyOf(name))
|
|
108
216
|
return COMPONENT_MODULE
|
|
109
217
|
}
|
|
@@ -114,10 +222,13 @@ export class TypeSession {
|
|
|
114
222
|
cwd: dirname(tsconfig),
|
|
115
223
|
fs: {
|
|
116
224
|
readFile: (name) =>
|
|
117
|
-
session.
|
|
118
|
-
|
|
225
|
+
name === session.opened
|
|
226
|
+
? session.sessionConfig()
|
|
227
|
+
: (session.shadowAt(name)?.text ??
|
|
228
|
+
session.standIn(name)),
|
|
119
229
|
fileExists: (name) =>
|
|
120
|
-
session.
|
|
230
|
+
name === session.opened ||
|
|
231
|
+
session.shadowAt(name) !== undefined ||
|
|
121
232
|
session.standIn(name) !== undefined
|
|
122
233
|
? true
|
|
123
234
|
: undefined,
|
|
@@ -183,6 +294,7 @@ export class TypeSession {
|
|
|
183
294
|
// nobody's listing — so the checker never read it and the refusal below blamed the app's
|
|
184
295
|
// tsconfig for a path this class had mangled.
|
|
185
296
|
const here = this.keyOf(directory)
|
|
297
|
+
this.listed.add(here)
|
|
186
298
|
const mine: string[] = []
|
|
187
299
|
// The key is the checker's where it is registered, so only the incoming directory needs
|
|
188
300
|
// keying — one `toPath` a call rather than one a shadow.
|
|
@@ -241,6 +353,7 @@ export class TypeSession {
|
|
|
241
353
|
* checker is not told what was under it.
|
|
242
354
|
*/
|
|
243
355
|
edited(paths: string[] | undefined): void {
|
|
356
|
+
this.routes = undefined
|
|
244
357
|
if (paths === undefined) {
|
|
245
358
|
this.everything = true
|
|
246
359
|
return
|
|
@@ -265,11 +378,18 @@ export class TypeSession {
|
|
|
265
378
|
}
|
|
266
379
|
continue
|
|
267
380
|
}
|
|
268
|
-
|
|
381
|
+
// A FILE IN A DIRECTORY THE CHECKER HAS NEVER LISTED is a directory move the watcher
|
|
382
|
+
// reported as the file alone — a save that creates `late/` and the page in it
|
|
383
|
+
// at once — and the listing it already holds of the parent does not have it.
|
|
384
|
+
if (directory || !this.listed.has(this.keyOf(dirname(path))))
|
|
385
|
+
this.everything = true
|
|
269
386
|
else this.edits.present.push(path)
|
|
270
387
|
}
|
|
271
388
|
}
|
|
272
389
|
|
|
390
|
+
/** Every directory the checker has asked this session to list (`entriesIn`). */
|
|
391
|
+
private listed = new Set<string>()
|
|
392
|
+
|
|
273
393
|
/**
|
|
274
394
|
* ONE SNAPSHOT TAKES EVERY EDIT WITH IT: the shadows written for it and whatever moved on disk.
|
|
275
395
|
*/
|
|
@@ -282,15 +402,21 @@ export class TypeSession {
|
|
|
282
402
|
this.edits = { present: [], deleted: [] }
|
|
283
403
|
this.everything = false
|
|
284
404
|
this.snapshots += 1
|
|
405
|
+
// AN EDIT NOBODY COULD NAME DROPS THE CACHE, and the files created with it are still named
|
|
406
|
+
// after it: a dropped cache re-reads what the project already listed, and a shadow written
|
|
407
|
+
// for a page in a directory it never listed is not among them.
|
|
408
|
+
if (everything)
|
|
409
|
+
await this.api.updateSnapshot({
|
|
410
|
+
openProjects: [this.opened],
|
|
411
|
+
fileChanges: { invalidateAll: true },
|
|
412
|
+
})
|
|
285
413
|
const snapshot = await this.api.updateSnapshot({
|
|
286
|
-
openProjects: [this.
|
|
287
|
-
fileChanges:
|
|
288
|
-
|
|
289
|
-
:
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
deleted: edits.deleted,
|
|
293
|
-
},
|
|
414
|
+
openProjects: [this.opened],
|
|
415
|
+
fileChanges: {
|
|
416
|
+
created: [...created, ...edits.present],
|
|
417
|
+
changed: [...changed, ...edits.present],
|
|
418
|
+
deleted: edits.deleted,
|
|
419
|
+
},
|
|
294
420
|
})
|
|
295
421
|
this.latest = snapshot.getProjects()[0]
|
|
296
422
|
return this.latest
|
|
@@ -353,6 +479,15 @@ export class TypeSession {
|
|
|
353
479
|
return resolve(dirname(this.tsconfig), path)
|
|
354
480
|
}
|
|
355
481
|
|
|
482
|
+
/**
|
|
483
|
+
* Where the checker holds a `.abide`'s projection, once `readsFor` has registered it.
|
|
484
|
+
*
|
|
485
|
+
* 19.15 — what a `<script server>` exports is read off it as a `.ts` module's exports are.
|
|
486
|
+
*/
|
|
487
|
+
projectionOf(abidePath: string): string {
|
|
488
|
+
return `${this.canonical(abidePath)}${SHADOW_SUFFIX}`
|
|
489
|
+
}
|
|
490
|
+
|
|
356
491
|
/**
|
|
357
492
|
* A file the checker reads out of `shadows`, reported to the next snapshot only when it is new,
|
|
358
493
|
* its text moved, or a stand-in answered for it until now.
|
|
@@ -399,6 +534,7 @@ export class TypeSession {
|
|
|
399
534
|
async readsFor(
|
|
400
535
|
sources: readonly (Pick<ParsedSource, 'path' | 'file'> & {
|
|
401
536
|
text?: string
|
|
537
|
+
position?: string | undefined
|
|
402
538
|
})[],
|
|
403
539
|
): Promise<{ answers: CheckerAnswer[]; warnings: Diagnosis[] }> {
|
|
404
540
|
const answered = await this.classify(sources)
|
|
@@ -421,6 +557,7 @@ export class TypeSession {
|
|
|
421
557
|
private async classify(
|
|
422
558
|
sources: readonly (Pick<ParsedSource, 'path' | 'file'> & {
|
|
423
559
|
text?: string
|
|
560
|
+
position?: string | undefined
|
|
424
561
|
})[],
|
|
425
562
|
): Promise<{ answers: CheckerAnswer[]; warnings: Diagnosis[] }> {
|
|
426
563
|
const warnings: Diagnosis[] = []
|
|
@@ -443,8 +580,14 @@ export class TypeSession {
|
|
|
443
580
|
// that scope on both sides. The projection holds both from offset 0.
|
|
444
581
|
const scripts = projection.text.slice(0, projection.scripts.end)
|
|
445
582
|
// A TEMPLATE POSITION IS A QUESTION TOO (14.2): a file that declares nothing but
|
|
446
|
-
// writes `{format(total)}` has a position only the checker can prove static.
|
|
447
|
-
|
|
583
|
+
// writes `{format(total)}` has a position only the checker can prove static. And a
|
|
584
|
+
// `<script server>` always is (19.15): what it exports is read off this shadow, a bare
|
|
585
|
+
// `export { touch } from …` included.
|
|
586
|
+
if (
|
|
587
|
+
!DECLARES.test(scripts) &&
|
|
588
|
+
projection.slots.size === 0 &&
|
|
589
|
+
source.file.servers.length === 0
|
|
590
|
+
) {
|
|
448
591
|
queries.push(undefined)
|
|
449
592
|
continue
|
|
450
593
|
}
|
|
@@ -463,11 +606,19 @@ export class TypeSession {
|
|
|
463
606
|
// AN UNCHANGED SHADOW IS NOT REPORTED, which is what a kept session saves on a save:
|
|
464
607
|
// the one page that moved is re-read rather than all of them.
|
|
465
608
|
this.register(map.shadowPath, projection.text, created, changed)
|
|
609
|
+
if (
|
|
610
|
+
placeOf(this.belowRoot(source.path)) === 'dependency' &&
|
|
611
|
+
!this.outside.has(map.shadowPath)
|
|
612
|
+
) {
|
|
613
|
+
this.outside.add(map.shadowPath)
|
|
614
|
+
changed.push(this.opened)
|
|
615
|
+
}
|
|
466
616
|
queries.push({
|
|
467
617
|
map,
|
|
468
618
|
spans: {
|
|
469
619
|
scriptsEnd: projection.scripts.end,
|
|
470
620
|
modules: projection.modules,
|
|
621
|
+
servers: projection.servers,
|
|
471
622
|
scripts: projection.scripts,
|
|
472
623
|
positions: projection.positions,
|
|
473
624
|
},
|
|
@@ -504,7 +655,8 @@ export class TypeSession {
|
|
|
504
655
|
sourceFile,
|
|
505
656
|
query.map.shadowPath,
|
|
506
657
|
query.spans,
|
|
507
|
-
|
|
658
|
+
// 23.19 — a mounted page's route is its position's.
|
|
659
|
+
source.position ?? this.belowRoot(source.path),
|
|
508
660
|
source.file,
|
|
509
661
|
)
|
|
510
662
|
answers[at] = read.answer
|
|
@@ -634,7 +786,10 @@ export class TypeSession {
|
|
|
634
786
|
* reported at its line — `abide check` emits the rest of the app without them.
|
|
635
787
|
*/
|
|
636
788
|
async diagnose(
|
|
637
|
-
sources: (Omit<ReadSource, 'quiet'> & {
|
|
789
|
+
sources: (Omit<ReadSource, 'quiet' | 'position'> & {
|
|
790
|
+
quiet?: boolean
|
|
791
|
+
position?: string | undefined
|
|
792
|
+
})[],
|
|
638
793
|
within: string,
|
|
639
794
|
scripts = true,
|
|
640
795
|
): Promise<{ diagnoses: Diagnosis[]; refused: Set<string> }> {
|
|
@@ -667,9 +822,11 @@ export class TypeSession {
|
|
|
667
822
|
// 30.26 — THE APP'S ROUTE TABLE, which `url`'s literal is held to: every page under the
|
|
668
823
|
// root, declared into abide's own `Routes`. A module rather than a script, since only a
|
|
669
824
|
// module augments one.
|
|
825
|
+
if (this.routes?.within !== within)
|
|
826
|
+
this.routes = { within, declaration: routesDeclaration(within) }
|
|
670
827
|
this.register(
|
|
671
828
|
`${this.canonical(within)}/${ROUTES_DECLARATION}`,
|
|
672
|
-
|
|
829
|
+
this.routes.declaration,
|
|
673
830
|
created,
|
|
674
831
|
changed,
|
|
675
832
|
)
|
|
@@ -749,10 +906,58 @@ export class TypeSession {
|
|
|
749
906
|
return { diagnoses: found, refused: refusing }
|
|
750
907
|
}
|
|
751
908
|
|
|
909
|
+
/**
|
|
910
|
+
* Where the author's `offset` in a `.abide` stands in what the checker reads, and the program
|
|
911
|
+
* `diagnose` last lowered it to — or `undefined` where `text` is not what was lowered.
|
|
912
|
+
*/
|
|
913
|
+
private askedAt(
|
|
914
|
+
path: string,
|
|
915
|
+
text: string,
|
|
916
|
+
offset: number,
|
|
917
|
+
):
|
|
918
|
+
| { project: Project; asked: { path: string; position: number } }
|
|
919
|
+
| undefined {
|
|
920
|
+
const lowered = this.loweredForInspect.get(this.keyOf(path))
|
|
921
|
+
const project = this.latest
|
|
922
|
+
if (
|
|
923
|
+
lowered === undefined ||
|
|
924
|
+
lowered.text !== text ||
|
|
925
|
+
project === undefined
|
|
926
|
+
)
|
|
927
|
+
return undefined
|
|
928
|
+
const asked = lowered.map.fromSource(offset)
|
|
929
|
+
if (asked === undefined || !this.shadows.has(this.keyOf(asked.path)))
|
|
930
|
+
return undefined
|
|
931
|
+
return { project, asked }
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
/**
|
|
935
|
+
* The members that may be written at `offset` in a `.abide`, or `undefined` where none can be.
|
|
936
|
+
*
|
|
937
|
+
* 38.98 — ASKED WHERE A HOVER IS (`askedAt`), so `config().` offers the fields 27.13 declared and
|
|
938
|
+
* a name read by name offers its value's members (31.1). MEMBERS ALONE: at an identifier the
|
|
939
|
+
* API rejects ("completion list needs auto imports"), which the language server answers as
|
|
940
|
+
* nothing offered.
|
|
941
|
+
*/
|
|
942
|
+
async complete(
|
|
943
|
+
path: string,
|
|
944
|
+
text: string,
|
|
945
|
+
offset: number,
|
|
946
|
+
triggerCharacter?: string,
|
|
947
|
+
): Promise<CompletionInfo | undefined> {
|
|
948
|
+
const at = this.askedAt(path, text, offset)
|
|
949
|
+
if (at === undefined) return undefined
|
|
950
|
+
return at.project.checker.getCompletionsAtPosition(
|
|
951
|
+
at.asked.path,
|
|
952
|
+
at.asked.position,
|
|
953
|
+
{ triggerCharacter },
|
|
954
|
+
)
|
|
955
|
+
}
|
|
956
|
+
|
|
752
957
|
/**
|
|
753
958
|
* The hover signature, documentation and definitions of the name at `offset` in a `.abide`.
|
|
754
959
|
*
|
|
755
|
-
* 38.
|
|
960
|
+
* 38.39 — WHAT THE NAME AT `offset` IN A `.abide` IS, asked of the program `diagnose` last lowered
|
|
756
961
|
* it to: its type and documentation for a hover, and where it is declared for a jump. The
|
|
757
962
|
* checked module is asked where the position was copied into it, which is what a member hangs
|
|
758
963
|
* off — `regions.join` is `Array`'s — and the projection where the lowering rewrote it, so a
|
|
@@ -767,17 +972,9 @@ export class TypeSession {
|
|
|
767
972
|
text: string,
|
|
768
973
|
offset: number,
|
|
769
974
|
): Promise<Inspection | undefined> {
|
|
770
|
-
const
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
lowered === undefined ||
|
|
774
|
-
lowered.text !== text ||
|
|
775
|
-
project === undefined
|
|
776
|
-
)
|
|
777
|
-
return undefined
|
|
778
|
-
const asked = lowered.map.fromSource(offset)
|
|
779
|
-
if (asked === undefined || !this.shadows.has(this.keyOf(asked.path)))
|
|
780
|
-
return undefined
|
|
975
|
+
const at = this.askedAt(path, text, offset)
|
|
976
|
+
if (at === undefined) return undefined
|
|
977
|
+
const { project, asked } = at
|
|
781
978
|
const checker = project.checker
|
|
782
979
|
const found = await checker.getSymbolAtPosition(
|
|
783
980
|
asked.path,
|
|
@@ -888,44 +1085,42 @@ export class TypeSession {
|
|
|
888
1085
|
}
|
|
889
1086
|
|
|
890
1087
|
const AMBIENT = 'abide-check.d.ts'
|
|
1088
|
+
|
|
1089
|
+
// What the checker asks for when it resolves an import of `x.abide` (`standIn`).
|
|
1090
|
+
const DECLARATION_SUFFIX = '.abide.d.ts'
|
|
1091
|
+
// Beside the app's tsconfig, so `extends` and every path in it resolve as they would from there.
|
|
1092
|
+
const SESSION_CONFIG = 'tsconfig.abide-session.json'
|
|
1093
|
+
const EXPORTED_DECLARATION_SUFFIX = '.d.abide.ts'
|
|
1094
|
+
|
|
1095
|
+
function isFile(path: string): boolean {
|
|
1096
|
+
try {
|
|
1097
|
+
return statSync(path).isFile()
|
|
1098
|
+
} catch {
|
|
1099
|
+
return false
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
891
1102
|
const ROUTES_DECLARATION = 'abide-routes.d.ts'
|
|
892
1103
|
const URL_SOURCE = Bun.fileURLToPath(new URL('../shared/url', import.meta.url))
|
|
893
1104
|
|
|
894
|
-
/**
|
|
1105
|
+
/**
|
|
1106
|
+
* 30.26 — every page under `root`, and every public file's address, as a key of abide's `Routes`, or
|
|
1107
|
+
* nothing where it has neither.
|
|
1108
|
+
*/
|
|
895
1109
|
function routesDeclaration(root: string): string {
|
|
896
1110
|
const routes = new Set<string>()
|
|
897
|
-
|
|
1111
|
+
// A refused tree is `checkApp`'s finding (38.33), never the checker's crash.
|
|
1112
|
+
const { abide, published } = appFiles(root, () => {})
|
|
1113
|
+
for (const path of abide)
|
|
1114
|
+
if (fileKind(path) === 'page') routes.add(routeOf(path))
|
|
1115
|
+
// 23.19 — and every page a mount places in the app, at its position.
|
|
1116
|
+
for (const { position } of mountedUnder(root, abide))
|
|
1117
|
+
if (fileKind(position) === 'page') routes.add(routeOf(position))
|
|
1118
|
+
for (const address of published.values()) routes.add(address)
|
|
898
1119
|
let keys = ''
|
|
899
1120
|
for (const route of [...routes].sort())
|
|
900
1121
|
keys += ` ${JSON.stringify(route)}: true\n`
|
|
901
1122
|
return `export {}\ndeclare module ${JSON.stringify(URL_SOURCE)} {\n interface Routes {\n${keys} }\n}\n`
|
|
902
1123
|
}
|
|
903
|
-
/**
|
|
904
|
-
* The routes of the pages under `directory`, added to `routes`.
|
|
905
|
-
*
|
|
906
|
-
* A WALK RATHER THAN A GLOB, because it runs on every check and a glob descends `node_modules` and
|
|
907
|
-
* the build before a filter can drop them — 15.6 ms against 5.7 from this repo's root, same pages.
|
|
908
|
-
*/
|
|
909
|
-
function routesUnder(
|
|
910
|
-
directory: string,
|
|
911
|
-
relative: string,
|
|
912
|
-
routes: Set<string>,
|
|
913
|
-
): void {
|
|
914
|
-
let entries: Dirent[]
|
|
915
|
-
try {
|
|
916
|
-
entries = readdirSync(directory, { withFileTypes: true })
|
|
917
|
-
} catch {
|
|
918
|
-
return
|
|
919
|
-
}
|
|
920
|
-
for (const entry of entries) {
|
|
921
|
-
if (entry.name.startsWith('.') || entry.name === 'node_modules')
|
|
922
|
-
continue
|
|
923
|
-
const path = `${relative}${entry.name}`
|
|
924
|
-
if (entry.isDirectory())
|
|
925
|
-
routesUnder(join(directory, entry.name), `${path}/`, routes)
|
|
926
|
-
else if (fileKind(path) === 'page') routes.add(routeOf(path))
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
1124
|
const AMBIENT_DECLARATIONS = `declare module '*.css' {}
|
|
930
1125
|
`
|
|
931
1126
|
|
|
@@ -946,7 +1141,7 @@ export type Diagnosis = {
|
|
|
946
1141
|
/**
|
|
947
1142
|
* Where something is in a file the author wrote, line and column from 1.
|
|
948
1143
|
*
|
|
949
|
-
* 38.
|
|
1144
|
+
* 38.39.
|
|
950
1145
|
*/
|
|
951
1146
|
export type Place = {
|
|
952
1147
|
path: string
|
|
@@ -957,7 +1152,7 @@ export type Place = {
|
|
|
957
1152
|
/**
|
|
958
1153
|
* The name under a cursor: how it is declared, what its doc comment says, and where.
|
|
959
1154
|
*
|
|
960
|
-
* 38.
|
|
1155
|
+
* 38.39.
|
|
961
1156
|
*/
|
|
962
1157
|
export type Inspection = {
|
|
963
1158
|
signature: string
|
|
@@ -134,7 +134,7 @@ async function objectOf(
|
|
|
134
134
|
}
|
|
135
135
|
const schema: Schema = { type: 'object', properties }
|
|
136
136
|
if (required.length > 0) schema.required = required
|
|
137
|
-
// 19.
|
|
137
|
+
// 19.19 — A MEMBER THE TYPE DOES NOT DECLARE IS REFUSED, where no index signature declares the
|
|
138
138
|
// rest: a caller outside the process sends whatever it likes, and a body writing its args to a
|
|
139
139
|
// row would write that too. An author who wants the rest declares a schema of their own.
|
|
140
140
|
schema.additionalProperties = false
|
package/src/compiler/checkApp.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { relative } from 'node:path' // bun exposes no path helpers of its own
|
|
2
|
-
import {
|
|
2
|
+
import { appFiles } from '#server/appTree.ts'
|
|
3
|
+
import { placeOf } from '#shared/routePattern.ts'
|
|
3
4
|
import {
|
|
4
5
|
type AppChecker,
|
|
5
6
|
emitApp,
|
|
6
7
|
resolverIn,
|
|
7
8
|
sourcesUnder,
|
|
8
9
|
withChecker,
|
|
10
|
+
withDependencies,
|
|
9
11
|
} from './compileApp.ts'
|
|
10
12
|
import { abideImportsOf } from './componentGraph.ts'
|
|
11
13
|
import {
|
|
@@ -15,6 +17,7 @@ import {
|
|
|
15
17
|
refusal,
|
|
16
18
|
} from './readSources.ts'
|
|
17
19
|
import type { Diagnosis } from './TypeSession.ts'
|
|
20
|
+
import { writeTypes } from './writeTypes.ts'
|
|
18
21
|
|
|
19
22
|
/**
|
|
20
23
|
* Every finding `abide check` reports for the app at `appRoot`.
|
|
@@ -36,27 +39,40 @@ export async function checkApp(
|
|
|
36
39
|
const { root, sources } = await sourcesUnder(appRoot)
|
|
37
40
|
return withChecker(root, kept, async (session) => {
|
|
38
41
|
const refused: Diagnosis[] = []
|
|
39
|
-
const
|
|
42
|
+
const everyOwn = parseSources(sources, refused)
|
|
43
|
+
await writeTypes(root, everyOwn, true, refused)
|
|
44
|
+
// A file whose projection was refused is found already, and every later step projects it.
|
|
45
|
+
const refusedPaths = new Set<string>()
|
|
46
|
+
for (const found of refused) refusedPaths.add(found.path)
|
|
47
|
+
const own: typeof everyOwn = []
|
|
48
|
+
for (const source of everyOwn)
|
|
49
|
+
if (!refusedPaths.has(source.path)) own.push(source)
|
|
50
|
+
const resolve = resolverIn()
|
|
51
|
+
const parsed = await withDependencies(root, own, resolve, refused)
|
|
40
52
|
const { read, warnings } = await readSources(session, parsed, refused)
|
|
53
|
+
// THE APP'S OWN FILES ARE REPORTED ON: a dependency's is compiled with them (34.27), and
|
|
54
|
+
// what is wrong inside a package is the package's to report.
|
|
55
|
+
const reported: ReadSource[] = []
|
|
56
|
+
for (const source of read)
|
|
57
|
+
if (placeOf(relative(root, source.path)) !== 'dependency')
|
|
58
|
+
reported.push(source)
|
|
41
59
|
const { diagnoses: found, refused: loweringRefused } =
|
|
42
|
-
await session.diagnose(
|
|
60
|
+
await session.diagnose(reported, root)
|
|
43
61
|
const { ambient, unkeyed } = await session.lints(root, parsed)
|
|
44
62
|
// A FILE WHOSE LOWERING REFUSED is `found` already, at its line — emitted too, it throws the
|
|
45
63
|
// same refusal at the root and takes the rest of the app's findings with it.
|
|
46
64
|
const emittable: ReadSource[] = []
|
|
47
65
|
for (const source of read)
|
|
48
66
|
if (!loweringRefused.has(source.path)) emittable.push(source)
|
|
67
|
+
// 23.16-23.18 — what the build's walk would refuse of the tree, at each file it names.
|
|
68
|
+
appFiles(root, (path, message) => {
|
|
69
|
+
refused.push(refusal(`${root}/${path}`, message))
|
|
70
|
+
})
|
|
49
71
|
if (refused.length === 0 && ambient.length === 0)
|
|
50
72
|
try {
|
|
51
73
|
warnings.push(
|
|
52
|
-
...(
|
|
53
|
-
|
|
54
|
-
root,
|
|
55
|
-
session,
|
|
56
|
-
emittable,
|
|
57
|
-
relative(root, buildDirectory(root)),
|
|
58
|
-
)
|
|
59
|
-
).warnings,
|
|
74
|
+
...(await emitApp(root, session, emittable, resolve))
|
|
75
|
+
.warnings,
|
|
60
76
|
)
|
|
61
77
|
} catch (thrown) {
|
|
62
78
|
refused.push(refusal(root, thrown))
|
|
@@ -65,10 +81,12 @@ export async function checkApp(
|
|
|
65
81
|
})
|
|
66
82
|
}
|
|
67
83
|
|
|
84
|
+
const FILES_CONFIG = /^\s*onConfig\s*\(/m
|
|
85
|
+
|
|
68
86
|
/**
|
|
69
87
|
* The findings for one file as an editor holds it, saved or not.
|
|
70
88
|
*
|
|
71
|
-
* 38.
|
|
89
|
+
* 38.11 — ONE FILE AS AN EDITOR HOLDS IT, saved or not: the same findings `checkApp` has for it, for
|
|
72
90
|
* `abide lsp` to publish. The rest of the app is read from disk, as it stands — save a file `held`
|
|
73
91
|
* has, which is one the editor holds too.
|
|
74
92
|
*
|
|
@@ -78,12 +96,11 @@ export async function checkApp(
|
|
|
78
96
|
*/
|
|
79
97
|
export async function checkSource(
|
|
80
98
|
checker: AppChecker,
|
|
81
|
-
root: string,
|
|
82
99
|
path: string,
|
|
83
100
|
text: string,
|
|
84
101
|
held: (path: string) => string | undefined = () => undefined,
|
|
85
102
|
): Promise<Diagnosis[]> {
|
|
86
|
-
const session = checker
|
|
103
|
+
const { session, root } = checker
|
|
87
104
|
const refused: Diagnosis[] = []
|
|
88
105
|
const [page] = parseSources([{ path, text }], refused)
|
|
89
106
|
if (page === undefined) return refused
|
|
@@ -91,7 +108,7 @@ export async function checkSource(
|
|
|
91
108
|
for (const imported of abideImportsOf(
|
|
92
109
|
path,
|
|
93
110
|
page.file,
|
|
94
|
-
resolverIn(
|
|
111
|
+
resolverIn(),
|
|
95
112
|
).values())
|
|
96
113
|
try {
|
|
97
114
|
imports.push({
|
|
@@ -102,6 +119,17 @@ export async function checkSource(
|
|
|
102
119
|
} catch {
|
|
103
120
|
// A component that cannot be read takes anything here.
|
|
104
121
|
}
|
|
122
|
+
// 27.13 — AND EVERY FILE FILING `onConfig`, quietly, so a page that only reads `config()` is
|
|
123
|
+
// typed by defaults it never imported — as `checkApp`'s whole-app program types it. The scan
|
|
124
|
+
// only names candidates (a statement opening on the call); the checker decides which file.
|
|
125
|
+
const lowered = new Set(imports.map((one) => one.path))
|
|
126
|
+
const { sources } = await checker.sources()
|
|
127
|
+
for (const other of sources) {
|
|
128
|
+
if (other.path === path || lowered.has(other.path)) continue
|
|
129
|
+
const text = held(other.path) ?? other.text
|
|
130
|
+
if (FILES_CONFIG.test(text))
|
|
131
|
+
imports.push({ path: other.path, text, quiet: true })
|
|
132
|
+
}
|
|
105
133
|
// A component that does not parse, or that the checker refuses, is its own file's finding and
|
|
106
134
|
// takes anything here: a quiet source's refusal is reported by nobody.
|
|
107
135
|
const { read, warnings } = await readSources(
|