@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.
Files changed (67) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/bin/abide.ts +31 -28
  3. package/docs/REGISTRY.md +30 -26
  4. package/docs/RULEBOOK.md +98 -83
  5. package/docs/abide.md +771 -522
  6. package/package.json +2 -1
  7. package/scaffold/AGENTS.md +13 -6
  8. package/src/compiler/TypeSession.ts +110 -35
  9. package/src/compiler/appMap.ts +96 -0
  10. package/src/compiler/argsSchemaOf.ts +1 -1
  11. package/src/compiler/checkApp.ts +35 -18
  12. package/src/compiler/checkedShadow.ts +40 -7
  13. package/src/compiler/compileApp.ts +228 -70
  14. package/src/compiler/compiledEntry.ts +10 -6
  15. package/src/compiler/componentGraph.ts +6 -7
  16. package/src/compiler/declaredExports.ts +3 -3
  17. package/src/compiler/emitClient.ts +3 -3
  18. package/src/compiler/emitComponent.ts +9 -9
  19. package/src/compiler/emitSource.ts +5 -9
  20. package/src/compiler/handlersUnder.ts +114 -73
  21. package/src/compiler/hooksCalled.ts +35 -0
  22. package/src/compiler/mountsOf.ts +30 -18
  23. package/src/compiler/parseAbide.ts +12 -12
  24. package/src/compiler/projectShadow.ts +70 -45
  25. package/src/compiler/readsIn.ts +2 -2
  26. package/src/compiler/scaffold.ts +24 -21
  27. package/src/compiler/serveLanguage.ts +0 -24
  28. package/src/compiler/sharedModuleWrites.ts +2 -2
  29. package/src/compiler/splitImports.ts +4 -4
  30. package/src/compiler/writeDefaultProject.ts +28 -1
  31. package/src/server/MODULE_QUERIES.ts +12 -3
  32. package/src/server/abideModules.ts +117 -51
  33. package/src/server/answerRequest.ts +9 -4
  34. package/src/server/answerRpc.ts +18 -16
  35. package/src/server/appTree.ts +83 -24
  36. package/src/server/commands.ts +5 -4
  37. package/src/server/componentModules.d.ts +1 -2
  38. package/src/server/createApp.ts +23 -11
  39. package/src/server/devWorker.ts +5 -1
  40. package/src/server/fenceServer.ts +36 -13
  41. package/src/server/hooks.ts +17 -4
  42. package/src/server/index.ts +1 -0
  43. package/src/server/installSourceMaps.ts +6 -1
  44. package/src/server/launcherOptions.ts +12 -13
  45. package/src/server/launcherPage.ts +2 -2
  46. package/src/server/nativeMenu.ts +3 -3
  47. package/src/server/preload.ts +13 -0
  48. package/src/server/runDesktop.ts +11 -16
  49. package/src/server/serveDev.ts +8 -2
  50. package/src/server/surfaceOf.ts +2 -0
  51. package/src/server/withAbideSources.ts +1 -1
  52. package/src/shared/ADDRESSES.ts +3 -0
  53. package/src/shared/LAUNCHER_FILES.ts +9 -0
  54. package/src/shared/RESERVED_DIRECTORIES.ts +13 -4
  55. package/src/shared/index.ts +2 -1
  56. package/src/{ui → shared}/menuBridge.ts +1 -1
  57. package/src/shared/menuItem.ts +2 -2
  58. package/src/shared/onMenu.ts +58 -0
  59. package/src/{ui → shared}/reportMenu.ts +7 -9
  60. package/src/shared/routePattern.ts +58 -20
  61. package/src/shared/rpc.ts +1 -1
  62. package/src/ui/Router.ts +0 -6
  63. package/src/compiler/syncTypes.ts +0 -32
  64. package/src/compiler/writeTypes.ts +0 -82
  65. package/src/server/LAUNCHER_FILES.ts +0 -9
  66. /package/scaffold/src/{page.abide → index.abide} +0 -0
  67. /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]/page.abide`) and the literal an author writes
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 { PUBLIC_DIRECTORY, SERVER_DIRECTORY } from './RESERVED_DIRECTORIES.ts'
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
- * AN ADDRESS RATHER THAN A NAME. The file name says which of the three it is, and only a file
52
- * whose place is an `address` (`placeOf`) is one: a `page.abide` under a `server` directory is a
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') return 'component'
62
- // The WHOLE basename: `homepage.abide` ends with `page.abide` and is a component.
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
- return name === 'error.abide' ? 'error' : 'component'
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 = 'address' | 'server' | 'public' | 'dependency' | 'skipped'
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.28: a package's, outside the root or under `node_modules`, whose directories
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 `public` — 23.15: under that reserved directory, the nearest to the root deciding
80
- * (23.16 refuses one inside the other);
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
- if (part === SERVER_DIRECTORY) return 'server'
92
- if (part === PUBLIC_DIRECTORY) return 'public'
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 DIRECTORY's below the root, whatever its name.
111
+ * The route a file answers at — its path below the root without its extension.
104
112
  *
105
- * 23.14 — `media/[id]/page.abide` is `/media/[id]`, and so is every other file beside it, which is
106
- * what lets a directory's handlers sit in as many files as an author likes (16.2). `path` is
107
- * relative to the app's root, as `fileKind`'s is.
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
- 'abide: this handler has no address yet. A handler is given one where it is mounted, exported from a <script server> (16.2).',
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