@abide/abide 0.123.3 → 0.124.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/ABIDE.md +8 -5
  2. package/CHANGELOG.md +31 -0
  3. package/docs/REGISTRY.md +45 -12
  4. package/docs/RULEBOOK.md +24 -14
  5. package/docs/abide.md +999 -459
  6. package/package.json +1 -1
  7. package/src/compiler/ARRAY_MUTATORS.ts +20 -0
  8. package/src/compiler/LoweredFile.ts +609 -0
  9. package/src/compiler/Lowering.ts +17 -0
  10. package/src/compiler/SHARED_NAMES.ts +2 -0
  11. package/src/compiler/ShadowHost.ts +670 -0
  12. package/src/compiler/ShadowMap.ts +1 -1
  13. package/src/compiler/TypeSession.ts +71 -1715
  14. package/src/compiler/appLints.ts +109 -0
  15. package/src/compiler/checkedShadow.ts +1 -1
  16. package/src/compiler/classifyRead.ts +16 -1
  17. package/src/compiler/compileApp.ts +2 -2
  18. package/src/compiler/declaredExports.ts +44 -17
  19. package/src/compiler/diagnoseSources.ts +232 -0
  20. package/src/compiler/formatAbide.ts +5 -6
  21. package/src/compiler/handlersUnder.ts +16 -35
  22. package/src/compiler/reactiveReads.ts +235 -0
  23. package/src/compiler/reactiveTypes.ts +13 -0
  24. package/src/compiler/readsIn.ts +180 -81
  25. package/src/compiler/routesDeclaration.ts +46 -0
  26. package/src/compiler/scaffold.ts +7 -2
  27. package/src/compiler/serveLanguage.ts +26 -20
  28. package/src/compiler/splitImports.ts +2 -2
  29. package/src/server/DesktopSession.ts +213 -0
  30. package/src/server/Followers.ts +399 -0
  31. package/src/server/MODULE_QUERIES.ts +6 -2
  32. package/src/server/PageRender.ts +201 -0
  33. package/src/server/abideModules.ts +7 -7
  34. package/src/server/answerFailure.ts +3 -2
  35. package/src/server/answerRequest.ts +6 -70
  36. package/src/server/answerRpc.ts +48 -260
  37. package/src/server/answerSocket.ts +41 -463
  38. package/src/server/appKit.ts +1 -1
  39. package/src/server/argsContract.ts +1 -1
  40. package/src/server/createApp.ts +8 -2
  41. package/src/server/csp.ts +1 -1
  42. package/src/server/deferredBuilds.ts +3 -16
  43. package/src/server/faceOf.ts +1 -1
  44. package/src/server/generatedSurfaces.ts +2 -1
  45. package/src/server/handlerFileMount.ts +1 -1
  46. package/src/server/index.ts +1 -1
  47. package/src/server/installServer.ts +5 -9
  48. package/src/server/invokeHandler.ts +230 -0
  49. package/src/server/mountHandlers.ts +184 -0
  50. package/src/server/openDesktopWindow.ts +308 -0
  51. package/src/server/preload.ts +18 -0
  52. package/src/server/principal.ts +1 -1
  53. package/src/server/remote.ts +1 -1
  54. package/src/server/render.ts +31 -59
  55. package/src/server/requestScope.ts +5 -36
  56. package/src/server/runDesktop.ts +19 -447
  57. package/src/server/seedBuffer.ts +11 -11
  58. package/src/server/servedApp.ts +1 -1
  59. package/src/server/shell.ts +1 -1
  60. package/src/server/surfaceOf.ts +1 -1
  61. package/src/server/versionOf.ts +1 -1
  62. package/src/shared/RPC_SERVER.ts +27 -31
  63. package/src/shared/Schema.ts +1 -2
  64. package/src/shared/index.ts +1 -1
  65. package/src/shared/{note.ts → issueReport.ts} +22 -0
  66. package/src/shared/loadCall.ts +66 -0
  67. package/src/shared/reactive/Keyed.ts +23 -15
  68. package/src/shared/reactive/Reactive.ts +34 -1001
  69. package/src/shared/reactive/byOf.ts +2 -5
  70. package/src/shared/reactive/callLoad.ts +8 -14
  71. package/src/shared/reactive/cellShape.ts +734 -0
  72. package/src/shared/reactive/index.ts +12 -24
  73. package/src/shared/reactive/pairReactive.ts +5 -3
  74. package/src/shared/reactive/producer.ts +121 -0
  75. package/src/shared/reactive/scoped.ts +3 -2
  76. package/src/shared/reactive/selections.ts +209 -0
  77. package/src/shared/reactive/watch.ts +3 -4
  78. package/src/shared/rpc.ts +8 -5
  79. package/src/shared/validateJson.ts +1 -2
  80. package/src/shared/writePath.ts +29 -0
  81. package/src/ui/Router.ts +53 -69
  82. package/src/ui/outboxQueue.ts +2 -8
  83. package/src/ui/outcomeOf.ts +120 -0
  84. package/src/ui/socketWire.ts +3 -3
  85. package/src/ui/wire.ts +9 -10
  86. package/src/shared/LOAD_OFFLINE.ts +0 -15
  87. package/src/shared/LOAD_SIGNAL.ts +0 -10
  88. package/src/shared/issuesOf.ts +0 -18
  89. 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 program that check
26
- // registered (`TypeSession`), so they answer about the same text.
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, (checker, path, text, offset) =>
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(params, async (checker, path, text, offset) => {
218
- await lowerApp(checker, held)
219
- return checker.session.references(path, text, offset)
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
- checker: AppChecker,
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
- await checkerOf(at.app).live(),
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, (checker, path, text, offset) =>
341
- checker.session.complete(path, text, offset, trigger),
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, TEXT_QUERY } from '#server/MODULE_QUERIES.ts'
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)}${TEXT_QUERY}`
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
@@ -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
+ }