@abide/abide 0.103.0 → 0.105.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 (88) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/bin/abide.ts +80 -173
  3. package/docs/REGISTRY.md +27 -27
  4. package/docs/RULEBOOK.md +68 -24
  5. package/docs/abide.md +31 -46
  6. package/package.json +1 -1
  7. package/src/compiler/Lowering.ts +15 -5
  8. package/src/compiler/Refusals.ts +57 -0
  9. package/src/compiler/TypeSession.ts +46 -89
  10. package/src/compiler/appLayout.ts +447 -0
  11. package/src/compiler/appMap.ts +21 -25
  12. package/src/compiler/checkApp.ts +61 -97
  13. package/src/compiler/checkedShadow.ts +12 -10
  14. package/src/compiler/classifyRead.ts +3 -6
  15. package/src/compiler/compileApp.ts +183 -478
  16. package/src/compiler/componentGraph.ts +2 -71
  17. package/src/compiler/declaredExports.ts +79 -60
  18. package/src/compiler/emitClient.ts +11 -13
  19. package/src/compiler/emitComponent.ts +21 -6
  20. package/src/compiler/emitSource.ts +20 -20
  21. package/src/compiler/handlersUnder.ts +46 -36
  22. package/src/compiler/hookCalls.ts +152 -0
  23. package/src/compiler/mountsOf.ts +31 -54
  24. package/src/compiler/projectShadow.ts +1 -1
  25. package/src/compiler/readSources.ts +27 -42
  26. package/src/compiler/readsIn.ts +5 -16
  27. package/src/compiler/resolverIn.ts +77 -0
  28. package/src/server/abideModules.ts +152 -146
  29. package/src/server/answerRequest.ts +18 -4
  30. package/src/server/answerSocket.ts +136 -76
  31. package/src/server/answerValue.ts +28 -15
  32. package/src/server/appTree.ts +59 -73
  33. package/src/server/claudeApi.ts +2 -5
  34. package/src/server/clientBundle.ts +198 -0
  35. package/src/server/coerceArgs.ts +1 -1
  36. package/src/server/commands.ts +93 -57
  37. package/src/server/createApp.ts +90 -16
  38. package/src/server/deferredBuilds.ts +175 -0
  39. package/src/server/devWorker.ts +107 -319
  40. package/src/server/faceOf.ts +120 -0
  41. package/src/server/fenceServer.ts +10 -18
  42. package/src/server/generatedSurfaces.ts +12 -53
  43. package/src/server/index.ts +3 -0
  44. package/src/server/placement.ts +167 -0
  45. package/src/server/remote.ts +14 -42
  46. package/src/server/render.ts +25 -17
  47. package/src/server/renderComponent.ts +9 -5
  48. package/src/server/requestScope.ts +10 -4
  49. package/src/server/responseRelay.ts +389 -0
  50. package/src/server/runCompiled.ts +9 -12
  51. package/src/server/runDesktop.ts +6 -9
  52. package/src/server/serveBuild.ts +26 -73
  53. package/src/server/serveDev.ts +19 -176
  54. package/src/server/streamEvent.ts +29 -0
  55. package/src/server/versionOf.ts +1 -2
  56. package/src/server/withAbideSources.ts +2 -1
  57. package/src/shared/HandlerRoute.ts +68 -0
  58. package/src/shared/REFUSAL_KEY.ts +5 -0
  59. package/src/shared/RESERVED_DIRECTORIES.ts +6 -0
  60. package/src/shared/answerOf.ts +105 -78
  61. package/src/shared/filledPath.ts +1 -9
  62. package/src/shared/framing.ts +127 -128
  63. package/src/shared/queryText.ts +15 -7
  64. package/src/shared/reactive/Cursor.ts +69 -124
  65. package/src/shared/reactive/Reactive.ts +296 -162
  66. package/src/shared/reactive/callLoad.ts +4 -18
  67. package/src/shared/reactive/index.ts +11 -8
  68. package/src/shared/reactive/pairReactive.ts +1 -1
  69. package/src/shared/reactive/scoped.ts +4 -3
  70. package/src/shared/reactive/share.ts +11 -1
  71. package/src/shared/routePattern.ts +0 -110
  72. package/src/shared/rpc.ts +17 -24
  73. package/src/shared/rpcRequest.ts +18 -47
  74. package/src/shared/takeEarlyCall.ts +12 -37
  75. package/src/shared/url.ts +1 -1
  76. package/src/ui/Level.ts +276 -0
  77. package/src/ui/List.ts +17 -17
  78. package/src/ui/Router.ts +20 -87
  79. package/src/ui/applyHead.ts +92 -79
  80. package/src/ui/index.ts +4 -2
  81. package/src/ui/reconcile.ts +4 -110
  82. package/src/ui/rows.ts +123 -2
  83. package/src/ui/wire.ts +12 -27
  84. package/src/compiler/hooksCalled.ts +0 -35
  85. package/src/server/clientBuildOptions.ts +0 -54
  86. package/src/server/refuseAddressCollision.ts +0 -37
  87. package/src/shared/reactive/chunksOfStream.ts +0 -57
  88. package/src/ui/MOUNTING_LEVEL.ts +0 -18
