@abide/abide 0.101.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 (161) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/bin/abide.ts +226 -164
  3. package/docs/REGISTRY.md +730 -0
  4. package/docs/RULEBOOK.md +2768 -0
  5. package/docs/abide.md +13127 -0
  6. package/package.json +6 -3
  7. package/scaffold/AGENTS.md +38 -0
  8. package/scaffold/CLAUDE.md +1 -0
  9. package/scaffold/src/layout.abide +18 -0
  10. package/src/compiler/Lowering.ts +78 -9
  11. package/src/compiler/ShadowMap.ts +2 -2
  12. package/src/compiler/TypeSession.ts +341 -71
  13. package/src/compiler/appMap.ts +96 -0
  14. package/src/compiler/argsSchemaOf.ts +1 -1
  15. package/src/compiler/checkApp.ts +53 -8
  16. package/src/compiler/checkedShadow.ts +73 -6
  17. package/src/compiler/classifyRead.ts +6 -4
  18. package/src/compiler/classifyTemplate.ts +1 -1
  19. package/src/compiler/compileApp.ts +598 -200
  20. package/src/compiler/compiledEntry.ts +48 -40
  21. package/src/compiler/componentGraph.ts +57 -71
  22. package/src/compiler/declaredExports.ts +26 -9
  23. package/src/compiler/emitClient.ts +142 -72
  24. package/src/compiler/emitComponent.ts +41 -12
  25. package/src/compiler/emitSource.ts +102 -13
  26. package/src/compiler/handlersUnder.ts +164 -76
  27. package/src/compiler/hooksCalled.ts +35 -0
  28. package/src/compiler/index.ts +7 -0
  29. package/src/compiler/mountsOf.ts +150 -0
  30. package/src/compiler/nearest.ts +14 -0
  31. package/src/compiler/parseAbide.ts +157 -20
  32. package/src/compiler/projectShadow.ts +250 -19
  33. package/src/compiler/reactiveTypes.ts +20 -7
  34. package/src/compiler/readSources.ts +15 -2
  35. package/src/compiler/readsIn.ts +62 -8
  36. package/src/compiler/renderMarkup.ts +3 -3
  37. package/src/compiler/scaffold.ts +37 -15
  38. package/src/compiler/serveLanguage.ts +101 -34
  39. package/src/compiler/sharedModuleWrites.ts +2 -2
  40. package/src/compiler/sourceMapOf.ts +193 -0
  41. package/src/compiler/splitImports.ts +205 -83
  42. package/src/compiler/tokenize.ts +14 -0
  43. package/src/compiler/writeDefaultProject.ts +29 -2
  44. package/src/server/CLIENT_BUILD.ts +11 -13
  45. package/src/server/DEFAULT_PORT.ts +1 -1
  46. package/src/server/MODULE_QUERIES.ts +27 -0
  47. package/src/server/TABLES.ts +7 -0
  48. package/src/server/VIRTUAL_DIRECTORY.ts +4 -5
  49. package/src/server/abideModules.ts +457 -0
  50. package/src/server/answerFailure.ts +16 -6
  51. package/src/server/answerRequest.ts +241 -107
  52. package/src/server/answerRpc.ts +107 -33
  53. package/src/server/answerSocket.ts +65 -20
  54. package/src/server/answerValue.ts +22 -9
  55. package/src/server/appDirectories.ts +1 -1
  56. package/src/server/appPlugins.ts +1 -1
  57. package/src/server/appTree.ts +238 -0
  58. package/src/server/argsFor.ts +2 -2
  59. package/src/server/assistant.ts +2 -2
  60. package/src/server/bridgeRelay.ts +6 -8
  61. package/src/server/claudeCode.ts +0 -6
  62. package/src/server/clientBuildOptions.ts +8 -3
  63. package/src/server/coerceArgs.ts +1 -1
  64. package/src/server/commands.ts +38 -37
  65. package/src/server/componentModules.d.ts +2 -1
  66. package/src/server/compressAnswer.ts +4 -4
  67. package/src/server/config.ts +25 -29
  68. package/src/server/configFields.ts +31 -25
  69. package/src/server/createApp.ts +77 -79
  70. package/src/server/devWorker.ts +118 -181
  71. package/src/server/executableName.ts +4 -0
  72. package/src/server/fenceServer.ts +57 -32
  73. package/src/server/generatedSurfaces.ts +99 -66
  74. package/src/server/hooks.ts +137 -52
  75. package/src/server/iconAccent.ts +191 -0
  76. package/src/server/index.ts +12 -7
  77. package/src/server/installSourceMaps.ts +93 -0
  78. package/src/server/launcherOptions.ts +33 -31
  79. package/src/server/launcherPage.ts +107 -64
  80. package/src/server/manifestOf.ts +16 -13
  81. package/src/server/namesUnder.ts +2 -2
  82. package/src/server/nativeMenu.ts +6 -6
  83. package/src/server/negotiateEncoding.ts +1 -1
  84. package/src/server/preload.ts +13 -0
  85. package/src/server/principal.ts +10 -5
  86. package/src/server/realOrAsGiven.ts +10 -0
  87. package/src/server/refuseAddressCollision.ts +21 -14
  88. package/src/server/remote.ts +49 -52
  89. package/src/server/render.ts +65 -94
  90. package/src/server/renderComponent.ts +5 -0
  91. package/src/server/reportHealth.ts +2 -2
  92. package/src/server/requestScope.ts +12 -1
  93. package/src/server/runBuild.ts +24 -0
  94. package/src/server/runCompiled.ts +5 -5
  95. package/src/server/runDesktop.ts +48 -28
  96. package/src/server/serveBuild.ts +43 -52
  97. package/src/server/serveDev.ts +79 -37
  98. package/src/server/shell.ts +7 -22
  99. package/src/server/sourceMappings.ts +139 -0
  100. package/src/server/surfaceOf.ts +20 -3
  101. package/src/server/testBuild.ts +1 -19
  102. package/src/server/withAbideSources.ts +117 -0
  103. package/src/shared/ADDRESSES.ts +10 -18
  104. package/src/shared/APP_META.ts +7 -0
  105. package/src/shared/CopyOptions.ts +1 -1
  106. package/src/shared/DEBUG_META.ts +1 -1
  107. package/src/shared/LAUNCHER_FILES.ts +9 -0
  108. package/src/shared/RESERVED_DIRECTORIES.ts +24 -0
  109. package/src/shared/cookies.ts +2 -2
  110. package/src/shared/csp.ts +1 -1
  111. package/src/shared/escapeText.ts +1 -1
  112. package/src/shared/filledPath.ts +85 -0
  113. package/src/shared/framing.ts +79 -6
  114. package/src/shared/headKeyOf.ts +2 -0
  115. package/src/shared/index.ts +2 -1
  116. package/src/shared/log.ts +27 -13
  117. package/src/shared/matchRoute.ts +23 -4
  118. package/src/{ui → shared}/menuBridge.ts +2 -2
  119. package/src/shared/menuItem.ts +4 -4
  120. package/src/shared/onMenu.ts +58 -0
  121. package/src/shared/reactive/Keyed.ts +2 -2
  122. package/src/shared/reactive/NotLanded.ts +1 -1
  123. package/src/shared/reactive/Reactive.ts +43 -43
  124. package/src/shared/reactive/argsContract.ts +4 -4
  125. package/src/shared/reactive/canonicalKey.ts +4 -4
  126. package/src/shared/reactive/chunksOfStream.ts +6 -0
  127. package/src/shared/reactive/index.ts +1 -1
  128. package/src/shared/reactive/recordWrites.ts +103 -5
  129. package/src/shared/reactive/structural.ts +1 -1
  130. package/src/shared/reactive/tracking.ts +1 -1
  131. package/src/shared/reactive/watch.ts +3 -3
  132. package/src/shared/refuse.ts +1 -1
  133. package/src/{ui → shared}/reportMenu.ts +8 -10
  134. package/src/shared/routePattern.ts +122 -48
  135. package/src/shared/rpc.ts +37 -8
  136. package/src/shared/rpcRequest.ts +27 -3
  137. package/src/shared/url.ts +4 -44
  138. package/src/shared/writePath.ts +1 -1
  139. package/src/ui/LAYER_NAMES.ts +1 -1
  140. package/src/ui/List.ts +3 -3
  141. package/src/ui/MOUNTING_LEVEL.ts +18 -0
  142. package/src/ui/Mismatch.ts +1 -1
  143. package/src/ui/RawRun.ts +1 -1
  144. package/src/ui/Router.ts +37 -36
  145. package/src/ui/Scope.ts +1 -1
  146. package/src/ui/TryRegion.ts +2 -2
  147. package/src/ui/applyHead.ts +33 -0
  148. package/src/ui/index.ts +1 -1
  149. package/src/ui/moveNode.ts +1 -1
  150. package/src/ui/reconcile.ts +2 -2
  151. package/src/ui/routing.ts +2 -2
  152. package/src/ui/setAttribute.ts +1 -1
  153. package/src/ui/setText.ts +1 -1
  154. package/src/ui/socketWire.ts +9 -7
  155. package/src/ui/wire.ts +1 -1
  156. package/scaffold/src/server/app.ts +0 -6
  157. package/scaffold/src/shared/.gitkeep +0 -0
  158. package/scaffold/src/ui/app.html +0 -8
  159. package/src/server/componentImports.ts +0 -56
  160. /package/scaffold/src/{ui/pages/page.abide → index.abide} +0 -0
  161. /package/src/{compiler → shared}/SPECIFIERS.ts +0 -0
