@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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// A ROUTE LITERAL, PARSED. One grammar for two things that would otherwise drift: the file a
|
|
2
|
-
// page is filed as (`invoices/[id]
|
|
2
|
+
// page is filed as (`invoices/[id].abide`) and the literal an author writes
|
|
3
3
|
// (`'/invoices/[id]'`). The bracket spelling is the file system's, so the address is read off the
|
|
4
4
|
// tree rather than declared beside it and the two cannot disagree.
|
|
5
5
|
//
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
// A LEAF: nothing imported but another leaf, because the compiler, the client router and a server
|
|
11
11
|
// router all read it and a constant crossing a seam lives where nothing pulls a graph in behind it.
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import { LAUNCHER_PAGE } from './LAUNCHER_FILES.ts'
|
|
14
|
+
import { RESERVED_DIRECTORIES } from './RESERVED_DIRECTORIES.ts'
|
|
14
15
|
|
|
15
16
|
type SegmentKind = 'static' | 'required' | 'optional' | 'rest'
|
|
16
17
|
|
|
@@ -48,9 +49,9 @@ export type FileKind = 'page' | 'layout' | 'error' | 'component'
|
|
|
48
49
|
/**
|
|
49
50
|
* Whether the `.abide` at `path` is a page, a layout, an error page or a component.
|
|
50
51
|
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* component.
|
|
52
|
+
* 23.21 — WHERE IT SITS, THEN ITS NAME. Every `.abide` directly in an address directory is a page
|
|
53
|
+
* but a `layout.abide`, an `error.abide` and the root's `launcher.abide`; one under a fence is a
|
|
54
|
+
* component whatever it is called.
|
|
54
55
|
* One test for every reader: the build's routes, the checker's `Routes` and the `route.params` a
|
|
55
56
|
* page is typed by.
|
|
56
57
|
*
|
|
@@ -58,26 +59,33 @@ export type FileKind = 'page' | 'layout' | 'error' | 'component'
|
|
|
58
59
|
* read the machine's directories as the app's.
|
|
59
60
|
*/
|
|
60
61
|
export function fileKind(path: string): FileKind {
|
|
61
|
-
if (placeOf(path) !== 'address'
|
|
62
|
-
|
|
62
|
+
if (placeOf(path) !== 'address' || path === LAUNCHER_PAGE)
|
|
63
|
+
return 'component'
|
|
63
64
|
const name = path.slice(path.lastIndexOf('/') + 1)
|
|
64
|
-
if (name === 'page.abide') return 'page'
|
|
65
65
|
if (name === 'layout.abide') return 'layout'
|
|
66
|
-
|
|
66
|
+
if (name === 'error.abide') return 'error'
|
|
67
|
+
return 'page'
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
/** Where a file sits in an app, which is what it is to every reader (`placeOf`). */
|
|
70
|
-
export type Place =
|
|
71
|
+
export type Place =
|
|
72
|
+
| 'address'
|
|
73
|
+
| 'server'
|
|
74
|
+
| 'public'
|
|
75
|
+
| 'lib'
|
|
76
|
+
| 'tests'
|
|
77
|
+
| 'dependency'
|
|
78
|
+
| 'skipped'
|
|
71
79
|
|
|
72
80
|
/**
|
|
73
81
|
* Where the file at `path`, relative to the app's root, sits — THE ONE ANSWER every walk, fence and
|
|
74
82
|
* emit reads, so no two of them disagree about a path.
|
|
75
83
|
*
|
|
76
|
-
* - `dependency` — 34.
|
|
84
|
+
* - `dependency` — 34.24: a package's, outside the root or under `node_modules`, whose directories
|
|
77
85
|
* are its package's layout and none of the app's;
|
|
78
86
|
* - `skipped` — under a dot directory, which no walk of the app enters;
|
|
79
|
-
* - `server` or `
|
|
80
|
-
* (23.16 refuses one inside
|
|
87
|
+
* - `server`, `public`, `lib` or `tests` — 23.15: under that fence, the nearest to the root deciding
|
|
88
|
+
* (23.16 refuses one inside another);
|
|
81
89
|
* - `address` — 23.14: everywhere else, its directories the segments of its route.
|
|
82
90
|
*/
|
|
83
91
|
export function placeOf(path: string): Place {
|
|
@@ -88,8 +96,8 @@ export function placeOf(path: string): Place {
|
|
|
88
96
|
const part = parts[index] as string
|
|
89
97
|
if (part === 'node_modules') return 'dependency'
|
|
90
98
|
if (part.startsWith('.')) return 'skipped'
|
|
91
|
-
|
|
92
|
-
if (part
|
|
99
|
+
// Each fence's place is its name.
|
|
100
|
+
if (RESERVED_DIRECTORIES.has(part)) return part as Place
|
|
93
101
|
}
|
|
94
102
|
return 'address'
|
|
95
103
|
}
|
|
@@ -100,18 +108,48 @@ export function skipped(name: string): boolean {
|
|
|
100
108
|
}
|
|
101
109
|
|
|
102
110
|
/**
|
|
103
|
-
* The route a file answers at — its
|
|
111
|
+
* The route a file answers at — its path below the root without its extension.
|
|
104
112
|
*
|
|
105
|
-
* 23.14 — `media/[id]
|
|
106
|
-
*
|
|
107
|
-
*
|
|
113
|
+
* 23.14 — `media/[id].abide` and `media/getGrid.ts` are `/media/[id]` and `/media/getGrid`; an
|
|
114
|
+
* `index.abide`, a `layout.abide` and an `error.abide` are their directory's. `path` is relative to
|
|
115
|
+
* the app's root, as `fileKind`'s is.
|
|
108
116
|
*/
|
|
109
117
|
export function routeOf(path: string): string {
|
|
110
118
|
const parts = path.split('/')
|
|
111
|
-
parts.pop()
|
|
119
|
+
const file = parts.pop() as string
|
|
120
|
+
// The extension alone: `[...rest].abide` carries dots of its own.
|
|
121
|
+
if (!DIRECTORY_FILES.has(file)) parts.push(file.replace(EXTENSION, ''))
|
|
112
122
|
return `/${parts.filter((part) => part !== '').join('/')}`
|
|
113
123
|
}
|
|
114
124
|
|
|
125
|
+
const EXTENSION = /\.(?:abide|ts)$/
|
|
126
|
+
|
|
127
|
+
/** 23.14, 23.18 — the names whose route is their directory's, and so a route's wherever they stand. */
|
|
128
|
+
export const DIRECTORY_FILES = new Set([
|
|
129
|
+
'index.abide',
|
|
130
|
+
'layout.abide',
|
|
131
|
+
'error.abide',
|
|
132
|
+
])
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The route the hooks and private handlers of the file at `path` are filed at.
|
|
136
|
+
*
|
|
137
|
+
* 37.1 — an address's own route; under a `lib` or a `tests` directory, the route of the directory
|
|
138
|
+
* holding it, so a component an admin page renders runs under the admin route's rungs; under a
|
|
139
|
+
* `server` or a `public` directory, and for a dependency's file, the root's.
|
|
140
|
+
*/
|
|
141
|
+
export function scopeOf(path: string): string {
|
|
142
|
+
// 38.82 — the launcher answers at no route, and files what it calls at the root's.
|
|
143
|
+
if (path === LAUNCHER_PAGE) return '/'
|
|
144
|
+
const place = placeOf(path)
|
|
145
|
+
if (place === 'address') return routeOf(path)
|
|
146
|
+
if (place !== 'lib' && place !== 'tests') return '/'
|
|
147
|
+
const parts = path.split('/')
|
|
148
|
+
// The fence `placeOf` named is the first one, and its name is its place.
|
|
149
|
+
const fence = parts.indexOf(place)
|
|
150
|
+
return `/${parts.slice(0, fence).join('/')}`
|
|
151
|
+
}
|
|
152
|
+
|
|
115
153
|
/**
|
|
116
154
|
* A `sort` comparator over two parsed patterns: negative where `a` is the more specific.
|
|
117
155
|
*
|
package/src/shared/rpc.ts
CHANGED
|
@@ -501,7 +501,7 @@ export const DELETE: Declare = declare('DELETE')
|
|
|
501
501
|
function addressOf(rpc: { address: string | undefined }): string {
|
|
502
502
|
if (rpc.address === undefined)
|
|
503
503
|
throw new Error(
|
|
504
|
-
|
|
504
|
+
"abide: this handler has no address yet. A handler is given one where it is mounted: a handler file's default (16.2), or a `const` in a <script server> (16.70).",
|
|
505
505
|
)
|
|
506
506
|
return rpc.address
|
|
507
507
|
}
|
package/src/ui/Router.ts
CHANGED
|
@@ -12,7 +12,6 @@ import { applyHead, withdrawHead } from './applyHead.ts'
|
|
|
12
12
|
import { LAYER_MOUNT, LAYER_NAVIGATE } from './LAYER_NAMES.ts'
|
|
13
13
|
import { MOUNTING_LEVEL } from './MOUNTING_LEVEL.ts'
|
|
14
14
|
import { beginLayer, endLayer } from './measureLayer.ts'
|
|
15
|
-
import { menuBridge } from './menuBridge.ts'
|
|
16
15
|
import { fillRegion, pageRegion, type Region } from './regions.ts'
|
|
17
16
|
import type { Scope, Stoppable } from './Scope.ts'
|
|
18
17
|
|
|
@@ -568,11 +567,6 @@ export class Router {
|
|
|
568
567
|
): void {
|
|
569
568
|
arrive(target.href, found.route.pattern, found.params)
|
|
570
569
|
this.chain = modules
|
|
571
|
-
// 38.88 — inside a desktop app, the window shows the menus this chain exports.
|
|
572
|
-
if (menuBridge() !== undefined)
|
|
573
|
-
void import('./reportMenu.ts').then((loaded) =>
|
|
574
|
-
loaded.reportMenu(modules as { menu?: unknown }[]),
|
|
575
|
-
)
|
|
576
570
|
this.standing = found.route
|
|
577
571
|
this.mounted = target.pathname
|
|
578
572
|
if (into === undefined) return
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
// 34.24 — THE TYPES A PLAIN `.ts` READS A `.abide` BY, written with no checker and no build: the
|
|
2
|
-
// files parsed and projected, and nothing else. What `abide sync` runs — and the scaffold's `prepare`
|
|
3
|
-
// with it, so a project is typed as soon as it is installed — and what the language server runs as
|
|
4
|
-
// a file is edited.
|
|
5
|
-
|
|
6
|
-
import { sourcesUnder } from './compileApp.ts'
|
|
7
|
-
import { parseSources } from './readSources.ts'
|
|
8
|
-
import type { Diagnosis } from './TypeSession.ts'
|
|
9
|
-
import { writeTypes } from './writeTypes.ts'
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Writes the types of every `.abide` under `appRoot`, `held` standing in for the text on disk of
|
|
13
|
-
* the files an editor has open; answers how many files it typed and what it refused. A file that
|
|
14
|
-
* does not parse, or whose projection is refused, is left as it last was and the rest are typed.
|
|
15
|
-
*/
|
|
16
|
-
export async function syncTypes(
|
|
17
|
-
appRoot: string,
|
|
18
|
-
held: ReadonlyMap<string, string> = new Map(),
|
|
19
|
-
): Promise<{ typed: number; refused: Diagnosis[] }> {
|
|
20
|
-
const { root, sources } = await sourcesUnder(appRoot)
|
|
21
|
-
const read: { path: string; text: string }[] = []
|
|
22
|
-
for (const source of sources)
|
|
23
|
-
read.push({
|
|
24
|
-
path: source.path,
|
|
25
|
-
text: held.get(source.path) ?? source.text,
|
|
26
|
-
})
|
|
27
|
-
const refused: Diagnosis[] = []
|
|
28
|
-
const parsed = parseSources(read, refused)
|
|
29
|
-
const unparsed = refused.length
|
|
30
|
-
await writeTypes(root, parsed, true, refused)
|
|
31
|
-
return { typed: parsed.length - (refused.length - unparsed), refused }
|
|
32
|
-
}
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
// 34.24 — WHAT A `.abide` EXPORTS, AS A PLAIN `.ts` IMPORTING IT IS TYPED. TypeScript reads no
|
|
2
|
-
// `.abide`, and a wildcard declaration can name a default alone, so the names a `<script module>` or
|
|
3
|
-
// a `<script server>` exports were untyped to every `.ts` outside abide's own checker. Each file's
|
|
4
|
-
// projection — its module and server blocks, exactly what that checker answers a `.abide` import
|
|
5
|
-
// with (`TypeSession.standIn`) — is written to `.abide/types`, mirroring the tree, where the app's
|
|
6
|
-
// `tsconfig` reaches it: `rootDirs` for a relative import, `paths` for `#src/*`.
|
|
7
|
-
//
|
|
8
|
-
// THE CHECKER IS NOT ASKED, because the API it answers over emits no declarations, and the
|
|
9
|
-
// projection is a module TypeScript infers the same types from. `@ts-nocheck` because the blocks are
|
|
10
|
-
// read as written — a reactive read by name is the checker's to answer (31.3) and `abide check`'s to
|
|
11
|
-
// report, and reported here too it would be one finding twice, at a path nobody wrote.
|
|
12
|
-
|
|
13
|
-
import { rmSync } from 'node:fs' // bun has no file removal of its own
|
|
14
|
-
import { dirname, relative } from 'node:path' // bun exposes no path helpers of its own
|
|
15
|
-
import { buildDirectory } from '#server/buildDirectory.ts'
|
|
16
|
-
import { namesUnder } from '#server/namesUnder.ts'
|
|
17
|
-
import { COMPONENT_MODULE } from './checkedShadow.ts'
|
|
18
|
-
import { formatDiagnosis } from './formatDiagnosis.ts'
|
|
19
|
-
import { projectShadow } from './projectShadow.ts'
|
|
20
|
-
import { type ParsedSource, refusal } from './readSources.ts'
|
|
21
|
-
import type { Diagnosis } from './TypeSession.ts'
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Writes each source's types beside the build, and removes the ones whose `.abide` is gone.
|
|
25
|
-
*
|
|
26
|
-
* A file is written only where its text moved, so an editor watching the directory re-reads what
|
|
27
|
-
* changed rather than everything. WITH `refused`, A SOURCE WHOSE PROJECTION IS REFUSED (34.3, …) is a
|
|
28
|
-
* finding at its first line and keeps the types it last had, as `parseSources` treats one that does
|
|
29
|
-
* not parse; without it the refusal is thrown, naming the file.
|
|
30
|
-
*/
|
|
31
|
-
export async function writeTypes(
|
|
32
|
-
root: string,
|
|
33
|
-
sources: readonly Pick<ParsedSource, 'path' | 'file'>[],
|
|
34
|
-
/** Whether `sources` is every file, so the types of one not among them are stale. */
|
|
35
|
-
whole = true,
|
|
36
|
-
refused?: Diagnosis[],
|
|
37
|
-
): Promise<void> {
|
|
38
|
-
const above = dirname(root)
|
|
39
|
-
// Mirrored below the directory above `root`, as the scaffold's `tsconfig` names it.
|
|
40
|
-
const types = `${buildDirectory(root)}/types`
|
|
41
|
-
const written = new Set<string>()
|
|
42
|
-
const writes: Promise<unknown>[] = []
|
|
43
|
-
for (const source of sources) {
|
|
44
|
-
const path = `${types}/${relative(above, source.path)}.ts`
|
|
45
|
-
written.add(path)
|
|
46
|
-
let projected: string
|
|
47
|
-
try {
|
|
48
|
-
projected = projectShadow(source.file).text
|
|
49
|
-
} catch (thrown) {
|
|
50
|
-
const found = refusal(source.path, thrown)
|
|
51
|
-
if (refused === undefined)
|
|
52
|
-
throw new Error(formatDiagnosis(found, process.cwd()))
|
|
53
|
-
refused.push(found)
|
|
54
|
-
continue
|
|
55
|
-
}
|
|
56
|
-
// The checker's own stand-in for the default: a component taking anything (`COMPONENT_MODULE`).
|
|
57
|
-
const text = `// @ts-nocheck\n${projected}\n${COMPONENT_MODULE}`
|
|
58
|
-
if (WRITTEN.get(path) === text) continue
|
|
59
|
-
writes.push(
|
|
60
|
-
Bun.file(path)
|
|
61
|
-
.text()
|
|
62
|
-
.catch(() => undefined)
|
|
63
|
-
.then((standing) => {
|
|
64
|
-
WRITTEN.set(path, text)
|
|
65
|
-
return standing === text ? undefined : Bun.write(path, text)
|
|
66
|
-
}),
|
|
67
|
-
)
|
|
68
|
-
}
|
|
69
|
-
await Promise.all(writes)
|
|
70
|
-
if (!whole) return
|
|
71
|
-
// Only below this root's own mirror: two apps beside one another share the directory above.
|
|
72
|
-
const mirror = `${types}/${relative(above, root)}`
|
|
73
|
-
for (const name of namesUnder(mirror))
|
|
74
|
-
if (!written.has(`${mirror}/${name}`)) {
|
|
75
|
-
rmSync(`${mirror}/${name}`)
|
|
76
|
-
WRITTEN.delete(`${mirror}/${name}`)
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// What this process last left at each path, so a compile run again (`abide dev`, the language
|
|
81
|
-
// server) reads back only the files whose text moved since.
|
|
82
|
-
const WRITTEN = new Map<string, string>()
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
// THE DESKTOP LAUNCHER'S FILES. Its page and the `launcher` options it exports sit at the app's root
|
|
2
|
-
// beside the root layout, a name the framework finds rather than an address (23.14); the icon is a
|
|
3
|
-
// public file, so it is wherever the address it answers at says (36.11).
|
|
4
|
-
|
|
5
|
-
/** The launcher's page and options. 38.80, 38.82. */
|
|
6
|
-
export const LAUNCHER_PAGE = 'launcher.abide'
|
|
7
|
-
|
|
8
|
-
/** The addresses of the desktop app's icon, the `.icns` first. 38.79. */
|
|
9
|
-
export const ICON_ADDRESSES = { icns: '/icon.icns', png: '/icon.png' } as const
|
|
File without changes
|
|
File without changes
|