@@ -1,12 +1,6 @@
1
- import { dirname, relative as relativeTo } from 'node:path' // bun exposes no path helpers of its own
2
- import {
3
- type FileKind,
4
- fileKind,
5
- placeOf,
6
- routeOf,
7
- } from '#shared/routePattern.ts'
1
+ import { dirname } from 'node:path' // bun exposes no path helpers of its own
8
2
  import { type AbideFile, scriptBodies } from './parseAbide.ts'
9
- import { abideImports, attributedImports } from './splitImports.ts'
3
+ import { abideImports } from './splitImports.ts'
10
4
 
11
5
  type Resolve = (specifier: string, from: string) => string | undefined
12
6
 
@@ -60,66 +54,3 @@ export function abideImportsOf(
60
54
  }
61
55
  return resolved
62
56
  }
63
-
64
- /** One source in the app graph: where it is below the root, what it is, and its address. */
65
- export type AppNode<Source> = Source & {
66
- relative: string
67
- /** 23.19 — the path it answers by: its own below the root, or the one its mount gives it. */
68
- position: string
69
- kind: FileKind
70
- /** 23.3 — the address of a page or an error page, and `undefined` for anything else. */
71
- route: string | undefined
72
- }
73
-
74
- /**
75
- * The app as one graph, built once per build or check: every source with its kind and address,
76
- * and which files are invoked as components.
77
- */
78
- export function appGraph<
79
- Source extends {
80
- path: string
81
- file: AbideFile
82
- position?: string | undefined
83
- },
84
- >(
85
- root: string,
86
- sources: readonly Source[],
87
- resolve: Resolve,
88
- ): { nodes: AppNode<Source>[]; invoked: Set<string> } {
89
- const nodes: AppNode<Source>[] = []
90
- for (const source of sources) {
91
- // Below the root for the app's own files, and above it — `../node_modules/…` — for a
92
- // dependency's (34.24).
93
- const relative = relativeTo(root, source.path)
94
- const position = source.position ?? relative
95
- // 34.20 — a server block of the app's own under a `server` or `public` directory, refused
96
- // here before anything reads its handlers: neither directory holds a component that runs.
97
- const place = placeOf(position)
98
- if (
99
- source.file.servers.length > 0 &&
100
- (place === 'server' || place === 'public')
101
- )
102
- throw new Error(
103
- `abide: ${relative} has a <script server>, and it is under a ${place} directory, where nothing has an address. Move the file out of it, or the code into a module under a server directory. RULEBOOK 34.20.`,
104
- )
105
- // 34.21 — a file imported as text is found at build, not when its module first loads.
106
- for (const script of scriptBodies(source.file))
107
- for (const { specifier } of attributedImports(script, 'text'))
108
- if (resolve(specifier, dirname(source.path)) === undefined)
109
- throw new Error(
110
- `abide: ${relative} imports ${specifier} as text, and there is no such file. RULEBOOK 34.21.`,
111
- )
112
- const kind = fileKind(position)
113
- nodes.push({
114
- ...source,
115
- relative,
116
- position,
117
- kind,
118
- route:
119
- kind === 'page' || kind === 'error'
120
- ? routeOf(position)
121
- : undefined,
122
- })
123
- }
124
- return { nodes, invoked: componentGraph(sources, resolve).invoked }
125
- }
@@ -44,6 +44,7 @@ import {
44
44
  isReactiveMember,
45
45
  valueTypeOf,
46
46
  } from './reactiveTypes.ts'
47
+ import type { Refusals } from './Refusals.ts'
47
48
  import type { Diagnosis } from './TypeSession.ts'
