@abide/abide 0.105.1 → 0.107.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 +28 -0
- package/bin/abide.ts +4 -1
- package/docs/REGISTRY.md +156 -44
- package/docs/RULEBOOK.md +392 -88
- package/docs/abide.md +7245 -10248
- package/package.json +4 -1
- package/scaffold/AGENTS.md +2 -0
- package/scaffold/src/getGreeting.ts +6 -0
- package/scaffold/src/index.abide +3 -1
- package/scaffold/src/tests/getGreeting.test.ts +6 -0
- package/scaffold/src/tests/index.test.ts +18 -0
- package/src/compiler/DOCS_SITE.ts +2 -0
- package/src/compiler/Lowering.ts +234 -14
- package/src/compiler/PositionTable.ts +14 -7
- package/src/compiler/PositionedRefusal.ts +13 -0
- package/src/compiler/Refusals.ts +18 -11
- package/src/compiler/SHARED_NAMES.ts +2 -0
- package/src/compiler/ShadowMap.ts +37 -0
- package/src/compiler/TEMPLATE_BLOCKS.ts +12 -0
- package/src/compiler/TypeSession.ts +410 -31
- package/src/compiler/appLayout.ts +18 -2
- package/src/compiler/argsSchemaOf.ts +55 -9
- package/src/compiler/checkApp.ts +33 -0
- package/src/compiler/checkedShadow.ts +36 -22
- package/src/compiler/citedClauses.ts +14 -0
- package/src/compiler/classifyRead.ts +6 -0
- package/src/compiler/classifyTemplate.ts +7 -4
- package/src/compiler/compileApp.ts +2 -2
- package/src/compiler/declaredExports.ts +27 -6
- package/src/compiler/emitClient.ts +306 -103
- package/src/compiler/handlersUnder.ts +4 -0
- package/src/compiler/invocationProps.ts +1 -1
- package/src/compiler/isIgnored.ts +27 -0
- package/src/compiler/parseAbide.ts +176 -76
- package/src/compiler/projectShadow.ts +30 -6
- package/src/compiler/readSources.ts +1 -1
- package/src/compiler/readsIn.ts +100 -1
- package/src/compiler/renderMarkup.ts +36 -5
- package/src/compiler/scaffold.ts +19 -6
- package/src/compiler/serveLanguage.ts +156 -21
- package/src/compiler/templateContext.ts +150 -0
- package/src/compiler/tokenize.ts +6 -0
- package/src/server/BRIDGE_VERSION.ts +1 -1
- package/src/server/Inbox.ts +8 -0
- package/src/server/MCP_PROTOCOL_VERSION.ts +1 -1
- package/src/server/MOUNTABLE.ts +6 -0
- package/src/server/abideModules.ts +8 -1
- package/src/server/answerRequest.ts +3 -1
- package/src/server/answerRpc.ts +26 -12
- package/src/server/answerSocket.ts +26 -1
- package/src/server/argsFor.ts +4 -3
- package/src/server/assistant.ts +21 -14
- package/src/server/bridgeRelay.ts +108 -20
- package/src/server/claudeApi.ts +19 -11
- package/src/server/claudeCode.ts +19 -14
- package/src/server/commands.ts +73 -9
- package/src/server/createApp.ts +41 -7
- package/src/server/devWorker.ts +3 -3
- package/src/server/generatedSurfaces.ts +37 -4
- package/src/server/muxUpgrade.ts +47 -0
- package/src/server/preload.ts +13 -2
- package/src/server/render.ts +4 -1
- package/src/server/runCompiled.ts +1 -0
- package/src/server/seedBuffer.ts +31 -1
- package/src/server/serveDev.ts +83 -72
- package/src/server/surfaceOf.ts +3 -0
- package/src/server/testMount.ts +53 -0
- package/src/shared/ADDRESSES.ts +1 -1
- package/src/shared/ERROR_META.ts +3 -0
- package/src/shared/Schema.ts +1 -1
- package/src/shared/WORK.ts +14 -12
- package/src/shared/applyPatch.ts +1 -1
- package/src/shared/boundValue.ts +81 -0
- package/src/shared/index.ts +7 -2
- package/src/shared/jsonSchema.ts +25 -2
- package/src/shared/log.ts +11 -1
- package/src/shared/reactive/Keyed.ts +30 -6
- package/src/shared/reactive/PATCHES.ts +1 -1
- package/src/shared/reactive/Reactive.ts +118 -60
- package/src/shared/reactive/canonicalKey.ts +1 -1
- package/src/shared/reactive/index.ts +13 -5
- package/src/shared/reactive/recordWrites.ts +1 -1
- package/src/shared/reactive/watch.ts +26 -4
- package/src/shared/rpc.ts +6 -1
- package/src/shared/url.ts +7 -3
- package/src/shared/validateJson.ts +1 -1
- package/src/ui/ArmRegion.ts +2 -2
- package/src/ui/List.ts +9 -9
- package/src/ui/RawRun.ts +1 -1
- package/src/ui/Router.ts +194 -36
- package/src/ui/Scope.ts +29 -0
- package/src/ui/adoptWalk.ts +3 -3
- package/src/ui/applyHead.ts +25 -3
- package/src/ui/bindElement.ts +4 -0
- package/src/ui/bindProperty.ts +70 -22
- package/src/ui/index.ts +1 -1
- package/src/ui/listenForRoutes.ts +14 -3
- package/src/ui/reconcile.ts +23 -28
- package/src/ui/regions.ts +9 -2
- package/src/ui/rows.ts +108 -28
|
@@ -302,6 +302,8 @@ function renderPosition(
|
|
|
302
302
|
|
|
303
303
|
const parts: string[] = []
|
|
304
304
|
let open = `<${node.tag}`
|
|
305
|
+
// 32.61 — a `<textarea>`'s or `<select>`'s bound value, which is written inside the tag.
|
|
306
|
+
let within: string | undefined
|
|
305
307
|
// WHAT IS NOT MARKUP IS NOT WRITTEN: a listener, `bind:element`, a refused URL, a bind whose
|
|
306
308
|
// value is not an attribute, and every merge member after the one the group is written at.
|
|
307
309
|
for (const one of classifyAttributes(node, lowering)) {
|
|
@@ -310,7 +312,10 @@ function renderPosition(
|
|
|
310
312
|
continue
|
|
311
313
|
}
|
|
312
314
|
if (one.kind === 'bound') {
|
|
313
|
-
if (one.reflects === undefined)
|
|
315
|
+
if (one.reflects === undefined) {
|
|
316
|
+
if (one.bind === 'value') within = boundRead(one, lowering)
|
|
317
|
+
continue
|
|
318
|
+
}
|
|
314
319
|
parts.push(JSON.stringify(open), boundMarkup(one, lowering))
|
|
315
320
|
open = ''
|
|
316
321
|
continue
|
|
@@ -349,7 +354,25 @@ function renderPosition(
|
|
|
349
354
|
// `<br>`, and a close tag written for one is markup no browser accepts.
|
|
350
355
|
if (node.children.length === 0 && VOID_ELEMENTS.has(node.lowerTag))
|
|
351
356
|
return parts.join(' + ')
|
|
357
|
+
// 32.61 — THE VALUE IN PLACE OF THE AUTHOR'S TEXT, led by the newline the parser drops after
|
|
358
|
+
// `<textarea>` so a value opening on one keeps it.
|
|
359
|
+
if (
|
|
360
|
+
within !== undefined &&
|
|
361
|
+
node.lowerTag === 'textarea' &&
|
|
362
|
+
node.children.length === 0
|
|
363
|
+
)
|
|
364
|
+
return [
|
|
365
|
+
...parts,
|
|
366
|
+
`'\\n' + ${lowering.use('escapeAttribute')}(${within})`,
|
|
367
|
+
JSON.stringify(`</${node.tag}>`),
|
|
368
|
+
].join(' + ')
|
|
352
369
|
const children = renderLevel(at.children, lowering, lane)
|
|
370
|
+
if (within !== undefined && node.lowerTag === 'select')
|
|
371
|
+
return [
|
|
372
|
+
...parts,
|
|
373
|
+
`${lowering.use('selectedMarkup')}(${concat(children)}, ${within})`,
|
|
374
|
+
JSON.stringify(`</${node.tag}>`),
|
|
375
|
+
].join(' + ')
|
|
353
376
|
return [...parts, ...children, JSON.stringify(`</${node.tag}>`)].join(' + ')
|
|
354
377
|
}
|
|
355
378
|
|
|
@@ -586,10 +609,7 @@ function boundMarkup(
|
|
|
586
609
|
written: ClassifiedAttribute & { kind: 'bound' },
|
|
587
610
|
lowering: Lowering,
|
|
588
611
|
): string {
|
|
589
|
-
const read =
|
|
590
|
-
lowering,
|
|
591
|
-
`${lowering.use('boundValue')}(${lowering.reference(written.attribute)})`,
|
|
592
|
-
)
|
|
612
|
+
const read = boundRead(written, lowering)
|
|
593
613
|
if (written.bind === 'value')
|
|
594
614
|
return `' value="' + ${lowering.use('escapeAttribute')}(${read}) + '"'`
|
|
595
615
|
const on =
|
|
@@ -599,6 +619,17 @@ function boundMarkup(
|
|
|
599
619
|
return `(${on} ? ' ${written.reflects}' : '')`
|
|
600
620
|
}
|
|
601
621
|
|
|
622
|
+
/** A bind's value at render, under 41.4's guard: `undefined` while it is in flight. */
|
|
623
|
+
function boundRead(
|
|
624
|
+
written: ClassifiedAttribute & { kind: 'bound' },
|
|
625
|
+
lowering: Lowering,
|
|
626
|
+
): string {
|
|
627
|
+
return slotValueOf(
|
|
628
|
+
lowering,
|
|
629
|
+
`${lowering.use('boundValue')}(${lowering.reference(written.attribute)})`,
|
|
630
|
+
)
|
|
631
|
+
}
|
|
632
|
+
|
|
602
633
|
/**
|
|
603
634
|
* A static attribute's markup: the bare name, or the name and its value's bytes (`staticBytes`).
|
|
604
635
|
*
|
package/src/compiler/scaffold.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// 38.1 — `abide scaffold <name>`: THE LAYOUT EVERY ABIDE APP HAS, written into `<name>`. There is
|
|
2
|
-
// nothing to choose, because what is scaffolded is a root layout
|
|
3
|
-
//
|
|
2
|
+
// nothing to choose, because what is scaffolded is a root layout, a page, a handler file the page
|
|
3
|
+
// calls, a test of that handler, and a test that mounts the page and counts the work a click does.
|
|
4
4
|
// A file that is already there is left as it is and reported skipped, so a scaffold run over a
|
|
5
5
|
// half-made project finishes it rather than overwriting it.
|
|
6
6
|
|
|
@@ -22,15 +22,22 @@ const ABIDE_DEPENDENCY = PACKAGE_ROOT.includes('/node_modules/')
|
|
|
22
22
|
? `^${abidePackage.version}`
|
|
23
23
|
: `file:${PACKAGE_ROOT}`
|
|
24
24
|
|
|
25
|
+
// THE HARNESS BESIDE IT, by the same rule: from a registry its latest, whose peer range on abide is
|
|
26
|
+
// what keeps the two in step; from a checkout the checkout's own harness.
|
|
27
|
+
const HARNESS_DEPENDENCY = PACKAGE_ROOT.includes('/node_modules/')
|
|
28
|
+
? 'latest'
|
|
29
|
+
: `file:${PACKAGE_ROOT.slice(0, PACKAGE_ROOT.lastIndexOf('/'))}/harness`
|
|
30
|
+
|
|
25
31
|
// The `#src` alias, written as the package's `imports` and again as the checker's `paths`.
|
|
26
32
|
const SOURCE_ALIAS = { '#src/*': ['./src/*', './src/*.ts'] }
|
|
27
33
|
|
|
28
34
|
/**
|
|
29
|
-
* 38.109 — the starter's `bunfig.toml`:
|
|
35
|
+
* 38.109, 38.119 — the starter's `bunfig.toml`: the harness's abide preload, which is a DOM a test
|
|
36
|
+
* can mount into and measure, then abide's own — the app's `.abide` files imported as modules, with
|
|
30
37
|
* nothing booted first.
|
|
31
38
|
*/
|
|
32
39
|
export const TEST_PRELOAD =
|
|
33
|
-
'# 38.109 — a
|
|
40
|
+
'# 38.109, 38.119 — a DOM to mount into and measure, then the app\'s `.abide` files as modules.\n[test]\npreload = ["@abide/harness/abide/preload"]\n'
|
|
34
41
|
|
|
35
42
|
// WRITTEN, NOT COPIED, beside the manifests: a packer drops a `bunfig.toml` or a `.gitignore` from
|
|
36
43
|
// the tarball (`bun pm pack` both, `npm pack` the second), so neither survives as a starter file.
|
|
@@ -51,6 +58,8 @@ function filesFor(name: string): Record<string, string> {
|
|
|
51
58
|
start: 'abide start',
|
|
52
59
|
// 38.108 — every address, and the file answering it.
|
|
53
60
|
routes: 'abide routes',
|
|
61
|
+
// 38.112 — the starter's own test, under the 38.109 preload.
|
|
62
|
+
test: 'bun test',
|
|
54
63
|
},
|
|
55
64
|
// ONE ALIAS FOR THE SOURCE, and two targets for it, because an extensionless
|
|
56
65
|
// specifier is how an import is written and only bun probes for the extension:
|
|
@@ -62,8 +71,12 @@ function filesFor(name: string): Record<string, string> {
|
|
|
62
71
|
imports: SOURCE_ALIAS,
|
|
63
72
|
dependencies: { '@abide/abide': ABIDE_DEPENDENCY },
|
|
64
73
|
// `Bun`, `bun:sqlite` and the rest of the runtime a server module reaches for are
|
|
65
|
-
// typed by these, and `abide check` reports every one untyped without them.
|
|
66
|
-
|
|
74
|
+
// typed by these, and `abide check` reports every one untyped without them. The
|
|
75
|
+
// harness is what the starter's tests measure the work a click does with (38.119).
|
|
76
|
+
devDependencies: {
|
|
77
|
+
'@abide/harness': HARNESS_DEPENDENCY,
|
|
78
|
+
'@types/bun': 'latest',
|
|
79
|
+
},
|
|
67
80
|
},
|
|
68
81
|
null,
|
|
69
82
|
4,
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { existsSync, type FSWatcher, watch } from 'node:fs' // bun exposes no file watcher or sync existence check of its own
|
|
2
2
|
import { dirname } from 'node:path' // bun exposes no path helpers of its own
|
|
3
3
|
import { realOrAsGiven } from '#server/realOrAsGiven.ts'
|
|
4
|
-
import { checkSource } from './checkApp.ts'
|
|
4
|
+
import { checkSource, lowerApp } from './checkApp.ts'
|
|
5
|
+
import { citedClauses } from './citedClauses.ts'
|
|
5
6
|
import { type AppChecker, openChecker } from './compileApp.ts'
|
|
7
|
+
import { DOCS_SITE } from './DOCS_SITE.ts'
|
|
8
|
+
import { isNamedAlone } from './isNamedAlone.ts'
|
|
6
9
|
import { nearest } from './nearest.ts'
|
|
7
10
|
import type { Diagnosis, Inspection, Place } from './TypeSession.ts'
|
|
8
11
|
|
|
@@ -12,9 +15,9 @@ import type { Diagnosis, Inspection, Place } from './TypeSession.ts'
|
|
|
12
15
|
//
|
|
13
16
|
// WHAT IT SPEAKS is the part of the protocol a diagnostic needs: the handshake, a document opened,
|
|
14
17
|
// changed and closed, and `publishDiagnostics` back. The document is synced whole (`Full`), so what
|
|
15
|
-
// is checked is exactly the editor's text, saved or not. A hover
|
|
16
|
-
//
|
|
17
|
-
// same text.
|
|
18
|
+
// is checked is exactly the editor's text, saved or not. A hover, a jump to a definition, a
|
|
19
|
+
// completion, the references to a name and a rename of it are asked of the program that check
|
|
20
|
+
// registered (`TypeSession`), so they answer about the same text.
|
|
18
21
|
//
|
|
19
22
|
// THE APP IS THE DOCUMENT'S. An editor starts this at the root of whatever it has open, which in a
|
|
20
23
|
// monorepo is not an app at all, so each document is checked against the root of the NEAREST
|
|
@@ -111,7 +114,7 @@ export async function serveLanguage(
|
|
|
111
114
|
},
|
|
112
115
|
severity: SEVERITY[diagnosis.category],
|
|
113
116
|
source: 'abide',
|
|
114
|
-
...(diagnosis
|
|
117
|
+
...codeOf(diagnosis),
|
|
115
118
|
message: diagnosis.message,
|
|
116
119
|
})
|
|
117
120
|
}
|
|
@@ -121,6 +124,10 @@ export async function serveLanguage(
|
|
|
121
124
|
})
|
|
122
125
|
}
|
|
123
126
|
|
|
127
|
+
// The text the editor holds for a file, where it holds one.
|
|
128
|
+
const held = (path: string): string | undefined =>
|
|
129
|
+
documents.get(Bun.pathToFileURL(spelled(path)).href)?.text
|
|
130
|
+
|
|
124
131
|
// An app's checker, opened on first use and kept for the session — and TOLD WHAT MOVED ON DISK,
|
|
125
132
|
// or it answers every check from the text it read when it opened: an export added to a `.ts`
|
|
126
133
|
// beside the page is "no exported member" until the editor restarts this. A project whose
|
|
@@ -155,6 +162,18 @@ export async function serveLanguage(
|
|
|
155
162
|
return checker
|
|
156
163
|
}
|
|
157
164
|
|
|
165
|
+
// An app with no page open is checked again only when one opens, and its checker is a process
|
|
166
|
+
// holding the whole program — closed rather than kept, or every app the editor ever visited
|
|
167
|
+
// keeps one for the session. Queued behind the checks, so none is in flight when it closes.
|
|
168
|
+
const closeIfUnopened = async (app: string): Promise<void> => {
|
|
169
|
+
for (const uri of documents.keys()) if (placed(uri)?.app === app) return
|
|
170
|
+
watchers.get(app)?.close()
|
|
171
|
+
watchers.delete(app)
|
|
172
|
+
const checker = checkers.get(app)
|
|
173
|
+
checkers.delete(app)
|
|
174
|
+
await checker?.close().catch(() => {})
|
|
175
|
+
}
|
|
176
|
+
|
|
158
177
|
const run = async (uri: string): Promise<void> => {
|
|
159
178
|
const document = documents.get(uri)
|
|
160
179
|
if (document === undefined) return
|
|
@@ -169,8 +188,7 @@ export async function serveLanguage(
|
|
|
169
188
|
await checkerOf(app),
|
|
170
189
|
path,
|
|
171
190
|
document.text,
|
|
172
|
-
|
|
173
|
-
documents.get(Bun.pathToFileURL(spelled(held)).href)?.text,
|
|
191
|
+
held,
|
|
174
192
|
)
|
|
175
193
|
} catch (thrown) {
|
|
176
194
|
// The checker's own failure, not the page's: the next check opens another.
|
|
@@ -210,14 +228,22 @@ export async function serveLanguage(
|
|
|
210
228
|
// A question about a document is about the text the editor shows, so a check still settling
|
|
211
229
|
// runs now rather than after the answer; and it is answered in the queue, after that check.
|
|
212
230
|
const inspect = (params: unknown): Promise<Inspection | undefined> =>
|
|
213
|
-
ask(params, (
|
|
214
|
-
session.inspect(path, text, offset),
|
|
231
|
+
ask(params, (checker, path, text, offset) =>
|
|
232
|
+
checker.session.inspect(path, text, offset),
|
|
215
233
|
)
|
|
216
234
|
|
|
235
|
+
// 38.116 — asked after every page of the app is lowered, which is what a page this one never
|
|
236
|
+
// imports needs to be among the references.
|
|
237
|
+
const references = (params: unknown) =>
|
|
238
|
+
ask(params, async (checker, path, text, offset) => {
|
|
239
|
+
await lowerApp(checker, held)
|
|
240
|
+
return checker.session.references(path, text, offset)
|
|
241
|
+
})
|
|
242
|
+
|
|
217
243
|
const ask = <Answer>(
|
|
218
244
|
params: unknown,
|
|
219
245
|
question: (
|
|
220
|
-
|
|
246
|
+
checker: AppChecker,
|
|
221
247
|
path: string,
|
|
222
248
|
text: string,
|
|
223
249
|
offset: number,
|
|
@@ -238,7 +264,7 @@ export async function serveLanguage(
|
|
|
238
264
|
const at = placed(uri)
|
|
239
265
|
if (document === undefined || at === undefined) return undefined
|
|
240
266
|
return question(
|
|
241
|
-
|
|
267
|
+
await checkerOf(at.app),
|
|
242
268
|
at.path,
|
|
243
269
|
document.text,
|
|
244
270
|
offsetOf(document.text, position),
|
|
@@ -262,7 +288,11 @@ export async function serveLanguage(
|
|
|
262
288
|
textDocumentSync: 1,
|
|
263
289
|
hoverProvider: true,
|
|
264
290
|
definitionProvider: true,
|
|
265
|
-
|
|
291
|
+
referencesProvider: true,
|
|
292
|
+
renameProvider: { prepareProvider: true },
|
|
293
|
+
completionProvider: {
|
|
294
|
+
triggerCharacters: ['.', '<', '#', ':', '/'],
|
|
295
|
+
},
|
|
266
296
|
},
|
|
267
297
|
serverInfo: { name: 'abide' },
|
|
268
298
|
},
|
|
@@ -327,8 +357,8 @@ export async function serveLanguage(
|
|
|
327
357
|
const trigger = (
|
|
328
358
|
params?.context as { triggerCharacter?: string } | undefined
|
|
329
359
|
)?.triggerCharacter
|
|
330
|
-
const found = await ask(params, (
|
|
331
|
-
session.complete(path, text, offset, trigger),
|
|
360
|
+
const found = await ask(params, (checker, path, text, offset) =>
|
|
361
|
+
checker.session.complete(path, text, offset, trigger),
|
|
332
362
|
)
|
|
333
363
|
const items: object[] = []
|
|
334
364
|
for (const entry of found?.entries ?? [])
|
|
@@ -358,12 +388,84 @@ export async function serveLanguage(
|
|
|
358
388
|
send({ id: message.id, result: locations })
|
|
359
389
|
return true
|
|
360
390
|
}
|
|
391
|
+
case 'textDocument/references': {
|
|
392
|
+
const found = await references(params)
|
|
393
|
+
const declarations = (
|
|
394
|
+
params?.context as
|
|
395
|
+
| { includeDeclaration?: boolean }
|
|
396
|
+
| undefined
|
|
397
|
+
)?.includeDeclaration
|
|
398
|
+
const locations: object[] = []
|
|
399
|
+
for (const place of found?.occurrences ?? [])
|
|
400
|
+
if (declarations !== false || !place.declaration)
|
|
401
|
+
locations.push(locationOf(spelled(place.path), place))
|
|
402
|
+
send({ id: message.id, result: locations })
|
|
403
|
+
return true
|
|
404
|
+
}
|
|
405
|
+
case 'textDocument/prepareRename': {
|
|
406
|
+
const found = await references(params)
|
|
407
|
+
send(
|
|
408
|
+
found === undefined
|
|
409
|
+
? { id: message.id, result: null }
|
|
410
|
+
: !found.renamable
|
|
411
|
+
? refusedRename(message.id, found.written)
|
|
412
|
+
: {
|
|
413
|
+
id: message.id,
|
|
414
|
+
result: {
|
|
415
|
+
range: rangeOf(found.name),
|
|
416
|
+
placeholder: found.written,
|
|
417
|
+
},
|
|
418
|
+
},
|
|
419
|
+
)
|
|
420
|
+
return true
|
|
421
|
+
}
|
|
422
|
+
// A rename is every reference given the new name — and a shorthand the half it renames,
|
|
423
|
+
// with the other written out beside it.
|
|
424
|
+
case 'textDocument/rename': {
|
|
425
|
+
const renamed = params?.newName as string
|
|
426
|
+
if (!isNamedAlone(renamed)) {
|
|
427
|
+
send({
|
|
428
|
+
id: message.id,
|
|
429
|
+
error: {
|
|
430
|
+
code: -32602,
|
|
431
|
+
message: `\`${renamed}\` is not a name.`,
|
|
432
|
+
},
|
|
433
|
+
})
|
|
434
|
+
return true
|
|
435
|
+
}
|
|
436
|
+
const found = await references(params)
|
|
437
|
+
if (found !== undefined && !found.renamable) {
|
|
438
|
+
send(refusedRename(message.id, found.written))
|
|
439
|
+
return true
|
|
440
|
+
}
|
|
441
|
+
const changes: Record<string, object[]> = {}
|
|
442
|
+
for (const place of found?.occurrences ?? []) {
|
|
443
|
+
const uri = Bun.pathToFileURL(spelled(place.path)).href
|
|
444
|
+
const newText =
|
|
445
|
+
place.shorthand === 'local'
|
|
446
|
+
? `${found?.written}: ${renamed}`
|
|
447
|
+
: place.shorthand === 'property'
|
|
448
|
+
? `${renamed}: ${found?.written}`
|
|
449
|
+
: renamed
|
|
450
|
+
const edits = changes[uri] ?? []
|
|
451
|
+
edits.push({ range: rangeOf(place), newText })
|
|
452
|
+
changes[uri] = edits
|
|
453
|
+
}
|
|
454
|
+
send({
|
|
455
|
+
id: message.id,
|
|
456
|
+
result: found === undefined ? null : { changes },
|
|
457
|
+
})
|
|
458
|
+
return true
|
|
459
|
+
}
|
|
361
460
|
case 'textDocument/didClose': {
|
|
362
461
|
const closed = params?.textDocument as { uri: string }
|
|
363
462
|
if (!documents.delete(closed.uri)) return true
|
|
364
463
|
clearTimeout(timers.get(closed.uri))
|
|
365
464
|
timers.delete(closed.uri)
|
|
366
465
|
publish(closed.uri, [], '')
|
|
466
|
+
const at = placed(closed.uri)
|
|
467
|
+
if (at !== undefined)
|
|
468
|
+
queue = queue.then(() => closeIfUnopened(at.app))
|
|
367
469
|
return true
|
|
368
470
|
}
|
|
369
471
|
}
|
|
@@ -459,14 +561,47 @@ function offsetOf(
|
|
|
459
561
|
|
|
460
562
|
/** A `Place` at `path`, lines and columns from 1, as the protocol's `Location`, from 0. */
|
|
461
563
|
function locationOf(path: string, place: Place): object {
|
|
564
|
+
return { uri: Bun.pathToFileURL(path).href, range: rangeOf(place) }
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/** A `Place`'s span, lines and columns from 1, as the protocol's `Range`, from 0. */
|
|
568
|
+
function rangeOf(place: Place): object {
|
|
569
|
+
return {
|
|
570
|
+
start: {
|
|
571
|
+
line: place.start.line - 1,
|
|
572
|
+
character: place.start.column - 1,
|
|
573
|
+
},
|
|
574
|
+
end: { line: place.end.line - 1, character: place.end.column - 1 },
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/** 38.116 — a rename refused, for a name declared where no author writes. */
|
|
579
|
+
function refusedRename(id: Message['id'], name: string): object {
|
|
462
580
|
return {
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
character: place.start.column - 1,
|
|
468
|
-
},
|
|
469
|
-
end: { line: place.end.line - 1, character: place.end.column - 1 },
|
|
581
|
+
id,
|
|
582
|
+
error: {
|
|
583
|
+
code: -32803,
|
|
584
|
+
message: `\`${name}\` is declared in a library, and a library is not renamed.`,
|
|
470
585
|
},
|
|
471
586
|
}
|
|
472
587
|
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* The protocol's `code` for a finding: the checker's own number, or the clause an abide finding
|
|
591
|
+
* cites, with the rulebook's page for it.
|
|
592
|
+
*
|
|
593
|
+
* 38.118.
|
|
594
|
+
*/
|
|
595
|
+
function codeOf(diagnosis: Diagnosis): {
|
|
596
|
+
code?: number | string
|
|
597
|
+
codeDescription?: { href: string }
|
|
598
|
+
} {
|
|
599
|
+
if (diagnosis.code !== 0) return { code: diagnosis.code }
|
|
600
|
+
const [clause] = citedClauses(diagnosis.message)
|
|
601
|
+
return clause === undefined
|
|
602
|
+
? {}
|
|
603
|
+
: {
|
|
604
|
+
code: clause,
|
|
605
|
+
codeDescription: { href: `${DOCS_SITE}/rulebook#${clause}` },
|
|
606
|
+
}
|
|
607
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { importedNames } from './splitImports.ts'
|
|
2
|
+
import { TEMPLATE_BLOCKS } from './TEMPLATE_BLOCKS.ts'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* What the markup at a cursor is waiting for: a block keyword after `{#`, `{:` or `{/`, a component
|
|
6
|
+
* tag after `<`, or an attribute inside a component's tag — `typed` being what is written of it so
|
|
7
|
+
* far. `undefined` in a script, a style, an expression, or text, which is the checker's to answer.
|
|
8
|
+
*/
|
|
9
|
+
export type TemplateContext =
|
|
10
|
+
| { kind: 'block'; typed: string; offered: string[] }
|
|
11
|
+
| { kind: 'tag'; typed: string; offered: string[] }
|
|
12
|
+
| { kind: 'attribute'; tag: string; typed: string; written: string[] }
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 38.117 — the template completion a cursor at `offset` in a `.abide`'s `text` stands in.
|
|
16
|
+
*
|
|
17
|
+
* READ OFF THE TEXT, NOT A PARSE: a tag or a block being typed is exactly the text the parser
|
|
18
|
+
* refuses, so the markup is scanned from the last script or style before the cursor.
|
|
19
|
+
*/
|
|
20
|
+
export function templateContext(
|
|
21
|
+
text: string,
|
|
22
|
+
offset: number,
|
|
23
|
+
): TemplateContext | undefined {
|
|
24
|
+
const markup = markupStart(text, offset)
|
|
25
|
+
if (markup === -1) return undefined
|
|
26
|
+
let depth = 0
|
|
27
|
+
let braceAt = -1
|
|
28
|
+
let tagAt = -1
|
|
29
|
+
for (let at = markup; at < offset; at += 1) {
|
|
30
|
+
const char = text[at]
|
|
31
|
+
if (depth > 0) {
|
|
32
|
+
if (char === '{') depth += 1
|
|
33
|
+
else if (char === '}') depth -= 1
|
|
34
|
+
else if (char === '"' || char === "'" || char === '`')
|
|
35
|
+
at = quoteEnd(text, at, offset)
|
|
36
|
+
continue
|
|
37
|
+
}
|
|
38
|
+
if (char === '{') {
|
|
39
|
+
depth = 1
|
|
40
|
+
braceAt = at
|
|
41
|
+
} else if (tagAt !== -1) {
|
|
42
|
+
if (char === '>') tagAt = -1
|
|
43
|
+
else if (char === '"' || char === "'")
|
|
44
|
+
at = quoteEnd(text, at, offset)
|
|
45
|
+
} else if (char === '<' && /[A-Za-z/]/.test(text[at + 1] ?? '')) {
|
|
46
|
+
tagAt = at
|
|
47
|
+
} else if (char === '<' && at + 1 === offset) tagAt = at
|
|
48
|
+
}
|
|
49
|
+
if (depth > 0) {
|
|
50
|
+
const block = /^\{([#:/])([a-z ]*)$/.exec(text.slice(braceAt, offset))
|
|
51
|
+
if (block === null) return undefined
|
|
52
|
+
return {
|
|
53
|
+
kind: 'block',
|
|
54
|
+
typed: block[2] as string,
|
|
55
|
+
offered: blocksAfter(block[1] as string, text, braceAt),
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (tagAt === -1) return undefined
|
|
59
|
+
const tag = text.slice(tagAt, offset)
|
|
60
|
+
const name = /^<([A-Za-z][\w$.]*)?$/.exec(tag)
|
|
61
|
+
if (name !== null)
|
|
62
|
+
return {
|
|
63
|
+
kind: 'tag',
|
|
64
|
+
typed: name[1] ?? '',
|
|
65
|
+
offered: componentsIn(text),
|
|
66
|
+
}
|
|
67
|
+
const component = /^<([A-Z][\w$]*)\s/.exec(tag)
|
|
68
|
+
const typing = /\s([\w$-]*)$/.exec(tag)
|
|
69
|
+
if (component === null || typing === null) return undefined
|
|
70
|
+
const written: string[] = []
|
|
71
|
+
// The values written so far, taken out, leave the names.
|
|
72
|
+
const names = tag
|
|
73
|
+
.slice(component[0].length, tag.length - (typing[1] as string).length)
|
|
74
|
+
.replace(/"[^"]*"|'[^']*'|\{[^{}]*\}/g, ' ')
|
|
75
|
+
for (const found of names.matchAll(/[A-Za-z_$][\w$:-]*/g))
|
|
76
|
+
written.push(found[0])
|
|
77
|
+
return {
|
|
78
|
+
kind: 'attribute',
|
|
79
|
+
tag: component[1] as string,
|
|
80
|
+
typed: typing[1] as string,
|
|
81
|
+
written,
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Where the markup holding `offset` starts, or -1 where `offset` is inside a script or a style. */
|
|
86
|
+
function markupStart(text: string, offset: number): number {
|
|
87
|
+
let start = 0
|
|
88
|
+
const lower = text.toLowerCase()
|
|
89
|
+
const opened = /<(script|style)\b[^>]*>/gi
|
|
90
|
+
for (;;) {
|
|
91
|
+
const open = opened.exec(text)
|
|
92
|
+
if (open === null || open.index >= offset) return start
|
|
93
|
+
const body = open.index + open[0].length
|
|
94
|
+
const close = lower.indexOf(
|
|
95
|
+
`</${(open[1] as string).toLowerCase()}`,
|
|
96
|
+
body,
|
|
97
|
+
)
|
|
98
|
+
if (offset >= body && (close === -1 || offset <= close)) return -1
|
|
99
|
+
if (close === -1) return -1
|
|
100
|
+
start = text.indexOf('>', close) + 1
|
|
101
|
+
opened.lastIndex = start
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** The offset of the quote closing the one at `at`, or the last one before `limit`. */
|
|
106
|
+
function quoteEnd(text: string, at: number, limit: number): number {
|
|
107
|
+
const quote = text[at]
|
|
108
|
+
for (let next = at + 1; next < limit; next += 1) {
|
|
109
|
+
if (text[next] === '\\') next += 1
|
|
110
|
+
else if (text[next] === quote) return next
|
|
111
|
+
}
|
|
112
|
+
return limit
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The keywords that may follow `sigil`: every block after `{#`, and after `{:` and `{/` the arms and
|
|
117
|
+
* the close of the innermost block still open before `at` — every one where none is.
|
|
118
|
+
*/
|
|
119
|
+
function blocksAfter(sigil: string, text: string, at: number): string[] {
|
|
120
|
+
const blocks = Object.keys(TEMPLATE_BLOCKS)
|
|
121
|
+
if (sigil === '#') return blocks
|
|
122
|
+
const open: string[] = []
|
|
123
|
+
const marks = new RegExp(`\\{([#/])(${blocks.join('|')})(?![\\w$])`, 'g')
|
|
124
|
+
for (const mark of text.slice(0, at).matchAll(marks)) {
|
|
125
|
+
if (mark[1] === '#') open.push(mark[2] as string)
|
|
126
|
+
else if (open[open.length - 1] === mark[2]) open.pop()
|
|
127
|
+
}
|
|
128
|
+
const innermost = open[open.length - 1]
|
|
129
|
+
if (sigil === '/') return innermost === undefined ? blocks : [innermost]
|
|
130
|
+
if (innermost !== undefined) return [...(TEMPLATE_BLOCKS[innermost] ?? [])]
|
|
131
|
+
const arms: string[] = []
|
|
132
|
+
for (const block of blocks) arms.push(...(TEMPLATE_BLOCKS[block] ?? []))
|
|
133
|
+
return arms
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* The components a tag in `text` may name: each TitleCase name its scripts import, and each
|
|
138
|
+
* `{#component}` it declares (32.41).
|
|
139
|
+
*/
|
|
140
|
+
function componentsIn(text: string): string[] {
|
|
141
|
+
const names = new Set<string>()
|
|
142
|
+
for (const script of text.matchAll(
|
|
143
|
+
/<script\b[^>]*>([\s\S]*?)(?:<\/script|$)/gi,
|
|
144
|
+
))
|
|
145
|
+
for (const { name } of importedNames(script[1] as string))
|
|
146
|
+
if (/^[A-Z]/.test(name)) names.add(name)
|
|
147
|
+
for (const found of text.matchAll(/\{#component\s+([A-Z][\w$]*)/g))
|
|
148
|
+
names.add(found[1] as string)
|
|
149
|
+
return [...names]
|
|
150
|
+
}
|
package/src/compiler/tokenize.ts
CHANGED
|
@@ -186,6 +186,12 @@ function scan(
|
|
|
186
186
|
while (true) {
|
|
187
187
|
let kind = scanner.scan()
|
|
188
188
|
if (kind === SyntaxKind.EndOfFile) return
|
|
189
|
+
// A `#` NAMING NOTHING scans as an empty private name and the scanner does not move past
|
|
190
|
+
// it, so `{#` typed at the end of a file never finished parsing. It is stepped over.
|
|
191
|
+
if (scanner.getTokenEnd() === scanner.getTokenStart()) {
|
|
192
|
+
scanner.resetTokenState(scanner.getTokenEnd() + 1)
|
|
193
|
+
continue
|
|
194
|
+
}
|
|
189
195
|
if (
|
|
190
196
|
expectsOperand &&
|
|
191
197
|
(kind === SyntaxKind.SlashToken ||
|
|
@@ -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.
|
|
3
|
+
export const BRIDGE_VERSION = '0.3.0'
|
package/src/server/Inbox.ts
CHANGED
|
@@ -20,6 +20,14 @@ export class Inbox<Item> implements AsyncIterable<Item> {
|
|
|
20
20
|
this.#wake?.()
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/** Closes this inbox and hands what nothing has read yet to a new one, for a new reader. */
|
|
24
|
+
handOff(): Inbox<Item> {
|
|
25
|
+
const next = new Inbox<Item>()
|
|
26
|
+
next.#items = this.#items.splice(0)
|
|
27
|
+
this.close()
|
|
28
|
+
return next
|
|
29
|
+
}
|
|
30
|
+
|
|
23
31
|
async *[Symbol.asyncIterator](): AsyncGenerator<Item> {
|
|
24
32
|
for (;;) {
|
|
25
33
|
if (this.#items.length > 0) {
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// The MCP protocol version abide's servers answer `initialize` with: `/__abide/mcp`, and the one a
|
|
2
|
-
// paired bridge's Claude Code reaches through the relay (
|
|
2
|
+
// paired bridge's Claude Code reaches through the relay (44.17).
|
|
3
3
|
export const MCP_PROTOCOL_VERSION = '2025-06-18'
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The key a bound component carries what `mount` needs under: its props as accessors, and a loader
|
|
3
|
+
* for its browser module. A registered symbol, because the module that sets it is generated text
|
|
4
|
+
* (`boundModule`) and spells it by its description.
|
|
5
|
+
*/
|
|
6
|
+
export const MOUNTABLE = Symbol.for('abide.mountable')
|
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
STYLE_QUERY,
|
|
44
44
|
TEXT_QUERY,
|
|
45
45
|
} from './MODULE_QUERIES.ts'
|
|
46
|
+
import { MOUNTABLE } from './MOUNTABLE.ts'
|
|
46
47
|
import { realOrAsGiven } from './realOrAsGiven.ts'
|
|
47
48
|
import { SERVER_TABLE } from './TABLES.ts'
|
|
48
49
|
import { VIRTUAL_DIRECTORY } from './VIRTUAL_DIRECTORY.ts'
|
|
@@ -449,6 +450,7 @@ function compiledAt(served: Held, path: string): CompiledFile {
|
|
|
449
450
|
// 34.16 — BOUND, NOT RAW. The compiled module is a function of accessors answering its markup (33.4);
|
|
450
451
|
// what an app's own module calls is a function of plain props answering a `Component` (35.4), which
|
|
451
452
|
// `render` invokes with nothing. The component is the module's one export (34.17).
|
|
453
|
+
// 38.119 — and it carries what `mount` hydrates it with: the same accessors, and its browser module.
|
|
452
454
|
function boundModule(path: string): string {
|
|
453
455
|
return `import component from ${JSON.stringify(path)}
|
|
454
456
|
export default (props = {}) => {
|
|
@@ -457,7 +459,12 @@ export default (props = {}) => {
|
|
|
457
459
|
const value = props[key]
|
|
458
460
|
accessors[key] = () => value
|
|
459
461
|
}
|
|
460
|
-
|
|
462
|
+
const bound = () => component(accessors)
|
|
463
|
+
bound[Symbol.for(${JSON.stringify(MOUNTABLE.description)})] = {
|
|
464
|
+
accessors,
|
|
465
|
+
client: () => import(${JSON.stringify(`${path}${CLIENT_QUERY}`)}),
|
|
466
|
+
}
|
|
467
|
+
return bound
|
|
461
468
|
}
|
|
462
469
|
`
|
|
463
470
|
}
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
import { APP_META } from '#shared/APP_META.ts'
|
|
19
19
|
import { DEBUG_META } from '#shared/DEBUG_META.ts'
|
|
20
20
|
import { decodedOrKept } from '#shared/decodedOrKept.ts'
|
|
21
|
+
import { ERROR_META } from '#shared/ERROR_META.ts'
|
|
21
22
|
import { escapeAttribute } from '#shared/escapeText.ts'
|
|
22
23
|
import { type Failed, isFailed } from '#shared/Failed.ts'
|
|
23
24
|
import { HandlerRoute } from '#shared/HandlerRoute.ts'
|
|
@@ -514,7 +515,8 @@ export async function appSources(parts: SourceParts): Promise<AppSources> {
|
|
|
514
515
|
const sources: AppSources = {
|
|
515
516
|
routes: rankRoutes([...rows.values()]),
|
|
516
517
|
errors,
|
|
517
|
-
|
|
518
|
+
// 35.18 — marked, so the browser hydrates no page over what an error page painted.
|
|
519
|
+
errorDocument: intoHead(documentOf([]), `<meta name="${ERROR_META}">`),
|
|
518
520
|
framework: client.framework,
|
|
519
521
|
published,
|
|
520
522
|
explained: parts.explained,
|