@abide/abide 0.102.0 → 0.104.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 +23 -0
- package/bin/abide.ts +31 -28
- package/docs/REGISTRY.md +40 -36
- package/docs/RULEBOOK.md +113 -92
- package/docs/abide.md +788 -554
- package/package.json +2 -1
- package/scaffold/AGENTS.md +13 -6
- package/src/compiler/TypeSession.ts +108 -35
- package/src/compiler/appMap.ts +91 -0
- package/src/compiler/argsSchemaOf.ts +1 -1
- package/src/compiler/checkApp.ts +35 -18
- package/src/compiler/checkedShadow.ts +40 -7
- package/src/compiler/compileApp.ts +271 -76
- package/src/compiler/compiledEntry.ts +10 -6
- package/src/compiler/componentGraph.ts +6 -7
- package/src/compiler/declaredExports.ts +3 -3
- package/src/compiler/emitClient.ts +3 -3
- package/src/compiler/emitComponent.ts +9 -9
- package/src/compiler/emitSource.ts +5 -9
- package/src/compiler/handlersUnder.ts +114 -73
- package/src/compiler/hooksCalled.ts +35 -0
- package/src/compiler/mountsOf.ts +30 -18
- package/src/compiler/parseAbide.ts +12 -12
- package/src/compiler/projectShadow.ts +71 -46
- package/src/compiler/readsIn.ts +2 -2
- package/src/compiler/scaffold.ts +24 -21
- package/src/compiler/serveLanguage.ts +0 -24
- package/src/compiler/sharedModuleWrites.ts +2 -2
- package/src/compiler/splitImports.ts +4 -4
- package/src/compiler/writeDefaultProject.ts +28 -1
- package/src/server/MODULE_QUERIES.ts +12 -3
- package/src/server/abideModules.ts +156 -53
- package/src/server/answerRequest.ts +9 -4
- package/src/server/answerRpc.ts +18 -16
- package/src/server/appTree.ts +92 -25
- package/src/server/commands.ts +5 -4
- package/src/server/componentModules.d.ts +1 -2
- package/src/server/createApp.ts +23 -11
- package/src/server/devWorker.ts +5 -1
- package/src/server/fenceServer.ts +36 -13
- package/src/server/generatedSurfaces.ts +12 -53
- package/src/server/hooks.ts +17 -4
- package/src/server/index.ts +1 -0
- package/src/server/installSourceMaps.ts +6 -1
- package/src/server/launcherOptions.ts +12 -13
- package/src/server/launcherPage.ts +2 -2
- package/src/server/nativeMenu.ts +3 -3
- package/src/server/preload.ts +13 -0
- package/src/server/runDesktop.ts +11 -16
- package/src/server/serveDev.ts +8 -2
- package/src/server/surfaceOf.ts +2 -0
- package/src/server/versionOf.ts +1 -2
- package/src/server/withAbideSources.ts +1 -1
- package/src/shared/ADDRESSES.ts +3 -0
- package/src/shared/LAUNCHER_FILES.ts +9 -0
- package/src/shared/RESERVED_DIRECTORIES.ts +19 -4
- package/src/shared/index.ts +2 -1
- package/src/{ui → shared}/menuBridge.ts +1 -1
- package/src/shared/menuItem.ts +2 -2
- package/src/shared/onMenu.ts +58 -0
- package/src/shared/reactive/Cursor.ts +66 -123
- package/src/shared/reactive/Reactive.ts +74 -28
- package/src/shared/reactive/callLoad.ts +4 -18
- package/src/shared/reactive/chunksOfStream.ts +10 -34
- package/src/{ui → shared}/reportMenu.ts +7 -9
- package/src/shared/routePattern.ts +74 -23
- package/src/shared/rpc.ts +1 -1
- package/src/ui/Router.ts +0 -6
- package/src/compiler/syncTypes.ts +0 -32
- package/src/compiler/writeTypes.ts +0 -82
- package/src/server/LAUNCHER_FILES.ts +0 -9
- /package/scaffold/src/{page.abide → index.abide} +0 -0
- /package/src/{compiler → shared}/SPECIFIERS.ts +0 -0
package/src/server/appTree.ts
CHANGED
|
@@ -1,28 +1,36 @@
|
|
|
1
|
-
// 23.14 — WHERE AN APP'S ADDRESSES ARE, read off the tree in one walk.
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
1
|
+
// 23.14 — WHERE AN APP'S ADDRESSES ARE, read off the tree in one walk. A file directly in a directory
|
|
2
|
+
// below the root is an address — a page, a layout, an error page or a handler file — and nothing
|
|
3
|
+
// else is there (23.22); a fence is never one (23.15): `server` fences a graph, `public` holds files
|
|
4
|
+
// answered as authored at the path of the directories above it, and `lib` and `tests` hold the rest.
|
|
5
|
+
// So listing a directory lists its routes.
|
|
6
6
|
//
|
|
7
|
-
// ONE WALK for the sources
|
|
8
|
-
// the refusals (23.16, 23.17, 23.18) — and
|
|
9
|
-
// beside `placeOf`, which answers the same
|
|
7
|
+
// ONE WALK for the sources, the handler files, the server and lib modules, and the public files
|
|
8
|
+
// `abide build` copies and `abide dev` answers, and the refusals (23.16, 23.17, 23.18, 23.22) — and
|
|
9
|
+
// ONE SKIP RULE, `skipped`, for every walk of the tree, beside `placeOf`, which answers the same
|
|
10
|
+
// question of one path.
|
|
10
11
|
//
|
|
11
12
|
// A WALK RATHER THAN A GLOB: a glob descends `node_modules` and the build output before a filter
|
|
12
13
|
// can drop them.
|
|
13
14
|
|
|
14
|
-
import { type Dirent, readdirSync } from 'node:fs' // bun has no directory listing of its own
|
|
15
|
+
import { type Dirent, existsSync, readdirSync } from 'node:fs' // bun has no directory listing or sync existence check of its own
|
|
15
16
|
import { ABIDE_ADDRESS } from '#shared/ADDRESSES.ts'
|
|
16
17
|
import {
|
|
18
|
+
HOLDS_SERVER,
|
|
19
|
+
LIB_DIRECTORY,
|
|
17
20
|
PUBLIC_DIRECTORY,
|
|
18
21
|
RESERVED_DIRECTORIES,
|
|
22
|
+
SERVER_DIRECTORY,
|
|
19
23
|
} from '#shared/RESERVED_DIRECTORIES.ts'
|
|
20
|
-
import {
|
|
24
|
+
import { DIRECTORY_FILES, routeOf, skipped } from '#shared/routePattern.ts'
|
|
21
25
|
|
|
22
26
|
/** What one walk of the app finds, every path relative to the root. */
|
|
23
|
-
|
|
27
|
+
type AppFiles = {
|
|
24
28
|
/** Every `.abide`, in path order. */
|
|
25
29
|
abide: string[]
|
|
30
|
+
/** Every handler file, in path order. */
|
|
31
|
+
handlers: string[]
|
|
32
|
+
/** Every module under a `server` or a `lib` directory, in path order — read for 37.13. */
|
|
33
|
+
modules: string[]
|
|
26
34
|
/** Every file under a `public` directory, to the address it is answered at below the mount. */
|
|
27
35
|
published: Map<string, string>
|
|
28
36
|
}
|
|
@@ -35,7 +43,8 @@ const THROW: Refuse = (_path, message) => {
|
|
|
35
43
|
}
|
|
36
44
|
|
|
37
45
|
/**
|
|
38
|
-
* The app's `.abide` sources
|
|
46
|
+
* The app's `.abide` sources, handler files, server and lib modules and public files, refusing a
|
|
47
|
+
* tree 23.16, 23.17, 23.18 or 23.22 forbids.
|
|
39
48
|
*
|
|
40
49
|
* `refuse` throws by default, which is the build's answer; `abide check` collects instead (38.33),
|
|
41
50
|
* and the checker's route table ignores them, since a refused tree is the build's to report.
|
|
@@ -43,9 +52,19 @@ const THROW: Refuse = (_path, message) => {
|
|
|
43
52
|
* A missing root is an app with nothing in it, not a fault.
|
|
44
53
|
*/
|
|
45
54
|
export function appFiles(root: string, refuse: Refuse = THROW): AppFiles {
|
|
46
|
-
const files: AppFiles = {
|
|
47
|
-
|
|
55
|
+
const files: AppFiles = {
|
|
56
|
+
abide: [],
|
|
57
|
+
handlers: [],
|
|
58
|
+
modules: [],
|
|
59
|
+
published: new Map(),
|
|
60
|
+
}
|
|
61
|
+
// 23.22, 38.25 — a root that is its package's directory holds the package's own files beside its
|
|
62
|
+
// pages.
|
|
63
|
+
const packaged = existsSync(`${root}/package.json`)
|
|
64
|
+
walk(root, '', undefined, files, refuse, packaged)
|
|
48
65
|
files.abide.sort()
|
|
66
|
+
files.handlers.sort()
|
|
67
|
+
files.modules.sort()
|
|
49
68
|
return files
|
|
50
69
|
}
|
|
51
70
|
|
|
@@ -57,6 +76,7 @@ function walk(
|
|
|
57
76
|
reserved: { path: string; name: string } | undefined,
|
|
58
77
|
files: AppFiles,
|
|
59
78
|
refuse: Refuse,
|
|
79
|
+
packaged = false,
|
|
60
80
|
): void {
|
|
61
81
|
let entries: Dirent[]
|
|
62
82
|
try {
|
|
@@ -71,7 +91,14 @@ function walk(
|
|
|
71
91
|
if (entry.isDirectory()) {
|
|
72
92
|
let within = reserved
|
|
73
93
|
if (RESERVED_DIRECTORIES.has(entry.name)) {
|
|
74
|
-
|
|
94
|
+
// 23.16 — `server` nests in the fences that hold code.
|
|
95
|
+
if (
|
|
96
|
+
reserved !== undefined &&
|
|
97
|
+
!(
|
|
98
|
+
entry.name === SERVER_DIRECTORY &&
|
|
99
|
+
HOLDS_SERVER.has(reserved.name)
|
|
100
|
+
)
|
|
101
|
+
)
|
|
75
102
|
refuse(
|
|
76
103
|
path,
|
|
77
104
|
`abide: ${path} is a ${entry.name} directory under the ${reserved.name} directory ${reserved.path}. RULEBOOK 23.16.`,
|
|
@@ -87,21 +114,49 @@ function walk(
|
|
|
87
114
|
)
|
|
88
115
|
continue
|
|
89
116
|
}
|
|
90
|
-
|
|
117
|
+
const abide = path.endsWith('.abide')
|
|
118
|
+
if (abide) files.abide.push(path)
|
|
91
119
|
if (reserved === undefined) {
|
|
120
|
+
if (!abide && !isModule(path)) {
|
|
121
|
+
if (packaged && relative === '' && !path.endsWith('.ts'))
|
|
122
|
+
continue
|
|
123
|
+
refuse(
|
|
124
|
+
path,
|
|
125
|
+
`abide: ${path} is directly in an address directory, which holds pages, layouts, error pages and handler files alone. Move it into a lib directory beside it. RULEBOOK 23.22.`,
|
|
126
|
+
)
|
|
127
|
+
continue
|
|
128
|
+
}
|
|
129
|
+
if (!abide) files.handlers.push(path)
|
|
92
130
|
// 23.17 — anything that could answer here would answer in abide's own namespace.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
} else if (fileKind(entry.name) !== 'component')
|
|
131
|
+
refuseFrameworkRoute(routeOf(path), path, refuse)
|
|
132
|
+
} else if (DIRECTORY_FILES.has(entry.name))
|
|
96
133
|
refuse(
|
|
97
134
|
path,
|
|
98
|
-
`abide: ${path} is
|
|
135
|
+
`abide: ${path} is under the ${reserved.name} directory ${reserved.path}, where nothing has an address, and ${entry.name} is a route's name. RULEBOOK 23.18.`,
|
|
99
136
|
)
|
|
100
137
|
else if (reserved.name === PUBLIC_DIRECTORY)
|
|
101
138
|
files.published.set(path, publicAddress(path, reserved.path))
|
|
139
|
+
else if (
|
|
140
|
+
(reserved.name === SERVER_DIRECTORY ||
|
|
141
|
+
reserved.name === LIB_DIRECTORY) &&
|
|
142
|
+
isModule(path)
|
|
143
|
+
)
|
|
144
|
+
files.modules.push(path)
|
|
102
145
|
}
|
|
103
146
|
}
|
|
104
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Whether `path` names a module the app runs — neither a declaration nor a test: directly in an
|
|
150
|
+
* address directory, a handler file rather than a stray.
|
|
151
|
+
*/
|
|
152
|
+
function isModule(path: string): boolean {
|
|
153
|
+
return (
|
|
154
|
+
path.endsWith('.ts') &&
|
|
155
|
+
!path.endsWith('.d.ts') &&
|
|
156
|
+
!path.endsWith('.test.ts')
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
|
|
105
160
|
/** Refuses `source` where `route` lies in the framework's own namespace. 23.17. */
|
|
106
161
|
export function refuseFrameworkRoute(
|
|
107
162
|
route: string,
|
|
@@ -150,18 +205,21 @@ export function abideUnder(root: string): string[] {
|
|
|
150
205
|
}
|
|
151
206
|
|
|
152
207
|
/**
|
|
153
|
-
* Whether
|
|
208
|
+
* Whether an app lies under `root`: a `.abide` anywhere or a handler file, stopping at the first.
|
|
154
209
|
*
|
|
155
210
|
* 38.26's question, asked of a root before anything is built from it, so asking it of a large tree
|
|
156
211
|
* is as cheap as the tree allows.
|
|
157
212
|
*/
|
|
158
|
-
export function
|
|
159
|
-
return !abideFilesUnder(root, '').next().done
|
|
213
|
+
export function holdsApp(root: string): boolean {
|
|
214
|
+
return !abideFilesUnder(root, '', true).next().done
|
|
160
215
|
}
|
|
161
216
|
|
|
217
|
+
// Every `.abide` under `directory` — and, where `handlers`, every handler file outside a fence.
|
|
162
218
|
function* abideFilesUnder(
|
|
163
219
|
directory: string,
|
|
164
220
|
relative: string,
|
|
221
|
+
handlers = false,
|
|
222
|
+
fenced = false,
|
|
165
223
|
): Generator<string> {
|
|
166
224
|
let entries: Dirent[]
|
|
167
225
|
try {
|
|
@@ -173,7 +231,16 @@ function* abideFilesUnder(
|
|
|
173
231
|
if (skipped(entry.name)) continue
|
|
174
232
|
const path = `${relative}${entry.name}`
|
|
175
233
|
if (entry.isDirectory())
|
|
176
|
-
yield* abideFilesUnder(
|
|
177
|
-
|
|
234
|
+
yield* abideFilesUnder(
|
|
235
|
+
`${directory}/${entry.name}`,
|
|
236
|
+
`${path}/`,
|
|
237
|
+
handlers,
|
|
238
|
+
fenced || RESERVED_DIRECTORIES.has(entry.name),
|
|
239
|
+
)
|
|
240
|
+
else if (
|
|
241
|
+
path.endsWith('.abide') ||
|
|
242
|
+
(handlers && !fenced && isModule(path))
|
|
243
|
+
)
|
|
244
|
+
yield path
|
|
178
245
|
}
|
|
179
246
|
}
|
package/src/server/commands.ts
CHANGED
|
@@ -33,7 +33,7 @@ type SourceCommands = Record<
|
|
|
33
33
|
| 'dev'
|
|
34
34
|
| 'build'
|
|
35
35
|
| 'check'
|
|
36
|
-
| '
|
|
36
|
+
| 'routes'
|
|
37
37
|
| 'compile'
|
|
38
38
|
| 'bundle'
|
|
39
39
|
| 'lsp',
|
|
@@ -151,9 +151,9 @@ function commandsOf(host: Host): Row[] {
|
|
|
151
151
|
'check',
|
|
152
152
|
),
|
|
153
153
|
fromSource(
|
|
154
|
-
'
|
|
155
|
-
'
|
|
156
|
-
'
|
|
154
|
+
'routes [--json]',
|
|
155
|
+
'every address the app answers, its file, and where each hook is filed',
|
|
156
|
+
'routes',
|
|
157
157
|
),
|
|
158
158
|
fromSource(
|
|
159
159
|
'compile',
|
|
@@ -227,6 +227,7 @@ function parsed(argv: string[]) {
|
|
|
227
227
|
method: { type: 'string' },
|
|
228
228
|
target: { type: 'string' },
|
|
229
229
|
platforms: { type: 'string' },
|
|
230
|
+
json: { type: 'boolean' },
|
|
230
231
|
help: { type: 'boolean', short: 'h' },
|
|
231
232
|
},
|
|
232
233
|
allowPositionals: true,
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
// 35.4 — what a `.ts` holds where it imports a `.abide`: the component, called with its props to
|
|
2
2
|
// bind them, answering the `Component` `render` takes (`abideModules`). A wildcard, so the props
|
|
3
3
|
// are not checked here; a declaration in a module would be an augmentation, which takes no wildcard.
|
|
4
|
-
// The
|
|
5
|
-
// before this.
|
|
4
|
+
// The component is all a `.abide` exports (34.17).
|
|
6
5
|
declare module '*.abide' {
|
|
7
6
|
const component: (
|
|
8
7
|
props?: Record<string, unknown>,
|
package/src/server/createApp.ts
CHANGED
|
@@ -14,7 +14,7 @@ import { servedTable } from './abideModules.ts'
|
|
|
14
14
|
import { type AppSources, answerRequest } from './answerRequest.ts'
|
|
15
15
|
import { Mux, type MuxConnection } from './answerSocket.ts'
|
|
16
16
|
import { appDirectories } from './appDirectories.ts'
|
|
17
|
-
import {
|
|
17
|
+
import { holdsApp } from './appTree.ts'
|
|
18
18
|
import { buildDirectory } from './buildDirectory.ts'
|
|
19
19
|
import { APP_PACKAGE, type AppPackage, config, packageUnder } from './config.ts'
|
|
20
20
|
import { McpSurface } from './generatedSurfaces.ts'
|
|
@@ -65,21 +65,33 @@ export async function createApp(): Promise<App> {
|
|
|
65
65
|
throw new Error(
|
|
66
66
|
`abide: createApp found no app. Looked for a .abide under ${resolve('src')}, or under ${process.cwd()} where it has no src.`,
|
|
67
67
|
)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
// stale.
|
|
71
|
-
const out =
|
|
72
|
-
process.env.NODE_ENV === 'test'
|
|
73
|
-
? await testBuild(root)
|
|
74
|
-
: buildDirectory(root)
|
|
75
|
-
const { loadBuild, serveBuild } = await import('./serveBuild.ts')
|
|
76
|
-
if (!(await serveBuild(root, out)))
|
|
68
|
+
const out = await servedBuild(root)
|
|
69
|
+
if (out === undefined)
|
|
77
70
|
throw new Error(
|
|
78
71
|
`abide: createApp found nothing under ${root} to build.`,
|
|
79
72
|
)
|
|
73
|
+
const { loadBuild } = await import('./serveBuild.ts')
|
|
80
74
|
return bootApp(originUnder(root), () => loadBuild({ root, out }))
|
|
81
75
|
}
|
|
82
76
|
|
|
77
|
+
/**
|
|
78
|
+
* The build of the app under `root`, served to this process's imports — or `undefined` where
|
|
79
|
+
* nothing under it builds. `fresh` builds it for its sources as they stand.
|
|
80
|
+
*
|
|
81
|
+
* 42.16 — under `bun test` the app is built for its sources as they stand rather than read off
|
|
82
|
+
* whatever `abide build` last left, which a test run would otherwise need first and could find
|
|
83
|
+
* stale. One answer for `createApp` and the test preload (38.109), so a test that boots the app
|
|
84
|
+
* reuses the build its imports were served from.
|
|
85
|
+
*/
|
|
86
|
+
export async function servedBuild(
|
|
87
|
+
root: string,
|
|
88
|
+
fresh = process.env.NODE_ENV === 'test',
|
|
89
|
+
): Promise<string | undefined> {
|
|
90
|
+
const out = fresh ? await testBuild(root) : buildDirectory(root)
|
|
91
|
+
const { serveBuild } = await import('./serveBuild.ts')
|
|
92
|
+
return (await serveBuild(root, out)) ? out : undefined
|
|
93
|
+
}
|
|
94
|
+
|
|
83
95
|
/**
|
|
84
96
|
* The app's root — `--root`, else `src` where the working directory holds one, else the working
|
|
85
97
|
* directory — or `undefined` where it is no app's.
|
|
@@ -99,7 +111,7 @@ export function appRoot(given: string | undefined): string | undefined {
|
|
|
99
111
|
let packaged = false
|
|
100
112
|
for (const directory of appDirectories(root))
|
|
101
113
|
packaged ||= existsSync(`${directory}/package.json`)
|
|
102
|
-
return packaged &&
|
|
114
|
+
return packaged && holdsApp(root) ? root : undefined
|
|
103
115
|
}
|
|
104
116
|
|
|
105
117
|
/**
|
package/src/server/devWorker.ts
CHANGED
|
@@ -378,7 +378,11 @@ async function open(
|
|
|
378
378
|
bundleInMemory(
|
|
379
379
|
root,
|
|
380
380
|
// 16.28, 16.29 — the browser's view of the server, as `abide build` gives it.
|
|
381
|
-
[
|
|
381
|
+
[
|
|
382
|
+
fenceServer(root, compilation.mounted),
|
|
383
|
+
abideModules(root, compilation),
|
|
384
|
+
...own,
|
|
385
|
+
],
|
|
382
386
|
paged,
|
|
383
387
|
publicAddresses,
|
|
384
388
|
compilation,
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// 16.29 — WHAT THE BROWSER'S BUNDLE SEES OF THE SERVER: nothing under a `server` directory, at any
|
|
2
2
|
// depth, so a secret or a database client cannot ride into the browser by being imported from the
|
|
3
|
-
// wrong file. A handler crosses as the wrapper
|
|
4
|
-
// server block (
|
|
3
|
+
// wrong file. A handler crosses as the wrapper the compiler wrote in its place — a handler file as
|
|
4
|
+
// that file, a private handler in its server block's place (16.28); this is the fence around
|
|
5
|
+
// everything else.
|
|
5
6
|
//
|
|
6
7
|
// A PLUGIN ON THE BUNDLE, NOT A REWRITE OF THE PAGES. A rewrite reaches the specifiers the compiler
|
|
7
8
|
// emitted; the bundle reaches every edge — a helper the page imports, importing a server module,
|
|
@@ -9,11 +10,16 @@
|
|
|
9
10
|
//
|
|
10
11
|
// REGISTERED AHEAD OF `abideModules`, AND IN ITS NAMESPACES: a load is answered by the first plugin
|
|
11
12
|
// that answers it, and a component or a file's text is loaded there rather than as a `file`.
|
|
13
|
+
//
|
|
14
|
+
// BY POSITION, NOT BY PATH (23.19): a directory a file mounts places its files where the app says,
|
|
15
|
+
// so a `server` directory inside it is fenced as the app's own would be, wherever the package lies.
|
|
12
16
|
|
|
17
|
+
import { resolve } from 'node:path' // bun exposes no path helpers of its own
|
|
13
18
|
import { SERVER_DIRECTORY } from '#shared/RESERVED_DIRECTORIES.ts'
|
|
14
19
|
import { placeOf } from '#shared/routePattern.ts'
|
|
15
20
|
import {
|
|
16
21
|
COMPONENT_NAMESPACE,
|
|
22
|
+
type Compilation,
|
|
17
23
|
STYLE_NAMESPACE,
|
|
18
24
|
TEXT_NAMESPACE,
|
|
19
25
|
} from './abideModules.ts'
|
|
@@ -25,24 +31,41 @@ const FENCED = ['file', COMPONENT_NAMESPACE, TEXT_NAMESPACE, STYLE_NAMESPACE]
|
|
|
25
31
|
/**
|
|
26
32
|
* The bundler plugin refusing everything under a `server` directory to the browser's bundle.
|
|
27
33
|
*
|
|
28
|
-
* `root` is its real path (`appRoot`), which is how the bundler names a file
|
|
29
|
-
* `abideModules`.
|
|
34
|
+
* `root` is its real path (`appRoot`), which is how the bundler names a file; `mounted` is the
|
|
35
|
+
* compilation's. Listed before `abideModules`.
|
|
30
36
|
*/
|
|
31
|
-
export function fenceServer(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
export function fenceServer(
|
|
38
|
+
root: string,
|
|
39
|
+
mounted: Compilation['mounted'],
|
|
40
|
+
): Bun.BunPlugin {
|
|
41
|
+
// Mounted directories first: one under the root's `node_modules` is the app's at its position.
|
|
42
|
+
const directories: [string, string][] = []
|
|
43
|
+
for (const [directory, at] of Object.entries(mounted))
|
|
44
|
+
directories.push([`${resolve(root, directory)}/`, at])
|
|
45
|
+
const positionOf = (path: string): string | undefined => {
|
|
46
|
+
for (const [directory, at] of directories)
|
|
47
|
+
if (path.startsWith(directory))
|
|
48
|
+
return `${at}${path.slice(directory.length)}`
|
|
49
|
+
return path.startsWith(`${root}/`)
|
|
50
|
+
? path.slice(root.length + 1)
|
|
51
|
+
: undefined
|
|
52
|
+
}
|
|
53
|
+
// Anything with a `server` directory in its path, and `placeOf` decides by its position — a
|
|
54
|
+
// load it passes on falls through to the next plugin's, or the bundler's own.
|
|
55
|
+
const filter = new RegExp(`/${SERVER_DIRECTORY}/`)
|
|
37
56
|
return {
|
|
38
57
|
name: 'abide-fence',
|
|
39
58
|
setup(build) {
|
|
40
59
|
for (const namespace of FENCED)
|
|
41
60
|
build.onLoad({ filter, namespace }, (args) => {
|
|
42
|
-
const
|
|
43
|
-
if (
|
|
61
|
+
const position = positionOf(args.path)
|
|
62
|
+
if (
|
|
63
|
+
position === undefined ||
|
|
64
|
+
placeOf(position) !== 'server'
|
|
65
|
+
)
|
|
66
|
+
return undefined
|
|
44
67
|
throw new Error(
|
|
45
|
-
`abide: ${
|
|
68
|
+
`abide: ${position.replace(/::\w+$/, '')} is under a server directory, and the browser's bundle imports it. RULEBOOK 16.29.`,
|
|
46
69
|
)
|
|
47
70
|
})
|
|
48
71
|
},
|
|
@@ -29,7 +29,7 @@ import { type Listed, surfaceOf } from './surfaceOf.ts'
|
|
|
29
29
|
const OPENAPI_LOG = namedLogger('abide:openapi')
|
|
30
30
|
const MCP_LOG = namedLogger('abide:mcp')
|
|
31
31
|
|
|
32
|
-
// What
|
|
32
|
+
// What OpenAPI adds to a listing: whether the handler is keyed.
|
|
33
33
|
type Published = Listed & { keyed: boolean }
|
|
34
34
|
|
|
35
35
|
/** Every handler `surface` lists (`surfaceOf`). */
|
|
@@ -212,18 +212,8 @@ export function openApi(
|
|
|
212
212
|
return Object.keys(document().paths).length > 0 ? document : undefined
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
-
|
|
216
|
-
type Tools = { tools: Published[]; resources: Published[] }
|
|
215
|
+
const published = new WeakMap<Map<string, Mounted>, Reactive<Published[]>>()
|
|
217
216
|
|
|
218
|
-
const published = new WeakMap<Map<string, Mounted>, Reactive<Tools>>()
|
|
219
|
-
|
|
220
|
-
function toolsOf(handlers: Map<string, Mounted>): Tools {
|
|
221
|
-
const found: Tools = { tools: [], resources: [] }
|
|
222
|
-
for (const one of listed(handlers, 'mcp'))
|
|
223
|
-
if (one.keyed || one.method !== 'GET') found.tools.push(one)
|
|
224
|
-
else found.resources.push(one)
|
|
225
|
-
return found
|
|
226
|
-
}
|
|
227
217
|
|
|
228
218
|
type JsonRpc = {
|
|
229
219
|
jsonrpc: '2.0'
|
|
@@ -264,18 +254,20 @@ export class McpSurface {
|
|
|
264
254
|
|
|
265
255
|
/** Whether the route table publishes any handler to it. 20.7. */
|
|
266
256
|
get opened(): boolean {
|
|
267
|
-
|
|
268
|
-
return tools.length > 0 || resources.length > 0
|
|
257
|
+
return this.#published().length > 0
|
|
269
258
|
}
|
|
270
259
|
|
|
271
|
-
#published():
|
|
272
|
-
|
|
260
|
+
#published(): Published[] {
|
|
261
|
+
// Every handler the MCP surface lists, each a tool — a read as much as a mutation. 20.9.
|
|
262
|
+
return overTable(published, this.#handlers, (handlers) =>
|
|
263
|
+
listed(handlers, 'mcp'),
|
|
264
|
+
)()
|
|
273
265
|
}
|
|
274
266
|
|
|
275
267
|
/** Every tool it lists. 20.9. */
|
|
276
268
|
list(): ToolDefinition[] {
|
|
277
269
|
const listing: ToolDefinition[] = []
|
|
278
|
-
for (const tool of this.#published()
|
|
270
|
+
for (const tool of this.#published()) {
|
|
279
271
|
if (tool.description === undefined)
|
|
280
272
|
MCP_LOG.warning(
|
|
281
273
|
`${tool.name} is published with no description.`,
|
|
@@ -305,7 +297,7 @@ export class McpSurface {
|
|
|
305
297
|
}
|
|
306
298
|
|
|
307
299
|
#tool(name: unknown): Published | undefined {
|
|
308
|
-
return this.#published().
|
|
300
|
+
return this.#published().find((one) => one.name === name)
|
|
309
301
|
}
|
|
310
302
|
|
|
311
303
|
async #called(
|
|
@@ -335,7 +327,7 @@ export class McpSurface {
|
|
|
335
327
|
return {
|
|
336
328
|
result: {
|
|
337
329
|
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
338
|
-
capabilities: { tools: {}
|
|
330
|
+
capabilities: { tools: {} },
|
|
339
331
|
serverInfo: appIdentity(),
|
|
340
332
|
},
|
|
341
333
|
}
|
|
@@ -343,19 +335,6 @@ export class McpSurface {
|
|
|
343
335
|
return { result: {} }
|
|
344
336
|
case 'tools/list':
|
|
345
337
|
return { result: { tools: this.list() } }
|
|
346
|
-
case 'resources/list':
|
|
347
|
-
return {
|
|
348
|
-
result: {
|
|
349
|
-
resources: this.#published().resources.map(
|
|
350
|
-
(resource) => ({
|
|
351
|
-
uri: `abide:${resource.address}`,
|
|
352
|
-
name: resource.name,
|
|
353
|
-
description: resource.description ?? '',
|
|
354
|
-
mimeType: 'application/json',
|
|
355
|
-
}),
|
|
356
|
-
),
|
|
357
|
-
},
|
|
358
|
-
}
|
|
359
338
|
case 'tools/call': {
|
|
360
339
|
const tool = this.#tool(params?.name)
|
|
361
340
|
if (tool === undefined)
|
|
@@ -375,25 +354,6 @@ export class McpSurface {
|
|
|
375
354
|
},
|
|
376
355
|
}
|
|
377
356
|
}
|
|
378
|
-
case 'resources/read': {
|
|
379
|
-
const uri = String(params?.uri ?? '')
|
|
380
|
-
const resource = this.#published().resources.find(
|
|
381
|
-
(one) => `abide:${one.address}` === uri,
|
|
382
|
-
)
|
|
383
|
-
if (resource === undefined)
|
|
384
|
-
return mcpFailure(-32602, `No resource at ${uri}.`)
|
|
385
|
-
const { text, failed } = await this.#called(
|
|
386
|
-
resource,
|
|
387
|
-
undefined,
|
|
388
|
-
asker,
|
|
389
|
-
)
|
|
390
|
-
if (failed) return mcpFailure(-32603, text)
|
|
391
|
-
return {
|
|
392
|
-
result: {
|
|
393
|
-
contents: [{ uri, mimeType: 'application/json', text }],
|
|
394
|
-
},
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
357
|
default:
|
|
398
358
|
return mcpFailure(-32601, `No method ${method}.`)
|
|
399
359
|
}
|
|
@@ -404,8 +364,7 @@ export class McpSurface {
|
|
|
404
364
|
* Answers one MCP JSON-RPC request over `surface`, or `undefined` where it publishes nothing.
|
|
405
365
|
*
|
|
406
366
|
* 20.9 — THE STREAMABLE-HTTP MCP ENDPOINT, answering JSON-RPC over `POST`, as the request's own
|
|
407
|
-
* caller:
|
|
408
|
-
* every mutation, is a tool.
|
|
367
|
+
* caller: every handler it lists is a tool, a read as much as a mutation.
|
|
409
368
|
*/
|
|
410
369
|
export async function answerMcp(
|
|
411
370
|
surface: McpSurface,
|
package/src/server/hooks.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// SECTION 37 — THE LIFECYCLE HOOKS, called from a
|
|
2
|
-
// than exported from it (37.1). Each call files its function here, at the ROUTE of the file that
|
|
1
|
+
// SECTION 37 — THE LIFECYCLE HOOKS, called from a `.abide`'s `<script server>` or a handler file
|
|
2
|
+
// rather than exported from it (37.1). Each call files its function here, at the ROUTE of the file that
|
|
3
3
|
// called it, and hands back what unfiles it; `createApp` reads what is filed once every module
|
|
4
4
|
// holding a server block has run.
|
|
5
5
|
//
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
} from '#shared/routePattern.ts'
|
|
23
23
|
import type { Schema } from '#shared/Schema.ts'
|
|
24
24
|
import { config } from './config.ts'
|
|
25
|
+
import type { LauncherOptions } from './launcherOptions.ts'
|
|
25
26
|
import { currentRequestScope } from './requestScope.ts'
|
|
26
27
|
|
|
27
28
|
export type AppRung = Middleware<{ request: Request }, Response>
|
|
@@ -51,17 +52,20 @@ export const HOOKS = {
|
|
|
51
52
|
options: ConfigOptions | undefined
|
|
52
53
|
}>[],
|
|
53
54
|
principal: [] as Filed<(claims: unknown) => unknown | Promise<unknown>>[],
|
|
55
|
+
launcher: [] as Filed<() => LauncherOptions>[],
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
const ROOT = '/'
|
|
57
59
|
|
|
58
|
-
// The route a hook called now is filed at — the server block being evaluated, or
|
|
60
|
+
// The route a hook called now is filed at — the server block or handler file being evaluated, or
|
|
61
|
+
// the root.
|
|
59
62
|
let filing: string | undefined
|
|
60
63
|
|
|
61
64
|
/**
|
|
62
65
|
* Sets the route the hooks called next are filed at, or the root again for `undefined`.
|
|
63
66
|
*
|
|
64
|
-
* 37.1 — what a compiled server module calls around its `<script server
|
|
67
|
+
* 37.1 — what a compiled server module calls around its `<script server>`, and the plugin around a
|
|
68
|
+
* handler file; nothing an app writes.
|
|
65
69
|
*/
|
|
66
70
|
export function fileRoute(route: string | undefined): void {
|
|
67
71
|
filing = route
|
|
@@ -208,6 +212,15 @@ export function onPrincipal(
|
|
|
208
212
|
return filed(HOOKS.principal, resolve)
|
|
209
213
|
}
|
|
210
214
|
|
|
215
|
+
/**
|
|
216
|
+
* Files what the desktop app's window and launcher are; returns what unfiles it.
|
|
217
|
+
*
|
|
218
|
+
* 38.80 — called in `src/launcher.abide` alone (38.107), and read as the app launches.
|
|
219
|
+
*/
|
|
220
|
+
export function onLauncher(options: () => LauncherOptions): () => void {
|
|
221
|
+
return filed(HOOKS.launcher, options)
|
|
222
|
+
}
|
|
223
|
+
|
|
211
224
|
/**
|
|
212
225
|
* Logs an unexpected error and hands it to every `onError` hook.
|
|
213
226
|
*
|
package/src/server/index.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// rebuild, and a test boots several apps in one process.
|
|
8
8
|
|
|
9
9
|
import { resolve } from 'node:path' // bun exposes no path helpers of its own
|
|
10
|
+
import { HANDLER_FACE } from './MODULE_QUERIES.ts'
|
|
10
11
|
import { decodeMappings, placeAt, type Segment } from './sourceMappings.ts'
|
|
11
12
|
|
|
12
13
|
/** Per `.abide` below the root, as its server module is served there: the file, and its `mappings`. */
|
|
@@ -63,7 +64,11 @@ function placeFrames(error: Error, frames: CallSite[]): string {
|
|
|
63
64
|
|
|
64
65
|
// A call site's column counts from 0 where the stack bun writes counts from 1.
|
|
65
66
|
function placed(frame: CallSite): string {
|
|
66
|
-
|
|
67
|
+
// A handler file's frames name the file the author wrote, not the face it is served as.
|
|
68
|
+
const file = (frame.getFileName() ?? '<anonymous>').replace(
|
|
69
|
+
HANDLER_FACE,
|
|
70
|
+
'',
|
|
71
|
+
)
|
|
67
72
|
const line = frame.getLineNumber() ?? 0
|
|
68
73
|
const column = frame.getColumnNumber() ?? 0
|
|
69
74
|
const registered = registeredFor(file)
|