@abide/abide 0.123.2 → 0.124.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ABIDE.md +8 -5
- package/CHANGELOG.md +58 -0
- package/bin/abide.ts +36 -65
- package/docs/REGISTRY.md +45 -12
- package/docs/RULEBOOK.md +24 -14
- package/docs/abide.md +999 -459
- package/package.json +1 -1
- package/src/compiler/ARRAY_MUTATORS.ts +20 -0
- package/src/compiler/Diagnostics.ts +198 -0
- package/src/compiler/KeptChecker.ts +97 -0
- package/src/compiler/LoweredFile.ts +609 -0
- package/src/compiler/Lowering.ts +18 -1
- package/src/compiler/PositionedRefusal.ts +1 -1
- 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 +76 -1743
- package/src/compiler/appLayout.ts +28 -20
- package/src/compiler/appLints.ts +109 -0
- package/src/compiler/argsDefaultsIn.ts +1 -1
- package/src/compiler/checkApp.ts +64 -68
- package/src/compiler/checkedShadow.ts +2 -2
- package/src/compiler/classifyRead.ts +16 -1
- package/src/compiler/compileApp.ts +102 -71
- package/src/compiler/declaredExports.ts +46 -21
- package/src/compiler/diagnoseSources.ts +232 -0
- package/src/compiler/emitClient.ts +1 -1
- package/src/compiler/formatAbide.ts +7 -8
- package/src/compiler/handlersUnder.ts +32 -63
- package/src/compiler/lineOf.ts +0 -19
- package/src/compiler/matchersOf.ts +3 -3
- package/src/compiler/mountsOf.ts +3 -3
- package/src/compiler/reactiveReads.ts +235 -0
- package/src/compiler/reactiveTypes.ts +13 -0
- package/src/compiler/readSources.ts +16 -18
- package/src/compiler/readsIn.ts +181 -82
- package/src/compiler/routesDeclaration.ts +46 -0
- package/src/compiler/scaffold.ts +7 -2
- package/src/compiler/serveLanguage.ts +46 -74
- package/src/compiler/sharedModuleWrites.ts +1 -1
- package/src/compiler/splitImports.ts +2 -2
- package/src/compiler/templateWarnings.ts +1 -2
- package/src/compiler/trackingWarnings.ts +1 -1
- package/src/server/CLIENT_BUILD.ts +4 -32
- 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/RESUME_MILLISECONDS.ts +2 -0
- package/src/server/abideModules.ts +7 -7
- package/src/server/answerFailure.ts +3 -2
- package/src/server/answerRequest.ts +16 -369
- package/src/server/answerRpc.ts +48 -302
- package/src/server/answerSocket.ts +41 -490
- package/src/server/appBuild.ts +377 -0
- package/src/server/appKit.ts +1 -1
- package/src/server/appPlugins.ts +15 -17
- package/src/server/appTree.ts +6 -5
- package/src/server/argsContract.ts +209 -0
- package/src/server/bridgeRelay.ts +153 -65
- package/src/server/claudeCode.ts +30 -127
- package/src/server/config.ts +6 -7
- package/src/server/createApp.ts +38 -66
- package/src/server/csp.ts +1 -1
- package/src/server/deferredBuilds.ts +3 -16
- package/src/server/devWorker.ts +11 -15
- package/src/server/faceOf.ts +1 -1
- package/src/server/generatedSurfaces.ts +2 -1
- package/src/server/handlerFileMount.ts +40 -0
- package/src/server/index.ts +3 -3
- 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/packageDirectory.ts +14 -0
- package/src/server/preload.ts +21 -2
- package/src/server/principal.ts +5 -4
- package/src/server/remote.ts +2 -2
- package/src/server/render.ts +31 -59
- package/src/server/requestScope.ts +5 -36
- package/src/server/runCompiled.ts +1 -1
- package/src/server/runDesktop.ts +20 -448
- package/src/server/seedBuffer.ts +12 -12
- package/src/server/serveDev.ts +24 -58
- package/src/server/servedApp.ts +390 -0
- package/src/server/shell.ts +1 -1
- package/src/server/signInChanged.ts +20 -0
- package/src/server/surfaceOf.ts +3 -2
- package/src/server/versionOf.ts +7 -16
- package/src/shared/RPC_SERVER.ts +27 -31
- package/src/shared/Schema.ts +1 -2
- package/src/shared/applyPatch.ts +1 -1
- 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/muxFrames.ts +1 -1
- package/src/shared/reactive/Keyed.ts +24 -16
- package/src/shared/reactive/Reactive.ts +37 -991
- package/src/shared/reactive/argsContract.ts +131 -4
- 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 -23
- package/src/shared/reactive/pairReactive.ts +5 -3
- package/src/shared/reactive/patchLog.ts +79 -0
- package/src/shared/reactive/patchMarks.ts +34 -0
- package/src/shared/reactive/producer.ts +121 -0
- package/src/shared/reactive/recordWrites.ts +11 -14
- package/src/shared/reactive/scoped.ts +3 -2
- package/src/shared/reactive/selections.ts +209 -0
- package/src/shared/reactive/share.ts +4 -3
- package/src/shared/reactive/watch.ts +3 -4
- package/src/shared/rpc.ts +20 -24
- package/src/shared/validateJson.ts +1 -2
- package/src/shared/writePath.ts +29 -0
- package/src/ui/ArmRegion.ts +10 -4
- package/src/ui/Level.ts +11 -20
- package/src/ui/LevelChain.ts +272 -0
- package/src/ui/Router.ts +106 -313
- package/src/ui/Scope.ts +17 -3
- package/src/ui/TryRegion.ts +22 -23
- package/src/ui/bindSpread.ts +2 -4
- package/src/ui/listenForRoutes.ts +3 -10
- package/src/ui/outboxQueue.ts +23 -11
- package/src/ui/outcomeOf.ts +120 -0
- package/src/ui/reconcile.ts +4 -6
- package/src/ui/routing.ts +2 -1
- package/src/ui/rows.ts +2 -2
- package/src/ui/setAttribute.ts +12 -3
- package/src/ui/socketWire.ts +28 -13
- package/src/ui/wire.ts +9 -10
- package/src/compiler/Finding.ts +0 -2
- package/src/compiler/Refusals.ts +0 -65
- package/src/compiler/formatDiagnosis.ts +0 -20
- package/src/server/appDirectories.ts +0 -9
- package/src/server/argsFor.ts +0 -39
- package/src/server/buildDirectory.ts +0 -10
- package/src/server/coerceArgs.ts +0 -109
- package/src/server/derivedArgs.ts +0 -26
- package/src/server/runBuild.ts +0 -24
- package/src/server/serveBuild.ts +0 -144
- package/src/server/testBuild.ts +0 -220
- 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
- package/src/shared/reactive/FOLLOWED.ts +0 -8
- package/src/shared/reactive/PATCHES.ts +0 -11
- package/src/shared/reactive/PATCH_SINKS.ts +0 -26
- package/src/shared/reactive/canonicalKey.ts +0 -95
|
@@ -1,18 +1,23 @@
|
|
|
1
|
-
import { existsSync
|
|
1
|
+
import { existsSync } from 'node:fs' // bun exposes no sync existence check of its own
|
|
2
2
|
import { dirname } from 'node:path' // bun exposes no path helpers of its own
|
|
3
3
|
|
|
4
4
|
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
|
|
8
|
+
import { type Diagnosis, formatDiagnosis } from './Diagnostics.ts'
|
|
9
9
|
import { DOCS_SITE } from './DOCS_SITE.ts'
|
|
10
10
|
import { formatAbide } from './formatAbide.ts'
|
|
11
|
-
import { formatDiagnosis } from './formatDiagnosis.ts'
|
|
12
11
|
import { isNamedAlone } from './isNamedAlone.ts'
|
|
12
|
+
import { KeptChecker } from './KeptChecker.ts'
|
|
13
13
|
import { lineOf, offsetOf } from './lineOf.ts'
|
|
14
|
+
import {
|
|
15
|
+
completionAt,
|
|
16
|
+
type Inspection,
|
|
17
|
+
type LoweredFile,
|
|
18
|
+
type Place,
|
|
19
|
+
} from './LoweredFile.ts'
|
|
14
20
|
import { nearest } from './nearest.ts'
|
|
15
|
-
import type { Diagnosis, Inspection, Place } from './TypeSession.ts'
|
|
16
21
|
|
|
17
22
|
// 38.11 — `abide lsp`: THE `.abide` LANGUAGE SERVER, OVER STDIO. It is `abide check` (38.3) asked
|
|
18
23
|
// about one open document at a time, so the squiggle an editor draws and the line CI fails on are
|
|
@@ -21,8 +26,8 @@ import type { Diagnosis, Inspection, Place } from './TypeSession.ts'
|
|
|
21
26
|
// WHAT IT SPEAKS is the part of the protocol a diagnostic needs: the handshake, a document opened,
|
|
22
27
|
// changed and closed, and `publishDiagnostics` back. The document is synced whole (`Full`), so what
|
|
23
28
|
// is checked is exactly the editor's text, saved or not. A hover, a jump to a definition, a
|
|
24
|
-
// completion, the references to a name and a rename of it are asked of the
|
|
25
|
-
// 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.
|
|
26
31
|
//
|
|
27
32
|
// THE APP IS THE DOCUMENT'S. An editor starts this at the root of whatever it has open, which in a
|
|
28
33
|
// monorepo is not an app at all, so each document is checked against the root of the NEAREST
|
|
@@ -64,8 +69,7 @@ export async function serveLanguage(
|
|
|
64
69
|
): Promise<void> {
|
|
65
70
|
const documents = new Map<string, Document>()
|
|
66
71
|
const timers = new Map<string, ReturnType<typeof setTimeout>>()
|
|
67
|
-
const checkers = new Map<string,
|
|
68
|
-
const watchers = new Map<string, FSWatcher>()
|
|
72
|
+
const checkers = new Map<string, KeptChecker>()
|
|
69
73
|
// Each app's real root, to the root the editor reached it by.
|
|
70
74
|
const links = new Map<string, string>()
|
|
71
75
|
let queue: Promise<void> = Promise.resolve()
|
|
@@ -133,37 +137,19 @@ export async function serveLanguage(
|
|
|
133
137
|
const held = (path: string): string | undefined =>
|
|
134
138
|
documents.get(Bun.pathToFileURL(spelled(path)).href)?.text
|
|
135
139
|
|
|
136
|
-
// An app's checker,
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
|
|
140
|
-
const checkerOf = async (app: string): Promise<AppChecker> => {
|
|
140
|
+
// An app's checker, kept for the session — and TOLD WHAT MOVED ON DISK, or it answers every
|
|
141
|
+
// check from the text it read when it opened: an export added to a `lib` module the page
|
|
142
|
+
// imports is "no exported member" until the editor restarts this (`KeptChecker`).
|
|
143
|
+
const checkerOf = (app: string): KeptChecker => {
|
|
141
144
|
let checker = checkers.get(app)
|
|
142
|
-
if (checker !== undefined && !checker.current()) {
|
|
143
|
-
checkers.delete(app)
|
|
144
|
-
await checker.close().catch(() => {})
|
|
145
|
-
checker = undefined
|
|
146
|
-
}
|
|
147
145
|
if (checker === undefined) {
|
|
148
|
-
checker =
|
|
146
|
+
checker = new KeptChecker(app, () => {
|
|
147
|
+
// What an open page says may be decided by the file that moved.
|
|
148
|
+
for (const uri of documents.keys())
|
|
149
|
+
if (placed(uri)?.app === app) check(uri)
|
|
150
|
+
})
|
|
149
151
|
checkers.set(app, checker)
|
|
150
152
|
}
|
|
151
|
-
if (!watchers.has(app))
|
|
152
|
-
watchers.set(
|
|
153
|
-
app,
|
|
154
|
-
watch(app, { recursive: true }, (_event, filename) => {
|
|
155
|
-
checkers
|
|
156
|
-
.get(app)
|
|
157
|
-
?.saw(
|
|
158
|
-
filename === null
|
|
159
|
-
? undefined
|
|
160
|
-
: `${app}/${filename}`,
|
|
161
|
-
)
|
|
162
|
-
// What an open page says may be decided by the file that moved.
|
|
163
|
-
for (const uri of documents.keys())
|
|
164
|
-
if (placed(uri)?.app === app) check(uri)
|
|
165
|
-
}),
|
|
166
|
-
)
|
|
167
153
|
return checker
|
|
168
154
|
}
|
|
169
155
|
|
|
@@ -172,11 +158,9 @@ export async function serveLanguage(
|
|
|
172
158
|
// keeps one for the session. Queued behind the checks, so none is in flight when it closes.
|
|
173
159
|
const closeIfUnopened = async (app: string): Promise<void> => {
|
|
174
160
|
for (const uri of documents.keys()) if (placed(uri)?.app === app) return
|
|
175
|
-
watchers.get(app)?.close()
|
|
176
|
-
watchers.delete(app)
|
|
177
161
|
const checker = checkers.get(app)
|
|
178
162
|
checkers.delete(app)
|
|
179
|
-
await checker?.close()
|
|
163
|
+
await checker?.close()
|
|
180
164
|
}
|
|
181
165
|
|
|
182
166
|
const run = async (uri: string): Promise<void> => {
|
|
@@ -189,17 +173,11 @@ export async function serveLanguage(
|
|
|
189
173
|
const { app, path } = at
|
|
190
174
|
let found: Diagnosis[]
|
|
191
175
|
try {
|
|
192
|
-
found = await
|
|
193
|
-
|
|
194
|
-
path,
|
|
195
|
-
document.text,
|
|
196
|
-
held,
|
|
176
|
+
found = await checkerOf(app).run((checker) =>
|
|
177
|
+
checkSource(checker, path, document.text, held),
|
|
197
178
|
)
|
|
198
179
|
} catch (thrown) {
|
|
199
180
|
// The checker's own failure, not the page's: the next check opens another.
|
|
200
|
-
const failed = checkers.get(app)
|
|
201
|
-
checkers.delete(app)
|
|
202
|
-
await failed?.close().catch(() => {})
|
|
203
181
|
found = [
|
|
204
182
|
{
|
|
205
183
|
path,
|
|
@@ -230,29 +208,30 @@ export async function serveLanguage(
|
|
|
230
208
|
)
|
|
231
209
|
}
|
|
232
210
|
|
|
233
|
-
// A question about a document is about the text the editor shows, so a check still settling
|
|
234
|
-
// runs now rather than after the answer; and it is answered in the queue, after that check.
|
|
235
211
|
const inspect = (params: unknown): Promise<Inspection | undefined> =>
|
|
236
|
-
ask(params, (
|
|
237
|
-
checker.session.inspect(path, text, offset),
|
|
238
|
-
)
|
|
212
|
+
ask(params, (lowered, text, offset) => lowered?.inspect(text, offset))
|
|
239
213
|
|
|
240
214
|
// 38.116 — asked after every page of the app is lowered, which is what a page this one never
|
|
241
215
|
// imports needs to be among the references.
|
|
242
216
|
const references = (params: unknown) =>
|
|
243
|
-
ask(
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
217
|
+
ask(
|
|
218
|
+
params,
|
|
219
|
+
(lowered, text, offset) => lowered?.references(text, offset),
|
|
220
|
+
true,
|
|
221
|
+
)
|
|
247
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).
|
|
248
227
|
const ask = <Answer>(
|
|
249
228
|
params: unknown,
|
|
250
229
|
question: (
|
|
251
|
-
|
|
252
|
-
path: string,
|
|
230
|
+
lowered: LoweredFile | undefined,
|
|
253
231
|
text: string,
|
|
254
232
|
offset: number,
|
|
255
|
-
) => Promise<Answer | undefined
|
|
233
|
+
) => Promise<Answer | undefined> | undefined,
|
|
234
|
+
everyPage = false,
|
|
256
235
|
): Promise<Answer | undefined> => {
|
|
257
236
|
const { textDocument, position } = params as {
|
|
258
237
|
textDocument: { uri: string }
|
|
@@ -268,9 +247,10 @@ export async function serveLanguage(
|
|
|
268
247
|
const document = documents.get(uri)
|
|
269
248
|
const at = placed(uri)
|
|
270
249
|
if (document === undefined || at === undefined) return undefined
|
|
250
|
+
const checker = await checkerOf(at.app).live()
|
|
251
|
+
if (everyPage) await lowerApp(checker, held)
|
|
271
252
|
return question(
|
|
272
|
-
|
|
273
|
-
at.path,
|
|
253
|
+
checker.session.lowered(at.path),
|
|
274
254
|
document.text,
|
|
275
255
|
offsetOf(document.text, position),
|
|
276
256
|
)
|
|
@@ -363,8 +343,8 @@ export async function serveLanguage(
|
|
|
363
343
|
const trigger = (
|
|
364
344
|
params?.context as { triggerCharacter?: string } | undefined
|
|
365
345
|
)?.triggerCharacter
|
|
366
|
-
const found = await ask(params, (
|
|
367
|
-
|
|
346
|
+
const found = await ask(params, (lowered, text, offset) =>
|
|
347
|
+
completionAt(lowered, text, offset, trigger),
|
|
368
348
|
)
|
|
369
349
|
const items: object[] = []
|
|
370
350
|
for (const entry of found?.entries ?? [])
|
|
@@ -544,24 +524,16 @@ export async function serveLanguage(
|
|
|
544
524
|
) as Message
|
|
545
525
|
buffer = buffer.slice(start + length)
|
|
546
526
|
if (!(await handle(message))) {
|
|
547
|
-
await closeAll(checkers
|
|
527
|
+
await closeAll(checkers)
|
|
548
528
|
return
|
|
549
529
|
}
|
|
550
530
|
}
|
|
551
531
|
}
|
|
552
|
-
await closeAll(checkers
|
|
532
|
+
await closeAll(checkers)
|
|
553
533
|
}
|
|
554
534
|
|
|
555
|
-
async function closeAll(
|
|
556
|
-
checkers
|
|
557
|
-
watchers: Map<string, FSWatcher>,
|
|
558
|
-
): Promise<void> {
|
|
559
|
-
for (const watcher of watchers.values()) watcher.close()
|
|
560
|
-
await Promise.all(
|
|
561
|
-
[...checkers.values()].map((checker) =>
|
|
562
|
-
checker.close().catch(() => {}),
|
|
563
|
-
),
|
|
564
|
-
)
|
|
535
|
+
async function closeAll(checkers: Map<string, KeptChecker>): Promise<void> {
|
|
536
|
+
await Promise.all([...checkers.values()].map((checker) => checker.close()))
|
|
565
537
|
}
|
|
566
538
|
|
|
567
539
|
/**
|
|
@@ -9,7 +9,7 @@ import type {
|
|
|
9
9
|
} from 'typescript/unstable/async'
|
|
10
10
|
|
|
11
11
|
import { classifyRead, FUNCTIONS } from './classifyRead.ts'
|
|
12
|
-
import type { Finding } from './
|
|
12
|
+
import type { Finding } from './Diagnostics.ts'
|
|
13
13
|
import type { Span } from './projectShadow.ts'
|
|
14
14
|
|
|
15
15
|
// 34.18 — A PLAIN `let` IN A `<script module>` THAT A `<script>` WRITES AS IT SETS UP. The block is
|
|
@@ -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,8 +1,8 @@
|
|
|
1
1
|
import type { CheckerAnswer } from './CheckerAnswer.ts'
|
|
2
2
|
import type { Read } from './classifyRead.ts'
|
|
3
3
|
import { isComponentTag } from './classifyTemplate.ts'
|
|
4
|
+
import { type Diagnosis, diagnosisAt } from './Diagnostics.ts'
|
|
4
5
|
import { isNamedAlone } from './isNamedAlone.ts'
|
|
5
|
-
import { diagnosisAt } from './lineOf.ts'
|
|
6
6
|
import { Lowering } from './Lowering.ts'
|
|
7
7
|
import type { AbideFile } from './parseAbide.ts'
|
|
8
8
|
import {
|
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
type TemplateNode,
|
|
13
13
|
visitBodies,
|
|
14
14
|
} from './TemplateNode.ts'
|
|
15
|
-
import type { Diagnosis } from './TypeSession.ts'
|
|
16
15
|
|
|
17
16
|
// 32.14 — A `{#for}` WITH NO KEY OVER A BODY THAT HOLDS STATE. Unkeyed, a row is matched to the
|
|
18
17
|
// item at its index, so a reorder hands one row's state — a component's own values, a field's text,
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
} from 'typescript/unstable/ast/is'
|
|
17
17
|
|
|
18
18
|
import { FUNCTIONS, TRANSPARENT } from './classifyRead.ts'
|
|
19
|
-
import type { Finding } from './
|
|
19
|
+
import type { Finding } from './Diagnostics.ts'
|
|
20
20
|
import type { PositionKind, PositionTable } from './PositionTable.ts'
|
|
21
21
|
|
|
22
22
|
// 14.14, 14.15, 14.16 — WHERE A READ STANDS IN A BODY THAT TRACKS, and the three ways it can stand
|
|
@@ -1,13 +1,9 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// in both is a build that silently serves nothing. The addresses it is answered at are
|
|
1
|
+
// WHAT THE BROWSER BUNDLE IS NAMED, once for the seams that have to agree about it: the compiler
|
|
2
|
+
// writes the entry, `abide build` precompresses every file, and the server answers both (38.5,
|
|
3
|
+
// 38.20). A leaf, with no imports of its own, for the reason every constant crossing a seam is one.
|
|
4
|
+
// Where the bundle lands in a build is `appBuild.ts`'s; the addresses it is answered at are
|
|
6
5
|
// `#shared/ADDRESSES.ts`.
|
|
7
6
|
|
|
8
|
-
/** The directory the bundle lands in, below the build's own output directory. */
|
|
9
|
-
export const CLIENT_DIRECTORY = 'client'
|
|
10
|
-
|
|
11
7
|
/**
|
|
12
8
|
* The module that mounts the app — the one bundle file whose name the shell writes out.
|
|
13
9
|
*
|
|
@@ -19,21 +15,6 @@ export const CLIENT_DIRECTORY = 'client'
|
|
|
19
15
|
*/
|
|
20
16
|
export const CLIENT_ENTRY = 'entry'
|
|
21
17
|
|
|
22
|
-
/**
|
|
23
|
-
* The manifest file `abide build` writes beside the client directory.
|
|
24
|
-
*
|
|
25
|
-
* 38.5 — the manifest `manifestOf` reads off the bundle, below the build's own output directory
|
|
26
|
-
* and BESIDE the client directory rather than in it: everything in there is answered under
|
|
27
|
-
* `CLIENT_ADDRESS` (38.20), and the manifest is the server's, not the browser's.
|
|
28
|
-
*/
|
|
29
|
-
export const MANIFEST = 'manifest.json'
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* The compilation `abide build` keeps beside the client directory: every `.abide` compiled, and
|
|
33
|
-
* the build's own tables (`Compilation`), which `abide start` serves without the compiler.
|
|
34
|
-
*/
|
|
35
|
-
export const COMPILATION = 'compilation.json'
|
|
36
|
-
|
|
37
18
|
/**
|
|
38
19
|
* The encodings a response can be compressed in, each with its precompressed file's suffix.
|
|
39
20
|
*
|
|
@@ -43,12 +24,3 @@ export const COMPILATION = 'compilation.json'
|
|
|
43
24
|
* a build that precompresses into files nothing ever serves.
|
|
44
25
|
*/
|
|
45
26
|
export const ENCODING_SUFFIX = { br: '.br', gzip: '.gz' } as const
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Where `abide build` copies every `public` directory, each file at its address, below its own
|
|
49
|
-
* output directory. The source's own name for one is `#shared/RESERVED_DIRECTORIES.ts`'s.
|
|
50
|
-
*
|
|
51
|
-
* 38.18 — a directory rather than the source tree: `abide start` boots what the build produced and
|
|
52
|
-
* reaches for nothing an app might have moved since.
|
|
53
|
-
*/
|
|
54
|
-
export const PUBLIC_DIRECTORY = 'public'
|
|
@@ -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
|
+
}
|