@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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abide/abide",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.104.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Isomorphic type-safe framework for reactive async interfaces for humans and machines",
|
|
6
6
|
"license": "MIT",
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"./routing": "./src/ui/routing.ts",
|
|
41
41
|
"./wire": "./src/ui/wire.ts",
|
|
42
42
|
"./server": "./src/server/index.ts",
|
|
43
|
+
"./test": "./src/server/preload.ts",
|
|
43
44
|
"./compiler": "./src/compiler/index.ts",
|
|
44
45
|
"./package.json": "./package.json"
|
|
45
46
|
},
|
package/scaffold/AGENTS.md
CHANGED
|
@@ -22,10 +22,17 @@ before writing code, rather than guessing from Svelte, Vue or React. It installs
|
|
|
22
22
|
|
|
23
23
|
## Layout
|
|
24
24
|
|
|
25
|
-
-
|
|
26
|
-
`layout.abide` wraps the pages below it
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
`
|
|
30
|
-
|
|
25
|
+
- A file's path below `src/` is its address. `src/invoices/index.abide` answers `/invoices`,
|
|
26
|
+
`src/invoices/[id].abide` answers `/invoices/123`, and `layout.abide` wraps the pages below it;
|
|
27
|
+
the outermost one writes `<html>`. Run `bun run routes` to see every address and the file behind it.
|
|
28
|
+
- A `.ts` beside the pages is a handler: `src/invoices/getInvoice.ts` holds
|
|
29
|
+
`export default GET(…)` and answers `/invoices/getInvoice`. It is the app's public API, for pages,
|
|
30
|
+
scripts, the CLI and MCP alike.
|
|
31
|
+
- A `.abide` cannot export. A `<script server>` in one holds that file's private handlers
|
|
32
|
+
(`const save = POST(…)`), callable by that file alone, and its hooks (`onStart`, `middleware`, …).
|
|
33
|
+
- A directory holds addresses and four fenced directories alone: `lib/` for components and modules,
|
|
34
|
+
`tests/` for tests, `server/` for modules the browser never loads (importing one from browser code
|
|
35
|
+
is a compile error), and `public/` for files served as they are (`src/blog/public/cover.png`
|
|
36
|
+
answers `/blog/cover.png`). Anything else beside the pages is a build error.
|
|
31
37
|
- Import the app's own modules as `#src/…` (see `imports` in `package.json`).
|
|
38
|
+
- A test imports `.abide` files directly: `bunfig.toml` preloads `@abide/abide/test`.
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
COMPONENT_MODULE,
|
|
22
22
|
COMPONENT_STAND_IN,
|
|
23
23
|
checkedShadow,
|
|
24
|
+
moduleConfigShadow,
|
|
24
25
|
} from './checkedShadow.ts'
|
|
25
26
|
import type { Finding } from './Finding.ts'
|
|
26
27
|
import { CHECKED_VALUES } from './Lowering.ts'
|
|
@@ -65,12 +66,17 @@ export class TypeSession {
|
|
|
65
66
|
*/
|
|
66
67
|
private shadows = new Map<string, { path: string; text: string }>()
|
|
67
68
|
/**
|
|
68
|
-
*
|
|
69
|
-
* `edited` hears anything move, so a
|
|
70
|
-
* reads nothing on disk.
|
|
69
|
+
* What `diagnose` last read off one walk of the root: the route table it declared (30.26) and the
|
|
70
|
+
* modules it read for their `onConfig` (27.13). Kept until `edited` hears anything move, so a
|
|
71
|
+
* check asked again and again while an editor types walks and reads nothing on disk.
|
|
71
72
|
*/
|
|
72
|
-
private
|
|
73
|
-
|
|
73
|
+
private walked:
|
|
74
|
+
| {
|
|
75
|
+
within: string
|
|
76
|
+
routes: string
|
|
77
|
+
configModules: Promise<[string, string][]>
|
|
78
|
+
}
|
|
79
|
+
| undefined = undefined
|
|
74
80
|
/** The checker's own key for a path: resolved, and case-folded where its host folds case. */
|
|
75
81
|
private keyOf: (path: string) => string
|
|
76
82
|
/**
|
|
@@ -101,7 +107,7 @@ export class TypeSession {
|
|
|
101
107
|
/**
|
|
102
108
|
* The project this session opens: the app's tsconfig, with every shadow in `outside` a root.
|
|
103
109
|
*
|
|
104
|
-
* 34.
|
|
110
|
+
* 34.25 — A DEPENDENCY'S `.abide` IS TYPED AS THE APP'S OWN, and its tsconfig's `include` never
|
|
105
111
|
* reaches `node_modules` or a linked package's directory. The session opens a config of its own —
|
|
106
112
|
* served from here, never written — that extends the app's, names those shadows as files, which
|
|
107
113
|
* TypeScript adds to what the base config includes (a linked package's shadow, asked for through
|
|
@@ -157,7 +163,7 @@ export class TypeSession {
|
|
|
157
163
|
/**
|
|
158
164
|
* The shadow registered at `name`, or at the same file through its directory's real path.
|
|
159
165
|
*
|
|
160
|
-
* 34.
|
|
166
|
+
* 34.25 — A LINKED PACKAGE IS ASKED FOR THROUGH ITS LINK: the checker resolves `kit/x.abide` to
|
|
161
167
|
* `node_modules/kit/x.d.abide.ts` and the re-export there to the shadow beside it, while the
|
|
162
168
|
* shadow is registered where bun resolved the file — the package's real directory.
|
|
163
169
|
*/
|
|
@@ -180,17 +186,17 @@ export class TypeSession {
|
|
|
180
186
|
}
|
|
181
187
|
|
|
182
188
|
private standIn(name: string): string | undefined {
|
|
183
|
-
// 34.
|
|
189
|
+
// 34.21 — a file imported as text is its text, which the projection names apart.
|
|
184
190
|
if (name.endsWith(`${SOURCE_SUFFIX}.d.ts`))
|
|
185
191
|
return isFile(name.slice(0, -`${SOURCE_SUFFIX}.d.ts`.length))
|
|
186
192
|
? 'declare const text: string\nexport default text\n'
|
|
187
193
|
: undefined
|
|
188
|
-
// 34.17
|
|
189
|
-
// resolves `./x.abide` by trying
|
|
190
|
-
//
|
|
191
|
-
//
|
|
194
|
+
// 34.17 — WHAT A `.abide` EXPORTS, to anything importing it: the component its checked
|
|
195
|
+
// module declares, and nothing else. The checker resolves `./x.abide` by trying
|
|
196
|
+
// `./x.abide.d.ts`; the bindings the projection re-exports for the build (16.70) are not
|
|
197
|
+
// the importer's to read.
|
|
192
198
|
//
|
|
193
|
-
// 34.
|
|
199
|
+
// 34.25 — AND THROUGH A PACKAGE'S `exports`, which the checker follows to the exact file and
|
|
194
200
|
// so asks for the arbitrary extension's declaration, `./x.d.abide.ts`, instead.
|
|
195
201
|
const abide = name.endsWith(DECLARATION_SUFFIX)
|
|
196
202
|
? name.slice(0, -'.d.ts'.length)
|
|
@@ -200,8 +206,7 @@ export class TypeSession {
|
|
|
200
206
|
if (abide !== undefined) {
|
|
201
207
|
if (!isFile(abide)) return undefined
|
|
202
208
|
const own = `./${basename(abide)}`
|
|
203
|
-
return `export
|
|
204
|
-
export { default } from '${own}${CHECKED_SUFFIX.slice(0, -'.ts'.length)}'
|
|
209
|
+
return `export { default } from '${own}${CHECKED_SUFFIX.slice(0, -'.ts'.length)}'
|
|
205
210
|
`
|
|
206
211
|
}
|
|
207
212
|
// A projection not registered — a file with nothing to read, or one this session has not
|
|
@@ -353,7 +358,7 @@ export { default } from '${own}${CHECKED_SUFFIX.slice(0, -'.ts'.length)}'
|
|
|
353
358
|
* checker is not told what was under it.
|
|
354
359
|
*/
|
|
355
360
|
edited(paths: string[] | undefined): void {
|
|
356
|
-
this.
|
|
361
|
+
this.walked = undefined
|
|
357
362
|
if (paths === undefined) {
|
|
358
363
|
this.everything = true
|
|
359
364
|
return
|
|
@@ -442,14 +447,49 @@ export { default } from '${own}${CHECKED_SUFFIX.slice(0, -'.ts'.length)}'
|
|
|
442
447
|
}
|
|
443
448
|
|
|
444
449
|
/**
|
|
445
|
-
*
|
|
450
|
+
* 34.25, 23.19 — makes `path`, a dependency's file, a root of the opened config, telling
|
|
451
|
+
* `changed` the config moved where it was not one already.
|
|
452
|
+
*/
|
|
453
|
+
private openOutside(path: string, changed: string[]): void {
|
|
454
|
+
if (this.outside.has(path)) return
|
|
455
|
+
this.outside.add(path)
|
|
456
|
+
if (!changed.includes(this.opened)) changed.push(this.opened)
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* The project, with each of `modules` outside the app's own a root of it.
|
|
446
461
|
*
|
|
447
462
|
* THE SNAPSHOT IS THE SESSION'S, AND A QUESTION IS ANY MODULE'S: what a handler declares
|
|
448
463
|
* (`declaredExports`) is asked of the same snapshot the reads were, taken only where something
|
|
449
464
|
* moved since.
|
|
465
|
+
*
|
|
466
|
+
* 23.19 — a handler file a mount places is a package's, which the app's `include` does not reach:
|
|
467
|
+
* what it exports is asked all the same (16.73), as a mounted `.abide`'s shadow is.
|
|
450
468
|
*/
|
|
451
|
-
|
|
452
|
-
|
|
469
|
+
async projectWith(
|
|
470
|
+
modules: readonly string[],
|
|
471
|
+
): Promise<Project | undefined> {
|
|
472
|
+
const changed: string[] = []
|
|
473
|
+
for (const path of modules) {
|
|
474
|
+
const canonical = this.canonical(path)
|
|
475
|
+
// The app's own is a root already: telling the config it moved reloads the project.
|
|
476
|
+
if (placeOf(this.belowRoot(canonical)) === 'dependency')
|
|
477
|
+
this.openOutside(canonical, changed)
|
|
478
|
+
}
|
|
479
|
+
const project = await this.currentProject([], changed)
|
|
480
|
+
if (project === undefined) return project
|
|
481
|
+
// A KEPT SESSION HEARS OF A FILE ONLY AS A WATCHER REPORTS IT, and one saved into a
|
|
482
|
+
// directory made since the last snapshot is reported as the directory: it is no root yet,
|
|
483
|
+
// and is told to the next snapshot as created.
|
|
484
|
+
const found = await Promise.all(
|
|
485
|
+
modules.map((path) => project.program.getSourceFile(path)),
|
|
486
|
+
)
|
|
487
|
+
const missing: string[] = []
|
|
488
|
+
for (let at = 0; at < modules.length; at += 1)
|
|
489
|
+
if (found[at] === undefined) missing.push(modules[at] as string)
|
|
490
|
+
return missing.length === 0
|
|
491
|
+
? project
|
|
492
|
+
: this.currentProject(missing, [...missing])
|
|
453
493
|
}
|
|
454
494
|
|
|
455
495
|
/**
|
|
@@ -482,7 +522,7 @@ export { default } from '${own}${CHECKED_SUFFIX.slice(0, -'.ts'.length)}'
|
|
|
482
522
|
/**
|
|
483
523
|
* Where the checker holds a `.abide`'s projection, once `readsFor` has registered it.
|
|
484
524
|
*
|
|
485
|
-
*
|
|
525
|
+
* 16.70 — what a `<script server>` binds is read off it as a `.ts` module's exports are.
|
|
486
526
|
*/
|
|
487
527
|
projectionOf(abidePath: string): string {
|
|
488
528
|
return `${this.canonical(abidePath)}${SHADOW_SUFFIX}`
|
|
@@ -581,8 +621,8 @@ export { default } from '${own}${CHECKED_SUFFIX.slice(0, -'.ts'.length)}'
|
|
|
581
621
|
const scripts = projection.text.slice(0, projection.scripts.end)
|
|
582
622
|
// A TEMPLATE POSITION IS A QUESTION TOO (14.2): a file that declares nothing but
|
|
583
623
|
// writes `{format(total)}` has a position only the checker can prove static. And a
|
|
584
|
-
// `<script server>` always is (
|
|
585
|
-
// `
|
|
624
|
+
// `<script server>` always is (16.70): what it binds is read off this shadow, re-exported
|
|
625
|
+
// under `PRIVATE_EXPORT`.
|
|
586
626
|
if (
|
|
587
627
|
!DECLARES.test(scripts) &&
|
|
588
628
|
projection.slots.size === 0 &&
|
|
@@ -606,13 +646,8 @@ export { default } from '${own}${CHECKED_SUFFIX.slice(0, -'.ts'.length)}'
|
|
|
606
646
|
// AN UNCHANGED SHADOW IS NOT REPORTED, which is what a kept session saves on a save:
|
|
607
647
|
// the one page that moved is re-read rather than all of them.
|
|
608
648
|
this.register(map.shadowPath, projection.text, created, changed)
|
|
609
|
-
if (
|
|
610
|
-
|
|
611
|
-
!this.outside.has(map.shadowPath)
|
|
612
|
-
) {
|
|
613
|
-
this.outside.add(map.shadowPath)
|
|
614
|
-
changed.push(this.opened)
|
|
615
|
-
}
|
|
649
|
+
if (placeOf(this.belowRoot(source.path)) === 'dependency')
|
|
650
|
+
this.openOutside(map.shadowPath, changed)
|
|
616
651
|
queries.push({
|
|
617
652
|
map,
|
|
618
653
|
spans: {
|
|
@@ -819,14 +854,30 @@ export { default } from '${own}${CHECKED_SUFFIX.slice(0, -'.ts'.length)}'
|
|
|
819
854
|
created,
|
|
820
855
|
changed,
|
|
821
856
|
)
|
|
857
|
+
if (this.walked?.within !== within) {
|
|
858
|
+
// A refused tree is `checkApp`'s finding (38.33), never the checker's crash.
|
|
859
|
+
const files = appFiles(within, () => {})
|
|
860
|
+
this.walked = {
|
|
861
|
+
within,
|
|
862
|
+
routes: routesDeclaration(within, files),
|
|
863
|
+
configModules: configModulesUnder(within, files),
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
// 27.13 — what a handler file defaults, beside it.
|
|
867
|
+
for (const [path, text] of await this.walked.configModules) {
|
|
868
|
+
const shadow = moduleConfigShadow(text)
|
|
869
|
+
const at = `${this.canonical(path)}${CONFIG_SUFFIX}`
|
|
870
|
+
// One that stops filing keeps its place and declares nothing, so the next snapshot is
|
|
871
|
+
// told it moved.
|
|
872
|
+
if (shadow !== undefined || this.shadows.has(this.keyOf(at)))
|
|
873
|
+
this.register(at, shadow ?? 'export {}\n', created, changed)
|
|
874
|
+
}
|
|
822
875
|
// 30.26 — THE APP'S ROUTE TABLE, which `url`'s literal is held to: every page under the
|
|
823
876
|
// root, declared into abide's own `Routes`. A module rather than a script, since only a
|
|
824
877
|
// module augments one.
|
|
825
|
-
if (this.routes?.within !== within)
|
|
826
|
-
this.routes = { within, declaration: routesDeclaration(within) }
|
|
827
878
|
this.register(
|
|
828
879
|
`${this.canonical(within)}/${ROUTES_DECLARATION}`,
|
|
829
|
-
this.routes
|
|
880
|
+
this.walked.routes,
|
|
830
881
|
created,
|
|
831
882
|
changed,
|
|
832
883
|
)
|
|
@@ -1106,10 +1157,11 @@ const URL_SOURCE = Bun.fileURLToPath(new URL('../shared/url', import.meta.url))
|
|
|
1106
1157
|
* 30.26 — every page under `root`, and every public file's address, as a key of abide's `Routes`, or
|
|
1107
1158
|
* nothing where it has neither.
|
|
1108
1159
|
*/
|
|
1109
|
-
function routesDeclaration(
|
|
1160
|
+
function routesDeclaration(
|
|
1161
|
+
root: string,
|
|
1162
|
+
{ abide, published }: ReturnType<typeof appFiles>,
|
|
1163
|
+
): string {
|
|
1110
1164
|
const routes = new Set<string>()
|
|
1111
|
-
// A refused tree is `checkApp`'s finding (38.33), never the checker's crash.
|
|
1112
|
-
const { abide, published } = appFiles(root, () => {})
|
|
1113
1165
|
for (const path of abide)
|
|
1114
1166
|
if (fileKind(path) === 'page') routes.add(routeOf(path))
|
|
1115
1167
|
// 23.19 — and every page a mount places in the app, at its position.
|
|
@@ -1272,3 +1324,24 @@ function flatten(diagnostic: {
|
|
|
1272
1324
|
message += `\n ${flatten(link as Parameters<typeof flatten>[0]).replaceAll('\n', '\n ')}`
|
|
1273
1325
|
return message
|
|
1274
1326
|
}
|
|
1327
|
+
|
|
1328
|
+
/** What a module that is no `.abide` is checked beside as, for the `Config` it files (27.13). */
|
|
1329
|
+
const CONFIG_SUFFIX = '.config-shadow.ts'
|
|
1330
|
+
|
|
1331
|
+
/**
|
|
1332
|
+
* Every handler file below `within` that names `onConfig`, with its text — the modules a server
|
|
1333
|
+
* runs that 27.13 types `Config` off, since 37.13 refuses a hook anywhere else that is no `.abide`.
|
|
1334
|
+
*/
|
|
1335
|
+
async function configModulesUnder(
|
|
1336
|
+
within: string,
|
|
1337
|
+
{ handlers }: ReturnType<typeof appFiles>,
|
|
1338
|
+
): Promise<[string, string][]> {
|
|
1339
|
+
const paths: string[] = []
|
|
1340
|
+
for (const name of handlers) paths.push(`${within}/${name}`)
|
|
1341
|
+
const texts = await Promise.all(paths.map((path) => Bun.file(path).text()))
|
|
1342
|
+
const found: [string, string][] = []
|
|
1343
|
+
for (let at = 0; at < paths.length; at += 1)
|
|
1344
|
+
if ((texts[at] as string).includes('onConfig'))
|
|
1345
|
+
found.push([paths[at] as string, texts[at] as string])
|
|
1346
|
+
return found
|
|
1347
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// 38.108 — THE APP'S MAP: every address it answers, the file answering each, and where every hook is
|
|
2
|
+
// filed. What `abide routes` prints, off the build's own reading of the tree — the routes and the
|
|
3
|
+
// handler mounts `compileApp` answers with — so the map and what the server answers are one walk.
|
|
4
|
+
//
|
|
5
|
+
// THE HOOKS ARE READ, NOT RUN, as the handlers are: a call of one of abide's hook names in a server
|
|
6
|
+
// block, a `<script>` or a handler file, at the route 37.1 files it at.
|
|
7
|
+
|
|
8
|
+
import { readFileSync } from 'node:fs' // bun exposes no sync read of its own
|
|
9
|
+
import { appFiles } from '#server/appTree.ts'
|
|
10
|
+
import { scopeOf } from '#shared/routePattern.ts'
|
|
11
|
+
import { compileApp } from './compileApp.ts'
|
|
12
|
+
import { hooksCalled } from './hooksCalled.ts'
|
|
13
|
+
import { parseAbide } from './parseAbide.ts'
|
|
14
|
+
|
|
15
|
+
/** One address the app answers, and the file answering it. */
|
|
16
|
+
type MappedRoute = {
|
|
17
|
+
method: string
|
|
18
|
+
address: string
|
|
19
|
+
file: string
|
|
20
|
+
kind: 'page' | 'handler' | 'private'
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** One hook call, the route it is filed at, and the file making it. */
|
|
24
|
+
type MappedHook = { hook: string; route: string; file: string }
|
|
25
|
+
|
|
26
|
+
/** Every address the app at `root` answers, and every hook it files. */
|
|
27
|
+
export async function appMap(
|
|
28
|
+
root: string,
|
|
29
|
+
): Promise<{ routes: MappedRoute[]; hooks: MappedHook[] }> {
|
|
30
|
+
const app = await compileApp(root)
|
|
31
|
+
const routes: MappedRoute[] = []
|
|
32
|
+
for (const route of app.routes)
|
|
33
|
+
routes.push({
|
|
34
|
+
method: 'GET',
|
|
35
|
+
address: route.pattern,
|
|
36
|
+
file: route.source,
|
|
37
|
+
kind: 'page',
|
|
38
|
+
})
|
|
39
|
+
for (const mount of app.mounts)
|
|
40
|
+
routes.push({
|
|
41
|
+
method: mount.method,
|
|
42
|
+
address: mount.address,
|
|
43
|
+
file: mount.source,
|
|
44
|
+
kind: mount.private ? 'private' : 'handler',
|
|
45
|
+
})
|
|
46
|
+
routes.sort((one, two) =>
|
|
47
|
+
one.address === two.address
|
|
48
|
+
? one.method < two.method
|
|
49
|
+
? -1
|
|
50
|
+
: 1
|
|
51
|
+
: one.address < two.address
|
|
52
|
+
? -1
|
|
53
|
+
: 1,
|
|
54
|
+
)
|
|
55
|
+
const hooks: MappedHook[] = []
|
|
56
|
+
const { abide, handlers } = appFiles(root, () => {})
|
|
57
|
+
for (const file of abide) {
|
|
58
|
+
const parsed = parseAbide(readFileSync(`${root}/${file}`, 'utf8'))
|
|
59
|
+
// Block by block: one block's last token would decide how the next one's first reads.
|
|
60
|
+
for (const script of [...parsed.servers, ...parsed.scripts])
|
|
61
|
+
for (const hook of hooksCalled(script))
|
|
62
|
+
hooks.push({ hook, route: scopeOf(file), file })
|
|
63
|
+
}
|
|
64
|
+
for (const file of handlers)
|
|
65
|
+
for (const hook of hooksCalled(readFileSync(`${root}/${file}`, 'utf8')))
|
|
66
|
+
hooks.push({ hook, route: scopeOf(file), file })
|
|
67
|
+
return { routes, hooks }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The map as `abide routes` prints it: one line an address, then one a hook. */
|
|
71
|
+
export function mapText(map: Awaited<ReturnType<typeof appMap>>): string {
|
|
72
|
+
const lines: string[] = []
|
|
73
|
+
const width = (values: string[]): number =>
|
|
74
|
+
values.reduce((most, value) => Math.max(most, value.length), 0)
|
|
75
|
+
const methods = width(map.routes.map((route) => route.method))
|
|
76
|
+
const addresses = width(map.routes.map((route) => route.address))
|
|
77
|
+
for (const route of map.routes)
|
|
78
|
+
lines.push(
|
|
79
|
+
`${route.method.padEnd(methods)} ${route.address.padEnd(addresses)} ${route.file}${route.kind === 'private' ? ' (private)' : ''}`,
|
|
80
|
+
)
|
|
81
|
+
if (map.hooks.length > 0) {
|
|
82
|
+
lines.push('', 'hooks')
|
|
83
|
+
const hooks = width(map.hooks.map((hook) => hook.hook))
|
|
84
|
+
const routes = width(map.hooks.map((hook) => hook.route))
|
|
85
|
+
for (const hook of map.hooks)
|
|
86
|
+
lines.push(
|
|
87
|
+
` ${hook.hook.padEnd(hooks)} ${hook.route.padEnd(routes)} ${hook.file}`,
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
return lines.join('\n')
|
|
91
|
+
}
|
|
@@ -134,7 +134,7 @@ async function objectOf(
|
|
|
134
134
|
}
|
|
135
135
|
const schema: Schema = { type: 'object', properties }
|
|
136
136
|
if (required.length > 0) schema.required = required
|
|
137
|
-
// 19.
|
|
137
|
+
// 19.18 — A MEMBER THE TYPE DOES NOT DECLARE IS REFUSED, where no index signature declares the
|
|
138
138
|
// rest: a caller outside the process sends whatever it likes, and a body writing its args to a
|
|
139
139
|
// row would write that too. An author who wants the rest declares a schema of their own.
|
|
140
140
|
schema.additionalProperties = false
|
package/src/compiler/checkApp.ts
CHANGED
|
@@ -4,12 +4,14 @@ import { placeOf } from '#shared/routePattern.ts'
|
|
|
4
4
|
import {
|
|
5
5
|
type AppChecker,
|
|
6
6
|
emitApp,
|
|
7
|
+
refuseModuleHooks,
|
|
7
8
|
resolverIn,
|
|
8
9
|
sourcesUnder,
|
|
9
10
|
withChecker,
|
|
10
11
|
withDependencies,
|
|
11
12
|
} from './compileApp.ts'
|
|
12
13
|
import { abideImportsOf } from './componentGraph.ts'
|
|
14
|
+
import { projectShadow } from './projectShadow.ts'
|
|
13
15
|
import {
|
|
14
16
|
parseSources,
|
|
15
17
|
type ReadSource,
|
|
@@ -17,7 +19,6 @@ import {
|
|
|
17
19
|
refusal,
|
|
18
20
|
} from './readSources.ts'
|
|
19
21
|
import type { Diagnosis } from './TypeSession.ts'
|
|
20
|
-
import { writeTypes } from './writeTypes.ts'
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* Every finding `abide check` reports for the app at `appRoot`.
|
|
@@ -39,18 +40,30 @@ export async function checkApp(
|
|
|
39
40
|
const { root, sources } = await sourcesUnder(appRoot)
|
|
40
41
|
return withChecker(root, kept, async (session) => {
|
|
41
42
|
const refused: Diagnosis[] = []
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
//
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
43
|
+
const own = parseSources(sources, refused)
|
|
44
|
+
// 23.16-23.18 and 37.13 — what the build's walk would refuse of the tree, at each file it names.
|
|
45
|
+
// 38.33 — each at its file, and every one rather than the first.
|
|
46
|
+
const atFile = (relative: string, thrown: unknown): void => {
|
|
47
|
+
refused.push(refusal(`${root}/${relative}`, thrown))
|
|
48
|
+
}
|
|
49
|
+
const { handlers, modules } = appFiles(root, atFile)
|
|
50
|
+
refuseModuleHooks(root, modules, atFile)
|
|
50
51
|
const resolve = resolverIn()
|
|
51
|
-
const
|
|
52
|
+
const { sources: everyParsed, handlers: handlerFiles } =
|
|
53
|
+
await withDependencies(root, own, handlers, resolve, refused)
|
|
54
|
+
// 38.33 — A FILE WHOSE PROJECTION IS REFUSED (34.3, 34.17) is a finding at its file, found
|
|
55
|
+
// here because every later step projects it and would throw the refusal for the whole app.
|
|
56
|
+
// AFTER THE IMPORTS ARE FOLLOWED, so a refused file another one imports is found too.
|
|
57
|
+
const parsed: typeof everyParsed = []
|
|
58
|
+
for (const source of everyParsed)
|
|
59
|
+
try {
|
|
60
|
+
projectShadow(source.file)
|
|
61
|
+
parsed.push(source)
|
|
62
|
+
} catch (thrown) {
|
|
63
|
+
refused.push(refusal(source.path, thrown))
|
|
64
|
+
}
|
|
52
65
|
const { read, warnings } = await readSources(session, parsed, refused)
|
|
53
|
-
// THE APP'S OWN FILES ARE REPORTED ON: a dependency's is compiled with them (34.
|
|
66
|
+
// THE APP'S OWN FILES ARE REPORTED ON: a dependency's is compiled with them (34.23), and
|
|
54
67
|
// what is wrong inside a package is the package's to report.
|
|
55
68
|
const reported: ReadSource[] = []
|
|
56
69
|
for (const source of read)
|
|
@@ -64,15 +77,19 @@ export async function checkApp(
|
|
|
64
77
|
const emittable: ReadSource[] = []
|
|
65
78
|
for (const source of read)
|
|
66
79
|
if (!loweringRefused.has(source.path)) emittable.push(source)
|
|
67
|
-
// 23.16-23.18 — what the build's walk would refuse of the tree, at each file it names.
|
|
68
|
-
appFiles(root, (path, message) => {
|
|
69
|
-
refused.push(refusal(`${root}/${path}`, message))
|
|
70
|
-
})
|
|
71
80
|
if (refused.length === 0 && ambient.length === 0)
|
|
72
81
|
try {
|
|
73
82
|
warnings.push(
|
|
74
|
-
...(
|
|
75
|
-
|
|
83
|
+
...(
|
|
84
|
+
await emitApp(
|
|
85
|
+
root,
|
|
86
|
+
session,
|
|
87
|
+
emittable,
|
|
88
|
+
resolve,
|
|
89
|
+
handlerFiles,
|
|
90
|
+
atFile,
|
|
91
|
+
)
|
|
92
|
+
).warnings,
|
|
76
93
|
)
|
|
77
94
|
} catch (thrown) {
|
|
78
95
|
refused.push(refusal(root, thrown))
|
|
@@ -127,7 +144,7 @@ export async function checkSource(
|
|
|
127
144
|
for (const other of sources) {
|
|
128
145
|
if (other.path === path || lowered.has(other.path)) continue
|
|
129
146
|
const text = held(other.path) ?? other.text
|
|
130
|
-
if (FILES_CONFIG.test(text))
|
|
147
|
+
if (text.includes('onConfig') && FILES_CONFIG.test(text))
|
|
131
148
|
imports.push({ path: other.path, text, quiet: true })
|
|
132
149
|
}
|
|
133
150
|
// A component that does not parse, or that the checker refuses, is its own file's finding and
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SERVER_SPECIFIER } from '#shared/SPECIFIERS.ts'
|
|
1
2
|
import type { CheckerAnswer } from './CheckerAnswer.ts'
|
|
2
3
|
import type { Finding } from './Finding.ts'
|
|
3
4
|
import {
|
|
@@ -11,7 +12,6 @@ import { holdsReactive } from './PositionTable.ts'
|
|
|
11
12
|
import type { AbideFile } from './parseAbide.ts'
|
|
12
13
|
import { NO_PROPS, type Origin, type Span } from './projectShadow.ts'
|
|
13
14
|
import { CHECKED_SUFFIX } from './ShadowMap.ts'
|
|
14
|
-
import { SERVER_SPECIFIER } from './SPECIFIERS.ts'
|
|
15
15
|
import { abideImports } from './splitImports.ts'
|
|
16
16
|
|
|
17
17
|
// 38.3 — A `.abide` FILE AS THE PROGRAM IT RUNS AS, for the checker to report on. The projection
|
|
@@ -48,10 +48,11 @@ export const COMPONENT_STAND_IN = '__component'
|
|
|
48
48
|
*/
|
|
49
49
|
function componentModule(props: string, generics?: string): string {
|
|
50
50
|
// 34.13 — generic over what the file's `<script>` declares, so a caller's props infer it, the
|
|
51
|
-
// signature's own parameters shadowing the opaque stand-ins the body was checked against.
|
|
51
|
+
// signature's own parameters shadowing the opaque stand-ins the body was checked against. 34.16 —
|
|
52
|
+
// a component no prop is required of is called with none, as the runtime's bound face takes it.
|
|
52
53
|
const parameters = generics === undefined ? '' : `<${generics}>`
|
|
53
54
|
return `
|
|
54
|
-
declare const ${COMPONENT_STAND_IN}: ${parameters}(record: ${props}) => import('${SERVER_SPECIFIER}').Component
|
|
55
|
+
declare const ${COMPONENT_STAND_IN}: ${parameters}(...record: {} extends ${props} ? [record?: ${props}] : [record: ${props}]) => import('${SERVER_SPECIFIER}').Component
|
|
55
56
|
export default ${COMPONENT_STAND_IN}
|
|
56
57
|
`
|
|
57
58
|
}
|
|
@@ -141,15 +142,22 @@ function propsOf(lowering: Lowering): string {
|
|
|
141
142
|
* Nothing where the file files none.
|
|
142
143
|
*/
|
|
143
144
|
function configDeclarations(lowering: Lowering): string {
|
|
145
|
+
const bound: string[] = []
|
|
146
|
+
for (const read of lowering.answer.reads)
|
|
147
|
+
if (read.shape === 'config')
|
|
148
|
+
bound.push(`${CHECKED_CONFIG}${read.start}`)
|
|
149
|
+
return configInterface(bound)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** `Config` extended by what each of `bound` holds — the binding of one `onConfig` call each. */
|
|
153
|
+
function configInterface(bound: readonly string[]): string {
|
|
154
|
+
if (bound.length === 0) return ''
|
|
144
155
|
let aliases = ''
|
|
145
156
|
let names = ''
|
|
146
|
-
for (const
|
|
147
|
-
if (read.shape !== 'config') continue
|
|
148
|
-
const name = `${CHECKED_CONFIG}${read.start}`
|
|
157
|
+
for (const name of bound) {
|
|
149
158
|
aliases += `type ${name}Type = typeof ${name}\n`
|
|
150
159
|
names += names === '' ? `${name}Type` : `, ${name}Type`
|
|
151
160
|
}
|
|
152
|
-
if (names === '') return ''
|
|
153
161
|
return `declare function ${CHECKED_CONFIG_OF}<Options>(on: (defaults: never, options?: Options) => () => void): <Defaults>(defaults: ((env: import('${SERVER_SPECIFIER}').Env) => Defaults) | null, options?: Options) => [Defaults] extends [object] ? Pick<Defaults, keyof Defaults> : {}
|
|
154
162
|
${aliases}declare module '${SERVER_SPECIFIER}' {
|
|
155
163
|
interface Config extends ${names} {}
|
|
@@ -157,6 +165,31 @@ ${aliases}declare module '${SERVER_SPECIFIER}' {
|
|
|
157
165
|
`
|
|
158
166
|
}
|
|
159
167
|
|
|
168
|
+
// A call standing as a statement at a module's top level, as a formatted module writes one.
|
|
169
|
+
const TOP_LEVEL_CONFIG = /^onConfig\s*\(/gm
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 27.13 FOR A HANDLER FILE, the one module that is no `.abide` and may call a hook (37.13): its
|
|
173
|
+
* text with each `onConfig(…)` at its top level bound as the lowering binds a server block's, and
|
|
174
|
+
* `Config` extended by each, or `undefined` where it files none.
|
|
175
|
+
*
|
|
176
|
+
* A MODULE OF ITS OWN BESIDE THE FILE, which the checker reads and nothing reports on: the file's own
|
|
177
|
+
* text is the one its diagnostics are placed in, and binding a call shifts every column after it.
|
|
178
|
+
*/
|
|
179
|
+
export function moduleConfigShadow(text: string): string | undefined {
|
|
180
|
+
const bound: string[] = []
|
|
181
|
+
let out = ''
|
|
182
|
+
let from = 0
|
|
183
|
+
for (const found of text.matchAll(TOP_LEVEL_CONFIG)) {
|
|
184
|
+
const name = `${CHECKED_CONFIG}${bound.length}`
|
|
185
|
+
bound.push(name)
|
|
186
|
+
out += `${text.slice(from, found.index)}const ${name} = ${CHECKED_CONFIG_OF}(onConfig)(`
|
|
187
|
+
from = found.index + found[0].length
|
|
188
|
+
}
|
|
189
|
+
if (bound.length === 0) return undefined
|
|
190
|
+
return `${out}${text.slice(from)}\n${configInterface(bound)}`
|
|
191
|
+
}
|
|
192
|
+
|
|
160
193
|
/**
|
|
161
194
|
* Points each `.abide` import in the script blocks at its checked module, and moves `origins` to
|
|
162
195
|
* match.
|