@abide/abide 0.102.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 +10 -0
- package/bin/abide.ts +31 -28
- package/docs/REGISTRY.md +30 -26
- package/docs/RULEBOOK.md +98 -83
- package/docs/abide.md +771 -522
- package/package.json +2 -1
- package/scaffold/AGENTS.md +13 -6
- package/src/compiler/TypeSession.ts +110 -35
- package/src/compiler/appMap.ts +96 -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 +228 -70
- 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 +70 -45
- 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 +117 -51
- package/src/server/answerRequest.ts +9 -4
- package/src/server/answerRpc.ts +18 -16
- package/src/server/appTree.ts +83 -24
- 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/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/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 +13 -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/{ui → shared}/reportMenu.ts +7 -9
- package/src/shared/routePattern.ts +58 -20
- 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
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
// WHICH FACE AN IMPORT GETS IS SPELLED ON ITS SPECIFIER (`MODULE_QUERIES`), because bun hands a plugin
|
|
8
8
|
// no import attributes: a compiled module asks for `?component`, a browser module for its own
|
|
9
9
|
// `?style`, a text import for `?text`. A `.abide` named bare is a module abide did not compile
|
|
10
|
-
// asking — on a server that is the bound face 34.16 states,
|
|
11
|
-
//
|
|
10
|
+
// asking — on a server that is the bound face 34.16 states, and in a browser the component module
|
|
11
|
+
// itself.
|
|
12
12
|
//
|
|
13
13
|
// THE TABLES — the route, error and handler tables, and in a browser the router's table and the
|
|
14
14
|
// entry — are the build's own modules, served from `__abide` below the root, where no app writes.
|
|
@@ -17,13 +17,16 @@
|
|
|
17
17
|
// that registered one, so a second app booted in the same process — a test's — adds its compilation
|
|
18
18
|
// to this one rather than registering a plugin nobody asks.
|
|
19
19
|
|
|
20
|
-
import { dirname, resolve } from 'node:path' // bun exposes no path helpers of its own
|
|
20
|
+
import { basename, dirname, resolve } from 'node:path' // bun exposes no path helpers of its own
|
|
21
|
+
import { SERVER_SPECIFIER } from '#shared/SPECIFIERS.ts'
|
|
21
22
|
import type { ServerTable } from './answerRequest.ts'
|
|
22
23
|
import type { MountRow } from './answerRpc.ts'
|
|
23
24
|
import { installSourceMaps, type SourceMaps } from './installSourceMaps.ts'
|
|
24
25
|
import {
|
|
25
26
|
CLIENT_QUERY,
|
|
26
27
|
COMPONENT_QUERY,
|
|
28
|
+
HANDLER_FACE,
|
|
29
|
+
HANDLER_SUFFIX,
|
|
27
30
|
STYLE_QUERY,
|
|
28
31
|
TEXT_QUERY,
|
|
29
32
|
} from './MODULE_QUERIES.ts'
|
|
@@ -51,8 +54,18 @@ export type CompiledFile = {
|
|
|
51
54
|
export type Compilation = {
|
|
52
55
|
/** Every `.abide` compiled, by its path below the root. */
|
|
53
56
|
files: Record<string, CompiledFile>
|
|
57
|
+
/**
|
|
58
|
+
* Every handler file, by its path below the root: the route its hooks are filed at (37.1), and
|
|
59
|
+
* the module a browser is handed in its place (16.74), `''` where the browser is handed none.
|
|
60
|
+
*/
|
|
61
|
+
handlers: Record<string, HandlerFace>
|
|
54
62
|
/** The build's own modules, by name below `__abide`. */
|
|
55
63
|
tables: Record<string, string>
|
|
64
|
+
/**
|
|
65
|
+
* 23.19 — every directory a file mounts, by its path below the root, to the position its files
|
|
66
|
+
* take: what the browser's fence reads a mounted file's place by (16.29).
|
|
67
|
+
*/
|
|
68
|
+
mounted: Record<string, string>
|
|
56
69
|
}
|
|
57
70
|
|
|
58
71
|
// A VIRTUAL FACE'S PATH: its file's, with a suffix that is not a query. Both registries key a module
|
|
@@ -68,29 +81,40 @@ export const COMPONENT_NAMESPACE = 'abide-component'
|
|
|
68
81
|
/** The namespaces a file's text and a component's stylesheet are loaded in. */
|
|
69
82
|
export const TEXT_NAMESPACE = 'abide-text'
|
|
70
83
|
export const STYLE_NAMESPACE = 'abide-style'
|
|
84
|
+
const RELATIVE_OR_SUBPATH = /^(?:\.\.?\/|#)/
|
|
71
85
|
|
|
72
|
-
/**
|
|
73
|
-
type
|
|
74
|
-
file: (path: string) => CompiledFile | undefined
|
|
75
|
-
table: (path: string) => string | undefined
|
|
76
|
-
}
|
|
86
|
+
/** One handler file, as `Compilation.handlers` holds it. */
|
|
87
|
+
type HandlerFace = { scope: string; client: string }
|
|
77
88
|
|
|
78
89
|
/** The plugin serving `compilation`'s files and tables, for one bundle. */
|
|
79
90
|
export function abideModules(
|
|
80
91
|
root: string,
|
|
81
92
|
compilation: Compilation,
|
|
82
93
|
): Bun.BunPlugin {
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
94
|
+
const held: Held = {
|
|
95
|
+
files: new Map(),
|
|
96
|
+
handlers: new Map(),
|
|
97
|
+
tables: new Map(),
|
|
98
|
+
}
|
|
99
|
+
addCompilation(realOrAsGiven(root), compilation, held)
|
|
100
|
+
return modulesPlugin(held)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* A compilation as the plugin keys it, and answers from: a compiled file, a handler file or a table
|
|
105
|
+
* by absolute path.
|
|
106
|
+
*/
|
|
107
|
+
type Held = {
|
|
108
|
+
files: Map<string, CompiledFile>
|
|
109
|
+
handlers: Map<string, HandlerFace>
|
|
110
|
+
tables: Map<string, string>
|
|
90
111
|
}
|
|
91
112
|
|
|
92
|
-
const
|
|
93
|
-
|
|
113
|
+
const RUNTIME: Held = {
|
|
114
|
+
files: new Map(),
|
|
115
|
+
handlers: new Map(),
|
|
116
|
+
tables: new Map(),
|
|
117
|
+
}
|
|
94
118
|
let registered = false
|
|
95
119
|
|
|
96
120
|
/**
|
|
@@ -99,16 +123,11 @@ let registered = false
|
|
|
99
123
|
*/
|
|
100
124
|
export function serveCompilation(root: string, compilation: Compilation): void {
|
|
101
125
|
const real = realOrAsGiven(root)
|
|
102
|
-
addCompilation(real, compilation,
|
|
126
|
+
addCompilation(real, compilation, RUNTIME)
|
|
103
127
|
installSourceMaps(serverMapsOf(compilation, real), real)
|
|
104
128
|
if (registered) return
|
|
105
129
|
registered = true
|
|
106
|
-
Bun.plugin(
|
|
107
|
-
modulesPlugin({
|
|
108
|
-
file: (path) => RUNTIME_FILES.get(path),
|
|
109
|
-
table: (path) => RUNTIME_TABLES.get(path),
|
|
110
|
-
}),
|
|
111
|
-
)
|
|
130
|
+
Bun.plugin(modulesPlugin(RUNTIME))
|
|
112
131
|
}
|
|
113
132
|
|
|
114
133
|
/** What the server's table exports: its routes and error pages, server blocks and handlers. */
|
|
@@ -146,14 +165,15 @@ export function serverMapsOf(
|
|
|
146
165
|
function addCompilation(
|
|
147
166
|
root: string,
|
|
148
167
|
compilation: Compilation,
|
|
149
|
-
|
|
150
|
-
tables: Map<string, string>,
|
|
168
|
+
held: Held,
|
|
151
169
|
): void {
|
|
152
170
|
// RESOLVED, not joined: a dependency's file is keyed above the root (`../node_modules/…`).
|
|
153
171
|
for (const [relative, compiled] of Object.entries(compilation.files))
|
|
154
|
-
files.set(resolve(root, relative), compiled)
|
|
172
|
+
held.files.set(resolve(root, relative), compiled)
|
|
173
|
+
for (const [relative, face] of Object.entries(compilation.handlers))
|
|
174
|
+
held.handlers.set(resolve(root, relative), face)
|
|
155
175
|
for (const [name, text] of Object.entries(compilation.tables))
|
|
156
|
-
tables.set(`${root}/${VIRTUAL_DIRECTORY}/${name}`, text)
|
|
176
|
+
held.tables.set(`${root}/${VIRTUAL_DIRECTORY}/${name}`, text)
|
|
157
177
|
}
|
|
158
178
|
|
|
159
179
|
// `Bun.resolveSync` asks EVERY abide plugin's `onResolve` again — this one's, and the runtime's, which
|
|
@@ -178,7 +198,7 @@ export function resolvedFile(specifier: string, from: string): string {
|
|
|
178
198
|
}
|
|
179
199
|
}
|
|
180
200
|
|
|
181
|
-
function modulesPlugin(served:
|
|
201
|
+
function modulesPlugin(served: Held): Bun.BunPlugin {
|
|
182
202
|
return {
|
|
183
203
|
name: 'abide-modules',
|
|
184
204
|
setup(build) {
|
|
@@ -264,12 +284,55 @@ function modulesPlugin(served: Served): Bun.BunPlugin {
|
|
|
264
284
|
dirname(pathOf(args.importer)),
|
|
265
285
|
args.path,
|
|
266
286
|
)
|
|
267
|
-
return served.
|
|
287
|
+
return served.tables.get(path) === undefined
|
|
268
288
|
? undefined
|
|
269
289
|
: { path, namespace: 'abide-table' }
|
|
270
290
|
},
|
|
271
291
|
)
|
|
272
292
|
|
|
293
|
+
// A HANDLER FILE IS FOUND BY WHERE ITS IMPORT RESOLVES, however the import spells it, and
|
|
294
|
+
// only from the app's own modules: a package's never names one, and asking bun again for
|
|
295
|
+
// each of a package's relative imports is a resolve per module of the bundle. IN A BUNDLE
|
|
296
|
+
// THE FILTER IS THE HANDLER FILES' NAMES: a hook matching every relative import costs the
|
|
297
|
+
// bundle abide's own `sideEffects`, and the barrel's rest — the validator, the csp — moved
|
|
298
|
+
// into the chunk every page loads (38.94). The runtime is told of each compilation after
|
|
299
|
+
// it registered, so it matches every relative or `#` import and asks.
|
|
300
|
+
// Each handler file's name without `.ts` — what an import of one ends in.
|
|
301
|
+
let stems: string[] | undefined
|
|
302
|
+
if (bundling) {
|
|
303
|
+
const names = new Set<string>()
|
|
304
|
+
for (const path of served.handlers.keys())
|
|
305
|
+
names.add(basename(path, '.ts'))
|
|
306
|
+
stems = [...names]
|
|
307
|
+
}
|
|
308
|
+
const handlerImport =
|
|
309
|
+
stems === undefined
|
|
310
|
+
? RELATIVE_OR_SUBPATH
|
|
311
|
+
: new RegExp(
|
|
312
|
+
`(?:^|/)(?:${stems.map(RegExp.escape).join('|')})(?:\\.ts)?$`,
|
|
313
|
+
)
|
|
314
|
+
if (stems?.length !== 0)
|
|
315
|
+
build.onResolve({ filter: handlerImport }, (args) => {
|
|
316
|
+
if (resolving || !RELATIVE_OR_SUBPATH.test(args.path))
|
|
317
|
+
return undefined
|
|
318
|
+
const importer = pathOf(args.importer)
|
|
319
|
+
if (importer.includes('/node_modules/')) return undefined
|
|
320
|
+
let path: string
|
|
321
|
+
try {
|
|
322
|
+
path = resolved(args.path, args.importer)
|
|
323
|
+
} catch {
|
|
324
|
+
return undefined
|
|
325
|
+
}
|
|
326
|
+
const face = served.handlers.get(path)
|
|
327
|
+
if (face === undefined) return undefined
|
|
328
|
+
// 20.4 — a handler the browser is handed no wrapper for is no import of its.
|
|
329
|
+
if (inBrowser(args.importer) && face.client === '')
|
|
330
|
+
throw new Error(
|
|
331
|
+
`abide: ${pathOf(args.importer)} imports ${args.path}, whose handler withholds itself from the browser (\`clients: { ui: false }\`). Call it from a <script server>, or let the browser have it. RULEBOOK 20.4.`,
|
|
332
|
+
)
|
|
333
|
+
return { path: `${path}${HANDLER_SUFFIX}` }
|
|
334
|
+
})
|
|
335
|
+
|
|
273
336
|
// A RELATIVE SPECIFIER A FACE IMPORTS AT RUN TIME is resolved from the file the face
|
|
274
337
|
// stands for: bun's runtime reads one from an importer in a namespace as no path at
|
|
275
338
|
// all. A bare one it resolves itself. Registered after every face, so a `.abide` is
|
|
@@ -294,6 +357,22 @@ function modulesPlugin(served: Served): Bun.BunPlugin {
|
|
|
294
357
|
}
|
|
295
358
|
},
|
|
296
359
|
)
|
|
360
|
+
// 16.74, 37.1 — A HANDLER FILE: the browser's is its wrapper, and the server's is the file
|
|
361
|
+
// as written, between the two calls that file its hooks at its route — on its first
|
|
362
|
+
// line, so every line of the file stays where its frames name it. ITS FACE is its path
|
|
363
|
+
// with a suffix, in bun's own namespace — so what it imports, relative, `#` or bare,
|
|
364
|
+
// resolves from its directory as the file's own imports do. A namespace of abide's own
|
|
365
|
+
// had bun resolve a bare one from the working directory: a second abide, run from `/`.
|
|
366
|
+
build.onLoad({ filter: HANDLER_FACE }, async (args) => {
|
|
367
|
+
const path = args.path.slice(0, -HANDLER_SUFFIX.length)
|
|
368
|
+
const face = served.handlers.get(path) as HandlerFace
|
|
369
|
+
if (browser) return { contents: face.client, loader: 'js' }
|
|
370
|
+
const text = await Bun.file(path).text()
|
|
371
|
+
return {
|
|
372
|
+
contents: `import { fileRoute as __fileRoute } from '${SERVER_SPECIFIER}';__fileRoute(${JSON.stringify(face.scope)});${text}\n;__fileRoute(undefined)\n`,
|
|
373
|
+
loader: 'ts',
|
|
374
|
+
}
|
|
375
|
+
})
|
|
297
376
|
build.onLoad(
|
|
298
377
|
{ filter: /.*/, namespace: 'abide-client' },
|
|
299
378
|
(args) => ({
|
|
@@ -307,10 +386,7 @@ function modulesPlugin(served: Served): Bun.BunPlugin {
|
|
|
307
386
|
build.onLoad({ filter: /.*/, namespace: 'abide-bound' }, (args) => {
|
|
308
387
|
const path = args.path.slice(0, -BOUND.length)
|
|
309
388
|
return {
|
|
310
|
-
contents: boundModule(
|
|
311
|
-
path,
|
|
312
|
-
compiledAt(served, path).server,
|
|
313
|
-
),
|
|
389
|
+
contents: boundModule(path),
|
|
314
390
|
loader: 'js',
|
|
315
391
|
}
|
|
316
392
|
})
|
|
@@ -339,7 +415,7 @@ function modulesPlugin(served: Served): Bun.BunPlugin {
|
|
|
339
415
|
build.onLoad(
|
|
340
416
|
{ filter: /.*/, namespace: 'abide-table' },
|
|
341
417
|
(args) => ({
|
|
342
|
-
contents: served.
|
|
418
|
+
contents: served.tables.get(args.path) ?? '',
|
|
343
419
|
loader: 'ts',
|
|
344
420
|
}),
|
|
345
421
|
)
|
|
@@ -355,8 +431,8 @@ function pathOf(importer: string): string {
|
|
|
355
431
|
|
|
356
432
|
// THE PATH BUN NAMES A FILE BY HAS ITS LINKS RESOLVED, and so has the root a compilation is served
|
|
357
433
|
// under (`serveCompilation`).
|
|
358
|
-
function compiledAt(served:
|
|
359
|
-
const compiled = served.
|
|
434
|
+
function compiledAt(served: Held, path: string): CompiledFile {
|
|
435
|
+
const compiled = served.files.get(path)
|
|
360
436
|
if (compiled === undefined)
|
|
361
437
|
throw new Error(
|
|
362
438
|
`abide: ${path} is imported, and no compile of this app reached it.`,
|
|
@@ -364,22 +440,12 @@ function compiledAt(served: Served, path: string): CompiledFile {
|
|
|
364
440
|
return compiled
|
|
365
441
|
}
|
|
366
442
|
|
|
367
|
-
const SCANNER = new Bun.Transpiler({ loader: 'ts' })
|
|
368
|
-
|
|
369
443
|
// 34.16 — BOUND, NOT RAW. The compiled module is a function of accessors answering its markup (33.4);
|
|
370
444
|
// what an app's own module calls is a function of plain props answering a `Component` (35.4), which
|
|
371
|
-
// `render` invokes with nothing.
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
// through a module still loading found none of its names.
|
|
376
|
-
function boundModule(path: string, server: string): string {
|
|
377
|
-
const specifier = JSON.stringify(path)
|
|
378
|
-
const names: string[] = []
|
|
379
|
-
for (const name of SCANNER.scan(server).exports)
|
|
380
|
-
if (name !== 'default') names.push(name)
|
|
381
|
-
return `import component from ${specifier}
|
|
382
|
-
${names.length === 0 ? '' : `export { ${names.join(', ')} } from ${specifier}\n`}export default (props = {}) => {
|
|
445
|
+
// `render` invokes with nothing. The component is the module's one export (34.17).
|
|
446
|
+
function boundModule(path: string): string {
|
|
447
|
+
return `import component from ${JSON.stringify(path)}
|
|
448
|
+
export default (props = {}) => {
|
|
383
449
|
const accessors = {}
|
|
384
450
|
for (const key in props) {
|
|
385
451
|
const value = props[key]
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
HEALTH_ADDRESS,
|
|
14
14
|
MCP_ADDRESS,
|
|
15
15
|
OPENAPI_ADDRESS,
|
|
16
|
+
PRIVATE_HANDLERS_ADDRESS,
|
|
16
17
|
} from '#shared/ADDRESSES.ts'
|
|
17
18
|
import { APP_META } from '#shared/APP_META.ts'
|
|
18
19
|
import { DEBUG_META } from '#shared/DEBUG_META.ts'
|
|
@@ -102,7 +103,7 @@ type Ranked = RankedError & {
|
|
|
102
103
|
/**
|
|
103
104
|
* One route of the app: the page at it, and the handler answering each method there.
|
|
104
105
|
*
|
|
105
|
-
* 19.1 — one file per method per route, so a route is a row and a method is a column, and 19.
|
|
106
|
+
* 19.1 — one file per method per route, so a route is a row and a method is a column, and 19.20's
|
|
106
107
|
* "the most specific route answering the method" is a walk down the rows asking each column.
|
|
107
108
|
*/
|
|
108
109
|
type RouteRow = {
|
|
@@ -196,7 +197,11 @@ export async function answerRequest(
|
|
|
196
197
|
// 37.3 — THE MUX'S UPGRADE PASSES THE APP LANE as every request does (`muxUpgrade`).
|
|
197
198
|
const admitted = muxAdmission(asked, request, app)
|
|
198
199
|
if (admitted !== undefined) return admitted
|
|
199
|
-
|
|
200
|
+
// 16.70 — a private handler is answered by the route table, below abide's own namespace.
|
|
201
|
+
if (
|
|
202
|
+
asked.startsWith(`${ABIDE_ADDRESS}/`) &&
|
|
203
|
+
!asked.startsWith(`${PRIVATE_HANDLERS_ADDRESS}/`)
|
|
204
|
+
) {
|
|
200
205
|
const answer = app.framework(asked, request)
|
|
201
206
|
if (answer === undefined) return notFound()
|
|
202
207
|
return readsOnly(request) ? answer : methodRefused(app, asked, READS)
|
|
@@ -222,7 +227,7 @@ export async function answerRequest(
|
|
|
222
227
|
refuse(400, 'The address is not a valid path.'),
|
|
223
228
|
)
|
|
224
229
|
|
|
225
|
-
// 19.
|
|
230
|
+
// 19.20 — THE MOST SPECIFIC ROUTE ANSWERING THE METHOD, an `OPTIONS` asking about the method
|
|
226
231
|
// its preflight names (16.24). A route that matches and answers some other method is held, so
|
|
227
232
|
// an address nothing answers the method at is a 405 naming what is answered there (15.17).
|
|
228
233
|
const preflight = request.method === 'OPTIONS'
|
|
@@ -271,7 +276,7 @@ export async function answerRequest(
|
|
|
271
276
|
// above it, the root's having run already, and an error in it reaches that route's `onError`.
|
|
272
277
|
const scope = currentRequestScope() as RequestScope
|
|
273
278
|
scope.route =
|
|
274
|
-
found.handler === undefined ? found.route.pattern : found.handler.
|
|
279
|
+
found.handler === undefined ? found.route.pattern : found.handler.scope
|
|
275
280
|
scope.params = found.params
|
|
276
281
|
const answer = found.route.answers.get(
|
|
277
282
|
found.handler ?? (found.route.page as Ranked),
|
package/src/server/answerRpc.ts
CHANGED
|
@@ -16,7 +16,6 @@ import type { Gated } from '#shared/reactive/argsContract.ts'
|
|
|
16
16
|
import { deadlineOf, heldGenerationOf } from '#shared/reactive/index.ts'
|
|
17
17
|
import type { Defaults } from '#shared/reactive/parameterDefaults.ts'
|
|
18
18
|
import { notFound, refuse } from '#shared/refuse.ts'
|
|
19
|
-
import { routeOf } from '#shared/routePattern.ts'
|
|
20
19
|
import type { Method, Rpc } from '#shared/rpc.ts'
|
|
21
20
|
import { argsInURL } from '#shared/rpcRequest.ts'
|
|
22
21
|
import { VERSION_HEADER } from '#shared/VERSION_HEADER.ts'
|
|
@@ -66,7 +65,14 @@ export type Mount = {
|
|
|
66
65
|
address: string
|
|
67
66
|
method: Method
|
|
68
67
|
source: string
|
|
68
|
+
/** The handler file's name, or the private handler's binding. */
|
|
69
69
|
name: string
|
|
70
|
+
/** What the source's module exports it as: `default` for a handler file. */
|
|
71
|
+
export: string
|
|
72
|
+
/** The route its hooks are filed at, which its rungs are read over (37.1, 16.72). */
|
|
73
|
+
scope: string
|
|
74
|
+
/** Whether it is a private handler, listed nowhere (16.71). */
|
|
75
|
+
private: boolean
|
|
70
76
|
/** Whether a browser is handed a wrapper for it. 20.4. */
|
|
71
77
|
ui: boolean
|
|
72
78
|
/** Whether the command line lists it — the shell's listing and completion, never whether `call` reaches it. 20.3, 38.62. */
|
|
@@ -97,18 +103,15 @@ export type Mount = {
|
|
|
97
103
|
|
|
98
104
|
/**
|
|
99
105
|
* A row of the table the build wrote, with the import of the module its handler is declared in —
|
|
100
|
-
* the file's server module, the one the route table
|
|
101
|
-
* handler.
|
|
106
|
+
* the handler file itself, or a private handler's `.abide` server module, the one the route table
|
|
107
|
+
* loads too, so a page and a call reach one handler.
|
|
102
108
|
*/
|
|
103
109
|
export type MountRow = Mount & { load: () => Promise<Record<string, unknown>> }
|
|
104
110
|
|
|
105
111
|
/** A handler's row, mounted: the handler imported and its args schema probed. */
|
|
106
112
|
export type Mounted = Mount & {
|
|
107
|
-
method: Method
|
|
108
|
-
/** The route of the file declaring it, which its hooks are filed at (37.1, 37.2). */
|
|
109
|
-
route: string
|
|
110
113
|
/**
|
|
111
|
-
* The rungs filed over `
|
|
114
|
+
* The rungs filed over `scope`, read once at mount (16.14) — what an http call and a socket
|
|
112
115
|
* subscription to it both pass through (37.2, 18.26).
|
|
113
116
|
*/
|
|
114
117
|
rungs: AppRung[]
|
|
@@ -148,12 +151,14 @@ export function mountedOwn(
|
|
|
148
151
|
{
|
|
149
152
|
address,
|
|
150
153
|
method: 'GET',
|
|
151
|
-
route: '/',
|
|
152
154
|
// The root's rungs run before routing (37.3), and nothing is filed below a route
|
|
153
155
|
// of abide's own.
|
|
154
156
|
rungs: [],
|
|
155
157
|
source: '',
|
|
156
158
|
name,
|
|
159
|
+
export: name,
|
|
160
|
+
scope: '/',
|
|
161
|
+
private: false,
|
|
157
162
|
ui: false,
|
|
158
163
|
cli: false,
|
|
159
164
|
mcp: false,
|
|
@@ -188,20 +193,17 @@ export async function mountHandlers(
|
|
|
188
193
|
const handlers = new Map<string, Mounted>()
|
|
189
194
|
for (let at = 0; at < table.length; at += 1) {
|
|
190
195
|
const mount = table[at] as Mount
|
|
191
|
-
const exported = (modules[at] as Record<string, unknown>)[mount.
|
|
196
|
+
const exported = (modules[at] as Record<string, unknown>)[mount.export]
|
|
192
197
|
const handler = exported as Rpc<unknown, unknown> & Gated<unknown>
|
|
193
198
|
const declared = argsDocumentOf(handler)
|
|
194
199
|
// 16.9, 16.10 — the address `rpc.url` and `rpc.raw` build on, under the mount — and what a
|
|
195
200
|
// page's render turns a call it made into the browser's request by (41.20).
|
|
196
201
|
handler.address = underMount(mount.address)
|
|
197
|
-
// 23.19 — the route its address answers below, which is its mount's for a package's file,
|
|
198
|
-
// never the file's place on disk.
|
|
199
|
-
const route = routeOf(mount.address)
|
|
200
202
|
handlers.set(handlerKey(mount.method, mount.address), {
|
|
201
203
|
...mount,
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
rungs: rungsOver(
|
|
204
|
+
// 37.1, 16.72 — the route its file's hooks are filed at, which the build read off
|
|
205
|
+
// where the file answers (23.19's mount for a package's), never its place on disk.
|
|
206
|
+
rungs: rungsOver(mount.scope),
|
|
205
207
|
handler,
|
|
206
208
|
argsDocument: declared ?? (mount.args as Document | undefined),
|
|
207
209
|
wireArgs: declared === undefined ? mount.args : undefined,
|
|
@@ -219,7 +221,7 @@ export function handlerKey(method: string, address: string): string {
|
|
|
219
221
|
* The `Response` to a call of `found` made over http, `params` being what its route matched.
|
|
220
222
|
*
|
|
221
223
|
* `found` answers the request's method — or, for an `OPTIONS`, the method its preflight asks
|
|
222
|
-
* about: which handler a request reaches is the route table's to say (19.
|
|
224
|
+
* about: which handler a request reaches is the route table's to say (19.20).
|
|
223
225
|
*/
|
|
224
226
|
export async function answerRpc(
|
|
225
227
|
found: Mounted,
|
package/src/server/appTree.ts
CHANGED
|
@@ -1,28 +1,35 @@
|
|
|
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
|
+
LIB_DIRECTORY,
|
|
17
19
|
PUBLIC_DIRECTORY,
|
|
18
20
|
RESERVED_DIRECTORIES,
|
|
21
|
+
SERVER_DIRECTORY,
|
|
19
22
|
} from '#shared/RESERVED_DIRECTORIES.ts'
|
|
20
|
-
import {
|
|
23
|
+
import { DIRECTORY_FILES, routeOf, skipped } from '#shared/routePattern.ts'
|
|
21
24
|
|
|
22
25
|
/** What one walk of the app finds, every path relative to the root. */
|
|
23
|
-
|
|
26
|
+
type AppFiles = {
|
|
24
27
|
/** Every `.abide`, in path order. */
|
|
25
28
|
abide: string[]
|
|
29
|
+
/** Every handler file, in path order. */
|
|
30
|
+
handlers: string[]
|
|
31
|
+
/** Every module under a `server` or a `lib` directory, in path order — run as it is imported. */
|
|
32
|
+
modules: string[]
|
|
26
33
|
/** Every file under a `public` directory, to the address it is answered at below the mount. */
|
|
27
34
|
published: Map<string, string>
|
|
28
35
|
}
|
|
@@ -35,7 +42,8 @@ const THROW: Refuse = (_path, message) => {
|
|
|
35
42
|
}
|
|
36
43
|
|
|
37
44
|
/**
|
|
38
|
-
* The app's `.abide` sources
|
|
45
|
+
* The app's `.abide` sources, handler files, server and lib modules and public files, refusing a
|
|
46
|
+
* tree 23.16, 23.17, 23.18 or 23.22 forbids.
|
|
39
47
|
*
|
|
40
48
|
* `refuse` throws by default, which is the build's answer; `abide check` collects instead (38.33),
|
|
41
49
|
* and the checker's route table ignores them, since a refused tree is the build's to report.
|
|
@@ -43,9 +51,19 @@ const THROW: Refuse = (_path, message) => {
|
|
|
43
51
|
* A missing root is an app with nothing in it, not a fault.
|
|
44
52
|
*/
|
|
45
53
|
export function appFiles(root: string, refuse: Refuse = THROW): AppFiles {
|
|
46
|
-
const files: AppFiles = {
|
|
47
|
-
|
|
54
|
+
const files: AppFiles = {
|
|
55
|
+
abide: [],
|
|
56
|
+
handlers: [],
|
|
57
|
+
modules: [],
|
|
58
|
+
published: new Map(),
|
|
59
|
+
}
|
|
60
|
+
// 23.22, 38.25 — a root that is its package's directory holds the package's own files beside its
|
|
61
|
+
// pages.
|
|
62
|
+
const packaged = existsSync(`${root}/package.json`)
|
|
63
|
+
walk(root, '', undefined, files, refuse, packaged)
|
|
48
64
|
files.abide.sort()
|
|
65
|
+
files.handlers.sort()
|
|
66
|
+
files.modules.sort()
|
|
49
67
|
return files
|
|
50
68
|
}
|
|
51
69
|
|
|
@@ -57,6 +75,7 @@ function walk(
|
|
|
57
75
|
reserved: { path: string; name: string } | undefined,
|
|
58
76
|
files: AppFiles,
|
|
59
77
|
refuse: Refuse,
|
|
78
|
+
packaged = false,
|
|
60
79
|
): void {
|
|
61
80
|
let entries: Dirent[]
|
|
62
81
|
try {
|
|
@@ -87,21 +106,49 @@ function walk(
|
|
|
87
106
|
)
|
|
88
107
|
continue
|
|
89
108
|
}
|
|
90
|
-
|
|
109
|
+
const abide = path.endsWith('.abide')
|
|
110
|
+
if (abide) files.abide.push(path)
|
|
91
111
|
if (reserved === undefined) {
|
|
112
|
+
if (!abide && !isModule(path)) {
|
|
113
|
+
if (packaged && relative === '' && !path.endsWith('.ts'))
|
|
114
|
+
continue
|
|
115
|
+
refuse(
|
|
116
|
+
path,
|
|
117
|
+
`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.`,
|
|
118
|
+
)
|
|
119
|
+
continue
|
|
120
|
+
}
|
|
121
|
+
if (!abide) files.handlers.push(path)
|
|
92
122
|
// 23.17 — anything that could answer here would answer in abide's own namespace.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
} else if (fileKind(entry.name) !== 'component')
|
|
123
|
+
refuseFrameworkRoute(routeOf(path), path, refuse)
|
|
124
|
+
} else if (DIRECTORY_FILES.has(entry.name))
|
|
96
125
|
refuse(
|
|
97
126
|
path,
|
|
98
|
-
`abide: ${path} is
|
|
127
|
+
`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
128
|
)
|
|
100
129
|
else if (reserved.name === PUBLIC_DIRECTORY)
|
|
101
130
|
files.published.set(path, publicAddress(path, reserved.path))
|
|
131
|
+
else if (
|
|
132
|
+
(reserved.name === SERVER_DIRECTORY ||
|
|
133
|
+
reserved.name === LIB_DIRECTORY) &&
|
|
134
|
+
isModule(path)
|
|
135
|
+
)
|
|
136
|
+
files.modules.push(path)
|
|
102
137
|
}
|
|
103
138
|
}
|
|
104
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Whether `path` names a module the app runs — neither a declaration nor a test: directly in an
|
|
142
|
+
* address directory, a handler file rather than a stray.
|
|
143
|
+
*/
|
|
144
|
+
function isModule(path: string): boolean {
|
|
145
|
+
return (
|
|
146
|
+
path.endsWith('.ts') &&
|
|
147
|
+
!path.endsWith('.d.ts') &&
|
|
148
|
+
!path.endsWith('.test.ts')
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
|
|
105
152
|
/** Refuses `source` where `route` lies in the framework's own namespace. 23.17. */
|
|
106
153
|
export function refuseFrameworkRoute(
|
|
107
154
|
route: string,
|
|
@@ -150,18 +197,21 @@ export function abideUnder(root: string): string[] {
|
|
|
150
197
|
}
|
|
151
198
|
|
|
152
199
|
/**
|
|
153
|
-
* Whether
|
|
200
|
+
* Whether an app lies under `root`: a `.abide` anywhere or a handler file, stopping at the first.
|
|
154
201
|
*
|
|
155
202
|
* 38.26's question, asked of a root before anything is built from it, so asking it of a large tree
|
|
156
203
|
* is as cheap as the tree allows.
|
|
157
204
|
*/
|
|
158
|
-
export function
|
|
159
|
-
return !abideFilesUnder(root, '').next().done
|
|
205
|
+
export function holdsApp(root: string): boolean {
|
|
206
|
+
return !abideFilesUnder(root, '', true).next().done
|
|
160
207
|
}
|
|
161
208
|
|
|
209
|
+
// Every `.abide` under `directory` — and, where `handlers`, every handler file outside a fence.
|
|
162
210
|
function* abideFilesUnder(
|
|
163
211
|
directory: string,
|
|
164
212
|
relative: string,
|
|
213
|
+
handlers = false,
|
|
214
|
+
fenced = false,
|
|
165
215
|
): Generator<string> {
|
|
166
216
|
let entries: Dirent[]
|
|
167
217
|
try {
|
|
@@ -173,7 +223,16 @@ function* abideFilesUnder(
|
|
|
173
223
|
if (skipped(entry.name)) continue
|
|
174
224
|
const path = `${relative}${entry.name}`
|
|
175
225
|
if (entry.isDirectory())
|
|
176
|
-
yield* abideFilesUnder(
|
|
177
|
-
|
|
226
|
+
yield* abideFilesUnder(
|
|
227
|
+
`${directory}/${entry.name}`,
|
|
228
|
+
`${path}/`,
|
|
229
|
+
handlers,
|
|
230
|
+
fenced || RESERVED_DIRECTORIES.has(entry.name),
|
|
231
|
+
)
|
|
232
|
+
else if (
|
|
233
|
+
path.endsWith('.abide') ||
|
|
234
|
+
(handlers && !fenced && isModule(path))
|
|
235
|
+
)
|
|
236
|
+
yield path
|
|
178
237
|
}
|
|
179
238
|
}
|
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>,
|