@@ -1,112 +1,200 @@
1
- // 16.2 AND 19.1 — EVERY HANDLER AN APP DECLARES, AND WHERE IT IS ANSWERED. A handler is an export of
2
- // a module under `server/rpc`, and its address is the module's path below that directory and then
3
- // the export's name — `default`, or an export named as its file, answering at the path alone.
1
+ // 16.2 AND 16.70 — EVERY HANDLER AN APP DECLARES, AND WHERE IT IS ANSWERED. A handler file's default
2
+ // export answers at that file's route (23.14), and is the app's public surface; a handler a
3
+ // `<script server>` declares is its file's alone, answered below `/__abide/handlers/` at the file's
4
+ // path and its binding's name, and listed nowhere (16.71).
4
5
  //
5
- // READ, NOT RUN. The build finds handlers by what their modules SAY rather than by importing them,
6
- // so a build never runs server code: an app's module may open a database at import, and nothing
7
- // about building a browser bundle is a reason to. The checker the build already holds lists what a
8
- // module exports and which call declares each (19.15) — which is also what makes an export that is
9
- // not a handler a refusal here rather than an address that silently never answers.
6
+ // READ, NOT RUN. The build finds handlers by what their files SAY rather than by importing them,
7
+ // so a build never runs server code: a handler file may open a database at import, and nothing
8
+ // about building a browser bundle is a reason to. The checker the build already holds lists what
9
+ // each module exports and which call declares each (16.73) — which is also what makes an export
10
+ // that is not a handler a refusal here rather than an address that silently never answers.
10
11
 