48
49
 
49
50
  /**
@@ -57,12 +58,16 @@ import type { Diagnosis } from './TypeSession.ts'
57
58
  * export here: it is erased, and has no address to answer at.
58
59
  *
59
60
  * `projectName` is the tsconfig the project was opened on, which a module outside it is refused by.
61
+ * 38.33 — WHAT IS REFUSED OF A MODULE IS REFUSED AT ITS FILE, `named[at]` for `paths[at]`, every
62
+ * export read either way: a module with a refusal is `undefined`, so nothing refuses it twice.
60
63
  */
61
64
  export async function declaredExports(
62
65
  project: Project,
63
66
  projectName: string,
64
67
  paths: readonly string[],
65
- ): Promise<DeclaredExport[][]> {
68
+ named: readonly string[],
69
+ refusals: Refusals,
70
+ ): Promise<(DeclaredExport[] | undefined)[]> {
66
71
  const checker = project.checker
67
72
  // ABIDE'S FILE AS THE CHECKER NAMES IT, not as this module does: it canonicalizes a path to
68
73
  // the disk's case rules, so on a case-insensitive disk `/Users/…/Code` comes back
@@ -71,79 +76,93 @@ export async function declaredExports(
71
76
  ?.path
72
77
  const abide = await abideIn(project)
73
78
  return allSettled(
74
- paths.map(async (path) => {
79
+ paths.map(async (path, at) => {
80
+ const file = named[at] as string
75
81
  const sourceFile = await project.program.getSourceFile(path)
76
- if (sourceFile === undefined)
77
- throw new Error(
78
- `abide: ${path} is not inside the project ${projectName}, so what it exports cannot be read (16.73). Give the app a tsconfig whose include reaches it.`,
82
+ if (sourceFile === undefined) {
83
+ refusals.at(
84
+ file,
85
+ new Error(
86
+ `abide: ${path} is not inside the project ${projectName}, so what it exports cannot be read (16.73). Give the app a tsconfig whose include reaches it.`,
87
+ ),
79
88
  )
89
+ return undefined
90
+ }
80
91
  const module = await checker.getSymbolAtLocation(sourceFile)
81
92
  if (module === undefined) return []
82
93
  const exports = await checker.getExportsOfModule(module)
94
+ let refused = false
83
95
  const found = await allSettled(
84
96
  exports.map(async (exported) => {
85
- const target = await aliasedSymbol(checker, exported)
86
- if ((target.flags & SymbolFlags.Value) === 0)
87
- return undefined
88
- const declared = await declaringMethod(
89
- checker,
90
- target,
91
- methodsPath,
92
- )
93
- const unread: Diagnosis[] = []
94
- for (const value of declared?.unread ?? []) {
95
- const file = value.getSourceFile()
96
- unread.push(
97
- diagnosisAt(
98
- file.fileName === sourceFile.fileName
99
- ? path
100
- : file.fileName,
101
- file.text,
102
- value.getStart(),
103
- 'warning',
104
- `abide: \`${value.parent.getText()}\` is not a value the build can read, so \`${exported.name}\`'s browser copy is built without it. Write it as a literal, or as a \`const\` whose type is one. RULEBOOK 18.15.`,
105
- ),
106
- )
107
- }
108
- // 19.9 — a handler's args, as its annotation types them.
109
- const handlerType =
110
- declared === undefined
111
- ? undefined
112
- : await checker.getTypeOfSymbol(target)
113
- if (handlerType !== undefined && abide.reactive) {
114
- const at = await nestedReactiveIn(
97
+ try {
98
+ const target = await aliasedSymbol(checker, exported)
99
+ if ((target.flags & SymbolFlags.Value) === 0)
100
+ return undefined
101
+ const declared = await declaringMethod(
115
102
  checker,
116
- abide.reactive,
117
- await valueOfRpc(checker, handlerType),
118
- '',
119
- { seen: new Set(), depth: 0 },
103
+ target,
104
+ methodsPath,
120
105
  )
121
- if (at !== undefined)
122
- throw new Error(
123
- `abide: ${path} — \`${exported.name}\` returns a \`Reactive\` at \`${at}\`, which the wire would carry as the function it is rather than the value it holds. Read it there, or return the \`Reactive\` itself to have it adopted (11.12). RULEBOOK 16.39.`,
106
+ const unread: Diagnosis[] = []
107
+ for (const value of declared?.unread ?? []) {
108
+ const file = value.getSourceFile()
109
+ unread.push(
110
+ diagnosisAt(
111
+ file.fileName === sourceFile.fileName
112
+ ? path
113
+ : file.fileName,
114
+ file.text,
115
+ value.getStart(),
116
+ 'warning',
117
+ `abide: \`${value.parent.getText()}\` is not a value the build can read, so \`${exported.name}\`'s browser copy is built without it. Write it as a literal, or as a \`const\` whose type is one. RULEBOOK 18.15.`,
118
+ ),
124
119
  )
125
- }
126
- return {
127
- name: exported.name,
128
- method: declared?.method,
129
- ui: declared?.ui ?? true,
130
- cli: declared?.cli ?? true,
131
- mcp: declared?.mcp ?? true,
132
- description: declared?.description,
133
- copy: declared?.copy,
134
- defaults: declared?.defaults,
135
- args:
136
- handlerType === undefined
120
+ }
121
+ // 19.9 — a handler's args, as its annotation types them.
122
+ const handlerType =
123
+ declared === undefined
137
124
  ? undefined
138
- : await argsSchemaOf(
139
- checker,
140
- handlerType,
141
- declared?.defaulted ?? [],
142
- ),
143
- unread,
125
+ : await checker.getTypeOfSymbol(target)
126
+ if (handlerType !== undefined && abide.reactive) {
127
+ const at = await nestedReactiveIn(
128
+ checker,
129
+ abide.reactive,
130
+ await valueOfRpc(checker, handlerType),
131
+ '',
132
+ { seen: new Set(), depth: 0 },
133
+ )
134
+ if (at !== undefined)
135
+ throw new Error(
136
+ `abide: ${path} — \`${exported.name}\` returns a \`Reactive\` at \`${at}\`, which the wire would carry as the function it is rather than the value it holds. Read it there, or return the \`Reactive\` itself to have it adopted (11.12). RULEBOOK 16.39.`,
137
+ )
138
+ }
139
+ return {
140
+ name: exported.name,
141
+ method: declared?.method,
142
+ ui: declared?.ui ?? true,
143
+ cli: declared?.cli ?? true,
144
+ mcp: declared?.mcp ?? true,
145
+ description: declared?.description,
146
+ copy: declared?.copy,
147
+ defaults: declared?.defaults,
148
+ args:
149
+ handlerType === undefined
150
+ ? undefined
151
+ : await argsSchemaOf(
152
+ checker,
153
+ handlerType,
154
+ declared?.defaulted ?? [],
155
+ ),
156
+ unread,
157
+ }
158
+ } catch (thrown) {
159
+ refused = true
160
+ refusals.at(file, thrown)
161
+ return undefined
144
162
  }
145
163
  }),
146
164
  )
165
+ if (refused) return undefined
147
166
  const handlers: DeclaredExport[] = []
148
167
  for (const entry of found)
149
168
  if (entry !== undefined) handlers.push(entry)
@@ -7,6 +7,7 @@ import {
7
7
  } from '#shared/ANCHORS.ts'
8
8
  import { anchorMarkup } from '#shared/anchorMarkup.ts'
9
9
  import { CHILDREN } from '#shared/CHILDREN.ts'
10
+ import { RUNTIME_SPECIFIER, SHARED_SPECIFIER } from '#shared/SPECIFIERS.ts'
10
11
  import { type CheckerAnswer, UNCHECKED } from './CheckerAnswer.ts'
11
12
  import {
12
13
  Cursor,
@@ -39,7 +40,6 @@ import {
39
40
  type ValueRender,
40
41
  } from './renderMarkup.ts'
41
42
  import { SHARED_NAMES } from './SHARED_NAMES.ts'
42
- import { RUNTIME_SPECIFIER, SHARED_SPECIFIER } from '#shared/SPECIFIERS.ts'
43
43
  import {
44
44
  insideShare,
45
45
  resumedShare,
@@ -213,19 +213,18 @@ export function emitClient(
213
213
  // to read when this level leaves or a level under it does. One thunk, which the apply below
214
214
  // reads too, so the markup expression is emitted once.
215
215
  const registered = heads
216
- ? `if (${ROUTED}) ${ROUTED}.head = () => ${headMarkup}\n`
216
+ ? `if (${ROUTED}) ${ROUTED}.setHead(() => ${headMarkup})\n`
217
217
  : ''
218
218
  const applied = heads
219
- ? `if (${ROUTED}) ${lowering.use('applyHead')}(${ROUTED}.head()); `
219
+ ? `if (${ROUTED}) ${lowering.use('applyHead')}(${ROUTED}.headMarkup()); `
220
220
  : ''
221
221
  // Applied with the fill either way, so the watch's first run only subscribes: what it would
222
222
  // write is already there, from the server or from the line above.
223
- if (liveHeads)
224
- lines.push(
225
- `if (${ROUTED}) ${lowering.use('liveHead')}(${SCOPE}, ${ROUTED}.head)`,
226
- )
223
+ if (liveHeads) lines.push(`if (${ROUTED}) ${ROUTED}.followHead(${SCOPE})`)
227
224
  // 39.1, 39.6 — A COMPONENT'S HEAD IS ITS PAGE'S, on a navigation as on the server: added to the
228
- // level the router is mounting (`contributeHead`), which a row grown later is not.
225
+ // level it stands in (`contributeHead`), a row grown later included. Asked of the
226
+ // region before the fill makes it look painted; owned by the component's scope as its first
227
+ // unit, so the head leaves with the region — and, 39.13, follows its value until then.
229
228
  const ownHead = routed ? undefined : headSink(file)
230
229
  const ownMarkup =
231
230
  ownHead === undefined
@@ -234,7 +233,8 @@ export function emitClient(
234
233
  const contributed =
235
234
  ownMarkup === "''"
236
235
  ? ''
237
- : `${lowering.use('contributeHead')}(${REGION_ROOT}, () => ${ownMarkup})\n`
236
+ : `const __headed = ${lowering.use('contributeHead')}(${REGION_ROOT}, () => ${ownMarkup})\n`
237
+ if (contributed !== '') lines.unshift(`${SCOPE}.own(__headed)`)
238
238
  const builds = carriesOwnMarkup && page.positions.length > 0
239
239
  let fills = contributed
240
240
  if (applied !== '' || builds) {
@@ -406,7 +406,7 @@ function deferredMarkup(
406
406
  function __markup(${REGION_ROOT}) {
407
407
  ${insideShare(lowering, SETTLED_AT, markup)}
408
408
  }
409
- if (${ROUTED}) { ${ROUTED}.markup = __markup; ${ROUTED}.share = ${SETTLED_AT}; return${declared ? ` ${SCOPE}` : ''} }
409
+ if (${ROUTED}) { ${ROUTED}.leaveMarkup(__markup, ${SETTLED_AT}); return${declared ? ` ${SCOPE}` : ''} }
410
410
  return __markup(${REGION_ROOT})`
411
411
  }
412
412
 
@@ -738,9 +738,7 @@ if (__r${slot} !== undefined) ${SCOPE}.stops.push(__r${slot})`,
738
738
  }
739
739
  }
740
740
  if (writes.length > 0)
741
- attributes.push(
742
- `const __a${slot} = ${element}\n${writes.join('\n')}`,
743
- )
741
+ attributes.push(`const __a${slot} = ${element}\n${writes.join('\n')}`)
744
742
  for (const unit of binds) attributes.push(unit)
745
743
  for (const unit of spreads) attributes.push(unit)
746
744
  return { handlers, attributes, elements }
@@ -22,6 +22,8 @@ const BODY = '__body'
22
22
  // A page's or a layout's deferred build (41.19).
23
23
  const BUILT = '__built'
24
24
  const BUILD = '__build'
25
+ // The calls renders had made as a component's setup began (41.28).
26
+ const CALLS_BEFORE = '__callsBefore'
25
27
 
26
28
  /**
27
29
  * The server module for a parsed `.abide` file: one ESM module exporting its component.
@@ -37,8 +39,8 @@ const BUILD = '__build'
37
39
  *
38
40
  * `settles` IS A PAGE OR A LAYOUT, which `render` builds a level at a time (41.19): its setup runs
39
41
  * at the call and its markup is built on the first read of `head` or `body`, so the calls the
40
- * setup started are given the turn to answer that a component built inside its caller's markup is
41
- * not. Every other component builds where it is called, and pays nothing for the difference.
42
+ * setup started are given the turn to answer. Every other component answers the same way only where
43
+ * its setup made a call (41.28), and builds where it is called otherwise.
42
44
  */
43
45
  export function emitComponent(
44
46
  file: AbideFile,
@@ -121,8 +123,8 @@ ${privateExports(file.servers.join('\n'))}`
121
123
  // child the markup invokes must not be set up twice. The build resumes the share scope the
122
124
  // setup ran in (10.5), since by the time it is read the caller's has long been left — and that
123
125
  // scope is `scope` on the answer, so `composePage` sets the level below up inside it too.
124
- const level = settles
125
- ? ` const ${SETTLED_AT} = ${lowering.use('currentShare')}()
126
+ const deferred =
127
+ (): string => ` const ${SETTLED_AT} = ${lowering.use('currentShare')}()
126
128
  let ${BUILT}
127
129
  const ${BUILD} = () => {
128
130
  if (${BUILT} !== undefined) return ${BUILT}
@@ -135,10 +137,23 @@ ${privateExports(file.servers.join('\n'))}`
135
137
  get body() { return ${BUILD}().body },
136
138
  get document() { return ${BUILD}().document },
137
139
  }`
138
- : build
140
+ // 41.28 — EVERY OTHER COMPONENT BUILDS WHERE IT IS CALLED unless its setup
141
+ // made a call, and then answers as a page does, for `renderComponent` to build after the turn.
142
+ // THE BUILD IS WRITTEN TWICE rather than always run through `__build`: the closure measured ~4%
143
+ // a row on `bench:component`'s card under JSC, the second copy nothing but server module bytes.
144
+ // One with no setup cannot call, and is written once.
145
+ const waits = !settles && componentBody.trim() !== ''
146
+ const level = settles
147
+ ? deferred()
148
+ : waits
149
+ ? ` if (${lowering.use('callsInRender')}() !== ${CALLS_BEFORE}) {
150
+ ${deferred()}
151
+ }
152
+ ${build}`
153
+ : build
139
154
  return `${moduleImports(lowering, () => SERVER_SPECIFIER, [moduleSetup, serverSetup, setup])}
140
155
  ${moduleSetup.body === '' ? '' : `${moduleSetup.body}\n`}${serverBody}export default function Component(${lowering.takesProps() ? `${PROPS} = {}` : ''}) {
141
- ${opened}${componentBody}
156
+ ${waits ? ` const ${CALLS_BEFORE} = ${lowering.use('callsInRender')}()\n` : ''}${opened}${componentBody}
142
157
  ${level}${closed}
143
158
  }
144
159
  `
@@ -1,5 +1,5 @@
1
1
  import { STYLE_QUERY } from '#server/MODULE_QUERIES.ts'
2
- import { fileKind, scopeOf } from '#shared/routePattern.ts'
2
+ import type { Placement } from '#server/placement.ts'
3
3
  import type { CheckerAnswer } from './CheckerAnswer.ts'
4
4
  import { emitClient } from './emitClient.ts'
5
5
  import { emitComponent } from './emitComponent.ts'
@@ -18,34 +18,34 @@ import { sourceMapOf } from './sourceMapOf.ts'
18
18
  * One `.abide`'s modules, in the mode the build compiles it in: its server module, its client
19
19
  * module (`''` where it has none) and its scoped stylesheet.
20
20
  *
21
- * Everything that mode depends on is the path below the app root and whether another file invokes
22
- * it (`componentGraph`), so a test handing over the same two facts compiles what the build would.
21
+ * Everything that mode depends on is where the file is placed (`placement`) and whether another
22
+ * file invokes it (`componentGraph`), so a test handing over the same facts compiles what the build
23
+ * would.
23
24
  */
24
25
  export function emitSource(
25
- relative: string,
26
- file: AbideFile,
27
- answer: CheckerAnswer,
28
- invoked: boolean,
26
+ source: Pick<Placement, 'relative' | 'kind' | 'scope'> & {
27
+ file: AbideFile
28
+ answer: CheckerAnswer
29
+ invoked: boolean
30
+ },
29
31
  /**
30
32
  * 16.28 — what the browser's module binds in place of the server block: the wrapper of each
31
33
  * private handler it declares, under the handler's name (`standInsOf`).
32
34
  */
33
35
  standIns = '',
34
36
  /** 38.97 — the `.abide`'s text, where the build wants each module's source map. */
35
- source?: string,
36
- /** 23.19 — the path it answers by, where a mount gives it one other than `relative`. */
37
- position = relative,
37
+ text?: string,
38
38
  ): {
39
39
  server: string
40
40
  client: string
41
- /** 38.97 — each module's `mappings` into the `.abide`, where `source` was given. */
41
+ /** 38.97 — each module's `mappings` into the `.abide`, where `text` was given. */
42
42
  serverMappings: string | undefined
43
43
  clientMappings: string | undefined
44
44
  sheet: string | undefined
45
45
  /** 39.8 — whether a `<style>` names a view transition, read here rather than at run time. */
46
46
  transitions: boolean
47
47
  } {
48
- const kind = fileKind(position)
48
+ const { relative, kind, file, answer, invoked } = source
49
49
  // 34.4 — A COMPONENT WITH STYLES: its elements carry its token, and the styles, scoped,
50
50
  // are a stylesheet its client module imports — which the bundle collects into the
51
51
  // stylesheet the document links (38.23).
@@ -87,9 +87,9 @@ export function emitSource(
87
87
  // has no `.client.ts`, so a file shipping no runtime is a fact about the build rather than a
88
88
  // claim about the bundler.
89
89
  const serverChunks: LoweredChunk[] | undefined =
90
- source === undefined ? undefined : []
90
+ text === undefined ? undefined : []
91
91
  const clientChunks: LoweredChunk[] | undefined =
92
- source === undefined ? undefined : []
92
+ text === undefined ? undefined : []
93
93
  const client = emitClient(
94
94
  file,
95
95
  answer,
@@ -107,7 +107,7 @@ export function emitSource(
107
107
  file,
108
108
  answer,
109
109
  kind !== 'component',
110
- scopeOf(position),
110
+ source.scope,
111
111
  serverChunks,
112
112
  )
113
113
  const name = relative.slice(relative.lastIndexOf('/') + 1)
@@ -118,24 +118,24 @@ export function emitSource(
118
118
  standIns,
119
119
  )
120
120
  let mapped: Mapped | undefined
121
- if (source !== undefined) {
121
+ if (text !== undefined) {
122
122
  mapped = MAPPED.get(relative)
123
123
  if (
124
124
  mapped === undefined ||
125
- mapped.source !== source ||
125
+ mapped.source !== text ||
126
126
  mapped.server !== server ||
127
127
  mapped.client !== browser
128
128
  ) {
129
129
  const projected = projectShadow(file).origins
130
130
  mapped = {
131
- source,
131
+ source: text,
132
132
  server,
133
133
  client: browser,
134
134
  serverMappings: sourceMapOf(
135
135
  server,
136
136
  serverChunks ?? [],
137
137
  projected,
138
- source,
138
+ text,
139
139
  ),
140
140
  clientMappings:
141
141
  browser === ''
@@ -144,7 +144,7 @@ export function emitSource(
144
144
  browser,
145
145
  clientChunks ?? [],
146
146
  projected,
147
- source,
147
+ text,
148
148
  ),
149
149
  }
150
150
  MAPPED.set(relative, mapped)
@@ -10,18 +10,14 @@
10
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
- import { refuseFrameworkRoute } from '#server/appTree.ts'
13
+ import { frameworkRouteRefusal } from '#server/appTree.ts'
14
14
  import { surfaceName } from '#server/surfaceOf.ts'
15
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
+ import { parseRoutePattern } from '#shared/routePattern.ts'
17
+ import type { PlacedHandler, PlacedSource } from './appLayout.ts'
22
18
  import { declaredExports } from './declaredExports.ts'
23
- import type { AbideFile } from './parseAbide.ts'
24
19
  import { PRIVATE_EXPORT } from './projectShadow.ts'
20
+ import type { Refusals } from './Refusals.ts'
25
21
  import type { Diagnosis, TypeSession } from './TypeSession.ts'
26
22
 
27
23
  /** A handler file, by its path and its path below the app's root. */
@@ -32,19 +28,11 @@ export type HandlerFile = {
32
28
  position: string
33
29
  }
34
30
 
35
- /** A `.abide` the build read. */
36
- type Source = HandlerFile & { file: AbideFile }
37
-
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
- }
31
+ /** A `.abide` the build read, placed. */
32
+ type Source = Pick<
33
+ PlacedSource,
34
+ 'path' | 'relative' | 'position' | 'place' | 'scope' | 'file'
35
+ >
48
36
 
49
37
  /**
50
38
  * The app's handler mounts, with the warnings the build raised reading them.
@@ -55,28 +43,49 @@ export const THROW: Refuse = (_relative, thrown) => {
55
43
  export async function handlersUnder(
56
44
  session: TypeSession,
57
45
  sources: readonly Source[],
58
- files: readonly HandlerFile[],
59
- refuse: Refuse = THROW,
46
+ files: readonly Pick<
47
+ PlacedHandler,
48
+ 'path' | 'relative' | 'route' | 'scope'
49
+ >[],
50
+ refusals: Refusals,
60
51
  ): Promise<{ mounts: Mount[]; warnings: Diagnosis[] }> {
61
52
  const serving: Source[] = []
62
53
  for (const source of sources)
63
54
  if (source.file.servers.length > 0) serving.push(source)
64
55
  const paths: string[] = []
65
- for (const file of files) paths.push(file.path)
56
+ const named: string[] = []
57
+ for (const file of files) {
58
+ paths.push(file.path)
59
+ named.push(file.relative)
60
+ }
66
61
  const project = await session.projectWith(paths)
67
- for (const source of serving) paths.push(session.projectionOf(source.path))
62
+ for (const source of serving) {
63
+ paths.push(session.projectionOf(source.path))
64
+ named.push(source.path)
65
+ }
66
+ // A module whose read was refused is `undefined`, refused at its file already.
68
67
  const declared =
69
68
  project === undefined || paths.length === 0
70
- ? []
71
- : await declaredExports(project, session.tsconfig, paths)
69
+ ? paths.map(() => [])
70
+ : await declaredExports(
71
+ project,
72
+ session.tsconfig,
73
+ paths,
74
+ named,
75
+ refusals,
76
+ )
72
77
  const mounts: Mount[] = []
73
78
  const warnings: Diagnosis[] = []
74
79
  const names = new Map<string, string>()
75
80
  for (let at = 0; at < files.length; at += 1) {
76
- const { relative, position } = files[at] as HandlerFile
81
+ const {
82
+ relative,
83
+ route: address,
84
+ scope,
85
+ } = files[at] as (typeof files)[number]
86
+ const exported = declared[at]
87
+ if (exported === undefined) continue
77
88
  try {
78
- const address = routeOf(position)
79
- const exported = declared[at] ?? []
80
89
  const handler = exported.find((each) => each.name === 'default')
81
90
  // 16.73 — the default and nothing else, declared by one of the five methods.
82
91
  for (const each of exported)
@@ -90,7 +99,8 @@ export async function handlersUnder(
90
99
  throw new Error(
91
100
  `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.`,
92
101
  )
93
- refuseFrameworkRoute(address, relative)
102
+ const framework = frameworkRouteRefusal(address, relative)
103
+ if (framework !== undefined) throw framework
94
104
  refuseUndeclaredParameters(address, handler.args, relative)
95
105
  // 20.13 — one name per handler on every generated surface.
96
106
  const surface = surfaceName(handler.method, address)
@@ -106,7 +116,7 @@ export async function handlersUnder(
106
116
  source: relative,
107
117
  name: address.slice(address.lastIndexOf('/') + 1),
108
118
  export: 'default',
109
- scope: scopeOf(position),
119
+ scope,
110
120
  private: false,
111
121
  ui: handler.ui,
112
122
  cli: handler.cli,
@@ -118,14 +128,14 @@ export async function handlersUnder(
118
128
  })
119
129
  warnings.push(...handler.unread)
120
130
  } catch (thrown) {
121
- refuse(relative, thrown)
131
+ refusals.at(relative, thrown)
122
132
  }
123
133
  }
124
134
  for (let at = 0; at < serving.length; at += 1) {
125
- const { relative, position } = serving[at] as Source
135
+ const { relative, position, place, scope } = serving[at] as Source
126
136
  // 34.24 — a dependency's file answers where a mount places it, and its handlers below the
127
137
  // path it has there.
128
- const below = placeOf(position) === 'dependency' ? relative : position
138
+ const below = place === 'dependency' ? relative : position
129
139
  for (const each of declared[files.length + at] ?? []) {
130
140
  if (each.method === undefined) continue
131
141
  if (!each.name.startsWith(PRIVATE_EXPORT)) continue
@@ -136,7 +146,7 @@ export async function handlersUnder(
136
146
  source: relative,
137
147
  name,
138
148
  export: each.name,
139
- scope: scopeOf(position),
149
+ scope,
140
150
  private: true,
141
151
  ui: each.ui,
142
152
  cli: false,