@abide/abide 0.123.3 → 0.124.1
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 +38 -0
- package/docs/REGISTRY.md +45 -12
- package/docs/RULEBOOK.md +29 -16
- 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 +46 -4
- 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/projectShadow.ts +1 -0
- 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/BRIDGE_VERSION.ts +1 -1
- 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 +46 -1002
- 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
|
@@ -5,15 +5,19 @@ import { realOrAsGiven } from '#server/realOrAsGiven.ts'
|
|
|
5
5
|
|
|
6
6
|
import { checkSource, lowerApp } from './checkApp.ts'
|
|
7
7
|
import { citedClauses } from './citedClauses.ts'
|
|
8
|
-
import type { AppChecker } from './compileApp.ts'
|
|
9
8
|
import { type Diagnosis, formatDiagnosis } from './Diagnostics.ts'
|
|
10
9
|
import { DOCS_SITE } from './DOCS_SITE.ts'
|
|
11
10
|
import { formatAbide } from './formatAbide.ts'
|
|
12
11
|
import { isNamedAlone } from './isNamedAlone.ts'
|
|
13
12
|
import { KeptChecker } from './KeptChecker.ts'
|
|
14
13
|
import { lineOf, offsetOf } from './lineOf.ts'
|
|
14
|
+
import {
|
|
15
|
+
completionAt,
|
|
16
|
+
type Inspection,
|
|
17
|
+
type LoweredFile,
|
|
18
|
+
type Place,
|
|
19
|
+
} from './LoweredFile.ts'
|
|
15
20
|
import { nearest } from './nearest.ts'
|
|
16
|
-
import type { Inspection, Place } from './TypeSession.ts'
|
|
17
21
|
|
|
18
22
|
// 38.11 — `abide lsp`: THE `.abide` LANGUAGE SERVER, OVER STDIO. It is `abide check` (38.3) asked
|
|
19
23
|
// about one open document at a time, so the squiggle an editor draws and the line CI fails on are
|
|
@@ -22,8 +26,8 @@ import type { Inspection, Place } from './TypeSession.ts'
|
|
|
22
26
|
// WHAT IT SPEAKS is the part of the protocol a diagnostic needs: the handshake, a document opened,
|
|
23
27
|
// changed and closed, and `publishDiagnostics` back. The document is synced whole (`Full`), so what
|
|
24
28
|
// is checked is exactly the editor's text, saved or not. A hover, a jump to a definition, a
|
|
25
|
-
// completion, the references to a name and a rename of it are asked of the
|
|
26
|
-
// registered (`
|
|
29
|
+
// completion, the references to a name and a rename of it are asked of the lowering that check
|
|
30
|
+
// registered (`LoweredFile`), so they answer about the same text.
|
|
27
31
|
//
|
|
28
32
|
// THE APP IS THE DOCUMENT'S. An editor starts this at the root of whatever it has open, which in a
|
|
29
33
|
// monorepo is not an app at all, so each document is checked against the root of the NEAREST
|
|
@@ -204,29 +208,30 @@ export async function serveLanguage(
|
|
|
204
208
|
)
|
|
205
209
|
}
|
|
206
210
|
|
|
207
|
-
// A question about a document is about the text the editor shows, so a check still settling
|
|
208
|
-
// runs now rather than after the answer; and it is answered in the queue, after that check.
|
|
209
211
|
const inspect = (params: unknown): Promise<Inspection | undefined> =>
|
|
210
|
-
ask(params, (
|
|
211
|
-
checker.session.inspect(path, text, offset),
|
|
212
|
-
)
|
|
212
|
+
ask(params, (lowered, text, offset) => lowered?.inspect(text, offset))
|
|
213
213
|
|
|
214
214
|
// 38.116 — asked after every page of the app is lowered, which is what a page this one never
|
|
215
215
|
// imports needs to be among the references.
|
|
216
216
|
const references = (params: unknown) =>
|
|
217
|
-
ask(
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
217
|
+
ask(
|
|
218
|
+
params,
|
|
219
|
+
(lowered, text, offset) => lowered?.references(text, offset),
|
|
220
|
+
true,
|
|
221
|
+
)
|
|
221
222
|
|
|
223
|
+
// A QUESTION ABOUT A DOCUMENT IS ASKED OF THE LOWERING ITS CHECK REGISTERED (`LoweredFile`), of
|
|
224
|
+
// the text the editor shows: a check still settling runs now rather than after the answer, and
|
|
225
|
+
// the question is answered in the queue, after that check. A lowering of another text answers
|
|
226
|
+
// nothing at an offset into this one, save the props a tag being typed offers (38.117).
|
|
222
227
|
const ask = <Answer>(
|
|
223
228
|
params: unknown,
|
|
224
229
|
question: (
|
|
225
|
-
|
|
226
|
-
path: string,
|
|
230
|
+
lowered: LoweredFile | undefined,
|
|
227
231
|
text: string,
|
|
228
232
|
offset: number,
|
|
229
|
-
) => Promise<Answer | undefined
|
|
233
|
+
) => Promise<Answer | undefined> | undefined,
|
|
234
|
+
everyPage = false,
|
|
230
235
|
): Promise<Answer | undefined> => {
|
|
231
236
|
const { textDocument, position } = params as {
|
|
232
237
|
textDocument: { uri: string }
|
|
@@ -242,9 +247,10 @@ export async function serveLanguage(
|
|
|
242
247
|
const document = documents.get(uri)
|
|
243
248
|
const at = placed(uri)
|
|
244
249
|
if (document === undefined || at === undefined) return undefined
|
|
250
|
+
const checker = await checkerOf(at.app).live()
|
|
251
|
+
if (everyPage) await lowerApp(checker, held)
|
|
245
252
|
return question(
|
|
246
|
-
|
|
247
|
-
at.path,
|
|
253
|
+
checker.session.lowered(at.path),
|
|
248
254
|
document.text,
|
|
249
255
|
offsetOf(document.text, position),
|
|
250
256
|
)
|
|
@@ -337,8 +343,8 @@ export async function serveLanguage(
|
|
|
337
343
|
const trigger = (
|
|
338
344
|
params?.context as { triggerCharacter?: string } | undefined
|
|
339
345
|
)?.triggerCharacter
|
|
340
|
-
const found = await ask(params, (
|
|
341
|
-
|
|
346
|
+
const found = await ask(params, (lowered, text, offset) =>
|
|
347
|
+
completionAt(lowered, text, offset, trigger),
|
|
342
348
|
)
|
|
343
349
|
const items: object[] = []
|
|
344
350
|
for (const entry of found?.entries ?? [])
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SyntaxKind } from 'typescript/unstable/ast'
|
|
2
2
|
|
|
3
|
-
import { COMPONENT_QUERY,
|
|
3
|
+
import { COMPONENT_QUERY, TEXT_SUFFIX } from '#server/MODULE_QUERIES.ts'
|
|
4
4
|
|
|
5
5
|
import { isContextualKeyword, type Token, tokenize } from './tokenize.ts'
|
|
6
6
|
|
|
@@ -473,7 +473,7 @@ export function importedNames(
|
|
|
473
473
|
*/
|
|
474
474
|
function facedSpecifier(specifier: string, lane: Lane): string | undefined {
|
|
475
475
|
if (specifier.endsWith(SOURCE_SUFFIX))
|
|
476
|
-
return `${specifier.slice(0, -SOURCE_SUFFIX.length)}${
|
|
476
|
+
return `${specifier.slice(0, -SOURCE_SUFFIX.length)}${TEXT_SUFFIX}`
|
|
477
477
|
if (lane === 'component' && specifier.endsWith('.abide'))
|
|
478
478
|
return `${specifier}${COMPONENT_QUERY}`
|
|
479
479
|
return undefined
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// The `abide-bridge` release a pairing command names, so a visitor's `bunx` runs the relay this
|
|
2
2
|
// server speaks. `packages/abide/tests/claudeCode.test.ts` holds it to the bridge package's own.
|
|
3
|
-
export const BRIDGE_VERSION = '0.5.
|
|
3
|
+
export const BRIDGE_VERSION = '0.5.2'
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// 38.71-38.75 — WHAT THE DESKTOP WINDOW SHOWS, apart from the window: the app booted here, one
|
|
2
|
+
// connected to elsewhere, or neither — and what the launcher did last, kept across launches.
|
|
3
|
+
|
|
4
|
+
import { mkdir } from 'node:fs/promises' // bun exposes no directory creation of its own
|
|
5
|
+
|
|
6
|
+
import { isFailed } from '#shared/Failed.ts'
|
|
7
|
+
|
|
8
|
+
import { configuredMount } from './config.ts'
|
|
9
|
+
import type { App } from './createApp.ts'
|
|
10
|
+
import { reportUnexpected } from './hooks.ts'
|
|
11
|
+
import type { Launcher } from './launcherOptions.ts'
|
|
12
|
+
import type { Launch } from './launcherPage.ts'
|
|
13
|
+
import { LISTENING } from './requestScope.ts'
|
|
14
|
+
|
|
15
|
+
/** What the launcher did last, and what its forms held, kept across launches. 38.73. */
|
|
16
|
+
type Remembered = {
|
|
17
|
+
last: { start: Record<string, string> } | { connect: string } | null
|
|
18
|
+
values: Record<string, string>
|
|
19
|
+
url: string
|
|
20
|
+
port: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type Failure = NonNullable<Launch['failure']>
|
|
24
|
+
|
|
25
|
+
const LOOPBACK = '127.0.0.1'
|
|
26
|
+
const CONNECT_TIMEOUT_MILLISECONDS = 5000
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The launcher's state machine: start an app here, connect to one elsewhere, disconnect, and
|
|
30
|
+
* remember which in `desktop.json` under `directory`.
|
|
31
|
+
*
|
|
32
|
+
* `boot` boots the app a start serves — `bootEntered` in the executable, anything an `App` in a test.
|
|
33
|
+
*/
|
|
34
|
+
export class DesktopSession {
|
|
35
|
+
private readonly stateFile: string
|
|
36
|
+
private readonly directory: string
|
|
37
|
+
private readonly boot: () => Promise<App>
|
|
38
|
+
readonly remembered: Remembered
|
|
39
|
+
/** What the window shows: the app served here, one elsewhere, or neither. */
|
|
40
|
+
private current: { url: string; app: App | undefined } | undefined
|
|
41
|
+
/** The last start or connect that was refused, until the page shows it. */
|
|
42
|
+
private failure: Failure | undefined
|
|
43
|
+
|
|
44
|
+
private constructor(
|
|
45
|
+
directory: string,
|
|
46
|
+
boot: () => Promise<App>,
|
|
47
|
+
remembered: Remembered,
|
|
48
|
+
) {
|
|
49
|
+
this.directory = directory
|
|
50
|
+
this.stateFile = `${directory}/desktop.json`
|
|
51
|
+
this.boot = boot
|
|
52
|
+
this.remembered = remembered
|
|
53
|
+
this.current = undefined
|
|
54
|
+
this.failure = undefined
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The session `directory`'s `desktop.json` remembers, or a fresh one where there is none. */
|
|
58
|
+
static async open(
|
|
59
|
+
directory: string,
|
|
60
|
+
boot: () => Promise<App>,
|
|
61
|
+
): Promise<DesktopSession> {
|
|
62
|
+
const remembered: Remembered = {
|
|
63
|
+
last: null,
|
|
64
|
+
values: {},
|
|
65
|
+
url: '',
|
|
66
|
+
port: 0,
|
|
67
|
+
...(await Bun.file(`${directory}/desktop.json`)
|
|
68
|
+
.json()
|
|
69
|
+
.catch(() => ({}))),
|
|
70
|
+
}
|
|
71
|
+
return new DesktopSession(directory, boot, remembered)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The address the window shows, `undefined` where it shows the launcher. */
|
|
75
|
+
get shown(): string | undefined {
|
|
76
|
+
return this.current?.url
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The refusal the page has yet to show, handed over once. */
|
|
80
|
+
takeFailure(): Failure | undefined {
|
|
81
|
+
const shown = this.failure
|
|
82
|
+
this.failure = undefined
|
|
83
|
+
return shown
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 38.72 — the form's values are the environment the app is booted under, so 27.8 coerces them
|
|
87
|
+
// and 27.7's schema refuses them as it would any other.
|
|
88
|
+
async start(values: Record<string, string>): Promise<Failure | undefined> {
|
|
89
|
+
await this.disconnect()
|
|
90
|
+
this.remembered.values = values
|
|
91
|
+
for (const [name, value] of Object.entries(values))
|
|
92
|
+
if (value === '') delete process.env[name]
|
|
93
|
+
else process.env[name] = value
|
|
94
|
+
let app: App
|
|
95
|
+
try {
|
|
96
|
+
app = await this.boot()
|
|
97
|
+
} catch (thrown) {
|
|
98
|
+
return this.refuse(refused('start', thrown))
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
await app
|
|
102
|
+
// 38.75.
|
|
103
|
+
.listen({ hostname: LOOPBACK, port: this.remembered.port })
|
|
104
|
+
// The port the last launch served on, so the app's origin — and its storage with
|
|
105
|
+
// it — is the one it had; any free one where that is taken.
|
|
106
|
+
.catch(() => app.listen({ hostname: LOOPBACK, port: 0 }))
|
|
107
|
+
} catch (thrown) {
|
|
108
|
+
await app.stop().catch(reportUnexpected)
|
|
109
|
+
return this.refuse(refused('start', thrown))
|
|
110
|
+
}
|
|
111
|
+
const server = LISTENING.server as Bun.Server<unknown>
|
|
112
|
+
this.remembered.port = server.port ?? 0
|
|
113
|
+
this.remembered.last = { start: values }
|
|
114
|
+
this.current = {
|
|
115
|
+
url: `${server.url.origin}${configuredMount()}/`,
|
|
116
|
+
app,
|
|
117
|
+
}
|
|
118
|
+
await this.remember()
|
|
119
|
+
return undefined
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async connect(given: string): Promise<Failure | undefined> {
|
|
123
|
+
const text = given.trim()
|
|
124
|
+
this.remembered.url = text
|
|
125
|
+
const url = URL.parse(
|
|
126
|
+
/^[a-z][a-z0-9+.-]*:/i.test(text) ? text : `https://${text}`,
|
|
127
|
+
)
|
|
128
|
+
if (
|
|
129
|
+
url === null ||
|
|
130
|
+
(url.protocol !== 'http:' && url.protocol !== 'https:')
|
|
131
|
+
)
|
|
132
|
+
return this.refuse({
|
|
133
|
+
form: 'connect',
|
|
134
|
+
message: `${text} is not an address.`,
|
|
135
|
+
issues: {},
|
|
136
|
+
})
|
|
137
|
+
try {
|
|
138
|
+
await fetch(url, {
|
|
139
|
+
method: 'HEAD',
|
|
140
|
+
redirect: 'manual',
|
|
141
|
+
signal: AbortSignal.timeout(CONNECT_TIMEOUT_MILLISECONDS),
|
|
142
|
+
})
|
|
143
|
+
} catch {
|
|
144
|
+
return this.refuse({
|
|
145
|
+
form: 'connect',
|
|
146
|
+
message: `Nothing answered at ${url.origin}.`,
|
|
147
|
+
issues: {},
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
await this.disconnect()
|
|
151
|
+
this.remembered.last = { connect: url.href }
|
|
152
|
+
this.current = { url: url.href, app: undefined }
|
|
153
|
+
await this.remember()
|
|
154
|
+
return undefined
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Stops the app served here, if one is, and shows neither. */
|
|
158
|
+
async disconnect(): Promise<void> {
|
|
159
|
+
const leaving = this.current
|
|
160
|
+
this.current = undefined
|
|
161
|
+
if (leaving?.app !== undefined)
|
|
162
|
+
await leaving.app.stop().catch(reportUnexpected)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** 38.74 — disconnects, and forgets what was done last so the next launch does not redo it. */
|
|
166
|
+
async forget(): Promise<void> {
|
|
167
|
+
this.remembered.last = null
|
|
168
|
+
await Promise.all([this.remember(), this.disconnect()])
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** 38.73 — WHAT IT DID LAST, done again, where the launcher still offers it. */
|
|
172
|
+
async resume(options: Launcher): Promise<void> {
|
|
173
|
+
const last = this.remembered.last
|
|
174
|
+
if (options.start && last !== null && 'start' in last)
|
|
175
|
+
await this.start(last.start)
|
|
176
|
+
else if (options.connect && last !== null && 'connect' in last)
|
|
177
|
+
await this.connect(last.connect)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private refuse(failure: Failure): Failure {
|
|
181
|
+
this.failure = failure
|
|
182
|
+
return failure
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private async remember(): Promise<void> {
|
|
186
|
+
await mkdir(this.directory, { recursive: true })
|
|
187
|
+
await Bun.write(
|
|
188
|
+
this.stateFile,
|
|
189
|
+
JSON.stringify(this.remembered, null, 4),
|
|
190
|
+
)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** A refusal as the form it came back to: 27.7's issues by field, or the error's message. */
|
|
195
|
+
function refused(form: 'start' | 'connect', thrown: unknown): Failure {
|
|
196
|
+
if (
|
|
197
|
+
isFailed(thrown) &&
|
|
198
|
+
thrown.data !== null &&
|
|
199
|
+
typeof thrown.data === 'object'
|
|
200
|
+
)
|
|
201
|
+
return {
|
|
202
|
+
form,
|
|
203
|
+
message: 'The configuration was refused.',
|
|
204
|
+
issues: Array.isArray(thrown.data)
|
|
205
|
+
? { '': thrown.data as string[] }
|
|
206
|
+
: (thrown.data as Record<string, string[]>),
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
form,
|
|
210
|
+
message: thrown instanceof Error ? thrown.message : String(thrown),
|
|
211
|
+
issues: {},
|
|
212
|
+
}
|
|
213
|
+
}
|