@abide/abide 0.101.0 → 0.103.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/bin/abide.ts +226 -164
- package/docs/REGISTRY.md +730 -0
- package/docs/RULEBOOK.md +2768 -0
- package/docs/abide.md +13127 -0
- package/package.json +6 -3
- package/scaffold/AGENTS.md +38 -0
- package/scaffold/CLAUDE.md +1 -0
- package/scaffold/src/layout.abide +18 -0
- package/src/compiler/Lowering.ts +78 -9
- package/src/compiler/ShadowMap.ts +2 -2
- package/src/compiler/TypeSession.ts +341 -71
- package/src/compiler/appMap.ts +96 -0
- package/src/compiler/argsSchemaOf.ts +1 -1
- package/src/compiler/checkApp.ts +53 -8
- package/src/compiler/checkedShadow.ts +73 -6
- package/src/compiler/classifyRead.ts +6 -4
- package/src/compiler/classifyTemplate.ts +1 -1
- package/src/compiler/compileApp.ts +598 -200
- package/src/compiler/compiledEntry.ts +48 -40
- package/src/compiler/componentGraph.ts +57 -71
- package/src/compiler/declaredExports.ts +26 -9
- package/src/compiler/emitClient.ts +142 -72
- package/src/compiler/emitComponent.ts +41 -12
- package/src/compiler/emitSource.ts +102 -13
- package/src/compiler/handlersUnder.ts +164 -76
- package/src/compiler/hooksCalled.ts +35 -0
- package/src/compiler/index.ts +7 -0
- package/src/compiler/mountsOf.ts +150 -0
- package/src/compiler/nearest.ts +14 -0
- package/src/compiler/parseAbide.ts +157 -20
- package/src/compiler/projectShadow.ts +250 -19
- package/src/compiler/reactiveTypes.ts +20 -7
- package/src/compiler/readSources.ts +15 -2
- package/src/compiler/readsIn.ts +62 -8
- package/src/compiler/renderMarkup.ts +3 -3
- package/src/compiler/scaffold.ts +37 -15
- package/src/compiler/serveLanguage.ts +101 -34
- package/src/compiler/sharedModuleWrites.ts +2 -2
- package/src/compiler/sourceMapOf.ts +193 -0
- package/src/compiler/splitImports.ts +205 -83
- package/src/compiler/tokenize.ts +14 -0
- package/src/compiler/writeDefaultProject.ts +29 -2
- package/src/server/CLIENT_BUILD.ts +11 -13
- package/src/server/DEFAULT_PORT.ts +1 -1
- package/src/server/MODULE_QUERIES.ts +27 -0
- package/src/server/TABLES.ts +7 -0
- package/src/server/VIRTUAL_DIRECTORY.ts +4 -5
- package/src/server/abideModules.ts +457 -0
- package/src/server/answerFailure.ts +16 -6
- package/src/server/answerRequest.ts +241 -107
- package/src/server/answerRpc.ts +107 -33
- package/src/server/answerSocket.ts +65 -20
- package/src/server/answerValue.ts +22 -9
- package/src/server/appDirectories.ts +1 -1
- package/src/server/appPlugins.ts +1 -1
- package/src/server/appTree.ts +238 -0
- package/src/server/argsFor.ts +2 -2
- package/src/server/assistant.ts +2 -2
- package/src/server/bridgeRelay.ts +6 -8
- package/src/server/claudeCode.ts +0 -6
- package/src/server/clientBuildOptions.ts +8 -3
- package/src/server/coerceArgs.ts +1 -1
- package/src/server/commands.ts +38 -37
- package/src/server/componentModules.d.ts +2 -1
- package/src/server/compressAnswer.ts +4 -4
- package/src/server/config.ts +25 -29
- package/src/server/configFields.ts +31 -25
- package/src/server/createApp.ts +77 -79
- package/src/server/devWorker.ts +118 -181
- package/src/server/executableName.ts +4 -0
- package/src/server/fenceServer.ts +57 -32
- package/src/server/generatedSurfaces.ts +99 -66
- package/src/server/hooks.ts +137 -52
- package/src/server/iconAccent.ts +191 -0
- package/src/server/index.ts +12 -7
- package/src/server/installSourceMaps.ts +93 -0
- package/src/server/launcherOptions.ts +33 -31
- package/src/server/launcherPage.ts +107 -64
- package/src/server/manifestOf.ts +16 -13
- package/src/server/namesUnder.ts +2 -2
- package/src/server/nativeMenu.ts +6 -6
- package/src/server/negotiateEncoding.ts +1 -1
- package/src/server/preload.ts +13 -0
- package/src/server/principal.ts +10 -5
- package/src/server/realOrAsGiven.ts +10 -0
- package/src/server/refuseAddressCollision.ts +21 -14
- package/src/server/remote.ts +49 -52
- package/src/server/render.ts +65 -94
- package/src/server/renderComponent.ts +5 -0
- package/src/server/reportHealth.ts +2 -2
- package/src/server/requestScope.ts +12 -1
- package/src/server/runBuild.ts +24 -0
- package/src/server/runCompiled.ts +5 -5
- package/src/server/runDesktop.ts +48 -28
- package/src/server/serveBuild.ts +43 -52
- package/src/server/serveDev.ts +79 -37
- package/src/server/shell.ts +7 -22
- package/src/server/sourceMappings.ts +139 -0
- package/src/server/surfaceOf.ts +20 -3
- package/src/server/testBuild.ts +1 -19
- package/src/server/withAbideSources.ts +117 -0
- package/src/shared/ADDRESSES.ts +10 -18
- package/src/shared/APP_META.ts +7 -0
- package/src/shared/CopyOptions.ts +1 -1
- package/src/shared/DEBUG_META.ts +1 -1
- package/src/shared/LAUNCHER_FILES.ts +9 -0
- package/src/shared/RESERVED_DIRECTORIES.ts +24 -0
- package/src/shared/cookies.ts +2 -2
- package/src/shared/csp.ts +1 -1
- package/src/shared/escapeText.ts +1 -1
- package/src/shared/filledPath.ts +85 -0
- package/src/shared/framing.ts +79 -6
- package/src/shared/headKeyOf.ts +2 -0
- package/src/shared/index.ts +2 -1
- package/src/shared/log.ts +27 -13
- package/src/shared/matchRoute.ts +23 -4
- package/src/{ui → shared}/menuBridge.ts +2 -2
- package/src/shared/menuItem.ts +4 -4
- package/src/shared/onMenu.ts +58 -0
- package/src/shared/reactive/Keyed.ts +2 -2
- package/src/shared/reactive/NotLanded.ts +1 -1
- package/src/shared/reactive/Reactive.ts +43 -43
- package/src/shared/reactive/argsContract.ts +4 -4
- package/src/shared/reactive/canonicalKey.ts +4 -4
- package/src/shared/reactive/chunksOfStream.ts +6 -0
- package/src/shared/reactive/index.ts +1 -1
- package/src/shared/reactive/recordWrites.ts +103 -5
- package/src/shared/reactive/structural.ts +1 -1
- package/src/shared/reactive/tracking.ts +1 -1
- package/src/shared/reactive/watch.ts +3 -3
- package/src/shared/refuse.ts +1 -1
- package/src/{ui → shared}/reportMenu.ts +8 -10
- package/src/shared/routePattern.ts +122 -48
- package/src/shared/rpc.ts +37 -8
- package/src/shared/rpcRequest.ts +27 -3
- package/src/shared/url.ts +4 -44
- package/src/shared/writePath.ts +1 -1
- package/src/ui/LAYER_NAMES.ts +1 -1
- package/src/ui/List.ts +3 -3
- package/src/ui/MOUNTING_LEVEL.ts +18 -0
- package/src/ui/Mismatch.ts +1 -1
- package/src/ui/RawRun.ts +1 -1
- package/src/ui/Router.ts +37 -36
- package/src/ui/Scope.ts +1 -1
- package/src/ui/TryRegion.ts +2 -2
- package/src/ui/applyHead.ts +33 -0
- package/src/ui/index.ts +1 -1
- package/src/ui/moveNode.ts +1 -1
- package/src/ui/reconcile.ts +2 -2
- package/src/ui/routing.ts +2 -2
- package/src/ui/setAttribute.ts +1 -1
- package/src/ui/setText.ts +1 -1
- package/src/ui/socketWire.ts +9 -7
- package/src/ui/wire.ts +1 -1
- package/scaffold/src/server/app.ts +0 -6
- package/scaffold/src/shared/.gitkeep +0 -0
- package/scaffold/src/ui/app.html +0 -8
- package/src/server/componentImports.ts +0 -56
- /package/scaffold/src/{ui/pages/page.abide → index.abide} +0 -0
- /package/src/{compiler → shared}/SPECIFIERS.ts +0 -0
package/src/compiler/scaffold.ts
CHANGED
|
@@ -1,5 +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
|
|
2
|
+
// nothing to choose, because what is scaffolded is a root layout and a page — the files the commands
|
|
3
|
+
// read — and the test preload.
|
|
3
4
|
// A file that is already there is left as it is and reported skipped, so a scaffold run over a
|
|
4
5
|
// half-made project finishes it rather than overwriting it.
|
|
5
6
|
|
|
@@ -7,8 +8,8 @@ import { withoutTrailingSlash } from '#server/withoutTrailingSlash.ts'
|
|
|
7
8
|
import abidePackage from '../../package.json' with { type: 'json' }
|
|
8
9
|
import { COMPILER_OPTIONS } from './writeDefaultProject.ts'
|
|
9
10
|
|
|
10
|
-
// THE STARTER'S OWN FILES, as files: a real document, a real page and
|
|
11
|
-
// stand. What depends on the name
|
|
11
|
+
// THE STARTER'S OWN FILES, as files: a real document, a real page and the agent notes, copied as
|
|
12
|
+
// they stand. What depends on the name, or what a packer drops (`filesFor`), is written here.
|
|
12
13
|
const STARTER = Bun.fileURLToPath(new URL('../../scaffold/', import.meta.url))
|
|
13
14
|
|
|
14
15
|
// THE ABIDE THAT WROTE THE PROJECT is the one it depends on. Installed from a registry, that is its
|
|
@@ -21,28 +22,44 @@ const ABIDE_DEPENDENCY = PACKAGE_ROOT.includes('/node_modules/')
|
|
|
21
22
|
? `^${abidePackage.version}`
|
|
22
23
|
: `file:${PACKAGE_ROOT}`
|
|
23
24
|
|
|
25
|
+
// The `#src` alias, written as the package's `imports` and again as the checker's `paths`.
|
|
26
|
+
const SOURCE_ALIAS = { '#src/*': ['./src/*', './src/*.ts'] }
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 38.109 — the starter's `bunfig.toml`: a test imports the app's `.abide` files as modules, with
|
|
30
|
+
* nothing booted first.
|
|
31
|
+
*/
|
|
32
|
+
export const TEST_PRELOAD =
|
|
33
|
+
'# 38.109 — a test imports the app\'s `.abide` files as modules, with nothing booted first.\n[test]\npreload = ["@abide/abide/test"]\n'
|
|
34
|
+
|
|
35
|
+
// WRITTEN, NOT COPIED, beside the manifests: a packer drops a `bunfig.toml` or a `.gitignore` from
|
|
36
|
+
// the tarball (`bun pm pack` both, `npm pack` the second), so neither survives as a starter file.
|
|
24
37
|
function filesFor(name: string): Record<string, string> {
|
|
25
38
|
return {
|
|
39
|
+
'bunfig.toml': TEST_PRELOAD,
|
|
40
|
+
'.gitignore': 'node_modules\n.abide\n',
|
|
26
41
|
'package.json': `${JSON.stringify(
|
|
27
42
|
{
|
|
28
43
|
name,
|
|
29
44
|
private: true,
|
|
30
45
|
type: 'module',
|
|
31
46
|
scripts: {
|
|
47
|
+
// 38.103 — the checker over `.abide`, which `tsc` does not read.
|
|
48
|
+
check: 'abide check',
|
|
32
49
|
dev: 'abide dev',
|
|
33
50
|
build: 'abide build',
|
|
34
51
|
start: 'abide start',
|
|
52
|
+
// 38.108 — every address, and the file answering it.
|
|
53
|
+
routes: 'abide routes',
|
|
35
54
|
},
|
|
36
|
-
//
|
|
37
|
-
// written and only bun probes for the extension:
|
|
38
|
-
// target as the exact file, so `#server/
|
|
39
|
-
// imports `any` and the checker could not type one of
|
|
40
|
-
// target is what bun resolves, and the one a `.ts` or
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
'#shared/*': ['./src/shared/*', './src/shared/*.ts'],
|
|
45
|
-
},
|
|
55
|
+
// ONE ALIAS FOR THE SOURCE, and two targets for it, because an extensionless
|
|
56
|
+
// specifier is how an import is written and only bun probes for the extension:
|
|
57
|
+
// TypeScript takes an `imports` target as the exact file, so `#src/media/server/db`
|
|
58
|
+
// alone left every name it imports `any` and the checker could not type one of
|
|
59
|
+
// them (31.3). The first target is what bun resolves, and the one a `.ts` or
|
|
60
|
+
// `.abide` specifier needs. Which side a module is on is its directory's to say
|
|
61
|
+
// (36.10), not its alias's.
|
|
62
|
+
imports: SOURCE_ALIAS,
|
|
46
63
|
dependencies: { '@abide/abide': ABIDE_DEPENDENCY },
|
|
47
64
|
// `Bun`, `bun:sqlite` and the rest of the runtime a server module reaches for are
|
|
48
65
|
// typed by these, and `abide check` reports every one untyped without them.
|
|
@@ -53,8 +70,14 @@ function filesFor(name: string): Record<string, string> {
|
|
|
53
70
|
)}\n`,
|
|
54
71
|
'tsconfig.json': `${JSON.stringify(
|
|
55
72
|
// TypeScript 7 includes no `@types` package it is not named, so the runtime's is named.
|
|
73
|
+
// 34.21 — the alias as a path too: the checker resolves a text import's declaration
|
|
74
|
+
// beside the file a path names, and takes an `imports` target as the exact file.
|
|
56
75
|
{
|
|
57
|
-
compilerOptions: {
|
|
76
|
+
compilerOptions: {
|
|
77
|
+
...COMPILER_OPTIONS,
|
|
78
|
+
types: ['bun'],
|
|
79
|
+
paths: SOURCE_ALIAS,
|
|
80
|
+
},
|
|
58
81
|
include: ['src'],
|
|
59
82
|
},
|
|
60
83
|
null,
|
|
@@ -81,7 +104,6 @@ export async function scaffold(
|
|
|
81
104
|
const written: string[] = []
|
|
82
105
|
const skipped: string[] = []
|
|
83
106
|
const files: [string, string | Blob][] = Object.entries(filesFor(name))
|
|
84
|
-
// The seam both sides import from is there from the first commit, `.gitkeep` and all.
|
|
85
107
|
for (const path of new Bun.Glob('**/*').scanSync({
|
|
86
108
|
cwd: STARTER,
|
|
87
109
|
dot: true,
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { type FSWatcher, watch } from 'node:fs' // bun exposes no file watcher of its own
|
|
2
|
-
import {
|
|
1
|
+
import { existsSync, type FSWatcher, watch } from 'node:fs' // bun exposes no file watcher or sync existence check of its own
|
|
2
|
+
import { dirname } from 'node:path' // bun exposes no path helpers of its own
|
|
3
|
+
import { realOrAsGiven } from '#server/realOrAsGiven.ts'
|
|
3
4
|
import { checkSource } from './checkApp.ts'
|
|
4
5
|
import { type AppChecker, openChecker } from './compileApp.ts'
|
|
6
|
+
import { nearest } from './nearest.ts'
|
|
5
7
|
import type { Diagnosis, Inspection, Place } from './TypeSession.ts'
|
|
6
8
|
|
|
7
|
-
// 38.
|
|
9
|
+
// 38.11 — `abide lsp`: THE `.abide` LANGUAGE SERVER, OVER STDIO. It is `abide check` (38.3) asked
|
|
8
10
|
// about one open document at a time, so the squiggle an editor draws and the line CI fails on are
|
|
9
11
|
// one finding rather than two implementations that agree most of the time.
|
|
10
12
|
//
|
|
@@ -15,10 +17,14 @@ import type { Diagnosis, Inspection, Place } from './TypeSession.ts'
|
|
|
15
17
|
// same text.
|
|
16
18
|
//
|
|
17
19
|
// THE APP IS THE DOCUMENT'S. An editor starts this at the root of whatever it has open, which in a
|
|
18
|
-
// monorepo is not an app at all, so each document is checked against the
|
|
19
|
-
//
|
|
20
|
+
// monorepo is not an app at all, so each document is checked against the root of the NEAREST
|
|
21
|
+
// package above it (`appRootOf`) — with one checker per app, kept for the session.
|
|
20
22
|
// `--root` pins every document to one.
|
|
21
23
|
//
|
|
24
|
+
// THE EDITOR'S SPELLING STOPS HERE. An app opened through a link (`/tmp` is one on macOS) is
|
|
25
|
+
// checked at its real path, the one bun resolves every import to (`openChecker`), so a path is
|
|
26
|
+
// made real on its way in (`placed`) and spelled under the link again on its way out (`spelled`).
|
|
27
|
+
//
|
|
22
28
|
// ONE CHECK AT A TIME. The checker holds one snapshot, and two checks interleaving would each
|
|
23
29
|
// register the other's text for a moment; a burst of keystrokes collapses onto the last one.
|
|
24
30
|
|
|
@@ -52,8 +58,33 @@ export async function serveLanguage(
|
|
|
52
58
|
const timers = new Map<string, ReturnType<typeof setTimeout>>()
|
|
53
59
|
const checkers = new Map<string, AppChecker>()
|
|
54
60
|
const watchers = new Map<string, FSWatcher>()
|
|
61
|
+
// Each app's real root, to the root the editor reached it by.
|
|
62
|
+
const links = new Map<string, string>()
|
|
55
63
|
let queue: Promise<void> = Promise.resolve()
|
|
56
64
|
|
|
65
|
+
// The document at `uri`, in its app's real spelling; `undefined` where no app holds it.
|
|
66
|
+
const placed = (uri: string): { app: string; path: string } | undefined => {
|
|
67
|
+
const path = Bun.fileURLToPath(uri)
|
|
68
|
+
const given = root ?? appRootOf(path)
|
|
69
|
+
if (given === undefined) return undefined
|
|
70
|
+
const app = realOrAsGiven(given)
|
|
71
|
+
if (app !== given) links.set(app, given)
|
|
72
|
+
return {
|
|
73
|
+
app,
|
|
74
|
+
path: path.startsWith(`${given}/`)
|
|
75
|
+
? `${app}${path.slice(given.length)}`
|
|
76
|
+
: path,
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// `path`, real, under the root the editor reached its app by.
|
|
81
|
+
const spelled = (path: string): string => {
|
|
82
|
+
for (const [app, given] of links)
|
|
83
|
+
if (path.startsWith(`${app}/`))
|
|
84
|
+
return `${given}${path.slice(app.length)}`
|
|
85
|
+
return path
|
|
86
|
+
}
|
|
87
|
+
|
|
57
88
|
const send = (message: object): void => {
|
|
58
89
|
const body = ENCODER.encode(
|
|
59
90
|
JSON.stringify({ jsonrpc: '2.0', ...message }),
|
|
@@ -118,8 +149,7 @@ export async function serveLanguage(
|
|
|
118
149
|
)
|
|
119
150
|
// What an open page says may be decided by the file that moved.
|
|
120
151
|
for (const uri of documents.keys())
|
|
121
|
-
if ((
|
|
122
|
-
check(uri)
|
|
152
|
+
if (placed(uri)?.app === app) check(uri)
|
|
123
153
|
}),
|
|
124
154
|
)
|
|
125
155
|
return checker
|
|
@@ -129,18 +159,18 @@ export async function serveLanguage(
|
|
|
129
159
|
const document = documents.get(uri)
|
|
130
160
|
if (document === undefined) return
|
|
131
161
|
const version = document.version
|
|
132
|
-
const path = Bun.fileURLToPath(uri)
|
|
133
|
-
const app = root ?? appRootOf(path)
|
|
134
162
|
// A `.abide` belonging to no app has nothing to be checked against.
|
|
135
|
-
|
|
163
|
+
const at = placed(uri)
|
|
164
|
+
if (at === undefined) return
|
|
165
|
+
const { app, path } = at
|
|
136
166
|
let found: Diagnosis[]
|
|
137
167
|
try {
|
|
138
168
|
found = await checkSource(
|
|
139
169
|
await checkerOf(app),
|
|
140
|
-
app,
|
|
141
170
|
path,
|
|
142
171
|
document.text,
|
|
143
|
-
(held) =>
|
|
172
|
+
(held) =>
|
|
173
|
+
documents.get(Bun.pathToFileURL(spelled(held)).href)?.text,
|
|
144
174
|
)
|
|
145
175
|
} catch (thrown) {
|
|
146
176
|
// The checker's own failure, not the page's: the next check opens another.
|
|
@@ -179,7 +209,20 @@ export async function serveLanguage(
|
|
|
179
209
|
|
|
180
210
|
// A question about a document is about the text the editor shows, so a check still settling
|
|
181
211
|
// runs now rather than after the answer; and it is answered in the queue, after that check.
|
|
182
|
-
const inspect = (params: unknown): Promise<Inspection | undefined> =>
|
|
212
|
+
const inspect = (params: unknown): Promise<Inspection | undefined> =>
|
|
213
|
+
ask(params, (session, path, text, offset) =>
|
|
214
|
+
session.inspect(path, text, offset),
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
const ask = <Answer>(
|
|
218
|
+
params: unknown,
|
|
219
|
+
question: (
|
|
220
|
+
session: AppChecker['session'],
|
|
221
|
+
path: string,
|
|
222
|
+
text: string,
|
|
223
|
+
offset: number,
|
|
224
|
+
) => Promise<Answer | undefined>,
|
|
225
|
+
): Promise<Answer | undefined> => {
|
|
183
226
|
const { textDocument, position } = params as {
|
|
184
227
|
textDocument: { uri: string }
|
|
185
228
|
position: { line: number; character: number }
|
|
@@ -192,12 +235,11 @@ export async function serveLanguage(
|
|
|
192
235
|
}
|
|
193
236
|
const answered = queue.then(async () => {
|
|
194
237
|
const document = documents.get(uri)
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
path,
|
|
238
|
+
const at = placed(uri)
|
|
239
|
+
if (document === undefined || at === undefined) return undefined
|
|
240
|
+
return question(
|
|
241
|
+
(await checkerOf(at.app)).session,
|
|
242
|
+
at.path,
|
|
201
243
|
document.text,
|
|
202
244
|
offsetOf(document.text, position),
|
|
203
245
|
)
|
|
@@ -220,6 +262,7 @@ export async function serveLanguage(
|
|
|
220
262
|
textDocumentSync: 1,
|
|
221
263
|
hoverProvider: true,
|
|
222
264
|
definitionProvider: true,
|
|
265
|
+
completionProvider: { triggerCharacters: ['.'] },
|
|
223
266
|
},
|
|
224
267
|
serverInfo: { name: 'abide' },
|
|
225
268
|
},
|
|
@@ -279,11 +322,39 @@ export async function serveLanguage(
|
|
|
279
322
|
})
|
|
280
323
|
return true
|
|
281
324
|
}
|
|
325
|
+
// 38.98 — the protocol's `CompletionItemKind` is the checker's own numbering.
|
|
326
|
+
case 'textDocument/completion': {
|
|
327
|
+
const trigger = (
|
|
328
|
+
params?.context as { triggerCharacter?: string } | undefined
|
|
329
|
+
)?.triggerCharacter
|
|
330
|
+
const found = await ask(params, (session, path, text, offset) =>
|
|
331
|
+
session.complete(path, text, offset, trigger),
|
|
332
|
+
)
|
|
333
|
+
const items: object[] = []
|
|
334
|
+
for (const entry of found?.entries ?? [])
|
|
335
|
+
items.push({
|
|
336
|
+
label: entry.name,
|
|
337
|
+
kind: entry.kind,
|
|
338
|
+
sortText: entry.sortText,
|
|
339
|
+
filterText: entry.filterText,
|
|
340
|
+
insertText: entry.insertText,
|
|
341
|
+
detail: entry.detail,
|
|
342
|
+
labelDetails: entry.labelDetails,
|
|
343
|
+
})
|
|
344
|
+
send({
|
|
345
|
+
id: message.id,
|
|
346
|
+
result:
|
|
347
|
+
found === undefined
|
|
348
|
+
? null
|
|
349
|
+
: { isIncomplete: found.isIncomplete, items },
|
|
350
|
+
})
|
|
351
|
+
return true
|
|
352
|
+
}
|
|
282
353
|
case 'textDocument/definition': {
|
|
283
354
|
const found = await inspect(params)
|
|
284
355
|
const locations: object[] = []
|
|
285
356
|
for (const place of found?.definitions ?? [])
|
|
286
|
-
locations.push(locationOf(place))
|
|
357
|
+
locations.push(locationOf(spelled(place.path), place))
|
|
287
358
|
send({ id: message.id, result: locations })
|
|
288
359
|
return true
|
|
289
360
|
}
|
|
@@ -348,20 +419,16 @@ async function closeAll(
|
|
|
348
419
|
}
|
|
349
420
|
|
|
350
421
|
/**
|
|
351
|
-
* The app a file at `path` belongs to: the
|
|
422
|
+
* The app a file at `path` belongs to: the root 38.25 finds from the nearest package above it.
|
|
352
423
|
*
|
|
353
|
-
*
|
|
354
|
-
*
|
|
424
|
+
* That package's `src` where it has one, and the package itself where it does not — the same root
|
|
425
|
+
* `abide dev` run in that package would find. `undefined` where no package lies above the file.
|
|
355
426
|
*/
|
|
356
427
|
function appRootOf(path: string): string | undefined {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
const up = dirname(at)
|
|
362
|
-
if (up === at) return found
|
|
363
|
-
at = up
|
|
364
|
-
}
|
|
428
|
+
const manifest = nearest(dirname(path), 'package.json')
|
|
429
|
+
if (manifest === undefined) return undefined
|
|
430
|
+
const at = dirname(manifest)
|
|
431
|
+
return existsSync(`${at}/src`) ? `${at}/src` : at
|
|
365
432
|
}
|
|
366
433
|
|
|
367
434
|
/**
|
|
@@ -390,10 +457,10 @@ function offsetOf(
|
|
|
390
457
|
return at + position.character
|
|
391
458
|
}
|
|
392
459
|
|
|
393
|
-
/** A `Place`, lines and columns from 1, as the protocol's `Location`, from 0. */
|
|
394
|
-
function locationOf(place: Place): object {
|
|
460
|
+
/** A `Place` at `path`, lines and columns from 1, as the protocol's `Location`, from 0. */
|
|
461
|
+
function locationOf(path: string, place: Place): object {
|
|
395
462
|
return {
|
|
396
|
-
uri: Bun.pathToFileURL(
|
|
463
|
+
uri: Bun.pathToFileURL(path).href,
|
|
397
464
|
range: {
|
|
398
465
|
start: {
|
|
399
466
|
line: place.start.line - 1,
|
|
@@ -11,7 +11,7 @@ import { classifyRead, FUNCTIONS } from './classifyRead.ts'
|
|
|
11
11
|
import type { Finding } from './Finding.ts'
|
|
12
12
|
import type { Span } from './projectShadow.ts'
|
|
13
13
|
|
|
14
|
-
// 34.
|
|
14
|
+
// 34.18 — A PLAIN `let` IN A `<script module>` THAT A `<script>` WRITES AS IT SETS UP. The block is
|
|
15
15
|
// module scope (34.2), which a server shares across every request, and setup runs inside a render —
|
|
16
16
|
// so the value one request leaves is the one the next request starts from, and its markup drifts
|
|
17
17
|
// from what the browser builds. A `reactive` is per request wherever it is declared (11.32), which
|
|
@@ -74,7 +74,7 @@ export async function sharedModuleWrites(
|
|
|
74
74
|
if (insideFunction(node)) continue
|
|
75
75
|
warnings.push({
|
|
76
76
|
at,
|
|
77
|
-
message: `abide: \`${name}\` is a plain \`let\` in <script module>, which a server shares across every request, and this <script> writes it while the page renders — so one request's value is where the next one starts. Declare it with \`reactive\`, which is per request wherever it stands, or move it into <script>. RULEBOOK 34.
|
|
77
|
+
message: `abide: \`${name}\` is a plain \`let\` in <script module>, which a server shares across every request, and this <script> writes it while the page renders — so one request's value is where the next one starts. Declare it with \`reactive\`, which is per request wherever it stands, or move it into <script>. RULEBOOK 34.18.`,
|
|
78
78
|
})
|
|
79
79
|
}
|
|
80
80
|
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// 38.97 — WHERE A COMPILED MODULE'S LINES CAME FROM IN ITS `.abide`. The emitters build a module
|
|
2
|
+
// as text around what `Lowering` hands them, so a chunk is placed by FINDING it in what they wrote
|
|
3
|
+
// rather than by threading offsets through every template string that assembles one:
|
|
4
|
+
//
|
|
5
|
+
// - a script block is many lines and lands whole, its imports each left as a `;` on as many lines
|
|
6
|
+
// (`splitImports`), so it is anchored by its longest line found once in the module and then read
|
|
7
|
+
// line for line;
|
|
8
|
+
// - a template expression is one line and may land more than once — a client module writes a slot
|
|
9
|
+
// in its markup and again in the effect that updates it — and every landing is the same source.
|
|
10
|
+
//
|
|
11
|
+
// What no chunk covers is the framework's own scaffolding, and maps to nothing.
|
|
12
|
+
|
|
13
|
+
import { encodeMappings, type Segment } from '#server/sourceMappings.ts'
|
|
14
|
+
import type { LoweredChunk } from './Lowering.ts'
|
|
15
|
+
import type { Origin } from './projectShadow.ts'
|
|
16
|
+
import { mapBack } from './ShadowMap.ts'
|
|
17
|
+
|
|
18
|
+
/** The module's `mappings`, into the `.abide` whose text is `source`. */
|
|
19
|
+
export function sourceMapOf(
|
|
20
|
+
output: string,
|
|
21
|
+
chunks: readonly LoweredChunk[],
|
|
22
|
+
projected: readonly Origin[],
|
|
23
|
+
source: string,
|
|
24
|
+
): string {
|
|
25
|
+
const sourceStarts = lineStarts(source)
|
|
26
|
+
const outputLines = output.split('\n')
|
|
27
|
+
const outputStarts = lineStarts(output)
|
|
28
|
+
const lines: Segment[][] = []
|
|
29
|
+
for (let index = 0; index < outputLines.length; index += 1) lines.push([])
|
|
30
|
+
// Where a projection offset is in the `.abide`, as a line and a column.
|
|
31
|
+
const place = (line: number, column: number, offset: number): void => {
|
|
32
|
+
const at = mapBack(projected, offset)
|
|
33
|
+
const sourceLine = lineAt(sourceStarts, at)
|
|
34
|
+
;(lines[line] as Segment[]).push([
|
|
35
|
+
column,
|
|
36
|
+
0,
|
|
37
|
+
sourceLine,
|
|
38
|
+
at - (sourceStarts[sourceLine] as number),
|
|
39
|
+
])
|
|
40
|
+
}
|
|
41
|
+
const claimed = new Set<number>()
|
|
42
|
+
|
|
43
|
+
const multiline: LoweredChunk[] = []
|
|
44
|
+
const single: LoweredChunk[] = []
|
|
45
|
+
for (const chunk of chunks)
|
|
46
|
+
(chunk.text.includes('\n') ? multiline : single).push(chunk)
|
|
47
|
+
multiline.sort((one, two) => two.text.length - one.text.length)
|
|
48
|
+
|
|
49
|
+
for (const chunk of multiline) {
|
|
50
|
+
const chunkLines = chunk.text.split('\n')
|
|
51
|
+
const first = anchor(chunkLines, outputLines, outputStarts, output)
|
|
52
|
+
if (first === undefined) continue
|
|
53
|
+
const chunkStarts = lineStarts(chunk.text)
|
|
54
|
+
for (let index = 0; index < chunkLines.length; index += 1) {
|
|
55
|
+
const line = first + index
|
|
56
|
+
const written = outputLines[line]
|
|
57
|
+
const lowered = chunkLines[index] as string
|
|
58
|
+
if (written === undefined || claimed.has(line)) continue
|
|
59
|
+
if (written.trim() !== lowered.trim() || lowered.trim() === '')
|
|
60
|
+
continue
|
|
61
|
+
claimed.add(line)
|
|
62
|
+
const start = chunkStarts[index] as number
|
|
63
|
+
const end = start + lowered.length
|
|
64
|
+
// Lines the emitter re-indented keep their text, and move by the margin it added.
|
|
65
|
+
const shift = written.length - lowered.length
|
|
66
|
+
for (const origin of chunk.origins) {
|
|
67
|
+
const runEnd = origin.at + Math.max(origin.length, 1)
|
|
68
|
+
if (origin.at >= end || runEnd <= start) continue
|
|
69
|
+
const from = Math.max(origin.at, start)
|
|
70
|
+
place(
|
|
71
|
+
line,
|
|
72
|
+
from - start + shift,
|
|
73
|
+
origin.source + (from - origin.at),
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// LONGEST FIRST, each landing claimed: `count() + 1` is found again inside the handler
|
|
80
|
+
// `() => { count.set(count() + 1) }` was lowered from, and is that handler's there.
|
|
81
|
+
single.sort((one, two) => two.text.trim().length - one.text.trim().length)
|
|
82
|
+
const taken: [number, number][] = []
|
|
83
|
+
const seen = new Set<string>()
|
|
84
|
+
for (const chunk of single) {
|
|
85
|
+
const text = chunk.text.trim()
|
|
86
|
+
if (text.length < 2) continue
|
|
87
|
+
// An expression an emitter asked for twice is one chunk.
|
|
88
|
+
const key = `${chunk.origins[0]?.source}:${text}`
|
|
89
|
+
if (seen.has(key)) continue
|
|
90
|
+
seen.add(key)
|
|
91
|
+
const lead = chunk.text.indexOf(text)
|
|
92
|
+
for (
|
|
93
|
+
let found = output.indexOf(text);
|
|
94
|
+
found !== -1;
|
|
95
|
+
found = output.indexOf(text, found + text.length)
|
|
96
|
+
) {
|
|
97
|
+
const end = found + text.length
|
|
98
|
+
if (!standsAlone(output, found, end)) continue
|
|
99
|
+
const line = lineAt(outputStarts, found)
|
|
100
|
+
if (claimed.has(line)) continue
|
|
101
|
+
if (taken.some(([from, to]) => found < to && end > from)) continue
|
|
102
|
+
taken.push([found, end])
|
|
103
|
+
const column = found - (outputStarts[line] as number)
|
|
104
|
+
for (const origin of chunk.origins) {
|
|
105
|
+
const at = origin.at - lead
|
|
106
|
+
if (at < 0 || at > text.length) continue
|
|
107
|
+
place(line, column + at, origin.source)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Two runs placed at one column — a pin and the copy after it — keep the later, which is the
|
|
113
|
+
// author's own text.
|
|
114
|
+
for (const segments of lines) {
|
|
115
|
+
segments.sort((one, two) => one[0] - two[0])
|
|
116
|
+
for (let at = segments.length - 1; at > 0; at -= 1)
|
|
117
|
+
if (
|
|
118
|
+
(segments[at - 1] as Segment)[0] ===
|
|
119
|
+
(segments[at] as Segment)[0]
|
|
120
|
+
)
|
|
121
|
+
segments.splice(at - 1, 1)
|
|
122
|
+
}
|
|
123
|
+
return encodeMappings(lines)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The module line `chunkLines` starts on: its longest line found exactly once, standing where the
|
|
128
|
+
* rest of the chunk agrees — or `undefined` where no line places it.
|
|
129
|
+
*/
|
|
130
|
+
function anchor(
|
|
131
|
+
chunkLines: readonly string[],
|
|
132
|
+
outputLines: readonly string[],
|
|
133
|
+
outputStarts: readonly number[],
|
|
134
|
+
output: string,
|
|
135
|
+
): number | undefined {
|
|
136
|
+
const order: number[] = []
|
|
137
|
+
for (let index = 0; index < chunkLines.length; index += 1) order.push(index)
|
|
138
|
+
order.sort(
|
|
139
|
+
(one, two) =>
|
|
140
|
+
(chunkLines[two] as string).trim().length -
|
|
141
|
+
(chunkLines[one] as string).trim().length,
|
|
142
|
+
)
|
|
143
|
+
for (const index of order) {
|
|
144
|
+
const text = (chunkLines[index] as string).trim()
|
|
145
|
+
if (text.length < 8) return undefined
|
|
146
|
+
const found = output.indexOf(text)
|
|
147
|
+
if (found === -1 || output.indexOf(text, found + 1) !== -1) continue
|
|
148
|
+
const first = lineAt(outputStarts, found) - index
|
|
149
|
+
if (first < 0) continue
|
|
150
|
+
let agreeing = 0
|
|
151
|
+
let written = 0
|
|
152
|
+
for (let at = 0; at < chunkLines.length; at += 1) {
|
|
153
|
+
const lowered = (chunkLines[at] as string).trim()
|
|
154
|
+
if (lowered === '') continue
|
|
155
|
+
written += 1
|
|
156
|
+
if (outputLines[first + at]?.trim() === lowered) agreeing += 1
|
|
157
|
+
}
|
|
158
|
+
if (agreeing * 2 >= written) return first
|
|
159
|
+
}
|
|
160
|
+
return undefined
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const NAME = /[\w$.]/
|
|
164
|
+
|
|
165
|
+
// Not the middle of a longer name, or a member of one: `count()` inside `__count()` is not it.
|
|
166
|
+
function standsAlone(output: string, start: number, end: number): boolean {
|
|
167
|
+
return (
|
|
168
|
+
!NAME.test(output[start - 1] ?? ' ') &&
|
|
169
|
+
!/[\w$]/.test(output[end] ?? ' ')
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function lineStarts(text: string): number[] {
|
|
174
|
+
const starts = [0]
|
|
175
|
+
for (
|
|
176
|
+
let at = text.indexOf('\n');
|
|
177
|
+
at !== -1;
|
|
178
|
+
at = text.indexOf('\n', at + 1)
|
|
179
|
+
)
|
|
180
|
+
starts.push(at + 1)
|
|
181
|
+
return starts
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function lineAt(starts: readonly number[], offset: number): number {
|
|
185
|
+
let low = 0
|
|
186
|
+
let high = starts.length - 1
|
|
187
|
+
while (low < high) {
|
|
188
|
+
const middle = (low + high + 1) >>> 1
|
|
189
|
+
if ((starts[middle] as number) <= offset) low = middle
|
|
190
|
+
else high = middle - 1
|
|
191
|
+
}
|
|
192
|
+
return low
|
|
193
|
+
}
|