@abide/abide 0.101.0 → 0.103.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 +26 -0
- package/bin/abide.ts +226 -164
- package/docs/REGISTRY.md +730 -0
- package/docs/RULEBOOK.md +2768 -0
- package/docs/abide.md +13127 -0
- package/package.json +6 -3
- package/scaffold/AGENTS.md +38 -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 +341 -71
- package/src/compiler/appMap.ts +96 -0
- package/src/compiler/argsSchemaOf.ts +1 -1
- package/src/compiler/checkApp.ts +53 -8
- package/src/compiler/checkedShadow.ts +73 -6
- package/src/compiler/classifyRead.ts +6 -4
- package/src/compiler/classifyTemplate.ts +1 -1
- package/src/compiler/compileApp.ts +598 -200
- package/src/compiler/compiledEntry.ts +48 -40
- package/src/compiler/componentGraph.ts +57 -71
- package/src/compiler/declaredExports.ts +26 -9
- package/src/compiler/emitClient.ts +142 -72
- package/src/compiler/emitComponent.ts +41 -12
- package/src/compiler/emitSource.ts +102 -13
- package/src/compiler/handlersUnder.ts +164 -76
- package/src/compiler/hooksCalled.ts +35 -0
- package/src/compiler/index.ts +7 -0
- package/src/compiler/mountsOf.ts +150 -0
- package/src/compiler/nearest.ts +14 -0
- package/src/compiler/parseAbide.ts +157 -20
- package/src/compiler/projectShadow.ts +250 -19
- package/src/compiler/reactiveTypes.ts +20 -7
- package/src/compiler/readSources.ts +15 -2
- package/src/compiler/readsIn.ts +62 -8
- package/src/compiler/renderMarkup.ts +3 -3
- package/src/compiler/scaffold.ts +37 -15
- package/src/compiler/serveLanguage.ts +101 -34
- package/src/compiler/sharedModuleWrites.ts +2 -2
- package/src/compiler/sourceMapOf.ts +193 -0
- package/src/compiler/splitImports.ts +205 -83
- package/src/compiler/tokenize.ts +14 -0
- package/src/compiler/writeDefaultProject.ts +29 -2
- package/src/server/CLIENT_BUILD.ts +11 -13
- package/src/server/DEFAULT_PORT.ts +1 -1
- package/src/server/MODULE_QUERIES.ts +27 -0
- package/src/server/TABLES.ts +7 -0
- package/src/server/VIRTUAL_DIRECTORY.ts +4 -5
- package/src/server/abideModules.ts +457 -0
- package/src/server/answerFailure.ts +16 -6
- package/src/server/answerRequest.ts +241 -107
- package/src/server/answerRpc.ts +107 -33
- package/src/server/answerSocket.ts +65 -20
- 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 +238 -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 +38 -37
- package/src/server/componentModules.d.ts +2 -1
- package/src/server/compressAnswer.ts +4 -4
- package/src/server/config.ts +25 -29
- package/src/server/configFields.ts +31 -25
- package/src/server/createApp.ts +77 -79
- package/src/server/devWorker.ts +118 -181
- package/src/server/executableName.ts +4 -0
- package/src/server/fenceServer.ts +57 -32
- package/src/server/generatedSurfaces.ts +99 -66
- package/src/server/hooks.ts +137 -52
- package/src/server/iconAccent.ts +191 -0
- package/src/server/index.ts +12 -7
- package/src/server/installSourceMaps.ts +93 -0
- package/src/server/launcherOptions.ts +33 -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 +6 -6
- package/src/server/negotiateEncoding.ts +1 -1
- package/src/server/preload.ts +13 -0
- 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 +48 -28
- package/src/server/serveBuild.ts +43 -52
- package/src/server/serveDev.ts +79 -37
- package/src/server/shell.ts +7 -22
- package/src/server/sourceMappings.ts +139 -0
- package/src/server/surfaceOf.ts +20 -3
- package/src/server/testBuild.ts +1 -19
- package/src/server/withAbideSources.ts +117 -0
- package/src/shared/ADDRESSES.ts +10 -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/LAUNCHER_FILES.ts +9 -0
- package/src/shared/RESERVED_DIRECTORIES.ts +24 -0
- 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 +2 -1
- package/src/shared/log.ts +27 -13
- package/src/shared/matchRoute.ts +23 -4
- package/src/{ui → shared}/menuBridge.ts +2 -2
- package/src/shared/menuItem.ts +4 -4
- package/src/shared/onMenu.ts +58 -0
- package/src/shared/reactive/Keyed.ts +2 -2
- package/src/shared/reactive/NotLanded.ts +1 -1
- package/src/shared/reactive/Reactive.ts +43 -43
- 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 +1 -1
- package/src/shared/reactive/watch.ts +3 -3
- package/src/shared/refuse.ts +1 -1
- package/src/{ui → shared}/reportMenu.ts +8 -10
- package/src/shared/routePattern.ts +122 -48
- package/src/shared/rpc.ts +37 -8
- 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 +37 -36
- 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/moveNode.ts +1 -1
- package/src/ui/reconcile.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 → index.abide} +0 -0
- /package/src/{compiler → shared}/SPECIFIERS.ts +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'
|
|
@@ -19,16 +21,19 @@ import {
|
|
|
19
21
|
COMPONENT_MODULE,
|
|
20
22
|
COMPONENT_STAND_IN,
|
|
21
23
|
checkedShadow,
|
|
24
|
+
moduleConfigShadow,
|
|
22
25
|
} from './checkedShadow.ts'
|
|
23
26
|
import type { Finding } from './Finding.ts'
|
|
24
27
|
import { CHECKED_VALUES } from './Lowering.ts'
|
|
25
28
|
import { diagnosisAt, lineOf } from './lineOf.ts'
|
|
29
|
+
import { mountedUnder } from './mountsOf.ts'
|
|
26
30
|
import type { AbideFile } from './parseAbide.ts'
|
|
27
31
|
import { projectShadow } from './projectShadow.ts'
|
|
28
32
|
import { abideIn } from './reactiveTypes.ts'
|
|
29
33
|
import type { ParsedSource, ReadSource } from './readSources.ts'
|
|
30
34
|
import { readsIn, type ShadowSpans } from './readsIn.ts'
|
|
31
|
-
import { CHECKED_SUFFIX, ShadowMap } from './ShadowMap.ts'
|
|
35
|
+
import { CHECKED_SUFFIX, SHADOW_SUFFIX, ShadowMap } from './ShadowMap.ts'
|
|
36
|
+
import { SOURCE_SUFFIX } from './splitImports.ts'
|
|
32
37
|
import { templateWarnings } from './templateWarnings.ts'
|
|
33
38
|
|
|
34
39
|
// Where 31.3 gets its answer. A `.abide` file is projected into a SHADOW `.ts` held in a virtual
|
|
@@ -60,6 +65,18 @@ export class TypeSession {
|
|
|
60
65
|
* text's offsets.
|
|
61
66
|
*/
|
|
62
67
|
private shadows = new Map<string, { path: string; text: string }>()
|
|
68
|
+
/**
|
|
69
|
+
* What `diagnose` last read off one walk of the root: the route table it declared (30.26) and the
|
|
70
|
+
* modules it read for their `onConfig` (27.13). Kept until `edited` hears anything move, so a
|
|
71
|
+
* check asked again and again while an editor types walks and reads nothing on disk.
|
|
72
|
+
*/
|
|
73
|
+
private walked:
|
|
74
|
+
| {
|
|
75
|
+
within: string
|
|
76
|
+
routes: string
|
|
77
|
+
configModules: Promise<[string, string][]>
|
|
78
|
+
}
|
|
79
|
+
| undefined = undefined
|
|
63
80
|
/** The checker's own key for a path: resolved, and case-folded where its host folds case. */
|
|
64
81
|
private keyOf: (path: string) => string
|
|
65
82
|
/**
|
|
@@ -83,27 +100,123 @@ export class TypeSession {
|
|
|
83
100
|
) {
|
|
84
101
|
this.api = api
|
|
85
102
|
this.tsconfig = tsconfig
|
|
103
|
+
this.opened = `${dirname(tsconfig)}/${SESSION_CONFIG}`
|
|
86
104
|
this.keyOf = keyOf
|
|
87
105
|
}
|
|
88
106
|
|
|
107
|
+
/**
|
|
108
|
+
* The project this session opens: the app's tsconfig, with every shadow in `outside` a root.
|
|
109
|
+
*
|
|
110
|
+
* 34.25 — A DEPENDENCY'S `.abide` IS TYPED AS THE APP'S OWN, and its tsconfig's `include` never
|
|
111
|
+
* reaches `node_modules` or a linked package's directory. The session opens a config of its own —
|
|
112
|
+
* served from here, never written — that extends the app's, names those shadows as files, which
|
|
113
|
+
* TypeScript adds to what the base config includes (a linked package's shadow, asked for through
|
|
114
|
+
* the link, is reached no other way), and allows the declaration a package's `exports` is asked
|
|
115
|
+
* for by (`standIn`).
|
|
116
|
+
*/
|
|
117
|
+
private readonly opened: string
|
|
118
|
+
|
|
119
|
+
/** The shadows of the files outside the app's own, as `opened` names them. */
|
|
120
|
+
private outside = new Set<string>()
|
|
121
|
+
|
|
122
|
+
private sessionConfig(): string {
|
|
123
|
+
const config: {
|
|
124
|
+
extends: string
|
|
125
|
+
compilerOptions: Record<string, unknown>
|
|
126
|
+
files?: string[]
|
|
127
|
+
} = {
|
|
128
|
+
extends: `./${basename(this.tsconfig)}`,
|
|
129
|
+
// A package's `exports` names `./x.abide` exactly, so the checker asks for its
|
|
130
|
+
// declaration as `./x.d.abide.ts` (`standIn`) — and takes the answer only with this.
|
|
131
|
+
compilerOptions: { allowArbitraryExtensions: true },
|
|
132
|
+
}
|
|
133
|
+
// An empty `files` is a config error, and the base's `include` alone is the app.
|
|
134
|
+
if (this.outside.size > 0) config.files = [...this.outside]
|
|
135
|
+
return JSON.stringify(config)
|
|
136
|
+
}
|
|
137
|
+
|
|
89
138
|
/**
|
|
90
139
|
* Checked-module paths a stand-in answered for, without the `.abide` being lowered.
|
|
91
140
|
*
|
|
92
|
-
* 38.
|
|
141
|
+
* 38.39 — A `.abide` THIS SESSION HAS NOT LOWERED IS STILL A MODULE ANOTHER CAN IMPORT. What its
|
|
93
142
|
* checked module exports does not depend on what the component holds (`COMPONENT_MODULE`), so an
|
|
94
143
|
* editor checking one page resolves the components it imports without lowering each of them.
|
|
95
144
|
* Remembered, so the lowering that replaces a stand-in is reported as a change to it.
|
|
96
145
|
*/
|
|
97
146
|
private stoodIn = new Set<string>()
|
|
98
147
|
|
|
99
|
-
|
|
100
|
-
|
|
148
|
+
/** The app's root, real — set by `openChecker`. */
|
|
149
|
+
root = ''
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* `path` below the app's root, where it lies under it.
|
|
153
|
+
*
|
|
154
|
+
* 23.14 — what a page's route is read off: the directories below the root are it, and the
|
|
155
|
+
* machine's directories above the root are not.
|
|
156
|
+
*/
|
|
157
|
+
belowRoot(path: string): string {
|
|
158
|
+
return path.startsWith(`${this.root}/`)
|
|
159
|
+
? path.slice(this.root.length + 1)
|
|
160
|
+
: path
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* The shadow registered at `name`, or at the same file through its directory's real path.
|
|
165
|
+
*
|
|
166
|
+
* 34.25 — A LINKED PACKAGE IS ASKED FOR THROUGH ITS LINK: the checker resolves `kit/x.abide` to
|
|
167
|
+
* `node_modules/kit/x.d.abide.ts` and the re-export there to the shadow beside it, while the
|
|
168
|
+
* shadow is registered where bun resolved the file — the package's real directory.
|
|
169
|
+
*/
|
|
170
|
+
private shadowAt(name: string): { path: string; text: string } | undefined {
|
|
171
|
+
const found = this.shadows.get(this.keyOf(name))
|
|
172
|
+
// Only a shadow is registered through a link, so any other name is spared the realpath.
|
|
173
|
+
if (
|
|
174
|
+
found !== undefined ||
|
|
175
|
+
!name.includes('/node_modules/') ||
|
|
176
|
+
!(name.endsWith(SHADOW_SUFFIX) || name.endsWith(CHECKED_SUFFIX))
|
|
177
|
+
)
|
|
178
|
+
return found
|
|
179
|
+
let real: string
|
|
101
180
|
try {
|
|
102
|
-
|
|
103
|
-
return undefined
|
|
181
|
+
real = realpathSync(dirname(name))
|
|
104
182
|
} catch {
|
|
105
183
|
return undefined
|
|
106
184
|
}
|
|
185
|
+
return this.shadows.get(this.keyOf(`${real}/${basename(name)}`))
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private standIn(name: string): string | undefined {
|
|
189
|
+
// 34.21 — a file imported as text is its text, which the projection names apart.
|
|
190
|
+
if (name.endsWith(`${SOURCE_SUFFIX}.d.ts`))
|
|
191
|
+
return isFile(name.slice(0, -`${SOURCE_SUFFIX}.d.ts`.length))
|
|
192
|
+
? 'declare const text: string\nexport default text\n'
|
|
193
|
+
: undefined
|
|
194
|
+
// 34.17 — WHAT A `.abide` EXPORTS, to anything importing it: the component its checked
|
|
195
|
+
// module declares, and nothing else. The checker resolves `./x.abide` by trying
|
|
196
|
+
// `./x.abide.d.ts`; the bindings the projection re-exports for the build (16.70) are not
|
|
197
|
+
// the importer's to read.
|
|
198
|
+
//
|
|
199
|
+
// 34.25 — AND THROUGH A PACKAGE'S `exports`, which the checker follows to the exact file and
|
|
200
|
+
// so asks for the arbitrary extension's declaration, `./x.d.abide.ts`, instead.
|
|
201
|
+
const abide = name.endsWith(DECLARATION_SUFFIX)
|
|
202
|
+
? name.slice(0, -'.d.ts'.length)
|
|
203
|
+
: name.endsWith(EXPORTED_DECLARATION_SUFFIX)
|
|
204
|
+
? `${name.slice(0, -EXPORTED_DECLARATION_SUFFIX.length)}.abide`
|
|
205
|
+
: undefined
|
|
206
|
+
if (abide !== undefined) {
|
|
207
|
+
if (!isFile(abide)) return undefined
|
|
208
|
+
const own = `./${basename(abide)}`
|
|
209
|
+
return `export { default } from '${own}${CHECKED_SUFFIX.slice(0, -'.ts'.length)}'
|
|
210
|
+
`
|
|
211
|
+
}
|
|
212
|
+
// A projection not registered — a file with nothing to read, or one this session has not
|
|
213
|
+
// projected yet — exports nothing, so the declaration above always resolves.
|
|
214
|
+
if (name.endsWith(`.abide${SHADOW_SUFFIX}`))
|
|
215
|
+
return isFile(name.slice(0, -SHADOW_SUFFIX.length))
|
|
216
|
+
? 'export {}\n'
|
|
217
|
+
: undefined
|
|
218
|
+
if (!name.endsWith(`.abide${CHECKED_SUFFIX}`)) return undefined
|
|
219
|
+
if (!isFile(name.slice(0, -CHECKED_SUFFIX.length))) return undefined
|
|
107
220
|
this.stoodIn.add(this.keyOf(name))
|
|
108
221
|
return COMPONENT_MODULE
|
|
109
222
|
}
|
|
@@ -114,10 +227,13 @@ export class TypeSession {
|
|
|
114
227
|
cwd: dirname(tsconfig),
|
|
115
228
|
fs: {
|
|
116
229
|
readFile: (name) =>
|
|
117
|
-
session.
|
|
118
|
-
|
|
230
|
+
name === session.opened
|
|
231
|
+
? session.sessionConfig()
|
|
232
|
+
: (session.shadowAt(name)?.text ??
|
|
233
|
+
session.standIn(name)),
|
|
119
234
|
fileExists: (name) =>
|
|
120
|
-
session.
|
|
235
|
+
name === session.opened ||
|
|
236
|
+
session.shadowAt(name) !== undefined ||
|
|
121
237
|
session.standIn(name) !== undefined
|
|
122
238
|
? true
|
|
123
239
|
: undefined,
|
|
@@ -183,6 +299,7 @@ export class TypeSession {
|
|
|
183
299
|
// nobody's listing — so the checker never read it and the refusal below blamed the app's
|
|
184
300
|
// tsconfig for a path this class had mangled.
|
|
185
301
|
const here = this.keyOf(directory)
|
|
302
|
+
this.listed.add(here)
|
|
186
303
|
const mine: string[] = []
|
|
187
304
|
// The key is the checker's where it is registered, so only the incoming directory needs
|
|
188
305
|
// keying — one `toPath` a call rather than one a shadow.
|
|
@@ -241,6 +358,7 @@ export class TypeSession {
|
|
|
241
358
|
* checker is not told what was under it.
|
|
242
359
|
*/
|
|
243
360
|
edited(paths: string[] | undefined): void {
|
|
361
|
+
this.walked = undefined
|
|
244
362
|
if (paths === undefined) {
|
|
245
363
|
this.everything = true
|
|
246
364
|
return
|
|
@@ -265,11 +383,18 @@ export class TypeSession {
|
|
|
265
383
|
}
|
|
266
384
|
continue
|
|
267
385
|
}
|
|
268
|
-
|
|
386
|
+
// A FILE IN A DIRECTORY THE CHECKER HAS NEVER LISTED is a directory move the watcher
|
|
387
|
+
// reported as the file alone — a save that creates `late/` and the page in it
|
|
388
|
+
// at once — and the listing it already holds of the parent does not have it.
|
|
389
|
+
if (directory || !this.listed.has(this.keyOf(dirname(path))))
|
|
390
|
+
this.everything = true
|
|
269
391
|
else this.edits.present.push(path)
|
|
270
392
|
}
|
|
271
393
|
}
|
|
272
394
|
|
|
395
|
+
/** Every directory the checker has asked this session to list (`entriesIn`). */
|
|
396
|
+
private listed = new Set<string>()
|
|
397
|
+
|
|
273
398
|
/**
|
|
274
399
|
* ONE SNAPSHOT TAKES EVERY EDIT WITH IT: the shadows written for it and whatever moved on disk.
|
|
275
400
|
*/
|
|
@@ -282,15 +407,21 @@ export class TypeSession {
|
|
|
282
407
|
this.edits = { present: [], deleted: [] }
|
|
283
408
|
this.everything = false
|
|
284
409
|
this.snapshots += 1
|
|
410
|
+
// AN EDIT NOBODY COULD NAME DROPS THE CACHE, and the files created with it are still named
|
|
411
|
+
// after it: a dropped cache re-reads what the project already listed, and a shadow written
|
|
412
|
+
// for a page in a directory it never listed is not among them.
|
|
413
|
+
if (everything)
|
|
414
|
+
await this.api.updateSnapshot({
|
|
415
|
+
openProjects: [this.opened],
|
|
416
|
+
fileChanges: { invalidateAll: true },
|
|
417
|
+
})
|
|
285
418
|
const snapshot = await this.api.updateSnapshot({
|
|
286
|
-
openProjects: [this.
|
|
287
|
-
fileChanges:
|
|
288
|
-
|
|
289
|
-
:
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
deleted: edits.deleted,
|
|
293
|
-
},
|
|
419
|
+
openProjects: [this.opened],
|
|
420
|
+
fileChanges: {
|
|
421
|
+
created: [...created, ...edits.present],
|
|
422
|
+
changed: [...changed, ...edits.present],
|
|
423
|
+
deleted: edits.deleted,
|
|
424
|
+
},
|
|
294
425
|
})
|
|
295
426
|
this.latest = snapshot.getProjects()[0]
|
|
296
427
|
return this.latest
|
|
@@ -316,14 +447,49 @@ export class TypeSession {
|
|
|
316
447
|
}
|
|
317
448
|
|
|
318
449
|
/**
|
|
319
|
-
*
|
|
450
|
+
* 34.25, 23.19 — makes `path`, a dependency's file, a root of the opened config, telling
|
|
451
|
+
* `changed` the config moved where it was not one already.
|
|
452
|
+
*/
|
|
453
|
+
private openOutside(path: string, changed: string[]): void {
|
|
454
|
+
if (this.outside.has(path)) return
|
|
455
|
+
this.outside.add(path)
|
|
456
|
+
if (!changed.includes(this.opened)) changed.push(this.opened)
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* The project, with each of `modules` outside the app's own a root of it.
|
|
320
461
|
*
|
|
321
462
|
* THE SNAPSHOT IS THE SESSION'S, AND A QUESTION IS ANY MODULE'S: what a handler declares
|
|
322
463
|
* (`declaredExports`) is asked of the same snapshot the reads were, taken only where something
|
|
323
464
|
* moved since.
|
|
465
|
+
*
|
|
466
|
+
* 23.19 — a handler file a mount places is a package's, which the app's `include` does not reach:
|
|
467
|
+
* what it exports is asked all the same (16.73), as a mounted `.abide`'s shadow is.
|
|
324
468
|
*/
|
|
325
|
-
|
|
326
|
-
|
|
469
|
+
async projectWith(
|
|
470
|
+
modules: readonly string[],
|
|
471
|
+
): Promise<Project | undefined> {
|
|
472
|
+
const changed: string[] = []
|
|
473
|
+
for (const path of modules) {
|
|
474
|
+
const canonical = this.canonical(path)
|
|
475
|
+
// The app's own is a root already: telling the config it moved reloads the project.
|
|
476
|
+
if (placeOf(this.belowRoot(canonical)) === 'dependency')
|
|
477
|
+
this.openOutside(canonical, changed)
|
|
478
|
+
}
|
|
479
|
+
const project = await this.currentProject([], changed)
|
|
480
|
+
if (project === undefined) return project
|
|
481
|
+
// A KEPT SESSION HEARS OF A FILE ONLY AS A WATCHER REPORTS IT, and one saved into a
|
|
482
|
+
// directory made since the last snapshot is reported as the directory: it is no root yet,
|
|
483
|
+
// and is told to the next snapshot as created.
|
|
484
|
+
const found = await Promise.all(
|
|
485
|
+
modules.map((path) => project.program.getSourceFile(path)),
|
|
486
|
+
)
|
|
487
|
+
const missing: string[] = []
|
|
488
|
+
for (let at = 0; at < modules.length; at += 1)
|
|
489
|
+
if (found[at] === undefined) missing.push(modules[at] as string)
|
|
490
|
+
return missing.length === 0
|
|
491
|
+
? project
|
|
492
|
+
: this.currentProject(missing, [...missing])
|
|
327
493
|
}
|
|
328
494
|
|
|
329
495
|
/**
|
|
@@ -353,6 +519,15 @@ export class TypeSession {
|
|
|
353
519
|
return resolve(dirname(this.tsconfig), path)
|
|
354
520
|
}
|
|
355
521
|
|
|
522
|
+
/**
|
|
523
|
+
* Where the checker holds a `.abide`'s projection, once `readsFor` has registered it.
|
|
524
|
+
*
|
|
525
|
+
* 16.70 — what a `<script server>` binds is read off it as a `.ts` module's exports are.
|
|
526
|
+
*/
|
|
527
|
+
projectionOf(abidePath: string): string {
|
|
528
|
+
return `${this.canonical(abidePath)}${SHADOW_SUFFIX}`
|
|
529
|
+
}
|
|
530
|
+
|
|
356
531
|
/**
|
|
357
532
|
* A file the checker reads out of `shadows`, reported to the next snapshot only when it is new,
|
|
358
533
|
* its text moved, or a stand-in answered for it until now.
|
|
@@ -399,6 +574,7 @@ export class TypeSession {
|
|
|
399
574
|
async readsFor(
|
|
400
575
|
sources: readonly (Pick<ParsedSource, 'path' | 'file'> & {
|
|
401
576
|
text?: string
|
|
577
|
+
position?: string | undefined
|
|
402
578
|
})[],
|
|
403
579
|
): Promise<{ answers: CheckerAnswer[]; warnings: Diagnosis[] }> {
|
|
404
580
|
const answered = await this.classify(sources)
|
|
@@ -421,6 +597,7 @@ export class TypeSession {
|
|
|
421
597
|
private async classify(
|
|
422
598
|
sources: readonly (Pick<ParsedSource, 'path' | 'file'> & {
|
|
423
599
|
text?: string
|
|
600
|
+
position?: string | undefined
|
|
424
601
|
})[],
|
|
425
602
|
): Promise<{ answers: CheckerAnswer[]; warnings: Diagnosis[] }> {
|
|
426
603
|
const warnings: Diagnosis[] = []
|
|
@@ -443,8 +620,14 @@ export class TypeSession {
|
|
|
443
620
|
// that scope on both sides. The projection holds both from offset 0.
|
|
444
621
|
const scripts = projection.text.slice(0, projection.scripts.end)
|
|
445
622
|
// 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
|
-
|
|
623
|
+
// writes `{format(total)}` has a position only the checker can prove static. And a
|
|
624
|
+
// `<script server>` always is (16.70): what it binds is read off this shadow, re-exported
|
|
625
|
+
// under `PRIVATE_EXPORT`.
|
|
626
|
+
if (
|
|
627
|
+
!DECLARES.test(scripts) &&
|
|
628
|
+
projection.slots.size === 0 &&
|
|
629
|
+
source.file.servers.length === 0
|
|
630
|
+
) {
|
|
448
631
|
queries.push(undefined)
|
|
449
632
|
continue
|
|
450
633
|
}
|
|
@@ -463,11 +646,14 @@ export class TypeSession {
|
|
|
463
646
|
// AN UNCHANGED SHADOW IS NOT REPORTED, which is what a kept session saves on a save:
|
|
464
647
|
// the one page that moved is re-read rather than all of them.
|
|
465
648
|
this.register(map.shadowPath, projection.text, created, changed)
|
|
649
|
+
if (placeOf(this.belowRoot(source.path)) === 'dependency')
|
|
650
|
+
this.openOutside(map.shadowPath, changed)
|
|
466
651
|
queries.push({
|
|
467
652
|
map,
|
|
468
653
|
spans: {
|
|
469
654
|
scriptsEnd: projection.scripts.end,
|
|
470
655
|
modules: projection.modules,
|
|
656
|
+
servers: projection.servers,
|
|
471
657
|
scripts: projection.scripts,
|
|
472
658
|
positions: projection.positions,
|
|
473
659
|
},
|
|
@@ -504,7 +690,8 @@ export class TypeSession {
|
|
|
504
690
|
sourceFile,
|
|
505
691
|
query.map.shadowPath,
|
|
506
692
|
query.spans,
|
|
507
|
-
|
|
693
|
+
// 23.19 — a mounted page's route is its position's.
|
|
694
|
+
source.position ?? this.belowRoot(source.path),
|
|
508
695
|
source.file,
|
|
509
696
|
)
|
|
510
697
|
answers[at] = read.answer
|
|
@@ -634,7 +821,10 @@ export class TypeSession {
|
|
|
634
821
|
* reported at its line — `abide check` emits the rest of the app without them.
|
|
635
822
|
*/
|
|
636
823
|
async diagnose(
|
|
637
|
-
sources: (Omit<ReadSource, 'quiet'> & {
|
|
824
|
+
sources: (Omit<ReadSource, 'quiet' | 'position'> & {
|
|
825
|
+
quiet?: boolean
|
|
826
|
+
position?: string | undefined
|
|
827
|
+
})[],
|
|
638
828
|
within: string,
|
|
639
829
|
scripts = true,
|
|
640
830
|
): Promise<{ diagnoses: Diagnosis[]; refused: Set<string> }> {
|
|
@@ -664,12 +854,30 @@ export class TypeSession {
|
|
|
664
854
|
created,
|
|
665
855
|
changed,
|
|
666
856
|
)
|
|
857
|
+
if (this.walked?.within !== within) {
|
|
858
|
+
// A refused tree is `checkApp`'s finding (38.33), never the checker's crash.
|
|
859
|
+
const files = appFiles(within, () => {})
|
|
860
|
+
this.walked = {
|
|
861
|
+
within,
|
|
862
|
+
routes: routesDeclaration(within, files),
|
|
863
|
+
configModules: configModulesUnder(within, files),
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
// 27.13 — what a handler file or a server module defaults, beside it.
|
|
867
|
+
for (const [path, text] of await this.walked.configModules) {
|
|
868
|
+
const shadow = moduleConfigShadow(text)
|
|
869
|
+
const at = `${this.canonical(path)}${CONFIG_SUFFIX}`
|
|
870
|
+
// One that stops filing keeps its place and declares nothing, so the next snapshot is
|
|
871
|
+
// told it moved.
|
|
872
|
+
if (shadow !== undefined || this.shadows.has(this.keyOf(at)))
|
|
873
|
+
this.register(at, shadow ?? 'export {}\n', created, changed)
|
|
874
|
+
}
|
|
667
875
|
// 30.26 — THE APP'S ROUTE TABLE, which `url`'s literal is held to: every page under the
|
|
668
876
|
// root, declared into abide's own `Routes`. A module rather than a script, since only a
|
|
669
877
|
// module augments one.
|
|
670
878
|
this.register(
|
|
671
879
|
`${this.canonical(within)}/${ROUTES_DECLARATION}`,
|
|
672
|
-
|
|
880
|
+
this.walked.routes,
|
|
673
881
|
created,
|
|
674
882
|
changed,
|
|
675
883
|
)
|
|
@@ -749,10 +957,58 @@ export class TypeSession {
|
|
|
749
957
|
return { diagnoses: found, refused: refusing }
|
|
750
958
|
}
|
|
751
959
|
|
|
960
|
+
/**
|
|
961
|
+
* Where the author's `offset` in a `.abide` stands in what the checker reads, and the program
|
|
962
|
+
* `diagnose` last lowered it to — or `undefined` where `text` is not what was lowered.
|
|
963
|
+
*/
|
|
964
|
+
private askedAt(
|
|
965
|
+
path: string,
|
|
966
|
+
text: string,
|
|
967
|
+
offset: number,
|
|
968
|
+
):
|
|
969
|
+
| { project: Project; asked: { path: string; position: number } }
|
|
970
|
+
| undefined {
|
|
971
|
+
const lowered = this.loweredForInspect.get(this.keyOf(path))
|
|
972
|
+
const project = this.latest
|
|
973
|
+
if (
|
|
974
|
+
lowered === undefined ||
|
|
975
|
+
lowered.text !== text ||
|
|
976
|
+
project === undefined
|
|
977
|
+
)
|
|
978
|
+
return undefined
|
|
979
|
+
const asked = lowered.map.fromSource(offset)
|
|
980
|
+
if (asked === undefined || !this.shadows.has(this.keyOf(asked.path)))
|
|
981
|
+
return undefined
|
|
982
|
+
return { project, asked }
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
/**
|
|
986
|
+
* The members that may be written at `offset` in a `.abide`, or `undefined` where none can be.
|
|
987
|
+
*
|
|
988
|
+
* 38.98 — ASKED WHERE A HOVER IS (`askedAt`), so `config().` offers the fields 27.13 declared and
|
|
989
|
+
* a name read by name offers its value's members (31.1). MEMBERS ALONE: at an identifier the
|
|
990
|
+
* API rejects ("completion list needs auto imports"), which the language server answers as
|
|
991
|
+
* nothing offered.
|
|
992
|
+
*/
|
|
993
|
+
async complete(
|
|
994
|
+
path: string,
|
|
995
|
+
text: string,
|
|
996
|
+
offset: number,
|
|
997
|
+
triggerCharacter?: string,
|
|
998
|
+
): Promise<CompletionInfo | undefined> {
|
|
999
|
+
const at = this.askedAt(path, text, offset)
|
|
1000
|
+
if (at === undefined) return undefined
|
|
1001
|
+
return at.project.checker.getCompletionsAtPosition(
|
|
1002
|
+
at.asked.path,
|
|
1003
|
+
at.asked.position,
|
|
1004
|
+
{ triggerCharacter },
|
|
1005
|
+
)
|
|
1006
|
+
}
|
|
1007
|
+
|
|
752
1008
|
/**
|
|
753
1009
|
* The hover signature, documentation and definitions of the name at `offset` in a `.abide`.
|
|
754
1010
|
*
|
|
755
|
-
* 38.
|
|
1011
|
+
* 38.39 — WHAT THE NAME AT `offset` IN A `.abide` IS, asked of the program `diagnose` last lowered
|
|
756
1012
|
* it to: its type and documentation for a hover, and where it is declared for a jump. The
|
|
757
1013
|
* checked module is asked where the position was copied into it, which is what a member hangs
|
|
758
1014
|
* off — `regions.join` is `Array`'s — and the projection where the lowering rewrote it, so a
|
|
@@ -767,17 +1023,9 @@ export class TypeSession {
|
|
|
767
1023
|
text: string,
|
|
768
1024
|
offset: number,
|
|
769
1025
|
): 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
|
|
1026
|
+
const at = this.askedAt(path, text, offset)
|
|
1027
|
+
if (at === undefined) return undefined
|
|
1028
|
+
const { project, asked } = at
|
|
781
1029
|
const checker = project.checker
|
|
782
1030
|
const found = await checker.getSymbolAtPosition(
|
|
783
1031
|
asked.path,
|
|
@@ -888,44 +1136,43 @@ export class TypeSession {
|
|
|
888
1136
|
}
|
|
889
1137
|
|
|
890
1138
|
const AMBIENT = 'abide-check.d.ts'
|
|
1139
|
+
|
|
1140
|
+
// What the checker asks for when it resolves an import of `x.abide` (`standIn`).
|
|
1141
|
+
const DECLARATION_SUFFIX = '.abide.d.ts'
|
|
1142
|
+
// Beside the app's tsconfig, so `extends` and every path in it resolve as they would from there.
|
|
1143
|
+
const SESSION_CONFIG = 'tsconfig.abide-session.json'
|
|
1144
|
+
const EXPORTED_DECLARATION_SUFFIX = '.d.abide.ts'
|
|
1145
|
+
|
|
1146
|
+
function isFile(path: string): boolean {
|
|
1147
|
+
try {
|
|
1148
|
+
return statSync(path).isFile()
|
|
1149
|
+
} catch {
|
|
1150
|
+
return false
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
891
1153
|
const ROUTES_DECLARATION = 'abide-routes.d.ts'
|
|
892
1154
|
const URL_SOURCE = Bun.fileURLToPath(new URL('../shared/url', import.meta.url))
|
|
893
1155
|
|
|
894
|
-
/**
|
|
895
|
-
|
|
1156
|
+
/**
|
|
1157
|
+
* 30.26 — every page under `root`, and every public file's address, as a key of abide's `Routes`, or
|
|
1158
|
+
* nothing where it has neither.
|
|
1159
|
+
*/
|
|
1160
|
+
function routesDeclaration(
|
|
1161
|
+
root: string,
|
|
1162
|
+
{ abide, published }: ReturnType<typeof appFiles>,
|
|
1163
|
+
): string {
|
|
896
1164
|
const routes = new Set<string>()
|
|
897
|
-
|
|
1165
|
+
for (const path of abide)
|
|
1166
|
+
if (fileKind(path) === 'page') routes.add(routeOf(path))
|
|
1167
|
+
// 23.19 — and every page a mount places in the app, at its position.
|
|
1168
|
+
for (const { position } of mountedUnder(root, abide))
|
|
1169
|
+
if (fileKind(position) === 'page') routes.add(routeOf(position))
|
|
1170
|
+
for (const address of published.values()) routes.add(address)
|
|
898
1171
|
let keys = ''
|
|
899
1172
|
for (const route of [...routes].sort())
|
|
900
1173
|
keys += ` ${JSON.stringify(route)}: true\n`
|
|
901
1174
|
return `export {}\ndeclare module ${JSON.stringify(URL_SOURCE)} {\n interface Routes {\n${keys} }\n}\n`
|
|
902
1175
|
}
|
|
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
1176
|
const AMBIENT_DECLARATIONS = `declare module '*.css' {}
|
|
930
1177
|
`
|
|
931
1178
|
|
|
@@ -946,7 +1193,7 @@ export type Diagnosis = {
|
|
|
946
1193
|
/**
|
|
947
1194
|
* Where something is in a file the author wrote, line and column from 1.
|
|
948
1195
|
*
|
|
949
|
-
* 38.
|
|
1196
|
+
* 38.39.
|
|
950
1197
|
*/
|
|
951
1198
|
export type Place = {
|
|
952
1199
|
path: string
|
|
@@ -957,7 +1204,7 @@ export type Place = {
|
|
|
957
1204
|
/**
|
|
958
1205
|
* The name under a cursor: how it is declared, what its doc comment says, and where.
|
|
959
1206
|
*
|
|
960
|
-
* 38.
|
|
1207
|
+
* 38.39.
|
|
961
1208
|
*/
|
|
962
1209
|
export type Inspection = {
|
|
963
1210
|
signature: string
|
|
@@ -1077,3 +1324,26 @@ function flatten(diagnostic: {
|
|
|
1077
1324
|
message += `\n ${flatten(link as Parameters<typeof flatten>[0]).replaceAll('\n', '\n ')}`
|
|
1078
1325
|
return message
|
|
1079
1326
|
}
|
|
1327
|
+
|
|
1328
|
+
/** What a module that is no `.abide` is checked beside as, for the `Config` it files (27.13). */
|
|
1329
|
+
const CONFIG_SUFFIX = '.config-shadow.ts'
|
|
1330
|
+
|
|
1331
|
+
/**
|
|
1332
|
+
* Every handler file and every module under a `server` directory below `within` that names
|
|
1333
|
+
* `onConfig`, with its text — the modules a server runs that 27.13 types `Config` off.
|
|
1334
|
+
*/
|
|
1335
|
+
async function configModulesUnder(
|
|
1336
|
+
within: string,
|
|
1337
|
+
{ handlers, modules }: ReturnType<typeof appFiles>,
|
|
1338
|
+
): Promise<[string, string][]> {
|
|
1339
|
+
const paths: string[] = []
|
|
1340
|
+
for (const name of handlers) paths.push(`${within}/${name}`)
|
|
1341
|
+
for (const name of modules)
|
|
1342
|
+
if (placeOf(name) === 'server') paths.push(`${within}/${name}`)
|
|
1343
|
+
const texts = await Promise.all(paths.map((path) => Bun.file(path).text()))
|
|
1344
|
+
const found: [string, string][] = []
|
|
1345
|
+
for (let at = 0; at < paths.length; at += 1)
|
|
1346
|
+
if ((texts[at] as string).includes('onConfig'))
|
|
1347
|
+
found.push([paths[at] as string, texts[at] as string])
|
|
1348
|
+
return found
|
|
1349
|
+
}
|