11
12
  import type { Mount } from '#server/answerRpc.ts'
12
- import { namesUnder } from '#server/namesUnder.ts'
13
- import { RPC_ADDRESS } from '#shared/ADDRESSES.ts'
13
+ import { refuseFrameworkRoute } from '#server/appTree.ts'
14
+ import { surfaceName } from '#server/surfaceOf.ts'
15
+ import { PRIVATE_HANDLERS_ADDRESS } from '#shared/ADDRESSES.ts'
16
+ import {
17
+ parseRoutePattern,
18
+ placeOf,
19
+ routeOf,
20
+ scopeOf,
21
+ } from '#shared/routePattern.ts'
14
22
  import { declaredExports } from './declaredExports.ts'
23
+ import type { AbideFile } from './parseAbide.ts'
24
+ import { PRIVATE_EXPORT } from './projectShadow.ts'
15
25
  import type { Diagnosis, TypeSession } from './TypeSession.ts'
16
26
 
17
- const HANDLER_DIRECTORY = 'server/rpc'
18
-
19
- // One module under the tree: where it is from the root, and its address path below the tree.
20
- type HandlerModule = { source: string; path: string }
21
-
22
- /** Every module under `server/rpc`. */
23
- function modulesUnder(root: string): HandlerModule[] {
24
- const modules: HandlerModule[] = []
25
- for (const name of namesUnder(
26
- `${root}/${HANDLER_DIRECTORY}`,
27
- '**/*.ts',
28
- ).sort())
29
- modules.push({
30
- source: `${HANDLER_DIRECTORY}/${name}`,
31
- path: name.replace(/\.ts$/, ''),
32
- })
33
- return modules
27
+ /** A handler file, by its path and its path below the app's root. */
28
+ export type HandlerFile = {
29
+ path: string
30
+ relative: string
31
+ /** 23.19 — the path it answers by: its own, or the one its mount gives it. */
32
+ position: string
34
33
  }
35
34
 
35
+ /** A `.abide` the build read. */
36
+ type Source = HandlerFile & { file: AbideFile }
37
+
36
38
  /**
37
- * Whether the app has any module under `server/rpc`.
38
- *
39
- * Asked before a checker is opened to read it: an app of handlers alone has no `.abide` to say it
40
- * is an app.
39
+ * Told of each thing the emit refuses, at the file below the root it names. The build's throws;
40
+ * `abide check` collects every one at its file (38.33).
41
41
  */
42
- export function declaresHandlers(root: string): boolean {
43
- return modulesUnder(root).length > 0
42
+ export type Refuse = (relative: string, thrown: unknown) => void
43
+
44
+ /** The build's answer to a refusal: the first one stops it. */
45
+ export const THROW: Refuse = (_relative, thrown) => {
46
+ throw thrown
44
47
  }
45
48
 
46
49
  /**
47
50
  * The app's handler mounts, with the warnings the build raised reading them.
48
51
  *
49
- * Off one read of the checker. 19.1 — one owner per address, and the error names both files. 18.15 —
50
- * with what the build could not read of a copy's options.
52
+ * Off one read of the checker. 18.15 — with what the build could not read of a copy's options.
53
+ * 19.1's one owner per route and method is `compileApp`'s, which holds the pages as well.
51
54
  */
52
55
  export async function handlersUnder(
53
- root: string,
54
56
  session: TypeSession,
57
+ sources: readonly Source[],
58
+ files: readonly HandlerFile[],
59
+ refuse: Refuse = THROW,
55
60
  ): Promise<{ mounts: Mount[]; warnings: Diagnosis[] }> {
56
- const modules = modulesUnder(root)
61
+ const serving: Source[] = []
62
+ for (const source of sources)
63
+ if (source.file.servers.length > 0) serving.push(source)
57
64
  const paths: string[] = []
58
- for (const { source } of modules) paths.push(`${root}/${source}`)
59
- const project = await session.project()
65
+ for (const file of files) paths.push(file.path)
66
+ const project = await session.projectWith(paths)
67
+ for (const source of serving) paths.push(session.projectionOf(source.path))
60
68
  const declared =
61
- project === undefined
69
+ project === undefined || paths.length === 0
62
70
  ? []
63
71
  : await declaredExports(project, session.tsconfig, paths)
64
72
  const mounts: Mount[] = []
65
73
  const warnings: Diagnosis[] = []
66
- const owners = new Map<string, string>()
67
- for (let at = 0; at < modules.length; at += 1) {
68
- const { source, path } = modules[at] as HandlerModule
69
- for (const {
70
- name: exported,
71
- method,
72
- ui,
73
- cli,
74
- mcp,
75
- description,
76
- copy,
77
- defaults,
78
- args,
79
- unread,
80
- } of declared[at] ?? []) {
81
- if (method === undefined)
74
+ const names = new Map<string, string>()
75
+ for (let at = 0; at < files.length; at += 1) {
76
+ const { relative, position } = files[at] as HandlerFile
77
+ try {
78
+ const address = routeOf(position)
79
+ const exported = declared[at] ?? []
80
+ const handler = exported.find((each) => each.name === 'default')
81
+ // 16.73 — the default and nothing else, declared by one of the five methods.
82
+ for (const each of exported)
83
+ if (each.name !== 'default' || each.method === undefined)
84
+ throw new Error(
85
+ each.name === 'default'
86
+ ? `abide: ${relative}'s default export is not declared as a handler. A handler file's default is declared by abide's \`GET(…)\` (or POST, PUT, PATCH, DELETE); anything else belongs under a lib directory. RULEBOOK 16.73.`
87
+ : `abide: ${relative} exports \`${each.name}\`, and a handler file exports its handler as its default and nothing else. Move \`${each.name}\` into a module under a lib or a server directory. RULEBOOK 16.73.`,
88
+ )
89
+ if (handler?.method === undefined)
82
90
  throw new Error(
83
- `abide: ${source} exports \`${exported}\`, which is not declared as a handler. A module under ${HANDLER_DIRECTORY} exports handlers alone, each declared by abide's \`GET(…)\` (or POST, PUT, PATCH, DELETE); anything else belongs beside it. RULEBOOK 19.15.`,
91
+ `abide: ${relative} is a handler file — a .ts directly in an address directory — and exports no handler as its default. Export one (\`export default GET(…)\`), or move the file into a lib directory. RULEBOOK 16.73, 23.22.`,
84
92
  )
85
- const atPath =
86
- exported === 'default' ||
87
- exported === path.slice(path.lastIndexOf('/') + 1)
88
- const address = `${RPC_ADDRESS}/${path}${atPath ? '' : `/${exported}`}`
89
- const owner = owners.get(address)
90
- if (owner !== undefined)
93
+ refuseFrameworkRoute(address, relative)
94
+ refuseUndeclaredParameters(address, handler.args, relative)
95
+ // 20.13 — one name per handler on every generated surface.
96
+ const surface = surfaceName(handler.method, address)
97
+ const named = names.get(surface)
98
+ if (named !== undefined)
91
99
  throw new Error(
92
- `abide: ${owner} and ${source} both declare a handler at ${address}. RULEBOOK 19.1.`,
100
+ `abide: ${named} and ${relative} are both named ${surface} on the generated surfaces. RULEBOOK 20.13.`,
93
101
  )
94
- owners.set(address, source)
102
+ names.set(surface, relative)
95
103
  mounts.push({
96
104
  address,
97
- method,
98
- source,
99
- name: exported,
100
- ui,
101
- cli,
102
- mcp,
103
- description,
104
- copy,
105
- defaults,
106
- args,
105
+ method: handler.method,
106
+ source: relative,
107
+ name: address.slice(address.lastIndexOf('/') + 1),
108
+ export: 'default',
109
+ scope: scopeOf(position),
110
+ private: false,
111
+ ui: handler.ui,
112
+ cli: handler.cli,
113
+ mcp: handler.mcp,
114
+ description: handler.description,
115
+ copy: handler.copy,
116
+ defaults: handler.defaults,
117
+ args: handler.args,
107
118
  })
108
- warnings.push(...unread)
119
+ warnings.push(...handler.unread)
120
+ } catch (thrown) {
121
+ refuse(relative, thrown)
122
+ }
123
+ }
124
+ for (let at = 0; at < serving.length; at += 1) {
125
+ const { relative, position } = serving[at] as Source
126
+ // 34.24 — a dependency's file answers where a mount places it, and its handlers below the
127
+ // path it has there.
128
+ const below = placeOf(position) === 'dependency' ? relative : position
129
+ for (const each of declared[files.length + at] ?? []) {
130
+ if (each.method === undefined) continue
131
+ if (!each.name.startsWith(PRIVATE_EXPORT)) continue
132
+ const name = each.name.slice(PRIVATE_EXPORT.length)
133
+ mounts.push({
134
+ address: privateAddress(below, name),
135
+ method: each.method,
136
+ source: relative,
137
+ name,
138
+ export: each.name,
139
+ scope: scopeOf(position),
140
+ private: true,
141
+ ui: each.ui,
142
+ cli: false,
143
+ mcp: false,
144
+ description: each.description,
145
+ copy: each.copy,
146
+ defaults: each.defaults,
147
+ args: each.args,
148
+ })
149
+ warnings.push(...each.unread)
109
150
  }
110
151
  }
111
152
  return { mounts, warnings }
112
153
  }
154
+
155
+ /**
156
+ * The address of the private handler `name` the `.abide` at `path` declares: below
157
+ * `PRIVATE_HANDLERS_ADDRESS`, the file's path without its extension, each segment's brackets and
158
+ * dots dropped so nothing in it reads as a parameter, then its name (16.70).
159
+ */
160
+ export function privateAddress(path: string, name: string): string {
161
+ const segments: string[] = []
162
+ for (const segment of path.replace(/\.abide$/, '').split('/'))
163
+ if (segment !== '' && segment !== '..')
164
+ segments.push(segment.replace(/[[\].]/g, ''))
165
+ return `${PRIVATE_HANDLERS_ADDRESS}/${segments.join('/')}/${name}`
166
+ }
167
+
168
+ /**
169
+ * Throws where a handler's `Args` does not declare a parameter its route carries.
170
+ *
171
+ * 16.67 — read off the JSON Schema its annotation derives, which is what the wire holds the args
172
+ * to: a parameter the schema does not name would reach the handler as a key it never typed.
173
+ */
174
+ function refuseUndeclaredParameters(
175
+ address: string,
176
+ args: unknown,
177
+ source: string,
178
+ ): void {
179
+ const schema = args as
180
+ | {
181
+ properties?: Record<string, unknown>
182
+ additionalProperties?: unknown
183
+ }
184
+ | undefined
185
+ // A record type — an index signature — declares every key.
186
+ const open =
187
+ schema?.additionalProperties !== undefined &&
188
+ schema.additionalProperties !== false
189
+ for (const segment of parseRoutePattern(address)) {
190
+ if (segment.kind === 'static' || open) continue
191
+ if (
192
+ schema?.properties !== undefined &&
193
+ segment.name in schema.properties
194
+ )
195
+ continue
196
+ throw new Error(
197
+ `abide: ${source} answers at ${address}, and its handler's args do not declare \`${segment.name}\`. RULEBOOK 16.67.`,
198
+ )
199
+ }
200
+ }
@@ -0,0 +1,35 @@
1
+ import { tokenize } from './tokenize.ts'
2
+
3
+ /** Every hook abide exports, by name. */
4
+ export const HOOK_NAMES: ReadonlySet<string> = new Set([
5
+ 'middleware',
6
+ 'onStart',
7
+ 'onStop',
8
+ 'onError',
9
+ 'onConfig',
10
+ 'onHealth',
11
+ 'onPrincipal',
12
+ 'onLauncher',
13
+ 'onMenu',
14
+ ])
15
+
16
+ /**
17
+ * Each of abide's hook names called in `source`, in the order written: the name, then `(`, and not
18
+ * a declaration — a member call included, since `server.onStart(…)` is abide's hook all the same.
19
+ * Read, not run — what `abide routes` lists and the build's refusals of a hook called where it is
20
+ * never read (38.107) go by.
21
+ */
22
+ export function hooksCalled(source: string): string[] {
23
+ const called: string[] = []
24
+ let previous = ''
25
+ let named: string | undefined
26
+ for (const token of tokenize(source)) {
27
+ if (named !== undefined && token.text === '(') called.push(named)
28
+ named =
29
+ HOOK_NAMES.has(token.text) && previous !== 'function'
30
+ ? token.text
31
+ : undefined
32
+ previous = token.text
33
+ }
34
+ return called
35
+ }
@@ -7,6 +7,13 @@
7
7
  // reached only by something already doing a build. Nothing here is in any client bundle, and
8
8
  // `compiler.test.ts` holds the two apart by asserting what `@abide/abide/server` exports.
9
9
 
10
+ // 34.14 — what serves what this compiles, to a bundle or to the runtime (`abideModules`).
11
+ export {
12
+ abideModules,
13
+ type Compilation,
14
+ type CompiledFile,
15
+ serveCompilation,
16
+ } from '#server/abideModules.ts'
10
17
  export { componentGraph } from './componentGraph.ts'
11
18
  export { emitClient, type SelfFill } from './emitClient.ts'
12
19
  export { emitComponent } from './emitComponent.ts'
@@ -0,0 +1,150 @@
1
+ // 23.19 — A PACKAGE'S DIRECTORY, MOUNTED WHERE THE APP SAYS.
2
+ // `import 'kit/admin' with { type: 'routes' }` in a file of the app's places every `.abide` and
3
+ // handler file under the package's `admin` directory at the importing file's directory, so its
4
+ // pages, layouts, error pages and handlers answer below it — and the app, not the package, decides
5
+ // where: the mount is a line in a file the app writes, at the position the app gave it.
6
+ //
7
+ // A FILE KEEPS ITS PATH AND GAINS A POSITION. It is compiled, served and keyed by where it lies, as
8
+ // any dependency's is (34.23); its POSITION — the path it would have below the root — is what
9
+ // `placeOf`, `fileKind` and `routeOf` read, so a mounted page is a page by the same test as the app's.
10
+
11
+ import { readFileSync, realpathSync, statSync } from 'node:fs' // bun exposes no sync read, stat or realpath of its own
12
+ import { dirname, relative, resolve } from 'node:path' // bun exposes no path helpers of its own
13
+ import { appFiles } from '#server/appTree.ts'
14
+ import { placeOf } from '#shared/routePattern.ts'
15
+ import { nearest } from './nearest.ts'
16
+ import { type AbideFile, parseAbide, scriptBodies } from './parseAbide.ts'
17
+ import { attributedImports, IMPORT_ATTRIBUTES } from './splitImports.ts'
18
+
19
+ /** A `.abide` on disk, and the position it takes in the app. */
20
+ type Placed = { path: string; position: string }
21
+
22
+ /**
23
+ * One directory a file mounts, and each `.abide` and each handler file under it at the position it
24
+ * takes in the app.
25
+ */
26
+ type DirectoryMount = {
27
+ specifier: string
28
+ directory: string
29
+ /** The position its files take below the root: the mounting file's directory, `/`-ended. */
30
+ at: string
31
+ files: Placed[]
32
+ handlers: Placed[]
33
+ }
34
+
35
+ /**
36
+ * Every directory the file at `path` mounts, its position in the app being `position` — or the
37
+ * refusal naming what one of them cannot be.
38
+ *
39
+ * `root` is the app's, real: a directory under it is the app's own already, with a position of its
40
+ * own, and mounting it would give its files a second (23.20).
41
+ */
42
+ export function mountsOf(
43
+ root: string,
44
+ path: string,
45
+ position: string,
46
+ file: AbideFile,
47
+ ): DirectoryMount[] | string {
48
+ const mounts: DirectoryMount[] = []
49
+ const at = position.includes('/')
50
+ ? `${position.slice(0, position.lastIndexOf('/'))}/`
51
+ : ''
52
+ for (const script of scriptBodies(file))
53
+ for (const { specifier } of attributedImports(script, 'routes')) {
54
+ const directory = mountedDirectory(specifier, dirname(path))
55
+ if (directory === undefined)
56
+ return `abide: ${position} mounts ${specifier}, and no directory answers to it. RULEBOOK 23.19.`
57
+ if (ownDirectory(root, directory))
58
+ return `abide: ${position} mounts ${specifier}, which is the app's own directory already. RULEBOOK 23.20.`
59
+ // The package's tree is the package's to refuse; a mount places what it finds.
60
+ const found = appFiles(directory, () => {})
61
+ const placed = (below: string): Placed => ({
62
+ path: `${directory}/${below}`,
63
+ position: `${at}${below}`,
64
+ })
65
+ mounts.push({
66
+ specifier,
67
+ directory,
68
+ at,
69
+ files: found.abide.map(placed),
70
+ handlers: found.handlers.map(placed),
71
+ })
72
+ }
73
+ return mounts
74
+ }
75
+
76
+ /**
77
+ * Every file mounted under the app at `root` whose own `.abide` are `relatives`, and every one those
78
+ * mount in turn, at its position — read for nothing but its mounts, and refusing nothing: the
79
+ * build is what refuses a mount (`withDependencies`).
80
+ *
81
+ * THE CHECKER'S `Routes` (30.26) asks this on every check, so a file is parsed only where its text
82
+ * names a mount at all.
83
+ */
84
+ export function mountedUnder(
85
+ root: string,
86
+ relatives: readonly string[],
87
+ ): Placed[] {
88
+ const queue: Placed[] = []
89
+ for (const relative of relatives)
90
+ queue.push({ path: `${root}/${relative}`, position: relative })
91
+ const found: Placed[] = []
92
+ const seen = new Set<string>()
93
+ for (let at = 0; at < queue.length; at += 1) {
94
+ const { path, position } = queue[at] as Placed
95
+ const text = readFileSync(path, 'utf8')
96
+ if (!IMPORT_ATTRIBUTES.routes.test(text)) continue
97
+ let file: AbideFile
98
+ try {
99
+ file = parseAbide(text)
100
+ } catch {
101
+ continue
102
+ }
103
+ const mounts = mountsOf(root, path, position, file)
104
+ if (typeof mounts === 'string') continue
105
+ for (const mount of mounts)
106
+ for (const placed of mount.files) {
107
+ if (seen.has(placed.path)) continue
108
+ seen.add(placed.path)
109
+ found.push(placed)
110
+ queue.push(placed)
111
+ }
112
+ }
113
+ return found
114
+ }
115
+
116
+ // Under the root and not a dependency's there: one of the app's own, with positions of its own.
117
+ function ownDirectory(root: string, directory: string): boolean {
118
+ if (directory === root) return true
119
+ const below = relative(root, directory)
120
+ return !below.startsWith('..') && placeOf(`${below}/-`) !== 'dependency'
121
+ }
122
+
123
+ /**
124
+ * The real directory `specifier` names from `from`: a relative path as written, and a bare one as a
125
+ * package and the path into it, found in the nearest `node_modules` above.
126
+ *
127
+ * A DIRECTORY, NOT A MODULE: a package's `exports` names its modules, and a mount asks for none of
128
+ * them, so its directory is found as bun would find the package and then walked.
129
+ */
130
+ function mountedDirectory(specifier: string, from: string): string | undefined {
131
+ let candidate: string | undefined
132
+ if (specifier.startsWith('./') || specifier.startsWith('../'))
133
+ candidate = resolve(from, specifier)
134
+ else {
135
+ const parts = specifier.split('/')
136
+ const nameLength = specifier.startsWith('@') ? 2 : 1
137
+ const name = parts.slice(0, nameLength).join('/')
138
+ const inside = parts.slice(nameLength).join('/')
139
+ const found = nearest(from, `node_modules/${name}`)
140
+ if (found === undefined) return undefined
141
+ candidate = inside === '' ? found : `${found}/${inside}`
142
+ }
143
+ try {
144
+ return statSync(candidate).isDirectory()
145
+ ? realpathSync(candidate)
146
+ : undefined
147
+ } catch {
148
+ return undefined
149
+ }
150
+ }
@@ -0,0 +1,14 @@
1
+ import { existsSync } from 'node:fs' // bun exposes no sync existence check of its own
2
+ import { dirname } from 'node:path' // bun exposes no path helpers of its own
3
+
4
+ /** The nearest file or directory at `name` at or above `from`, or `undefined`. */
5
+ export function nearest(from: string, name: string): string | undefined {
6
+ let at = from
7
+ while (true) {
8
+ const candidate = `${at}/${name}`
9
+ if (existsSync(candidate)) return candidate
10
+ const up = dirname(at)
11
+ if (up === at) return undefined
12
+ at = up
13
+ }
14
+ }