@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,7 +1,9 @@
1
+ import { SERVER_SPECIFIER } from '#shared/SPECIFIERS.ts'
1
2
  import { type CheckerAnswer, UNCHECKED } from './CheckerAnswer.ts'
2
3
  import { Cursor, placeTemplate } from './Cursor.ts'
3
4
  import { type LoweredChunk, Lowering, PROPS } from './Lowering.ts'
4
5
  import type { AbideFile } from './parseAbide.ts'
6
+ import { privateExports } from './projectShadow.ts'
5
7
  import {
6
8
  concat,
7
9
  definitions,
@@ -11,7 +13,6 @@ import {
11
13
  renderMarkup,
12
14
  SERVER_LANE,
13
15
  } from './renderMarkup.ts'
14
- import { SERVER_SPECIFIER } from './SPECIFIERS.ts'
15
16
  import { insideShare, SETTLED_AT, shareScope } from './shareScope.ts'
16
17
  import { splitImports, withoutProps } from './splitImports.ts'
17
18
  import type { TemplateNode } from './TemplateNode.ts'
@@ -43,8 +44,8 @@ export function emitComponent(
43
44
  file: AbideFile,
44
45
  answer: CheckerAnswer = UNCHECKED,
45
46
  settles = false,
46
- /** The route a `<script server>` files its hooks at (37.1), where the file is under one. */
47
- route?: string,
47
+ /** The route a `<script server>` files its hooks at (37.1). */
48
+ route = '/',
48
49
  /** 38.97 — where given, every chunk the module is written around (`sourceMapOf`). */
49
50
  chunks?: LoweredChunk[],
50
51
  ): string {
@@ -75,21 +76,20 @@ export function emitComponent(
75
76
  // 31.1 and 31.4 hold INSIDE a `.abide` file, not just in its template, so the script block is
76
77
  // lowered with the same rewrite the slots get.
77
78
  //
78
- // 34.2 — A `<script module>` IS MODULE SCOPE, here as in the browser, so what it exports is the
79
- // module's (34.17). What must stay per request is a `reactive`, which is by 11.32 wherever it
80
- // is declared.
79
+ // 34.2 — A `<script module>` IS MODULE SCOPE, here as in the browser. What must stay per request
80
+ // is a `reactive`, which is by 11.32 wherever it is declared.
81
81
  const moduleSetup = splitImports(lowering.modules())
82
- // 34.20 — A `<script server>` IS MODULE SCOPE ON THIS SIDE ALONE, run between two calls that
82
+ // 34.19 — A `<script server>` IS MODULE SCOPE ON THIS SIDE ALONE, run between two calls that
83
83
  // file every hook it makes at the file's route (37.1). Its body cannot await at its top level
84
84
  // (34.3), so nothing else runs inside the window.
85
85
  const serverSetup = splitImports(lowering.servers(), 'source')
86
86
  const serverBody =
87
87
  serverSetup.body === ''
88
88
  ? ''
89
- : `${lowering.use('fileRoute')}(${JSON.stringify(route ?? '/')})
89
+ : `${lowering.use('fileRoute')}(${JSON.stringify(route)})
90
90
  ${serverSetup.body}
91
91
  ${lowering.use('fileRoute')}(undefined)
92
- `
92
+ ${privateExports(file.servers.join('\n'))}`
93
93
  const setup = splitImports(lowering.scripts())
94
94
  const componentBody = setup.body
95
95
 
@@ -1,5 +1,5 @@
1
1
  import { STYLE_QUERY } from '#server/MODULE_QUERIES.ts'
2
- import { fileKind, placeOf, routeOf } from '#shared/routePattern.ts'
2
+ import { fileKind, scopeOf } from '#shared/routePattern.ts'
3
3
  import type { CheckerAnswer } from './CheckerAnswer.ts'
4
4
  import { emitClient } from './emitClient.ts'
5
5
  import { emitComponent } from './emitComponent.ts'
@@ -27,8 +27,8 @@ export function emitSource(
27
27
  answer: CheckerAnswer,
28
28
  invoked: boolean,
29
29
  /**
30
- * 34.23 — what the browser's module exports in place of the server block: the wrapper of each
31
- * handler it declares, under the handler's name (`standInsOf`).
30
+ * 16.28 — what the browser's module binds in place of the server block: the wrapper of each
31
+ * private handler it declares, under the handler's name (`standInsOf`).
32
32
  */
33
33
  standIns = '',
34
34
  /** 38.97 — the `.abide`'s text, where the build wants each module's source map. */
@@ -46,7 +46,6 @@ export function emitSource(
46
46
  transitions: boolean
47
47
  } {
48
48
  const kind = fileKind(position)
49
- const routed = placeOf(position) === 'address'
50
49
  // 34.4 — A COMPONENT WITH STYLES: its elements carry its token, and the styles, scoped,
51
50
  // are a stylesheet its client module imports — which the bundle collects into the
52
51
  // stylesheet the document links (38.23).
@@ -108,7 +107,7 @@ export function emitSource(
108
107
  file,
109
108
  answer,
110
109
  kind !== 'component',
111
- routed ? routeOf(position) : undefined,
110
+ scopeOf(position),
112
111
  serverChunks,
113
112
  )
114
113
  const name = relative.slice(relative.lastIndexOf('/') + 1)
@@ -174,10 +173,7 @@ type Mapped = {
174
173
  // only where all three texts match, so two apps in one process sharing a path cannot cross.
175
174
  const MAPPED = new Map<string, Mapped>()
176
175
 
177
- /**
178
- * The browser's module with each handler's wrapper after it, or the wrappers alone where it had
179
- * nothing else to carry: a file a page imports for its handlers is imported all the same.
180
- */
176
+ /** The browser's module with each private handler's wrapper after it. */
181
177
  function withStandIns(client: string, standIns: string): string {
182
178
  if (standIns === '') return client
183
179
  return client === '' ? standIns : `${client}\n${standIns}`
@@ -1,35 +1,50 @@
1
- // 16.2 AND 19.15 — EVERY HANDLER AN APP DECLARES, AND WHERE IT IS ANSWERED. A handler is an export
2
- // of a `<script server>` in a `.abide` of the app's own, and its address is that file's
3
- // route — its directory's (23.14) — and then the export's name. A directory's handlers may sit in as
4
- // many of its files as an author likes; two of them exporting one name is 19.1's refusal.
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).
5
5
  //
6
- // READ, NOT RUN. The build finds handlers by what their blocks SAY rather than by importing them,
7
- // so a build never runs server code: a server block may open a database at import, and nothing
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
8
  // about building a browser bundle is a reason to. The checker the build already holds lists what
9
- // the file's projection exports and which call declares each (19.15) — which is also what makes an
10
- // export that is not a handler a refusal here rather than an address that silently never answers.
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.
11
11
 
12
12
  import type { Mount } from '#server/answerRpc.ts'
13
13
  import { refuseFrameworkRoute } from '#server/appTree.ts'
14
14
  import { surfaceName } from '#server/surfaceOf.ts'
15
- import { parseRoutePattern, placeOf, routeOf } from '#shared/routePattern.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'
16
22
  import { declaredExports } from './declaredExports.ts'
17
23
  import type { AbideFile } from './parseAbide.ts'
24
+ import { PRIVATE_EXPORT } from './projectShadow.ts'
18
25
  import type { Diagnosis, TypeSession } from './TypeSession.ts'
19
26
 
20
- /** A `.abide` the build read, by its path and its path below the app's root. */
21
- type Source = {
27
+ /** A handler file, by its path and its path below the app's root. */
28
+ export type HandlerFile = {
22
29
  path: string
23
30
  relative: string
24
31
  /** 23.19 — the path it answers by: its own, or the one its mount gives it. */
25
32
  position: string
26
- file: AbideFile
27
33
  }
28
34
 
29
- const TRANSPILER = new Bun.Transpiler({ loader: 'ts' })
35
+ /** A `.abide` the build read. */
36
+ type Source = HandlerFile & { file: AbideFile }
30
37
 
31
- // 19.22 — as the transpiler prints it, one statement to a line; `export * as name` names its export.
32
- const STAR_EXPORT = /^export \* from /m
38
+ /**
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
+ */
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
47
+ }
33
48
 
34
49
  /**
35
50
  * The app's handler mounts, with the warnings the build raised reading them.
@@ -40,64 +55,45 @@ const STAR_EXPORT = /^export \* from /m
40
55
  export async function handlersUnder(
41
56
  session: TypeSession,
42
57
  sources: readonly Source[],
58
+ files: readonly HandlerFile[],
59
+ refuse: Refuse = THROW,
43
60
  ): Promise<{ mounts: Mount[]; warnings: Diagnosis[] }> {
44
- // 34.31 — a dependency's server block has no address of its own: what it declares is mounted
45
- // where a file of the app's re-exports it, and as that file's.
46
61
  const serving: Source[] = []
47
- for (const source of sources) {
48
- if (source.file.servers.length === 0) continue
49
- if (
50
- STAR_EXPORT.test(
51
- TRANSPILER.transformSync(source.file.servers.join('\n')),
52
- )
53
- )
54
- throw new Error(
55
- `abide: ${source.relative} re-exports with \`export *\` from its <script server>, whose names the build cannot read without running it. Name each handler it re-exports. RULEBOOK 19.22.`,
56
- )
57
- if (placeOf(source.position) !== 'dependency') serving.push(source)
58
- }
59
- const project = await session.project()
62
+ for (const source of sources)
63
+ if (source.file.servers.length > 0) serving.push(source)
64
+ const paths: string[] = []
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 || serving.length === 0
69
+ project === undefined || paths.length === 0
62
70
  ? []
63
- : await declaredExports(
64
- project,
65
- session.tsconfig,
66
- serving.map((source) => session.projectionOf(source.path)),
67
- )
71
+ : await declaredExports(project, session.tsconfig, paths)
68
72
  const mounts: Mount[] = []
69
73
  const warnings: Diagnosis[] = []
70
74
  const names = new Map<string, string>()
71
- for (let at = 0; at < serving.length; at += 1) {
72
- const { relative, position, file } = serving[at] as Source
73
- const route = routeOf(position)
74
- // 19.15 — what THE SERVER BLOCK exports, out of everything the file's module does: its
75
- // component and its `<script module>`'s names are the module's own.
76
- const exported = new Set(
77
- TRANSPILER.scan(file.servers.join('\n')).exports,
78
- )
79
- for (const {
80
- name,
81
- method,
82
- ui,
83
- cli,
84
- mcp,
85
- description,
86
- copy,
87
- defaults,
88
- args,
89
- unread,
90
- } of declared[at] ?? []) {
91
- if (!exported.has(name)) continue
92
- if (method === undefined)
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)
93
90
  throw new Error(
94
- `abide: ${relative} exports \`${name}\` from its <script server>, which is not declared as a handler. A server block exports handlers alone, each declared by abide's \`GET(…)\` (or POST, PUT, PATCH, DELETE); anything else belongs in a module of its own. 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.`,
95
92
  )
96
- const address = `${route === '/' ? '' : route}/${name}`
97
93
  refuseFrameworkRoute(address, relative)
98
- refuseUndeclaredParameters(address, args, relative, name)
94
+ refuseUndeclaredParameters(address, handler.args, relative)
99
95
  // 20.13 — one name per handler on every generated surface.
100
- const surface = surfaceName(method, address)
96
+ const surface = surfaceName(handler.method, address)
101
97
  const named = names.get(surface)
102
98
  if (named !== undefined)
103
99
  throw new Error(
@@ -106,23 +102,69 @@ export async function handlersUnder(
106
102
  names.set(surface, relative)
107
103
  mounts.push({
108
104
  address,
109
- method,
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,
118
+ })
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,
110
136
  source: relative,
111
137
  name,
112
- ui,
113
- cli,
114
- mcp,
115
- description,
116
- copy,
117
- defaults,
118
- args,
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,
119
148
  })
120
- warnings.push(...unread)
149
+ warnings.push(...each.unread)
121
150
  }
122
151
  }
123
152
  return { mounts, warnings }
124
153
  }
125
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
+
126
168
  /**
127
169
  * Throws where a handler's `Args` does not declare a parameter its route carries.
128
170
  *
@@ -133,7 +175,6 @@ function refuseUndeclaredParameters(
133
175
  address: string,
134
176
  args: unknown,
135
177
  source: string,
136
- exported: string,
137
178
  ): void {
138
179
  const schema = args as
139
180
  | {
@@ -153,7 +194,7 @@ function refuseUndeclaredParameters(
153
194
  )
154
195
  continue
155
196
  throw new Error(
156
- `abide: ${source}'s \`${exported}\` answers at ${address}, and its args do not declare \`${segment.name}\`. RULEBOOK 16.67.`,
197
+ `abide: ${source} answers at ${address}, and its handler's args do not declare \`${segment.name}\`. RULEBOOK 16.67.`,
157
198
  )
158
199
  }
159
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
+ }
@@ -1,16 +1,16 @@
1
- // 23.19 — A PACKAGE'S DIRECTORY, MOUNTED WHERE THE APP SAYS. `import 'kit/admin' with { type: 'routes' }`
2
- // in a file of the app's places every `.abide` under the package's `admin` directory at the importing
3
- // file's directory, so its pages, layouts, error pages and handlers answer below it — and the app,
4
- // not the package, decides where: the mount is a line in a file the app writes, at the position the
5
- // app gave it.
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
6
  //
7
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.27); its POSITION — the path it would have below the root — is what
8
+ // any dependency's is (34.23); its POSITION — the path it would have below the root — is what
9
9
  // `placeOf`, `fileKind` and `routeOf` read, so a mounted page is a page by the same test as the app's.
10
10
 
11
11
  import { readFileSync, realpathSync, statSync } from 'node:fs' // bun exposes no sync read, stat or realpath of its own
12
12
  import { dirname, relative, resolve } from 'node:path' // bun exposes no path helpers of its own
13
- import { abideUnder } from '#server/appTree.ts'
13
+ import { appFiles } from '#server/appTree.ts'
14
14
  import { placeOf } from '#shared/routePattern.ts'
15
15
  import { nearest } from './nearest.ts'
16
16
  import { type AbideFile, parseAbide, scriptBodies } from './parseAbide.ts'
@@ -19,11 +19,17 @@ import { attributedImports, IMPORT_ATTRIBUTES } from './splitImports.ts'
19
19
  /** A `.abide` on disk, and the position it takes in the app. */
20
20
  type Placed = { path: string; position: string }
21
21
 
22
- /** One directory a file mounts, and each `.abide` under it at the position it takes in the app. */
23
- export type Mounted = {
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 = {
24
27
  specifier: string
25
28
  directory: string
29
+ /** The position its files take below the root: the mounting file's directory, `/`-ended. */
30
+ at: string
26
31
  files: Placed[]
32
+ handlers: Placed[]
27
33
  }
28
34
 
29
35
  /**
@@ -38,8 +44,8 @@ export function mountsOf(
38
44
  path: string,
39
45
  position: string,
40
46
  file: AbideFile,
41
- ): Mounted[] | string {
42
- const mounts: Mounted[] = []
47
+ ): DirectoryMount[] | string {
48
+ const mounts: DirectoryMount[] = []
43
49
  const at = position.includes('/')
44
50
  ? `${position.slice(0, position.lastIndexOf('/'))}/`
45
51
  : ''
@@ -50,13 +56,19 @@ export function mountsOf(
50
56
  return `abide: ${position} mounts ${specifier}, and no directory answers to it. RULEBOOK 23.19.`
51
57
  if (ownDirectory(root, directory))
52
58
  return `abide: ${position} mounts ${specifier}, which is the app's own directory already. RULEBOOK 23.20.`
53
- const files: Placed[] = []
54
- for (const below of abideUnder(directory))
55
- files.push({
56
- path: `${directory}/${below}`,
57
- position: `${at}${below}`,
58
- })
59
- mounts.push({ specifier, directory, files })
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
+ })
60
72
  }
61
73
  return mounts
62
74
  }
@@ -40,7 +40,7 @@ export type AbideFile = {
40
40
  */
41
41
  modules: string[]
42
42
  /**
43
- * `<script server>` bodies in source order — 34.20, module scope on a server alone. Apart from
43
+ * `<script server>` bodies in source order — 34.19, module scope on a server alone. Apart from
44
44
  * `modules` because the browser's module never carries them.
45
45
  */
46
46
  servers: string[]
@@ -177,12 +177,12 @@ export function parseAbide(source: string): AbideFile {
177
177
  // position the client walks to after it is a different node and the page hydrates
178
178
  // against the wrong tree.
179
179
  const closing = source.slice(at + 2, close).trim()
180
- // 34.34 — a `<script>` is raw text whose body takes its close tag with it, so one met
180
+ // 34.29 — a `<script>` is raw text whose body takes its close tag with it, so one met
181
181
  // here is what is left of a block that ended early: a `</script>` in a comment or a
182
182
  // string in it, and the source after it would be rendered as the page.
183
183
  if (sameTag(closing, 'script'))
184
184
  throw new Error(
185
- 'abide: a </script> that closes no <script> — a block ends at the first `</script>` in it, even one in a comment or a string. Write it `<\\/script>` there. RULEBOOK 34.34.',
185
+ 'abide: a </script> that closes no <script> — a block ends at the first `</script>` in it, even one in a comment or a string. Write it `<\\/script>` there. RULEBOOK 34.29.',
186
186
  )
187
187
  const innermost = open[open.length - 1]
188
188
  if (typeof innermost === 'string' && sameTag(innermost, closing)) {
@@ -880,10 +880,10 @@ function sameTag(left: string, right: string): boolean {
880
880
  /**
881
881
  * The list a `<script>`'s body belongs in, or `undefined` where it is markup.
882
882
  *
883
- * 34.1 is the bare tag, 34.2 the bare `module` attribute and 34.20 the bare `server` one; anything
883
+ * 34.1 is the bare tag, 34.2 the bare `module` attribute and 34.19 the bare `server` one; anything
884
884
  * else on the tag — `src`, `type`, `defer`, `nonce` —
885
885
  * is a `<script>` the author wrote for the DOCUMENT, and lifting it into component setup moves it
886
- * out of the markup and throws its attributes away — save beside `server`, which 34.33 refuses.
886
+ * out of the markup and throws its attributes away — save beside `server`, which 34.28 refuses.
887
887
  *
888
888
  * 34.13 — BUT `generics="…"` IS THE COMPONENT'S. 34.12 — `lang` is refused on any `<script>`: every
889
889
  * block is TypeScript, so it says nothing, and read as markup it took the author's whole setup out of
@@ -911,16 +911,16 @@ function scriptBlock(
911
911
  )
912
912
  else if (attribute.lowerName !== 'generics') markup = true
913
913
  }
914
- // 34.33 — a `server` attribute on a tag that would be markup ships the server's source to the
914
+ // 34.28 — a `server` attribute on a tag that would be markup ships the server's source to the
915
915
  // document and the browser's module, so it is refused rather than demoted.
916
916
  if (server && markup)
917
917
  throw new Error(
918
- 'abide: a <script> carrying `server` is a server block, written `<script server>` with nothing beside it. RULEBOOK 34.33.',
918
+ 'abide: a <script> carrying `server` is a server block, written `<script server>` with nothing beside it. RULEBOOK 34.28.',
919
919
  )
920
920
  if (markup) return undefined
921
921
  if (module && server)
922
922
  throw new Error(
923
- 'abide: a <script> is `module` or `server`, not both — a server block is module scope already. RULEBOOK 34.20.',
923
+ 'abide: a <script> is `module` or `server`, not both — a server block is module scope already. RULEBOOK 34.19.',
924
924
  )
925
925
  return server ? servers : module ? modules : scripts
926
926
  }
@@ -948,16 +948,16 @@ function rawBody(
948
948
  if (tag.selfClosing) return { body: '', end: tag.end }
949
949
  // The body ends at the first literal close tag, as a browser's does, so a `</script>` inside
950
950
  // a string or a comment in a script block ends it early — and the real close tag after it
951
- // is refused where the template meets it (34.34).
951
+ // is refused where the template meets it (34.29).
952
952
  const close = closeTagAt(source, lowerTag, tag.end)
953
953
  if (close === null)
954
954
  return { body: source.slice(tag.end), end: source.length }
955
- // 34.35 — a component block's early end is refused AT the early end, because what is left
956
- // over can open a `<script>` of its own that takes the real close tag, and then 34.34 meets
955
+ // 34.30 — a component block's early end is refused AT the early end, because what is left
956
+ // over can open a `<script>` of its own that takes the real close tag, and then 34.29 meets
957
957
  // nothing to refuse while the block's source between them renders as the page.
958
958
  if (code && !startsToken(source, tag.end, close.start))
959
959
  throw new Error(
960
- 'abide: a </script> inside a string, a comment or a regex ends the <script> block there, as a browser would. Write it `<\\/script>`. RULEBOOK 34.35.',
960
+ 'abide: a </script> inside a string, a comment or a regex ends the <script> block there, as a browser would. Write it `<\\/script>`. RULEBOOK 34.30.',
961
961
  )
962
962
  return { body: source.slice(tag.end, close.start), end: close.end }
963
963
  }