@abide/abide 0.109.0 → 0.110.1
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 +12 -0
- package/docs/REGISTRY.md +6 -5
- package/docs/RULEBOOK.md +24 -13
- package/docs/abide.md +163 -159
- package/package.json +1 -1
- package/scaffold/AGENTS.md +12 -6
- package/scaffold/src/{tests/getGreeting.test.ts → getGreeting.test.ts} +1 -1
- package/scaffold/src/{tests/index.test.ts → index.test.ts} +1 -1
- package/src/compiler/appLayout.ts +5 -15
- package/src/compiler/compileApp.ts +0 -2
- package/src/compiler/handlersUnder.ts +1 -1
- package/src/compiler/scaffold.ts +5 -5
- package/src/server/abideModules.ts +0 -5
- package/src/server/appTree.ts +38 -34
- package/src/server/clientBundle.ts +2 -2
- package/src/server/fenceServer.ts +25 -31
- package/src/server/generatedSurfaces.ts +35 -1
- package/src/server/placement.ts +27 -51
- package/src/server/serveDev.ts +0 -1
- package/src/shared/RESERVED_DIRECTORIES.ts +7 -20
package/package.json
CHANGED
package/scaffold/AGENTS.md
CHANGED
|
@@ -17,7 +17,7 @@ before writing code, rather than guessing from Svelte, Vue or React. It installs
|
|
|
17
17
|
TypeScript service do not read `.abide` files, so a clean `tsc` says nothing about them. Run
|
|
18
18
|
`bun run check` after every change, and treat its warnings as bugs: several of them name a read
|
|
19
19
|
that will not update when its value changes.
|
|
20
|
-
- `bun run test` — runs `bun test`. `src/
|
|
20
|
+
- `bun run test` — runs `bun test`. `src/getGreeting.test.ts` is the pattern: a test imports a
|
|
21
21
|
handler or a `.abide` file and calls it.
|
|
22
22
|
- `bun run dev` — serves the app on a local port and rebuilds when a file changes.
|
|
23
23
|
- `bun run build` then `bun run start` — the production build, and the app served from it.
|
|
@@ -32,9 +32,15 @@ before writing code, rather than guessing from Svelte, Vue or React. It installs
|
|
|
32
32
|
scripts, the CLI and MCP alike.
|
|
33
33
|
- A `.abide` cannot export. A `<script server>` in one holds that file's private handlers
|
|
34
34
|
(`const save = POST(…)`), callable by that file alone, and its hooks (`onStart`, `middleware`, …).
|
|
35
|
-
- A directory holds addresses and
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
- A directory holds addresses, tests, `.d.ts` declarations and two fenced directories alone: `lib/` for components and
|
|
36
|
+
modules, and `public/` for files served as they are (`src/blog/public/cover.png` answers
|
|
37
|
+
`/blog/cover.png`). Anything else beside the pages is a build error.
|
|
38
|
+
- A test is a `.test.ts`, kept beside the file it tests — a page, a handler, or a module in `lib/`.
|
|
39
|
+
A browser test (playwright) is a `.test.ts` too, under `e2e/` beside `src/`: `bun test` reads
|
|
40
|
+
`src/` alone (`root` in `bunfig.toml`), so it never runs one.
|
|
41
|
+
- A module the browser never loads is named `<name>.server.ts` and lives in `lib/`
|
|
42
|
+
(`src/invoices/lib/db.server.ts`); importing one from browser code is a compile error. A module it
|
|
43
|
+
imports is not fenced by it.
|
|
39
44
|
- Import the app's own modules as `#src/…` (see `imports` in `package.json`).
|
|
40
|
-
- A test imports `.abide` files directly: `bunfig.toml` preloads `@abide/abide/
|
|
45
|
+
- A test imports `.abide` files directly: `bunfig.toml` preloads `@abide/harness/abide/preload`,
|
|
46
|
+
a DOM to mount into and then `@abide/abide/test`.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { expect, test } from 'bun:test'
|
|
2
2
|
import { flushSync } from '@abide/abide'
|
|
3
3
|
import { measure, mount } from '@abide/harness/abide'
|
|
4
|
-
import Index from '
|
|
4
|
+
import Index from './index.abide'
|
|
5
5
|
|
|
6
6
|
test('a click rewrites the count and builds nothing', async () => {
|
|
7
7
|
using _page = await mount(Index({}))
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { relative as relativeTo } from 'node:path' // bun exposes no path helpers of its own
|
|
2
|
-
import type { Compilation } from '#server/abideModules.ts'
|
|
3
2
|
import { appFiles } from '#server/appTree.ts'
|
|
4
3
|
import { type Placement, placement, routeOf } from '#server/placement.ts'
|
|
5
4
|
import { LAUNCHER_PAGE } from '#shared/LAUNCHER_FILES.ts'
|
|
@@ -77,7 +76,6 @@ export type AppLayout = {
|
|
|
77
76
|
errors: AppRoute[]
|
|
78
77
|
/** Every public file's source path to its address. 36.11. */
|
|
79
78
|
published: Map<string, string>
|
|
80
|
-
mounted: Compilation['mounted']
|
|
81
79
|
/** 30.26 — every address a link may name: a page's, and a public file's. */
|
|
82
80
|
addresses: string[]
|
|
83
81
|
/** What the tree refuses, each at its file; a build stops naming every one. */
|
|
@@ -160,7 +158,6 @@ export async function appLayout(root: string): Promise<AppLayout> {
|
|
|
160
158
|
routes,
|
|
161
159
|
errors,
|
|
162
160
|
published,
|
|
163
|
-
mounted: found.mounted,
|
|
164
161
|
addresses: [...addresses].sort(),
|
|
165
162
|
refused: refusals.found,
|
|
166
163
|
unparsed: unparsed.found,
|
|
@@ -201,7 +198,6 @@ export async function withDependencies(
|
|
|
201
198
|
): Promise<{
|
|
202
199
|
sources: ParsedSource[]
|
|
203
200
|
handlers: HandlerFile[]
|
|
204
|
-
mounted: Compilation['mounted']
|
|
205
201
|
/** 23.19 — each mounted `.abide`'s position, by its path; one at its own path is not here. */
|
|
206
202
|
positions: Map<string, string>
|
|
207
203
|
}> {
|
|
@@ -215,7 +211,6 @@ export async function withDependencies(
|
|
|
215
211
|
})
|
|
216
212
|
// The mounted handler files placed so far; the app's own is never mounted (23.20).
|
|
217
213
|
const handled = new Set<string>()
|
|
218
|
-
const mounted: Compilation['mounted'] = {}
|
|
219
214
|
const byPath = new Map<string, ParsedSource>()
|
|
220
215
|
for (const source of all) byPath.set(source.path, source)
|
|
221
216
|
const positions = new Map<string, string>()
|
|
@@ -258,7 +253,6 @@ export async function withDependencies(
|
|
|
258
253
|
continue
|
|
259
254
|
}
|
|
260
255
|
mountedBy.set(mount.directory, position)
|
|
261
|
-
mounted[relativeTo(root, mount.directory)] = mount.at
|
|
262
256
|
for (const placed of mount.handlers) {
|
|
263
257
|
// Two mounts can overlap, one directory inside the other; a file is placed once.
|
|
264
258
|
if (handled.has(placed.path)) continue
|
|
@@ -283,7 +277,7 @@ export async function withDependencies(
|
|
|
283
277
|
if (!byPath.has(target)) await add(target)
|
|
284
278
|
}
|
|
285
279
|
}
|
|
286
|
-
return { sources: all, handlers,
|
|
280
|
+
return { sources: all, handlers, positions }
|
|
287
281
|
}
|
|
288
282
|
|
|
289
283
|
/** Whether a page or an error page has anything to paint: a template, or a head. 23.3, 35.14. */
|
|
@@ -294,8 +288,7 @@ export function paints(file: AbideFile): boolean {
|
|
|
294
288
|
/**
|
|
295
289
|
* Refuses what a source's place forbids it.
|
|
296
290
|
*
|
|
297
|
-
* 34.20 — a server block under a `
|
|
298
|
-
* runs. 34.21 — a file imported as text that is not there, found at build rather than when its
|
|
291
|
+
* 34.20 — a server block under a `public` directory, which holds no component that runs. 34.21 — a file imported as text that is not there, found at build rather than when its
|
|
299
292
|
* module first loads. 35.16 — of the files with an address, the root layout alone writes the
|
|
300
293
|
* document.
|
|
301
294
|
*/
|
|
@@ -306,14 +299,11 @@ function refusePlace(
|
|
|
306
299
|
refusals: Refusals,
|
|
307
300
|
): void {
|
|
308
301
|
const { relative, place, kind } = placed
|
|
309
|
-
if (
|
|
310
|
-
source.file.servers.length > 0 &&
|
|
311
|
-
(place === 'server' || place === 'public')
|
|
312
|
-
)
|
|
302
|
+
if (source.file.servers.length > 0 && place === 'public')
|
|
313
303
|
refusals.at(
|
|
314
304
|
relative,
|
|
315
305
|
new Error(
|
|
316
|
-
`abide: ${relative} has a <script server>, and it is under a
|
|
306
|
+
`abide: ${relative} has a <script server>, and it is under a public directory, where nothing has an address. Move the file out of it, or the code into a server file under a lib directory. RULEBOOK 34.20.`,
|
|
317
307
|
),
|
|
318
308
|
)
|
|
319
309
|
const from = source.path.slice(0, source.path.lastIndexOf('/'))
|
|
@@ -340,7 +330,7 @@ function refusePlace(
|
|
|
340
330
|
}
|
|
341
331
|
|
|
342
332
|
/**
|
|
343
|
-
* Refuses, naming the module, every module under a `
|
|
333
|
+
* Refuses, naming the module, every module under a `lib` directory that calls a hook.
|
|
344
334
|
*
|
|
345
335
|
* 37.13 — a hook called in a module that is neither a `.abide` nor a handler file runs as the module
|
|
346
336
|
* is imported, before any file's route is set, and not at all where nothing imports it.
|
|
@@ -172,7 +172,6 @@ export function compilationOf(app: CompiledApp): Compilation {
|
|
|
172
172
|
files: app.files,
|
|
173
173
|
handlers: app.handlers,
|
|
174
174
|
tables: app.tables,
|
|
175
|
-
mounted: app.mounted,
|
|
176
175
|
}
|
|
177
176
|
}
|
|
178
177
|
|
|
@@ -211,7 +210,6 @@ export async function compiled(
|
|
|
211
210
|
files: {},
|
|
212
211
|
tables: {},
|
|
213
212
|
handlers: {},
|
|
214
|
-
mounted: layout.mounted,
|
|
215
213
|
routes: [],
|
|
216
214
|
mounts: [],
|
|
217
215
|
sources: layout.own,
|
|
@@ -95,7 +95,7 @@ export async function handlersUnder(
|
|
|
95
95
|
throw new Error(
|
|
96
96
|
each.name === 'default'
|
|
97
97
|
? `abide: ${relative}'s default export is not declared as a handler. A handler file's default is declared by abide's \`GET(…)\` (or POST, PUT, PATCH, DELETE); anything else belongs under a lib directory. RULEBOOK 16.73.`
|
|
98
|
-
: `abide: ${relative} exports \`${each.name}\`, and a handler file exports its handler as its default and nothing else. Move \`${each.name}\` into a module under a lib
|
|
98
|
+
: `abide: ${relative} exports \`${each.name}\`, and a handler file exports its handler as its default and nothing else. Move \`${each.name}\` into a module under a lib directory. RULEBOOK 16.73.`,
|
|
99
99
|
)
|
|
100
100
|
if (handler?.method === undefined)
|
|
101
101
|
throw new Error(
|
package/src/compiler/scaffold.ts
CHANGED
|
@@ -34,10 +34,10 @@ const SOURCE_ALIAS = { '#src/*': ['./src/*', './src/*.ts'] }
|
|
|
34
34
|
/**
|
|
35
35
|
* 38.109, 38.119 — the starter's `bunfig.toml`: the harness's abide preload, which is a DOM a test
|
|
36
36
|
* can mount into and measure, then abide's own — the app's `.abide` files imported as modules, with
|
|
37
|
-
* nothing booted first.
|
|
37
|
+
* nothing booted first. Its test root is `src`, so a browser test under `e2e` is playwright's alone.
|
|
38
38
|
*/
|
|
39
39
|
export const TEST_PRELOAD =
|
|
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'
|
|
40
|
+
'# 38.109, 38.119 — a DOM to mount into and measure, then the app\'s `.abide` files as modules.\n[test]\nroot = "src"\npreload = ["@abide/harness/abide/preload"]\n'
|
|
41
41
|
|
|
42
42
|
// WRITTEN, NOT COPIED, beside the manifests: a packer drops a `bunfig.toml` or a `.gitignore` from
|
|
43
43
|
// the tarball (`bun pm pack` both, `npm pack` the second), so neither survives as a starter file.
|
|
@@ -63,11 +63,11 @@ function filesFor(name: string): Record<string, string> {
|
|
|
63
63
|
},
|
|
64
64
|
// ONE ALIAS FOR THE SOURCE, and two targets for it, because an extensionless
|
|
65
65
|
// specifier is how an import is written and only bun probes for the extension:
|
|
66
|
-
// TypeScript takes an `imports` target as the exact file, so `#src/media/
|
|
66
|
+
// TypeScript takes an `imports` target as the exact file, so `#src/media/lib/db.server`
|
|
67
67
|
// alone left every name it imports `any` and the checker could not type one of
|
|
68
68
|
// them (31.3). The first target is what bun resolves, and the one a `.ts` or
|
|
69
|
-
// `.abide` specifier needs. Which side a module is on is its
|
|
70
|
-
//
|
|
69
|
+
// `.abide` specifier needs. Which side a module is on is its name's to say (36.10),
|
|
70
|
+
// not its alias's.
|
|
71
71
|
imports: SOURCE_ALIAS,
|
|
72
72
|
dependencies: { '@abide/abide': ABIDE_DEPENDENCY },
|
|
73
73
|
// `Bun`, `bun:sqlite` and the rest of the runtime a server module reaches for are
|
|
@@ -75,11 +75,6 @@ export type Compilation = {
|
|
|
75
75
|
handlers: Record<string, HandlerFace>
|
|
76
76
|
/** The build's own modules, by name below `__abide`. */
|
|
77
77
|
tables: Record<string, string>
|
|
78
|
-
/**
|
|
79
|
-
* 23.19 — every directory a file mounts, by its path below the root, to the position its files
|
|
80
|
-
* take: what the browser's fence reads a mounted file's place by (16.29).
|
|
81
|
-
*/
|
|
82
|
-
mounted: Record<string, string>
|
|
83
78
|
}
|
|
84
79
|
|
|
85
80
|
const RELATIVE_OR_SUBPATH = /^(?:\.\.?\/|#)/
|
package/src/server/appTree.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// 23.14 — WHERE AN APP'S ADDRESSES ARE, read off the tree in one walk. A file directly in a directory
|
|
2
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): `
|
|
4
|
-
//
|
|
5
|
-
// So listing a directory lists its routes.
|
|
3
|
+
// else is there but its tests (23.22); a fence is never one (23.15): `public` holds files answered as
|
|
4
|
+
// authored at the path of the directories above it, and `lib` holds the rest, a server file among it
|
|
5
|
+
// (23.23). So listing a directory lists its routes.
|
|
6
6
|
//
|
|
7
|
-
// ONE WALK for the sources, the handler files, the
|
|
8
|
-
//
|
|
7
|
+
// ONE WALK for the sources, the handler files, the lib modules, and the public files `abide build`
|
|
8
|
+
// copies and `abide dev` answers, and the refusals (23.16, 23.17, 23.18, 23.22, 23.23) — each
|
|
9
9
|
// file what `placeOf` says of its path, so the walk and every reader of one path agree, and ONE SKIP
|
|
10
10
|
// RULE, `skipped`, for every walk of the tree.
|
|
11
11
|
//
|
|
@@ -16,12 +16,18 @@ import { type Dirent, existsSync, readdirSync } from 'node:fs' // bun has no dir
|
|
|
16
16
|
import type { Refusals } from '#compiler/Refusals.ts'
|
|
17
17
|
import { ABIDE_ADDRESS } from '#shared/ADDRESSES.ts'
|
|
18
18
|
import {
|
|
19
|
-
HOLDS_SERVER,
|
|
20
19
|
PUBLIC_DIRECTORY,
|
|
21
20
|
RESERVED_DIRECTORIES,
|
|
22
|
-
SERVER_DIRECTORY,
|
|
23
21
|
} from '#shared/RESERVED_DIRECTORIES.ts'
|
|
24
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
DIRECTORY_FILES,
|
|
24
|
+
isDeclarationFile,
|
|
25
|
+
isServerFile,
|
|
26
|
+
isTestFile,
|
|
27
|
+
placeOf,
|
|
28
|
+
routeOf,
|
|
29
|
+
skipped,
|
|
30
|
+
} from './placement.ts'
|
|
25
31
|
|
|
26
32
|
/** What one walk of the app finds, every path relative to the root. */
|
|
27
33
|
type AppFiles = {
|
|
@@ -29,15 +35,15 @@ type AppFiles = {
|
|
|
29
35
|
abide: string[]
|
|
30
36
|
/** Every handler file, in path order. */
|
|
31
37
|
handlers: string[]
|
|
32
|
-
/** Every module under a `
|
|
38
|
+
/** Every module under a `lib` directory, in path order — read for 37.13. */
|
|
33
39
|
modules: string[]
|
|
34
40
|
/** Every file under a `public` directory, to the address it is answered at below the mount. */
|
|
35
41
|
published: Map<string, string>
|
|
36
42
|
}
|
|
37
43
|
|
|
38
44
|
/**
|
|
39
|
-
* The app's `.abide` sources, handler files,
|
|
40
|
-
*
|
|
45
|
+
* The app's `.abide` sources, handler files, lib modules and public files, refusing a tree 23.16,
|
|
46
|
+
* 23.17, 23.18, 23.22 or 23.23 forbids into `refusals`, each at its path below the root.
|
|
41
47
|
*
|
|
42
48
|
* A missing root is an app with nothing in it, not a fault.
|
|
43
49
|
*/
|
|
@@ -82,14 +88,7 @@ function walk(
|
|
|
82
88
|
if (entry.isDirectory()) {
|
|
83
89
|
let within = reserved
|
|
84
90
|
if (RESERVED_DIRECTORIES.has(entry.name)) {
|
|
85
|
-
|
|
86
|
-
if (
|
|
87
|
-
reserved !== undefined &&
|
|
88
|
-
!(
|
|
89
|
-
entry.name === SERVER_DIRECTORY &&
|
|
90
|
-
HOLDS_SERVER.has(reserved.name)
|
|
91
|
-
)
|
|
92
|
-
)
|
|
91
|
+
if (reserved !== undefined)
|
|
93
92
|
refusals.at(
|
|
94
93
|
path,
|
|
95
94
|
new Error(
|
|
@@ -107,17 +106,27 @@ function walk(
|
|
|
107
106
|
)
|
|
108
107
|
continue
|
|
109
108
|
}
|
|
109
|
+
const place = placeOf(path)
|
|
110
|
+
if (isServerFile(path) && place !== 'lib') {
|
|
111
|
+
refusals.at(
|
|
112
|
+
path,
|
|
113
|
+
new Error(
|
|
114
|
+
`abide: ${path} is a server file under no lib directory. Move it into a lib directory beside it. RULEBOOK 23.23.`,
|
|
115
|
+
),
|
|
116
|
+
)
|
|
117
|
+
continue
|
|
118
|
+
}
|
|
110
119
|
const abide = path.endsWith('.abide')
|
|
111
120
|
if (abide) files.abide.push(path)
|
|
112
|
-
const place = placeOf(path)
|
|
113
121
|
if (place === 'address') {
|
|
122
|
+
if (isTestFile(path) || isDeclarationFile(path)) continue
|
|
114
123
|
if (!abide && !isModule(path)) {
|
|
115
124
|
if (packaged && relative === '' && !path.endsWith('.ts'))
|
|
116
125
|
continue
|
|
117
126
|
refusals.at(
|
|
118
127
|
path,
|
|
119
128
|
new Error(
|
|
120
|
-
`abide: ${path} is directly in an address directory, which holds pages, layouts, error pages and
|
|
129
|
+
`abide: ${path} is directly in an address directory, which holds pages, layouts, error pages, handler files, test files and declaration files alone. Move it into a lib directory beside it. RULEBOOK 23.22.`,
|
|
121
130
|
),
|
|
122
131
|
)
|
|
123
132
|
continue
|
|
@@ -139,21 +148,13 @@ function walk(
|
|
|
139
148
|
)
|
|
140
149
|
else if (place === 'public')
|
|
141
150
|
files.published.set(path, publicAddress(path, fence.path))
|
|
142
|
-
else if (
|
|
143
|
-
files.modules.push(path)
|
|
151
|
+
else if (place === 'lib' && isModule(path)) files.modules.push(path)
|
|
144
152
|
}
|
|
145
153
|
}
|
|
146
154
|
|
|
147
|
-
/**
|
|
148
|
-
* Whether `path` names a module the app runs — neither a declaration nor a test: directly in an
|
|
149
|
-
* address directory, a handler file rather than a stray.
|
|
150
|
-
*/
|
|
155
|
+
/** Whether `path` names a module the app runs — neither a declaration nor a test. */
|
|
151
156
|
function isModule(path: string): boolean {
|
|
152
|
-
return (
|
|
153
|
-
path.endsWith('.ts') &&
|
|
154
|
-
!path.endsWith('.d.ts') &&
|
|
155
|
-
!path.endsWith('.test.ts')
|
|
156
|
-
)
|
|
157
|
+
return path.endsWith('.ts') && !isDeclarationFile(path) && !isTestFile(path)
|
|
157
158
|
}
|
|
158
159
|
|
|
159
160
|
/** The refusal of `source` where `route` lies in the framework's own namespace, or `undefined`. 23.17. */
|
|
@@ -199,7 +200,8 @@ export function holdsApp(root: string): boolean {
|
|
|
199
200
|
return !appSourcesUnder(root, '').next().done
|
|
200
201
|
}
|
|
201
202
|
|
|
202
|
-
// Every `.abide` under `directory`, and every handler file — a module whose place is an address
|
|
203
|
+
// Every `.abide` under `directory`, and every handler file — a module whose place is an address, and
|
|
204
|
+
// no server file (23.23).
|
|
203
205
|
function* appSourcesUnder(
|
|
204
206
|
directory: string,
|
|
205
207
|
relative: string,
|
|
@@ -217,7 +219,9 @@ function* appSourcesUnder(
|
|
|
217
219
|
yield* appSourcesUnder(`${directory}/${entry.name}`, `${path}/`)
|
|
218
220
|
else if (
|
|
219
221
|
path.endsWith('.abide') ||
|
|
220
|
-
(isModule(path) &&
|
|
222
|
+
(isModule(path) &&
|
|
223
|
+
!isServerFile(path) &&
|
|
224
|
+
placeOf(path) === 'address')
|
|
221
225
|
)
|
|
222
226
|
yield path
|
|
223
227
|
}
|
|
@@ -52,10 +52,10 @@ export async function clientBundle(
|
|
|
52
52
|
// tables' is not on disk to open.
|
|
53
53
|
root,
|
|
54
54
|
// 16.28, 16.29 — the browser's view of the server: a `<script server>` is its stand-ins and a
|
|
55
|
-
//
|
|
55
|
+
// server file refuses — FENCED FIRST, before the plugin that would serve the module. 38.43
|
|
56
56
|
// — then the app's own, as `bunfig.toml` declares them.
|
|
57
57
|
plugins: [
|
|
58
|
-
fenceServer(root
|
|
58
|
+
fenceServer(root),
|
|
59
59
|
abideModules(root, compilation),
|
|
60
60
|
...(await appPlugins(root)),
|
|
61
61
|
],
|
|
@@ -1,63 +1,57 @@
|
|
|
1
|
-
// 16.29 — WHAT THE BROWSER'S BUNDLE SEES OF THE SERVER:
|
|
2
|
-
//
|
|
3
|
-
// wrong file. A handler crosses as the wrapper the compiler wrote in its place — a handler
|
|
4
|
-
// that file, a private handler in its server block's place (16.28); this is the fence around
|
|
1
|
+
// 16.29 — WHAT THE BROWSER'S BUNDLE SEES OF THE SERVER: no server file, `<name>.server.<extension>`,
|
|
2
|
+
// wherever it lies, so a secret or a database client cannot ride into the browser by being imported
|
|
3
|
+
// from the wrong file. A handler crosses as the wrapper the compiler wrote in its place — a handler
|
|
4
|
+
// file as that file, a private handler in its server block's place (16.28); this is the fence around
|
|
5
5
|
// everything else.
|
|
6
6
|
//
|
|
7
7
|
// A PLUGIN ON THE BUNDLE, NOT A REWRITE OF THE PAGES. A rewrite reaches the specifiers the compiler
|
|
8
|
-
// emitted; the bundle reaches every edge — a helper the page imports, importing a server
|
|
8
|
+
// emitted; the bundle reaches every edge — a helper the page imports, importing a server file,
|
|
9
9
|
// included.
|
|
10
10
|
//
|
|
11
11
|
// REGISTERED AHEAD OF `abideModules`, AND IN ITS NAMESPACES: a load is answered by the first plugin
|
|
12
12
|
// that answers it, and a component or a file's text is loaded there rather than as a `file`.
|
|
13
13
|
//
|
|
14
|
-
// BY
|
|
15
|
-
//
|
|
14
|
+
// BY NAME, NOT BY PLACE: the file's own name says it, so a dependency's or a mounted package's is
|
|
15
|
+
// fenced as the app's own is, and a module it imports is not (36.10).
|
|
16
16
|
|
|
17
|
-
import { SERVER_DIRECTORY } from '#shared/RESERVED_DIRECTORIES.ts'
|
|
18
|
-
import type { Compilation } from './abideModules.ts'
|
|
19
17
|
import {
|
|
20
18
|
COMPONENT_NAMESPACE,
|
|
21
19
|
realFileOf,
|
|
22
20
|
STYLE_NAMESPACE,
|
|
23
21
|
TEXT_NAMESPACE,
|
|
24
22
|
} from './faceOf.ts'
|
|
25
|
-
import {
|
|
23
|
+
import { isServerFile } from './placement.ts'
|
|
26
24
|
|
|
27
25
|
// Every namespace a browser bundle loads a file of the app's in: its modules, its components, a
|
|
28
26
|
// file's text and a component's stylesheet.
|
|
29
27
|
const FENCED = ['file', COMPONENT_NAMESPACE, TEXT_NAMESPACE, STYLE_NAMESPACE]
|
|
30
28
|
|
|
31
29
|
/**
|
|
32
|
-
* The bundler plugin refusing
|
|
30
|
+
* The bundler plugin refusing every server file to the browser's bundle.
|
|
33
31
|
*
|
|
34
|
-
* `root` is its real path (`appRoot`), which is how the bundler names a file
|
|
35
|
-
*
|
|
32
|
+
* `root` is its real path (`appRoot`), which is how the bundler names a file. Listed before
|
|
33
|
+
* `abideModules`.
|
|
36
34
|
*/
|
|
37
|
-
export function fenceServer(
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
// load it passes on falls through to the next plugin's, or the bundler's own.
|
|
43
|
-
const filter = new RegExp(`/${SERVER_DIRECTORY}/`)
|
|
35
|
+
export function fenceServer(root: string): Bun.BunPlugin {
|
|
36
|
+
// Anything whose own name could be one, a face's suffix included, and `isServerFile` decides by
|
|
37
|
+
// the file a face stands for — a load it passes on falls through to the next plugin's, or the
|
|
38
|
+
// bundler's own.
|
|
39
|
+
const filter = /\.server\.[^/]*$/
|
|
44
40
|
return {
|
|
45
41
|
name: 'abide-fence',
|
|
46
42
|
setup(build) {
|
|
47
43
|
for (const namespace of FENCED)
|
|
48
44
|
build.onLoad({ filter, namespace }, (args) => {
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
)
|
|
58
|
-
return undefined
|
|
45
|
+
const path = realFileOf(args.path)
|
|
46
|
+
if (!isServerFile(path)) return undefined
|
|
47
|
+
const dependency = path.lastIndexOf('/node_modules/')
|
|
48
|
+
const shown = path.startsWith(`${root}/`)
|
|
49
|
+
? path.slice(root.length + 1)
|
|
50
|
+
: dependency === -1
|
|
51
|
+
? path
|
|
52
|
+
: path.slice(dependency + 1)
|
|
59
53
|
throw new Error(
|
|
60
|
-
`abide: ${
|
|
54
|
+
`abide: ${shown} is a server file, and the browser's bundle imports it. RULEBOOK 16.29.`,
|
|
61
55
|
)
|
|
62
56
|
})
|
|
63
57
|
},
|
|
@@ -253,6 +253,14 @@ export class McpSurface {
|
|
|
253
253
|
handlers: Map<string, Mounted>,
|
|
254
254
|
answer: (request: Request) => Promise<Response>,
|
|
255
255
|
) {
|
|
256
|
+
// 20.20 — a tool's arguments are an object, so args that can never be one are no tool.
|
|
257
|
+
for (const mounted of handlers.values()) {
|
|
258
|
+
const tool = surfaceOf(mounted, 'mcp')
|
|
259
|
+
if (tool !== undefined && toolInput(tool.schema) === undefined)
|
|
260
|
+
throw new Error(
|
|
261
|
+
`abide: ${tool.name}'s args can never be an object, which is all an MCP tool is called with — give it object args, or \`clients: { mcp: false }\`. RULEBOOK 20.20.`,
|
|
262
|
+
)
|
|
263
|
+
}
|
|
256
264
|
this.#handlers = handlers
|
|
257
265
|
this.#answer = answer
|
|
258
266
|
}
|
|
@@ -280,7 +288,7 @@ export class McpSurface {
|
|
|
280
288
|
listing.push({
|
|
281
289
|
name: tool.name,
|
|
282
290
|
description: tool.description ?? '',
|
|
283
|
-
inputSchema: tool.schema
|
|
291
|
+
inputSchema: toolInput(tool.schema),
|
|
284
292
|
// 20.14 — what the method promises: a `GET` is safe, and a `GET`, a `PUT` and a
|
|
285
293
|
// `DELETE` idempotent. Any other write may destroy, and nothing says it does not.
|
|
286
294
|
annotations: {
|
|
@@ -411,6 +419,32 @@ function logCall(
|
|
|
411
419
|
)
|
|
412
420
|
}
|
|
413
421
|
|
|
422
|
+
/**
|
|
423
|
+
* `schema` as a tool's `inputSchema`, its top-level `type` `object` whatever it declared there, or
|
|
424
|
+
* `undefined` where no value it admits is an object. 20.19, 20.20.
|
|
425
|
+
*/
|
|
426
|
+
function toolInput(schema: unknown): object | undefined {
|
|
427
|
+
if (schema === undefined || schema === true) return { type: 'object' }
|
|
428
|
+
if (!admitsObject(schema)) return undefined
|
|
429
|
+
return { ...(schema as object), type: 'object' }
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** Whether a JSON Schema admits some object — its `type`, and each of `anyOf`, `oneOf` and `allOf`. */
|
|
433
|
+
function admitsObject(schema: unknown): boolean {
|
|
434
|
+
if (schema === true) return true
|
|
435
|
+
if (schema === null || typeof schema !== 'object') return false
|
|
436
|
+
const { type, anyOf, oneOf, allOf } = schema as Record<string, unknown>
|
|
437
|
+
if (
|
|
438
|
+
type !== undefined &&
|
|
439
|
+
type !== 'object' &&
|
|
440
|
+
!(Array.isArray(type) && type.includes('object'))
|
|
441
|
+
)
|
|
442
|
+
return false
|
|
443
|
+
if (Array.isArray(anyOf) && !anyOf.some(admitsObject)) return false
|
|
444
|
+
if (Array.isArray(oneOf) && !oneOf.some(admitsObject)) return false
|
|
445
|
+
return !Array.isArray(allOf) || allOf.every(admitsObject)
|
|
446
|
+
}
|
|
447
|
+
|
|
414
448
|
/**
|
|
415
449
|
* Whether a tool declares an `outputSchema`: MCP's structured content is an object, so only a
|
|
416
450
|
* result schema that is one. 20.16.
|